-Labels applied by the Magento team
+Community Maintainers
+The members of this team have been recognized for their outstanding commitment to maintaining and improving Magento. Magento has granted them permission to accept, merge, and reject pull requests, as well as review issues, and thanks these Community Maintainers for their valuable contributions.
+
+
+
+
-| Label | Description |
-| ------------- |-------------|
-|  | Affects Documentation domain. |
-|  | Affects the Product team (mostly feature requests or business logic change). |
-|  | Affects Architect Group (mostly to make decisions around technology changes). |
-|  | The pull request has been accepted and will be merged into mainline code. |
-|  | The pull request has been rejected and will not be merged into mainline code. Possible reasons can include but are not limited to: issue has already been fixed in another code contribution, or there is an issue with the code contribution. |
-|  | The Magento Team has confirmed that this issue contains the minimum required information to reproduce. |
-|  | The Magento Team has validated the issue and an internal ticket has been created. |
-|  | The internal ticket is currently in progress, fix is scheduled to be delivered. |
-|  | The Magento Team needs additional information from the reporter to properly prioritize and process the issue or pull request. |
+Top Contributors
+Magento is thankful for any contribution that can improve our code base, documentation or increase test coverage. We always recognize our most active members, as their contributions are the foundation of the Magento Open Source platform.
+
+
+
+
+Labels applied by the Magento team
+We apply labels to public Pull Requests and Issues to help other participants retrieve additional information about current progress, component assignments, Magento release lines, and much more.
+Please review the Code Contributions guide for detailed information on labels used in Magento 2 repositories.
Reporting security issues
-To report security vulnerabilities in Magento software or web sites, please e-mail security@magento.com . Please do not report security issues using GitHub. Be sure to encrypt your e-mail with our encryption key if it includes sensitive information. Learn more about reporting security issues here .
+To report security vulnerabilities in Magento software or web sites, please create a Bugcrowd researcher account there to submit and follow-up your issue. Learn more about reporting security issues here .
Stay up-to-date on the latest security news and patches for Magento by signing up for Security Alert Notifications .
diff --git a/app/.htaccess b/app/.htaccess
index 93169e4eb44ff..707c26b075e16 100644
--- a/app/.htaccess
+++ b/app/.htaccess
@@ -1,2 +1,8 @@
-Order deny,allow
-Deny from all
+
+ order allow,deny
+ deny from all
+
+= 2.4>
+ Require all denied
+
+
diff --git a/app/bootstrap.php b/app/bootstrap.php
index 6701a9f4dd51e..8e901cac9bfb8 100644
--- a/app/bootstrap.php
+++ b/app/bootstrap.php
@@ -14,12 +14,12 @@
if (!defined('PHP_VERSION_ID') || !(PHP_VERSION_ID === 70002 || PHP_VERSION_ID === 70004 || PHP_VERSION_ID >= 70006)) {
if (PHP_SAPI == 'cli') {
echo 'Magento supports 7.0.2, 7.0.4, and 7.0.6 or later. ' .
- 'Please read http://devdocs.magento.com/guides/v1.0/install-gde/system-requirements.html';
+ 'Please read http://devdocs.magento.com/guides/v2.2/install-gde/system-requirements.html';
} else {
echo <<
Magento supports PHP 7.0.2, 7.0.4, and 7.0.6 or later. Please read
-
+
Magento System Requirements .
HTML;
@@ -31,8 +31,6 @@
// Sets default autoload mappings, may be overridden in Bootstrap::create
\Magento\Framework\App\Bootstrap::populateAutoloader(BP, []);
-require_once BP . '/app/functions.php';
-
/* Custom umask value may be provided in optional mage_umask file in root */
$umaskFile = BP . '/magento_umask';
$mask = file_exists($umaskFile) ? octdec(file_get_contents($umaskFile)) : 002;
@@ -49,12 +47,21 @@
unset($_SERVER['ORIG_PATH_INFO']);
}
-if (!empty($_SERVER['MAGE_PROFILER'])
+if (
+ (!empty($_SERVER['MAGE_PROFILER']) || file_exists(BP . '/var/profiler.flag'))
&& isset($_SERVER['HTTP_ACCEPT'])
&& strpos($_SERVER['HTTP_ACCEPT'], 'text/html') !== false
) {
- \Magento\Framework\Profiler::applyConfig(
- $_SERVER['MAGE_PROFILER'],
+ $profilerConfig = isset($_SERVER['MAGE_PROFILER']) && strlen($_SERVER['MAGE_PROFILER'])
+ ? $_SERVER['MAGE_PROFILER']
+ : trim(file_get_contents(BP . '/var/profiler.flag'));
+
+ if ($profilerConfig) {
+ $profilerConfig = json_decode($profilerConfig, true) ?: $profilerConfig;
+ }
+
+ Magento\Framework\Profiler::applyConfig(
+ $profilerConfig,
BP,
!empty($_SERVER['HTTP_X_REQUESTED_WITH']) && $_SERVER['HTTP_X_REQUESTED_WITH'] == 'XMLHttpRequest'
);
diff --git a/app/code/Magento/AdminNotification/Controller/Adminhtml/Notification/MarkAsRead.php b/app/code/Magento/AdminNotification/Controller/Adminhtml/Notification/MarkAsRead.php
index 79f69ab5da88d..6b5e0681139cf 100644
--- a/app/code/Magento/AdminNotification/Controller/Adminhtml/Notification/MarkAsRead.php
+++ b/app/code/Magento/AdminNotification/Controller/Adminhtml/Notification/MarkAsRead.php
@@ -28,11 +28,11 @@ public function execute()
)->markAsRead(
$notificationId
);
- $this->messageManager->addSuccess(__('The message has been marked as Read.'));
+ $this->messageManager->addSuccessMessage(__('The message has been marked as Read.'));
} catch (\Magento\Framework\Exception\LocalizedException $e) {
- $this->messageManager->addError($e->getMessage());
+ $this->messageManager->addErrorMessage($e->getMessage());
} catch (\Exception $e) {
- $this->messageManager->addException(
+ $this->messageManager->addExceptionMessage(
$e,
__("We couldn't mark the notification as Read because of an error.")
);
diff --git a/app/code/Magento/AdminNotification/Controller/Adminhtml/Notification/MassMarkAsRead.php b/app/code/Magento/AdminNotification/Controller/Adminhtml/Notification/MassMarkAsRead.php
index 9e61b8ff4b83c..9ae4a7cdac0b9 100644
--- a/app/code/Magento/AdminNotification/Controller/Adminhtml/Notification/MassMarkAsRead.php
+++ b/app/code/Magento/AdminNotification/Controller/Adminhtml/Notification/MassMarkAsRead.php
@@ -23,7 +23,7 @@ public function execute()
{
$ids = $this->getRequest()->getParam('notification');
if (!is_array($ids)) {
- $this->messageManager->addError(__('Please select messages.'));
+ $this->messageManager->addErrorMessage(__('Please select messages.'));
} else {
try {
foreach ($ids as $id) {
@@ -32,13 +32,13 @@ public function execute()
$model->setIsRead(1)->save();
}
}
- $this->messageManager->addSuccess(
+ $this->messageManager->addSuccessMessage(
__('A total of %1 record(s) have been marked as Read.', count($ids))
);
} catch (\Magento\Framework\Exception\LocalizedException $e) {
- $this->messageManager->addError($e->getMessage());
+ $this->messageManager->addErrorMessage($e->getMessage());
} catch (\Exception $e) {
- $this->messageManager->addException(
+ $this->messageManager->addExceptionMessage(
$e,
__("We couldn't mark the notification as Read because of an error.")
);
diff --git a/app/code/Magento/AdminNotification/Controller/Adminhtml/Notification/MassRemove.php b/app/code/Magento/AdminNotification/Controller/Adminhtml/Notification/MassRemove.php
index 6c0dfd1db7d16..f4cafa09c7e45 100644
--- a/app/code/Magento/AdminNotification/Controller/Adminhtml/Notification/MassRemove.php
+++ b/app/code/Magento/AdminNotification/Controller/Adminhtml/Notification/MassRemove.php
@@ -23,7 +23,7 @@ public function execute()
{
$ids = $this->getRequest()->getParam('notification');
if (!is_array($ids)) {
- $this->messageManager->addError(__('Please select messages.'));
+ $this->messageManager->addErrorMessage(__('Please select messages.'));
} else {
try {
foreach ($ids as $id) {
@@ -32,13 +32,14 @@ public function execute()
$model->setIsRemove(1)->save();
}
}
- $this->messageManager->addSuccess(__('Total of %1 record(s) have been removed.', count($ids)));
+ $this->messageManager->addSuccessMessage(__('Total of %1 record(s) have been removed.', count($ids)));
} catch (\Magento\Framework\Exception\LocalizedException $e) {
- $this->messageManager->addError($e->getMessage());
+ $this->messageManager->addErrorMessage($e->getMessage());
} catch (\Exception $e) {
- $this->messageManager->addException($e, __("We couldn't remove the messages because of an error."));
+ $this->messageManager
+ ->addExceptionMessage($e, __("We couldn't remove the messages because of an error."));
}
}
- $this->getResponse()->setRedirect($this->_redirect->getRedirectUrl($this->getUrl('*')));
+ $this->_redirect('adminhtml/*/');
}
}
diff --git a/app/code/Magento/AdminNotification/Controller/Adminhtml/Notification/Remove.php b/app/code/Magento/AdminNotification/Controller/Adminhtml/Notification/Remove.php
index 17f911339cb61..bec101fc27d48 100644
--- a/app/code/Magento/AdminNotification/Controller/Adminhtml/Notification/Remove.php
+++ b/app/code/Magento/AdminNotification/Controller/Adminhtml/Notification/Remove.php
@@ -31,11 +31,12 @@ public function execute()
try {
$model->setIsRemove(1)->save();
- $this->messageManager->addSuccess(__('The message has been removed.'));
+ $this->messageManager->addSuccessMessage(__('The message has been removed.'));
} catch (\Magento\Framework\Exception\LocalizedException $e) {
- $this->messageManager->addError($e->getMessage());
+ $this->messageManager->addErrorMessage($e->getMessage());
} catch (\Exception $e) {
- $this->messageManager->addException($e, __("We couldn't remove the messages because of an error."));
+ $this->messageManager
+ ->addExceptionMessage($e, __("We couldn't remove the messages because of an error."));
}
$this->_redirect('adminhtml/*/');
diff --git a/app/code/Magento/AdminNotification/Controller/Adminhtml/System/Message/ListAction.php b/app/code/Magento/AdminNotification/Controller/Adminhtml/System/Message/ListAction.php
index c332440276083..6088afbc2e1a4 100644
--- a/app/code/Magento/AdminNotification/Controller/Adminhtml/System/Message/ListAction.php
+++ b/app/code/Magento/AdminNotification/Controller/Adminhtml/System/Message/ListAction.php
@@ -59,8 +59,10 @@ public function execute()
if (empty($result)) {
$result[] = [
'severity' => (string)\Magento\Framework\Notification\MessageInterface::SEVERITY_NOTICE,
- 'text' => 'You have viewed and resolved all recent system notices. '
- . 'Please refresh the web page to clear the notice alert.',
+ 'text' => __(
+ 'You have viewed and resolved all recent system notices. '
+ . 'Please refresh the web page to clear the notice alert.'
+ )
];
}
$this->getResponse()->representJson($this->jsonHelper->jsonEncode($result));
diff --git a/app/code/Magento/AdminNotification/Test/Mftf/LICENSE.txt b/app/code/Magento/AdminNotification/Test/Mftf/LICENSE.txt
new file mode 100644
index 0000000000000..49525fd99da9c
--- /dev/null
+++ b/app/code/Magento/AdminNotification/Test/Mftf/LICENSE.txt
@@ -0,0 +1,48 @@
+
+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/AdminNotification/Test/Mftf/LICENSE_AFL.txt b/app/code/Magento/AdminNotification/Test/Mftf/LICENSE_AFL.txt
new file mode 100644
index 0000000000000..f39d641b18a19
--- /dev/null
+++ b/app/code/Magento/AdminNotification/Test/Mftf/LICENSE_AFL.txt
@@ -0,0 +1,48 @@
+
+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/AdminNotification/Test/Mftf/README.md b/app/code/Magento/AdminNotification/Test/Mftf/README.md
new file mode 100644
index 0000000000000..33f88ba74200a
--- /dev/null
+++ b/app/code/Magento/AdminNotification/Test/Mftf/README.md
@@ -0,0 +1,3 @@
+# Admin Notification Functional Tests
+
+The Functional Test Module for **Magento Admin Notification** module.
diff --git a/app/code/Magento/AdminNotification/Test/Unit/Model/System/Message/CacheOutdatedTest.php b/app/code/Magento/AdminNotification/Test/Unit/Model/System/Message/CacheOutdatedTest.php
index 2fbfc43aa8775..f49911c3e7a93 100644
--- a/app/code/Magento/AdminNotification/Test/Unit/Model/System/Message/CacheOutdatedTest.php
+++ b/app/code/Magento/AdminNotification/Test/Unit/Model/System/Message/CacheOutdatedTest.php
@@ -62,6 +62,9 @@ public function testGetIdentity($expectedSum, $cacheTypes)
$this->assertEquals($expectedSum, $this->_messageModel->getIdentity());
}
+ /**
+ * @return array
+ */
public function getIdentityDataProvider()
{
$cacheTypeMock1 = $this->createPartialMock(\stdClass::class, ['getCacheType']);
@@ -95,6 +98,9 @@ public function testIsDisplayed($expected, $allowed, $cacheTypes)
$this->assertEquals($expected, $this->_messageModel->isDisplayed());
}
+ /**
+ * @return array
+ */
public function isDisplayedDataProvider()
{
$cacheTypesMock = $this->createPartialMock(\stdClass::class, ['getCacheType']);
diff --git a/app/code/Magento/AdminNotification/Test/Unit/Model/System/Message/Media/Synchronization/ErrorTest.php b/app/code/Magento/AdminNotification/Test/Unit/Model/System/Message/Media/Synchronization/ErrorTest.php
index 2c259db868851..b490efd8e9683 100644
--- a/app/code/Magento/AdminNotification/Test/Unit/Model/System/Message/Media/Synchronization/ErrorTest.php
+++ b/app/code/Magento/AdminNotification/Test/Unit/Model/System/Message/Media/Synchronization/ErrorTest.php
@@ -72,6 +72,9 @@ public function testIsDisplayed($expectedFirstRun, $data)
$this->assertEquals($expectedFirstRun, $model->isDisplayed());
}
+ /**
+ * @return array
+ */
public function isDisplayedDataProvider()
{
return [
diff --git a/app/code/Magento/AdminNotification/Test/Unit/Model/System/Message/SecurityTest.php b/app/code/Magento/AdminNotification/Test/Unit/Model/System/Message/SecurityTest.php
index 1e71570a5e30b..c6f61fee862ba 100644
--- a/app/code/Magento/AdminNotification/Test/Unit/Model/System/Message/SecurityTest.php
+++ b/app/code/Magento/AdminNotification/Test/Unit/Model/System/Message/SecurityTest.php
@@ -76,6 +76,9 @@ public function testIsDisplayed($expectedResult, $cached, $response)
$this->assertEquals($expectedResult, $this->_messageModel->isDisplayed());
}
+ /**
+ * @return array
+ */
public function isDisplayedDataProvider()
{
return [
diff --git a/app/code/Magento/AdminNotification/composer.json b/app/code/Magento/AdminNotification/composer.json
index afb820a2e6c93..ae1b8dc7d14ff 100644
--- a/app/code/Magento/AdminNotification/composer.json
+++ b/app/code/Magento/AdminNotification/composer.json
@@ -2,16 +2,16 @@
"name": "magento/module-admin-notification",
"description": "N/A",
"require": {
- "php": "7.0.2|7.0.4|~7.0.6|~7.1.0",
+ "php": "~7.0.13|~7.1.0",
"magento/module-store": "100.2.*",
"magento/module-backend": "100.2.*",
"magento/module-media-storage": "100.2.*",
- "magento/framework": "100.2.*",
- "magento/module-ui": "100.2.*",
+ "magento/framework": "101.0.*",
+ "magento/module-ui": "101.0.*",
"lib-libxml": "*"
},
"type": "magento2-module",
- "version": "100.2.0-dev",
+ "version": "100.2.4",
"license": [
"OSL-3.0",
"AFL-3.0"
diff --git a/app/code/Magento/AdminNotification/etc/adminhtml/menu.xml b/app/code/Magento/AdminNotification/etc/adminhtml/menu.xml
index fbed5c0960b73..04d700b9f90ce 100644
--- a/app/code/Magento/AdminNotification/etc/adminhtml/menu.xml
+++ b/app/code/Magento/AdminNotification/etc/adminhtml/menu.xml
@@ -7,6 +7,6 @@
-->
-
+
diff --git a/app/code/Magento/AdminNotification/i18n/en_US.csv b/app/code/Magento/AdminNotification/i18n/en_US.csv
index 16c5abb9db0d2..db5a4c9254814 100644
--- a/app/code/Magento/AdminNotification/i18n/en_US.csv
+++ b/app/code/Magento/AdminNotification/i18n/en_US.csv
@@ -48,3 +48,4 @@ Severity,Severity
"Date Added","Date Added"
Message,Message
Actions,Actions
+"You have viewed and resolved all recent system notices. Please refresh the web page to clear the notice alert.","You have viewed and resolved all recent system notices. Please refresh the web page to clear the notice alert."
diff --git a/app/code/Magento/AdminNotification/view/adminhtml/layout/adminhtml_notification_block.xml b/app/code/Magento/AdminNotification/view/adminhtml/layout/adminhtml_notification_block.xml
index 7778c1dd5ca98..c68313211c2e6 100644
--- a/app/code/Magento/AdminNotification/view/adminhtml/layout/adminhtml_notification_block.xml
+++ b/app/code/Magento/AdminNotification/view/adminhtml/layout/adminhtml_notification_block.xml
@@ -20,14 +20,14 @@
0
-
+
Severity
severity
Magento\AdminNotification\Block\Grid\Renderer\Severity
-
+
Date Added
date_added
@@ -37,14 +37,14 @@
col-date
-
+
Message
title
Magento\AdminNotification\Block\Grid\Renderer\Notice
-
+
Actions
0
diff --git a/app/code/Magento/AdminNotification/view/adminhtml/templates/system/messages/popup.phtml b/app/code/Magento/AdminNotification/view/adminhtml/templates/system/messages/popup.phtml
index a97293547e132..0448daaf17644 100644
--- a/app/code/Magento/AdminNotification/view/adminhtml/templates/system/messages/popup.phtml
+++ b/app/code/Magento/AdminNotification/view/adminhtml/templates/system/messages/popup.phtml
@@ -19,20 +19,12 @@
-
+
\ No newline at end of file
diff --git a/app/code/Magento/AdminNotification/view/adminhtml/web/js/system/messages/popup.js b/app/code/Magento/AdminNotification/view/adminhtml/web/js/system/messages/popup.js
new file mode 100644
index 0000000000000..39c61d6e07d29
--- /dev/null
+++ b/app/code/Magento/AdminNotification/view/adminhtml/web/js/system/messages/popup.js
@@ -0,0 +1,26 @@
+/**
+ * Copyright © Magento, Inc. All rights reserved.
+ * See COPYING.txt for license details.
+ */
+
+define([
+ 'jquery',
+ 'Magento_Ui/js/modal/modal'
+], function ($, modal) {
+ 'use strict';
+
+ return function (data, element) {
+
+ if (modal.modal) {
+ modal.modal.html($(element).html());
+ } else {
+ modal.modal = $(element).modal({
+ modalClass: data.class,
+ type: 'popup',
+ buttons: []
+ });
+ }
+
+ modal.modal.modal('openModal');
+ };
+});
diff --git a/app/code/Magento/AdvancedPricingImportExport/Controller/Adminhtml/Export/GetFilter.php b/app/code/Magento/AdvancedPricingImportExport/Controller/Adminhtml/Export/GetFilter.php
index 02413a1899cd7..818bcda1da65f 100644
--- a/app/code/Magento/AdvancedPricingImportExport/Controller/Adminhtml/Export/GetFilter.php
+++ b/app/code/Magento/AdvancedPricingImportExport/Controller/Adminhtml/Export/GetFilter.php
@@ -37,10 +37,10 @@ public function execute()
);
return $resultLayout;
} catch (\Exception $e) {
- $this->messageManager->addError($e->getMessage());
+ $this->messageManager->addErrorMessage($e->getMessage());
}
} else {
- $this->messageManager->addError(__('Please correct the data sent.'));
+ $this->messageManager->addErrorMessage(__('Please correct the data sent.'));
}
/** @var \Magento\Backend\Model\View\Result\Redirect $resultRedirect */
$resultRedirect = $this->resultFactory->create(ResultFactory::TYPE_REDIRECT);
diff --git a/app/code/Magento/AdvancedPricingImportExport/Model/Export/AdvancedPricing.php b/app/code/Magento/AdvancedPricingImportExport/Model/Export/AdvancedPricing.php
index 7ddd5e3bb2a36..3122a0a7ee648 100644
--- a/app/code/Magento/AdvancedPricingImportExport/Model/Export/AdvancedPricing.php
+++ b/app/code/Magento/AdvancedPricingImportExport/Model/Export/AdvancedPricing.php
@@ -79,6 +79,11 @@ class AdvancedPricing extends \Magento\CatalogImportExport\Model\Export\Product
ImportAdvancedPricing::COL_TIER_PRICE_TYPE => ''
];
+ /**
+ * @var string[]
+ */
+ private $websiteCodesMap = [];
+
/**
* @param \Magento\Framework\Stdlib\DateTime\TimezoneInterface $localeDate
* @param \Magento\Eav\Model\Config $config
@@ -98,7 +103,6 @@ class AdvancedPricing extends \Magento\CatalogImportExport\Model\Export\Product
* @param \Magento\CatalogImportExport\Model\Export\RowCustomizerInterface $rowCustomizer
* @param ImportProduct\StoreResolver $storeResolver
* @param \Magento\Customer\Api\GroupRepositoryInterface $groupRepository
- * @throws \Magento\Framework\Exception\LocalizedException
* @SuppressWarnings(PHPMD.ExcessiveParameterList)
*/
public function __construct(
@@ -187,6 +191,7 @@ protected function initTypeModels()
* Export process
*
* @return string
+ * @throws \Magento\Framework\Exception\LocalizedException
*/
public function export()
{
@@ -213,6 +218,7 @@ public function export()
break;
}
}
+
return $writer->getContents();
}
@@ -255,70 +261,111 @@ public function filterAttributeCollection(\Magento\Eav\Model\ResourceModel\Entit
*/
protected function getExportData()
{
+ if ($this->_passTierPrice) {
+ return [];
+ }
+
$exportData = [];
try {
- $rawData = $this->collectRawData();
- $productIds = array_keys($rawData);
- if (isset($productIds)) {
- if (!$this->_passTierPrice) {
- $exportData = array_merge(
- $exportData,
- $this->getTierPrices($productIds, ImportAdvancedPricing::TABLE_TIER_PRICE)
- );
+ $productsByStores = $this->loadCollection();
+ if (!empty($productsByStores)) {
+ $productLinkIds = array_map(
+ function (array $productData) {
+ return $productData[Store::DEFAULT_STORE_ID][$this->getProductEntityLinkField()];
+ },
+ $productsByStores
+ );
+ $tierPricesData = $this->getTierPrices(
+ $productLinkIds,
+ ImportAdvancedPricing::TABLE_TIER_PRICE
+ );
+
+ $exportData = $this->correctExportData(
+ $productsByStores,
+ $tierPricesData
+ );
+ if (!empty($exportData)) {
+ asort($exportData);
}
}
- if ($exportData) {
- $exportData = $this->correctExportData($exportData);
- }
- if (isset($exportData)) {
- asort($exportData);
- }
} catch (\Exception $e) {
$this->_logger->critical($e);
}
+
return $exportData;
}
/**
- * Correct export data.
+ * @param array $tierPriceData Tier price information.
*
- * @param array $exportData
- * @return array
- * @SuppressWarnings(PHPMD.UnusedLocalVariable)
+ * @return array Formatted for export tier price information.
*/
- protected function correctExportData($exportData)
+ private function createExportRow(array $tierPriceData): array
{
- $customExportData = [];
- foreach ($exportData as $key => $row) {
- $exportRow = $this->templateExportData;
- foreach ($exportRow as $keyTemplate => $valueTemplate) {
- if (isset($row[$keyTemplate])) {
- if (in_array($keyTemplate, $this->_priceWebsite)) {
- $exportRow[$keyTemplate] = $this->_getWebsiteCode(
- $row[$keyTemplate]
- );
- } elseif (in_array($keyTemplate, $this->_priceCustomerGroup)) {
- $exportRow[$keyTemplate] = $this->_getCustomerGroupById(
- $row[$keyTemplate],
- isset($row[ImportAdvancedPricing::VALUE_ALL_GROUPS])
- ? $row[ImportAdvancedPricing::VALUE_ALL_GROUPS]
- : null
- );
- unset($exportRow[ImportAdvancedPricing::VALUE_ALL_GROUPS]);
- } elseif ($keyTemplate === ImportAdvancedPricing::COL_TIER_PRICE) {
- $exportRow[$keyTemplate] = $row[ImportAdvancedPricing::COL_TIER_PRICE_PERCENTAGE_VALUE]
- ? $row[ImportAdvancedPricing::COL_TIER_PRICE_PERCENTAGE_VALUE]
- : $row[ImportAdvancedPricing::COL_TIER_PRICE];
- $exportRow[ImportAdvancedPricing::COL_TIER_PRICE_TYPE]
- = $this->tierPriceTypeValue($row[ImportAdvancedPricing::COL_TIER_PRICE_PERCENTAGE_VALUE]);
- } else {
- $exportRow[$keyTemplate] = $row[$keyTemplate];
- }
+ $exportRow = $this->templateExportData;
+ foreach (array_keys($exportRow) as $keyTemplate) {
+ if (array_key_exists($keyTemplate, $tierPriceData)) {
+ if (in_array($keyTemplate, $this->_priceWebsite)) {
+ $exportRow[$keyTemplate] = $this->_getWebsiteCode(
+ $tierPriceData[$keyTemplate]
+ );
+ } elseif (in_array($keyTemplate, $this->_priceCustomerGroup)) {
+ $exportRow[$keyTemplate] = $this->_getCustomerGroupById(
+ $tierPriceData[$keyTemplate],
+ $tierPriceData[ImportAdvancedPricing::VALUE_ALL_GROUPS]
+ );
+ unset($exportRow[ImportAdvancedPricing::VALUE_ALL_GROUPS]);
+ } elseif ($keyTemplate
+ === ImportAdvancedPricing::COL_TIER_PRICE
+ ) {
+ $exportRow[$keyTemplate]
+ = $tierPriceData[ImportAdvancedPricing::COL_TIER_PRICE_PERCENTAGE_VALUE]
+ ? $tierPriceData[ImportAdvancedPricing::COL_TIER_PRICE_PERCENTAGE_VALUE]
+ : $tierPriceData[ImportAdvancedPricing::COL_TIER_PRICE];
+ $exportRow[ImportAdvancedPricing::COL_TIER_PRICE_TYPE]
+ = $this->tierPriceTypeValue($tierPriceData);
+ } else {
+ $exportRow[$keyTemplate] = $tierPriceData[$keyTemplate];
}
}
+ }
+
+ return $exportRow;
+ }
- $customExportData[$key] = $exportRow;
- unset($exportRow);
+ /**
+ * Correct export data.
+ *
+ * @param array $productsData
+ * @param array $tierPricesData
+ *
+ * @return array
+ * @SuppressWarnings(PHPMD.UnusedLocalVariable)
+ */
+ protected function correctExportData(
+ array $productsData,
+ array $tierPricesData
+ ): array {
+ //Assigning SKUs to tier prices data.
+ $productLinkIdToSkuMap = [];
+ foreach ($productsData as $productData) {
+ $productLinkIdToSkuMap[$productData[Store::DEFAULT_STORE_ID][$this->getProductEntityLinkField()]]
+ = $productData[Store::DEFAULT_STORE_ID]['sku'];
+ }
+ unset($productData);
+ $linkedTierPricesData = [];
+ foreach ($tierPricesData as $tierPriceData) {
+ $sku = $productLinkIdToSkuMap[$tierPriceData['product_link_id']];
+ $linkedTierPricesData[] = array_merge(
+ $tierPriceData,
+ [ImportAdvancedPricing::COL_SKU => $sku]
+ );
+ }
+ unset($sku, $tierPriceData);
+
+ $customExportData = [];
+ foreach ($linkedTierPricesData as $row) {
+ $customExportData[] = $this->createExportRow($row);
}
return $customExportData;
@@ -327,12 +374,13 @@ protected function correctExportData($exportData)
/**
* Check type for tier price.
*
- * @param string $tierPricePercentage
+ * @param array $tierPriceData
+ *
* @return string
*/
- private function tierPriceTypeValue($tierPricePercentage)
+ private function tierPriceTypeValue(array $tierPriceData): string
{
- return $tierPricePercentage
+ return $tierPriceData[ImportAdvancedPricing::COL_TIER_PRICE_PERCENTAGE_VALUE]
? ImportAdvancedPricing::TIER_PRICE_TYPE_PERCENT
: ImportAdvancedPricing::TIER_PRICE_TYPE_FIXED;
}
@@ -340,54 +388,52 @@ private function tierPriceTypeValue($tierPricePercentage)
/**
* Get tier prices.
*
- * @param array $listSku
+ * @param string[] $productLinksIds
* @param string $table
* @return array|bool
* @SuppressWarnings(PHPMD.NPathComplexity)
* @SuppressWarnings(PHPMD.CyclomaticComplexity)
*/
- protected function getTierPrices(array $listSku, $table)
+ protected function getTierPrices(array $productLinksIds, $table)
{
+ $exportFilter = null;
+ $price = null;
if (isset($this->_parameters[\Magento\ImportExport\Model\Export::FILTER_ELEMENT_GROUP])) {
$exportFilter = $this->_parameters[\Magento\ImportExport\Model\Export::FILTER_ELEMENT_GROUP];
}
+ $productEntityLinkField = $this->getProductEntityLinkField();
+
if ($table == ImportAdvancedPricing::TABLE_TIER_PRICE) {
$selectFields = [
- ImportAdvancedPricing::COL_SKU => 'cpe.sku',
- ImportAdvancedPricing::COL_TIER_PRICE_WEBSITE => 'ap.website_id',
- ImportAdvancedPricing::VALUE_ALL_GROUPS => 'ap.all_groups',
- ImportAdvancedPricing::COL_TIER_PRICE_CUSTOMER_GROUP => 'ap.customer_group_id',
- ImportAdvancedPricing::COL_TIER_PRICE_QTY => 'ap.qty',
- ImportAdvancedPricing::COL_TIER_PRICE => 'ap.value',
+ ImportAdvancedPricing::COL_TIER_PRICE_WEBSITE => 'ap.website_id',
+ ImportAdvancedPricing::VALUE_ALL_GROUPS => 'ap.all_groups',
+ ImportAdvancedPricing::COL_TIER_PRICE_CUSTOMER_GROUP => 'ap.customer_group_id',
+ ImportAdvancedPricing::COL_TIER_PRICE_QTY => 'ap.qty',
+ ImportAdvancedPricing::COL_TIER_PRICE => 'ap.value',
ImportAdvancedPricing::COL_TIER_PRICE_PERCENTAGE_VALUE => 'ap.percentage_value',
+ 'product_link_id' => 'ap.'
+ .$productEntityLinkField,
];
- if (isset($exportFilter) && !empty($exportFilter)) {
- $price = $exportFilter['tier_price'];
- }
- }
- if ($listSku) {
- if (isset($exportFilter) && !empty($exportFilter)) {
- $date = $exportFilter[\Magento\Catalog\Model\Category::KEY_UPDATED_AT];
- if (isset($date[0]) && !empty($date[0])) {
- $updatedAtFrom = $this->_localeDate->date($date[0], null, false)->format('Y-m-d H:i:s');
- }
- if (isset($date[1]) && !empty($date[1])) {
- $updatedAtTo = $this->_localeDate->date($date[1], null, false)->format('Y-m-d H:i:s');
+ if ($exportFilter) {
+ if (array_key_exists('tier_price', $exportFilter)) {
+ $price = $exportFilter['tier_price'];
}
}
+ } else {
+ throw new \InvalidArgumentException('Proper table name needed');
+ }
+
+ if ($productLinksIds) {
try {
- $productEntityLinkField = $this->getProductEntityLinkField();
$select = $this->_connection->select()
->from(
- ['cpe' => $this->_resource->getTableName('catalog_product_entity')],
- $selectFields
- )
- ->joinInner(
['ap' => $this->_resource->getTableName($table)],
- 'ap.' . $productEntityLinkField . ' = cpe.' . $productEntityLinkField,
- []
+ $selectFields
)
- ->where('cpe.entity_id IN (?)', $listSku);
+ ->where(
+ 'ap.'.$productEntityLinkField.' IN (?)',
+ $productLinksIds
+ );
if (isset($price[0]) && !empty($price[0])) {
$select->where('ap.value >= ?', $price[0]);
@@ -398,18 +444,16 @@ protected function getTierPrices(array $listSku, $table)
if (isset($price[0]) && !empty($price[0]) || isset($price[1]) && !empty($price[1])) {
$select->orWhere('ap.percentage_value IS NOT NULL');
}
- if (isset($updatedAtFrom) && !empty($updatedAtFrom)) {
- $select->where('cpe.updated_at >= ?', $updatedAtFrom);
- }
- if (isset($updatedAtTo) && !empty($updatedAtTo)) {
- $select->where('cpe.updated_at <= ?', $updatedAtTo);
- }
+
$exportData = $this->_connection->fetchAll($select);
} catch (\Exception $e) {
return false;
}
+
+ return $exportData;
+ } else {
+ return false;
}
- return $exportData;
}
/**
@@ -417,36 +461,50 @@ protected function getTierPrices(array $listSku, $table)
*
* @param int $websiteId
* @return string
+ * @throws \Magento\Framework\Exception\LocalizedException
*/
- protected function _getWebsiteCode($websiteId)
+ protected function _getWebsiteCode(int $websiteId): string
{
- $storeName = ($websiteId == 0)
- ? ImportAdvancedPricing::VALUE_ALL_WEBSITES
- : $this->_storeManager->getWebsite($websiteId)->getCode();
- $currencyCode = '';
- if ($websiteId == 0) {
- $currencyCode = $this->_storeManager->getWebsite($websiteId)->getBaseCurrencyCode();
- }
- if ($storeName && $currencyCode) {
- return $storeName . ' [' . $currencyCode . ']';
- } else {
- return $storeName;
+ if (!array_key_exists($websiteId, $this->websiteCodesMap)) {
+ $storeName = ($websiteId == 0)
+ ? ImportAdvancedPricing::VALUE_ALL_WEBSITES
+ : $this->_storeManager->getWebsite($websiteId)->getCode();
+ $currencyCode = '';
+ if ($websiteId == 0) {
+ $currencyCode = $this->_storeManager->getWebsite($websiteId)
+ ->getBaseCurrencyCode();
+ }
+
+ if ($storeName && $currencyCode) {
+ $code = $storeName.' ['.$currencyCode.']';
+ } else {
+ $code = $storeName;
+ }
+ $this->websiteCodesMap[$websiteId] = $code;
}
+
+ return $this->websiteCodesMap[$websiteId];
}
/**
* Get Customer Group By Id
*
* @param int $customerGroupId
- * @param null $allGroups
+ * @param int $allGroups
* @return string
+ * @throws \Magento\Framework\Exception\LocalizedException
+ * @throws \Magento\Framework\Exception\NoSuchEntityException
*/
- protected function _getCustomerGroupById($customerGroupId, $allGroups = null)
- {
- if ($allGroups) {
+ protected function _getCustomerGroupById(
+ int $customerGroupId,
+ int $allGroups = 0
+ ): string {
+ if ($allGroups !== 0) {
return ImportAdvancedPricing::VALUE_ALL_GROUPS;
} else {
- return $this->_groupRepository->getById($customerGroupId)->getCode();
+ return $this->_groupRepository
+ ->getById($customerGroupId)
+ ->getCode();
}
}
diff --git a/app/code/Magento/AdvancedPricingImportExport/Model/Import/AdvancedPricing.php b/app/code/Magento/AdvancedPricingImportExport/Model/Import/AdvancedPricing.php
index 23829d3725119..754f5fd6c8c20 100644
--- a/app/code/Magento/AdvancedPricingImportExport/Model/Import/AdvancedPricing.php
+++ b/app/code/Magento/AdvancedPricingImportExport/Model/Import/AdvancedPricing.php
@@ -8,7 +8,6 @@
use Magento\CatalogImportExport\Model\Import\Product as ImportProduct;
use Magento\CatalogImportExport\Model\Import\Product\RowValidatorInterface as ValidatorInterface;
use Magento\ImportExport\Model\Import\ErrorProcessing\ProcessingErrorAggregatorInterface;
-use Magento\Framework\App\ResourceConnection;
/**
* Class AdvancedPricing
@@ -394,7 +393,7 @@ protected function saveAndReplaceAdvancedPrices()
? $rowData[self::COL_TIER_PRICE] : 0,
'percentage_value' => $rowData[self::COL_TIER_PRICE_TYPE] === self::TIER_PRICE_TYPE_PERCENT
? $rowData[self::COL_TIER_PRICE] : null,
- 'website_id' => $this->getWebsiteId($rowData[self::COL_TIER_PRICE_WEBSITE])
+ 'website_id' => $this->getWebSiteId($rowData[self::COL_TIER_PRICE_WEBSITE])
];
}
}
@@ -619,6 +618,7 @@ protected function processCountNewPrices(array $tierPrices)
* Get product entity link field
*
* @return string
+ * @throws \Exception
*/
private function getProductEntityLinkField()
{
diff --git a/app/code/Magento/AdvancedPricingImportExport/Model/Import/AdvancedPricing/Validator.php b/app/code/Magento/AdvancedPricingImportExport/Model/Import/AdvancedPricing/Validator.php
index 25a9fc244fe51..d939a3f7c392e 100644
--- a/app/code/Magento/AdvancedPricingImportExport/Model/Import/AdvancedPricing/Validator.php
+++ b/app/code/Magento/AdvancedPricingImportExport/Model/Import/AdvancedPricing/Validator.php
@@ -28,6 +28,7 @@ public function __construct($validators = [])
*
* @param array $value
* @return bool
+ * @throws \Zend_Validate_Exception
*/
public function isValid($value)
{
diff --git a/app/code/Magento/AdvancedPricingImportExport/Test/Mftf/LICENSE.txt b/app/code/Magento/AdvancedPricingImportExport/Test/Mftf/LICENSE.txt
new file mode 100644
index 0000000000000..2b7359b7dfcb4
--- /dev/null
+++ b/app/code/Magento/AdvancedPricingImportExport/Test/Mftf/LICENSE.txt
@@ -0,0 +1,48 @@
+
+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/AdvancedPricingImportExport/Test/Mftf/LICENSE_AFL.txt b/app/code/Magento/AdvancedPricingImportExport/Test/Mftf/LICENSE_AFL.txt
new file mode 100644
index 0000000000000..f39d641b18a19
--- /dev/null
+++ b/app/code/Magento/AdvancedPricingImportExport/Test/Mftf/LICENSE_AFL.txt
@@ -0,0 +1,48 @@
+
+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/AdvancedPricingImportExport/Test/Mftf/README.md b/app/code/Magento/AdvancedPricingImportExport/Test/Mftf/README.md
new file mode 100644
index 0000000000000..7b4d0f3f0b12b
--- /dev/null
+++ b/app/code/Magento/AdvancedPricingImportExport/Test/Mftf/README.md
@@ -0,0 +1,3 @@
+# Advanced Pricing Import Export Functional Tests
+
+The Functional Test Module for **Magento Advanced Pricing Import Export** module.
diff --git a/app/code/Magento/AdvancedPricingImportExport/Test/Unit/Model/Export/AdvancedPricingTest.php b/app/code/Magento/AdvancedPricingImportExport/Test/Unit/Model/Export/AdvancedPricingTest.php
index 48b4c58918740..c927aad6ac714 100644
--- a/app/code/Magento/AdvancedPricingImportExport/Test/Unit/Model/Export/AdvancedPricingTest.php
+++ b/app/code/Magento/AdvancedPricingImportExport/Test/Unit/Model/Export/AdvancedPricingTest.php
@@ -213,7 +213,7 @@ protected function setUp()
'_getCustomerGroupById',
'correctExportData'
]);
- $this->advancedPricing = $this->getMockbuilder(
+ $this->advancedPricing = $this->getMockBuilder(
\Magento\AdvancedPricingImportExport\Model\Export\AdvancedPricing::class
)
->setMethods($mockMethods)
@@ -347,6 +347,7 @@ protected function tearDown()
* @param $object
* @param $property
* @return mixed
+ * @throws \ReflectionException
*/
protected function getPropertyValue($object, $property)
{
@@ -362,6 +363,8 @@ protected function getPropertyValue($object, $property)
* @param $object
* @param $property
* @param $value
+ * @return mixed
+ * @throws \ReflectionException
*/
protected function setPropertyValue(&$object, $property, $value)
{
diff --git a/app/code/Magento/AdvancedPricingImportExport/Test/Unit/Model/Import/AdvancedPricing/Validator/TierPriceTest.php b/app/code/Magento/AdvancedPricingImportExport/Test/Unit/Model/Import/AdvancedPricing/Validator/TierPriceTest.php
index bb64acb558320..2c930237da831 100644
--- a/app/code/Magento/AdvancedPricingImportExport/Test/Unit/Model/Import/AdvancedPricing/Validator/TierPriceTest.php
+++ b/app/code/Magento/AdvancedPricingImportExport/Test/Unit/Model/Import/AdvancedPricing/Validator/TierPriceTest.php
@@ -181,6 +181,9 @@ public function testIsValidAddMessagesCall($value, $hasEmptyColumns, $customerGr
$this->tierPrice->isValid($value);
}
+ /**
+ * @return array
+ */
public function isValidResultFalseDataProvider()
{
return [
@@ -286,6 +289,9 @@ public function isValidResultFalseDataProvider()
];
}
+ /**
+ * @return array
+ */
public function isValidAddMessagesCallDataProvider()
{
return [
@@ -340,6 +346,7 @@ public function isValidAddMessagesCallDataProvider()
* @param object $object
* @param string $property
* @return mixed
+ * @throws \ReflectionException
*/
protected function getPropertyValue($object, $property)
{
@@ -357,6 +364,7 @@ protected function getPropertyValue($object, $property)
* @param string $property
* @param mixed $value
* @return object
+ * @throws \ReflectionException
*/
protected function setPropertyValue(&$object, $property, $value)
{
diff --git a/app/code/Magento/AdvancedPricingImportExport/Test/Unit/Model/Import/AdvancedPricing/Validator/WebsiteTest.php b/app/code/Magento/AdvancedPricingImportExport/Test/Unit/Model/Import/AdvancedPricing/Validator/WebsiteTest.php
index 5111b4932d7a8..b46e286e75007 100644
--- a/app/code/Magento/AdvancedPricingImportExport/Test/Unit/Model/Import/AdvancedPricing/Validator/WebsiteTest.php
+++ b/app/code/Magento/AdvancedPricingImportExport/Test/Unit/Model/Import/AdvancedPricing/Validator/WebsiteTest.php
@@ -27,7 +27,7 @@ class WebsiteTest extends \PHPUnit\Framework\TestCase
protected function setUp()
{
- $this->webSiteModel = $this->getMockBuilder(\Magento\Store\Model\WebSite::class)
+ $this->webSiteModel = $this->getMockBuilder(\Magento\Store\Model\Website::class)
->setMethods(['getBaseCurrency'])
->disableOriginalConstructor()
->getMock();
@@ -114,6 +114,9 @@ public function testGetAllWebsitesValue()
$this->assertEquals($expectedResult, $result);
}
+ /**
+ * @return array
+ */
public function isValidReturnDataProvider()
{
return [
diff --git a/app/code/Magento/AdvancedPricingImportExport/Test/Unit/Model/Import/AdvancedPricing/ValidatorTest.php b/app/code/Magento/AdvancedPricingImportExport/Test/Unit/Model/Import/AdvancedPricing/ValidatorTest.php
index d9fce98826105..5ca534284a48d 100644
--- a/app/code/Magento/AdvancedPricingImportExport/Test/Unit/Model/Import/AdvancedPricing/ValidatorTest.php
+++ b/app/code/Magento/AdvancedPricingImportExport/Test/Unit/Model/Import/AdvancedPricing/ValidatorTest.php
@@ -77,6 +77,9 @@ public function testInit()
$this->validator->init(null);
}
+ /**
+ * @return array
+ */
public function isValidDataProvider()
{
return [
diff --git a/app/code/Magento/AdvancedPricingImportExport/Test/Unit/Model/Import/AdvancedPricingTest.php b/app/code/Magento/AdvancedPricingImportExport/Test/Unit/Model/Import/AdvancedPricingTest.php
index 6d130d93ee6a5..15a3ae3a22d4a 100644
--- a/app/code/Magento/AdvancedPricingImportExport/Test/Unit/Model/Import/AdvancedPricingTest.php
+++ b/app/code/Magento/AdvancedPricingImportExport/Test/Unit/Model/Import/AdvancedPricingTest.php
@@ -209,6 +209,10 @@ public function testGetEntityTypeCode()
* Test method validateRow against its result.
*
* @dataProvider validateRowResultDataProvider
+ * @param array $rowData
+ * @param string|null $behavior
+ * @param bool $expectedResult
+ * @throws \ReflectionException
*/
public function testValidateRowResult($rowData, $behavior, $expectedResult)
{
@@ -234,6 +238,10 @@ public function testValidateRowResult($rowData, $behavior, $expectedResult)
* Test method validateRow whether AddRowError is called.
*
* @dataProvider validateRowAddRowErrorCallDataProvider
+ * @param array $rowData
+ * @param string|null $behavior
+ * @param string $error
+ * @throws \ReflectionException
*/
public function testValidateRowAddRowErrorCall($rowData, $behavior, $error)
{
@@ -324,6 +332,13 @@ public function testSaveAdvancedPricing()
* Take into consideration different data and check relative internal calls.
*
* @dataProvider saveAndReplaceAdvancedPricesAppendBehaviourDataProvider
+ * @param array $data
+ * @param string $tierCustomerGroupId
+ * @param string $groupCustomerGroupId
+ * @param string $tierWebsiteId
+ * @param string $groupWebsiteId
+ * @param array $expectedTierPrices
+ * @throws \ReflectionException
*/
public function testSaveAndReplaceAdvancedPricesAppendBehaviourDataAndCalls(
$data,
@@ -768,6 +783,9 @@ public function testSaveProductPrices($priceData, $oldSkus, $priceIn, $callNum)
$this->invokeMethod($this->advancedPricing, 'saveProductPrices', [$priceData, 'table']);
}
+ /**
+ * @return array
+ */
public function saveProductPricesDataProvider()
{
return [
@@ -839,6 +857,9 @@ public function testDeleteProductTierPrices(
);
}
+ /**
+ * @return array
+ */
public function deleteProductTierPricesDataProvider()
{
return [
@@ -921,6 +942,9 @@ public function testProcessCountExistingPrices(
$this->invokeMethod($this->advancedPricing, 'processCountExistingPrices', [$prices, 'table']);
}
+ /**
+ * @return array
+ */
public function processCountExistingPricesDataProvider()
{
return [
@@ -947,6 +971,7 @@ public function processCountExistingPricesDataProvider()
* @param $object
* @param $property
* @return mixed
+ * @throws \ReflectionException
*/
protected function getPropertyValue($object, $property)
{
@@ -963,6 +988,8 @@ protected function getPropertyValue($object, $property)
* @param $object
* @param $property
* @param $value
+ * @return mixed
+ * @throws \ReflectionException
*/
protected function setPropertyValue(&$object, $property, $value)
{
@@ -981,7 +1008,8 @@ protected function setPropertyValue(&$object, $property, $value)
* @param string $method
* @param array $args
*
- * @return mixed the method result.
+ * @return mixed
+ * @throws \ReflectionException
*/
private function invokeMethod($object, $method, $args = [])
{
@@ -998,6 +1026,7 @@ private function invokeMethod($object, $method, $args = [])
* @param array $methods
*
* @return \PHPUnit_Framework_MockObject_MockObject
+ * @throws \ReflectionException
*/
private function getAdvancedPricingMock($methods = [])
{
diff --git a/app/code/Magento/AdvancedPricingImportExport/composer.json b/app/code/Magento/AdvancedPricingImportExport/composer.json
index 228464ecd6304..458827b9ab18a 100644
--- a/app/code/Magento/AdvancedPricingImportExport/composer.json
+++ b/app/code/Magento/AdvancedPricingImportExport/composer.json
@@ -2,18 +2,18 @@
"name": "magento/module-advanced-pricing-import-export",
"description": "N/A",
"require": {
- "php": "7.0.2|7.0.4|~7.0.6|~7.1.0",
- "magento/module-catalog": "101.1.*",
+ "php": "~7.0.13|~7.1.0",
+ "magento/module-catalog": "102.0.*",
"magento/module-catalog-inventory": "100.2.*",
- "magento/module-eav": "100.2.*",
+ "magento/module-eav": "101.0.*",
"magento/module-import-export": "100.2.*",
"magento/module-catalog-import-export": "100.2.*",
- "magento/module-customer": "100.2.*",
+ "magento/module-customer": "101.0.*",
"magento/module-store": "100.2.*",
- "magento/framework": "100.2.*"
+ "magento/framework": "101.0.*"
},
"type": "magento2-module",
- "version": "100.2.0-dev",
+ "version": "100.2.4",
"license": [
"OSL-3.0",
"AFL-3.0"
diff --git a/app/code/Magento/Analytics/Api/Data/LinkInterface.php b/app/code/Magento/Analytics/Api/Data/LinkInterface.php
new file mode 100644
index 0000000000000..6597dff868b9f
--- /dev/null
+++ b/app/code/Magento/Analytics/Api/Data/LinkInterface.php
@@ -0,0 +1,24 @@
+' . $element->getLabel() . '';
+ $html .= '';
+ return $this->decorateRowHtml($element, $html);
+ }
+
+ /**
+ * @param \Magento\Framework\Data\Form\Element\AbstractElement $element
+ * @param string $html
+ * @return string
+ */
+ private function decorateRowHtml(\Magento\Framework\Data\Form\Element\AbstractElement $element, $html)
+ {
+ return sprintf(
+ ' ',
+ $element->getHtmlId(),
+ $html
+ );
+ }
+}
diff --git a/app/code/Magento/Analytics/Block/Adminhtml/System/Config/CollectionTimeLabel.php b/app/code/Magento/Analytics/Block/Adminhtml/System/Config/CollectionTimeLabel.php
new file mode 100644
index 0000000000000..34f2b7d53d9be
--- /dev/null
+++ b/app/code/Magento/Analytics/Block/Adminhtml/System/Config/CollectionTimeLabel.php
@@ -0,0 +1,53 @@
+localeResolver = $localeResolver ?:
+ ObjectManager::getInstance()->get(\Magento\Framework\Locale\ResolverInterface::class);
+ parent::__construct($context, $data);
+ }
+
+ /**
+ * Add current time zone to comment, properly translated according to locale
+ *
+ * @param \Magento\Framework\Data\Form\Element\AbstractElement $element
+ * @return string
+ */
+ public function render(\Magento\Framework\Data\Form\Element\AbstractElement $element)
+ {
+ $timeZoneCode = $this->_localeDate->getConfigTimezone();
+ $locale = $this->localeResolver->getLocale();
+ $getLongTimeZoneName = \IntlTimeZone::createTimeZone($timeZoneCode)
+ ->getDisplayName(false, \IntlTimeZone::DISPLAY_LONG, $locale);
+ $element->setData(
+ 'comment',
+ sprintf("%s (%s)", $getLongTimeZoneName, $timeZoneCode)
+ );
+ return parent::render($element);
+ }
+}
diff --git a/app/code/Magento/Analytics/Block/Adminhtml/System/Config/SubscriptionStatusLabel.php b/app/code/Magento/Analytics/Block/Adminhtml/System/Config/SubscriptionStatusLabel.php
new file mode 100644
index 0000000000000..c09213c7f009d
--- /dev/null
+++ b/app/code/Magento/Analytics/Block/Adminhtml/System/Config/SubscriptionStatusLabel.php
@@ -0,0 +1,64 @@
+subscriptionStatusProvider = $labelStatusProvider;
+ }
+
+ /**
+ * Add Subscription status to comment
+ *
+ * @param \Magento\Framework\Data\Form\Element\AbstractElement $element
+ * @return string
+ */
+ public function render(\Magento\Framework\Data\Form\Element\AbstractElement $element)
+ {
+ $element->setData(
+ 'comment',
+ $this->prepareLabelValue()
+ );
+ return parent::render($element);
+ }
+
+ /**
+ * Prepare label for subscription status
+ *
+ * @return string
+ */
+ private function prepareLabelValue()
+ {
+ return __('Subscription status') . ': ' . __($this->subscriptionStatusProvider->getStatus());
+ }
+}
diff --git a/app/code/Magento/Analytics/Block/Adminhtml/System/Config/Vertical.php b/app/code/Magento/Analytics/Block/Adminhtml/System/Config/Vertical.php
new file mode 100644
index 0000000000000..99606e10f99d9
--- /dev/null
+++ b/app/code/Magento/Analytics/Block/Adminhtml/System/Config/Vertical.php
@@ -0,0 +1,41 @@
+' . $element->getHint() . '';
+ $html .= '';
+ return $this->decorateRowHtml($element, $html);
+ }
+
+ /**
+ * Decorates row HTML for custom element style
+ *
+ * @param \Magento\Framework\Data\Form\Element\AbstractElement $element
+ * @param string $html
+ * @return string
+ */
+ private function decorateRowHtml(\Magento\Framework\Data\Form\Element\AbstractElement $element, $html)
+ {
+ $rowHtml = sprintf('%s ', $html);
+ $rowHtml .= sprintf(
+ '%s %s ',
+ $element->getHtmlId(),
+ $element->getLabelHtml($element->getHtmlId(), "[WEBSITE]"),
+ $element->getElementHtml()
+ );
+ return $rowHtml;
+ }
+}
diff --git a/app/code/Magento/Analytics/Controller/Adminhtml/BIEssentials/SignUp.php b/app/code/Magento/Analytics/Controller/Adminhtml/BIEssentials/SignUp.php
new file mode 100644
index 0000000000000..a90a971cf41b4
--- /dev/null
+++ b/app/code/Magento/Analytics/Controller/Adminhtml/BIEssentials/SignUp.php
@@ -0,0 +1,64 @@
+config = $config;
+ parent::__construct($context);
+ }
+
+ /**
+ * Check admin permissions for this controller
+ *
+ * @return boolean
+ */
+ protected function _isAllowed()
+ {
+ return $this->_authorization->isAllowed('Magento_Analytics::bi_essentials');
+ }
+
+ /**
+ * Provides link to BI Essentials signup
+ *
+ * @return \Magento\Framework\Controller\AbstractResult
+ */
+ public function execute()
+ {
+ return $this->resultRedirectFactory->create()->setUrl(
+ $this->config->getValue($this->urlBIEssentialsConfigPath)
+ );
+ }
+}
diff --git a/app/code/Magento/Analytics/Controller/Adminhtml/Reports/Show.php b/app/code/Magento/Analytics/Controller/Adminhtml/Reports/Show.php
new file mode 100644
index 0000000000000..1b0e5c92420de
--- /dev/null
+++ b/app/code/Magento/Analytics/Controller/Adminhtml/Reports/Show.php
@@ -0,0 +1,75 @@
+reportUrlProvider = $reportUrlProvider;
+ parent::__construct($context);
+ }
+
+ /**
+ * Check admin permissions for this controller.
+ *
+ * @return boolean
+ */
+ protected function _isAllowed()
+ {
+ return $this->_authorization->isAllowed('Magento_Analytics::analytics_settings');
+ }
+
+ /**
+ * Redirect to resource with reports.
+ *
+ * @return Redirect $resultRedirect
+ */
+ public function execute()
+ {
+ /** @var Redirect $resultRedirect */
+ $resultRedirect = $this->resultFactory->create(ResultFactory::TYPE_REDIRECT);
+ try {
+ $resultRedirect->setUrl($this->reportUrlProvider->getUrl());
+ } catch (SubscriptionUpdateException $e) {
+ $this->getMessageManager()->addNoticeMessage($e->getMessage());
+ $resultRedirect->setPath('adminhtml');
+ } catch (LocalizedException $e) {
+ $this->getMessageManager()->addExceptionMessage($e, $e->getMessage());
+ $resultRedirect->setPath('adminhtml');
+ } catch (\Exception $e) {
+ $this->getMessageManager()->addExceptionMessage(
+ $e,
+ __('Sorry, there has been an error processing your request. Please try again later.')
+ );
+ $resultRedirect->setPath('adminhtml');
+ }
+
+ return $resultRedirect;
+ }
+}
diff --git a/app/code/Magento/Analytics/Controller/Adminhtml/Subscription/Retry.php b/app/code/Magento/Analytics/Controller/Adminhtml/Subscription/Retry.php
new file mode 100644
index 0000000000000..122cf74123cc9
--- /dev/null
+++ b/app/code/Magento/Analytics/Controller/Adminhtml/Subscription/Retry.php
@@ -0,0 +1,73 @@
+subscriptionHandler = $subscriptionHandler;
+ parent::__construct($context);
+ }
+
+ /**
+ * Check admin permissions for this controller
+ *
+ * @return boolean
+ */
+ protected function _isAllowed()
+ {
+ return $this->_authorization->isAllowed('Magento_Analytics::analytics_settings');
+ }
+
+ /**
+ * Retry process of subscription.
+ *
+ * @return Redirect
+ */
+ public function execute()
+ {
+ /** @var Redirect $resultRedirect */
+ $resultRedirect = $this->resultFactory->create(ResultFactory::TYPE_REDIRECT);
+ try {
+ $resultRedirect->setPath('adminhtml');
+ $this->subscriptionHandler->processEnabled();
+ } catch (LocalizedException $e) {
+ $this->getMessageManager()->addExceptionMessage($e, $e->getMessage());
+ } catch (\Exception $e) {
+ $this->getMessageManager()->addExceptionMessage(
+ $e,
+ __('Sorry, there has been an error processing your request. Please try again later.')
+ );
+ }
+
+ return $resultRedirect;
+ }
+}
diff --git a/app/code/Magento/Analytics/Cron/CollectData.php b/app/code/Magento/Analytics/Cron/CollectData.php
new file mode 100644
index 0000000000000..ff0b3e4f67638
--- /dev/null
+++ b/app/code/Magento/Analytics/Cron/CollectData.php
@@ -0,0 +1,53 @@
+exportDataHandler = $exportDataHandler;
+ $this->subscriptionStatus = $subscriptionStatus;
+ }
+
+ /**
+ * @return bool
+ */
+ public function execute()
+ {
+ if ($this->subscriptionStatus->getStatus() === SubscriptionStatusProvider::ENABLED) {
+ $this->exportDataHandler->prepareExportData();
+ }
+
+ return true;
+ }
+}
diff --git a/app/code/Magento/Analytics/Cron/SignUp.php b/app/code/Magento/Analytics/Cron/SignUp.php
new file mode 100644
index 0000000000000..c17b9b8c381c3
--- /dev/null
+++ b/app/code/Magento/Analytics/Cron/SignUp.php
@@ -0,0 +1,101 @@
+connector = $connector;
+ $this->configWriter = $configWriter;
+ $this->flagManager = $flagManager;
+ $this->reinitableConfig = $reinitableConfig;
+ }
+
+ /**
+ * Execute scheduled subscription operation
+ * In case of failure writes message to notifications inbox
+ *
+ * @return bool
+ */
+ public function execute()
+ {
+ $attemptsCount = $this->flagManager->getFlagData(SubscriptionHandler::ATTEMPTS_REVERSE_COUNTER_FLAG_CODE);
+
+ if (($attemptsCount === null) || ($attemptsCount <= 0)) {
+ $this->deleteAnalyticsCronExpr();
+ $this->flagManager->deleteFlag(SubscriptionHandler::ATTEMPTS_REVERSE_COUNTER_FLAG_CODE);
+ return false;
+ }
+
+ $attemptsCount -= 1;
+ $this->flagManager->saveFlag(SubscriptionHandler::ATTEMPTS_REVERSE_COUNTER_FLAG_CODE, $attemptsCount);
+ $signUpResult = $this->connector->execute('signUp');
+ if ($signUpResult === false) {
+ return false;
+ }
+
+ $this->deleteAnalyticsCronExpr();
+ $this->flagManager->deleteFlag(SubscriptionHandler::ATTEMPTS_REVERSE_COUNTER_FLAG_CODE);
+ return true;
+ }
+
+ /**
+ * Delete cron schedule setting into config.
+ *
+ * Delete cron schedule setting for subscription handler into config and
+ * re-initialize config cache to avoid auto-generate new schedule items.
+ *
+ * @return bool
+ */
+ private function deleteAnalyticsCronExpr()
+ {
+ $this->configWriter->delete(SubscriptionHandler::CRON_STRING_PATH);
+ $this->reinitableConfig->reinit();
+ return true;
+ }
+}
diff --git a/app/code/Magento/Analytics/Cron/Update.php b/app/code/Magento/Analytics/Cron/Update.php
new file mode 100644
index 0000000000000..9062a7bac7551
--- /dev/null
+++ b/app/code/Magento/Analytics/Cron/Update.php
@@ -0,0 +1,92 @@
+connector = $connector;
+ $this->configWriter = $configWriter;
+ $this->reinitableConfig = $reinitableConfig;
+ $this->flagManager = $flagManager;
+ $this->analyticsToken = $analyticsToken;
+ }
+
+ /**
+ * Execute scheduled update operation
+ *
+ * @return bool
+ */
+ public function execute()
+ {
+ $result = false;
+ $attemptsCount = $this->flagManager
+ ->getFlagData(SubscriptionUpdateHandler::SUBSCRIPTION_UPDATE_REVERSE_COUNTER_FLAG_CODE);
+
+ if ($attemptsCount) {
+ $attemptsCount -= 1;
+ $result = $this->connector->execute('update');
+ }
+
+ if ($result || ($attemptsCount <= 0) || (!$this->analyticsToken->isTokenExist())) {
+ $this->flagManager
+ ->deleteFlag(SubscriptionUpdateHandler::SUBSCRIPTION_UPDATE_REVERSE_COUNTER_FLAG_CODE);
+ $this->flagManager->deleteFlag(SubscriptionUpdateHandler::PREVIOUS_BASE_URL_FLAG_CODE);
+ $this->configWriter->delete(SubscriptionUpdateHandler::UPDATE_CRON_STRING_PATH);
+ $this->reinitableConfig->reinit();
+ }
+
+ return $result;
+ }
+}
diff --git a/app/code/Magento/Analytics/LICENSE.txt b/app/code/Magento/Analytics/LICENSE.txt
new file mode 100644
index 0000000000000..49525fd99da9c
--- /dev/null
+++ b/app/code/Magento/Analytics/LICENSE.txt
@@ -0,0 +1,48 @@
+
+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/Analytics/LICENSE_AFL.txt b/app/code/Magento/Analytics/LICENSE_AFL.txt
new file mode 100644
index 0000000000000..f39d641b18a19
--- /dev/null
+++ b/app/code/Magento/Analytics/LICENSE_AFL.txt
@@ -0,0 +1,48 @@
+
+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/Analytics/Model/AnalyticsToken.php b/app/code/Magento/Analytics/Model/AnalyticsToken.php
new file mode 100644
index 0000000000000..ccec4d1bbe958
--- /dev/null
+++ b/app/code/Magento/Analytics/Model/AnalyticsToken.php
@@ -0,0 +1,92 @@
+reinitableConfig = $reinitableConfig;
+ $this->config = $config;
+ $this->configWriter = $configWriter;
+ }
+
+ /**
+ * Get Magento BI token value.
+ *
+ * @return string|null
+ */
+ public function getToken()
+ {
+ return $this->config->getValue($this->tokenPath);
+ }
+
+ /**
+ * Stores Magento BI token value.
+ *
+ * @param string $value
+ *
+ * @return bool
+ */
+ public function storeToken($value)
+ {
+ $this->configWriter->save($this->tokenPath, $value);
+ $this->reinitableConfig->reinit();
+
+ return true;
+ }
+
+ /**
+ * Check Magento BI token value exist.
+ *
+ * @return bool
+ */
+ public function isTokenExist()
+ {
+ return (bool)$this->getToken();
+ }
+}
diff --git a/app/code/Magento/Analytics/Model/Config.php b/app/code/Magento/Analytics/Model/Config.php
new file mode 100644
index 0000000000000..ba508187b4b9f
--- /dev/null
+++ b/app/code/Magento/Analytics/Model/Config.php
@@ -0,0 +1,40 @@
+data = $data;
+ }
+
+ /**
+ * Get config value by key.
+ *
+ * @param string|null $key
+ * @param string|null $default
+ * @return array
+ */
+ public function get($key = null, $default = null)
+ {
+ return $this->data->get($key, $default);
+ }
+}
diff --git a/app/code/Magento/Analytics/Model/Config/Backend/Baseurl/SubscriptionUpdateHandler.php b/app/code/Magento/Analytics/Model/Config/Backend/Baseurl/SubscriptionUpdateHandler.php
new file mode 100644
index 0000000000000..6e6f008d49f7e
--- /dev/null
+++ b/app/code/Magento/Analytics/Model/Config/Backend/Baseurl/SubscriptionUpdateHandler.php
@@ -0,0 +1,107 @@
+analyticsToken = $analyticsToken;
+ $this->flagManager = $flagManager;
+ $this->reinitableConfig = $reinitableConfig;
+ $this->configWriter = $configWriter;
+ }
+
+ /**
+ * Activate process of subscription update handling.
+ *
+ * @param string $url
+ * @return bool
+ */
+ public function processUrlUpdate(string $url)
+ {
+ if ($this->analyticsToken->isTokenExist()) {
+ if (!$this->flagManager->getFlagData(self::PREVIOUS_BASE_URL_FLAG_CODE)) {
+ $this->flagManager->saveFlag(self::PREVIOUS_BASE_URL_FLAG_CODE, $url);
+ }
+
+ $this->flagManager
+ ->saveFlag(self::SUBSCRIPTION_UPDATE_REVERSE_COUNTER_FLAG_CODE, $this->attemptsInitValue);
+ $this->configWriter->save(self::UPDATE_CRON_STRING_PATH, $this->cronExpression);
+ $this->reinitableConfig->reinit();
+ }
+
+ return true;
+ }
+}
diff --git a/app/code/Magento/Analytics/Model/Config/Backend/CollectionTime.php b/app/code/Magento/Analytics/Model/Config/Backend/CollectionTime.php
new file mode 100644
index 0000000000000..e26ad01fc74bf
--- /dev/null
+++ b/app/code/Magento/Analytics/Model/Config/Backend/CollectionTime.php
@@ -0,0 +1,91 @@
+configWriter = $configWriter;
+ parent::__construct($context, $registry, $config, $cacheTypeList, $resource, $resourceCollection, $data);
+ }
+
+ /**
+ * {@inheritdoc}
+ *
+ * {@inheritdoc}. Set schedule setting for cron.
+ *
+ * @return Value
+ */
+ public function afterSave()
+ {
+ $result = preg_match('#(?\d{2}),(?\d{2}),(?\d{2})#', $this->getValue(), $time);
+
+ if (!$result) {
+ throw new LocalizedException(__('Time value has an unsupported format'));
+ }
+
+ $cronExprArray = [
+ $time['min'], # Minute
+ $time['hour'], # Hour
+ '*', # Day of the Month
+ '*', # Month of the Year
+ '*', # Day of the Week
+ ];
+
+ $cronExprString = join(' ', $cronExprArray);
+
+ try {
+ $this->configWriter->save(self::CRON_SCHEDULE_PATH, $cronExprString);
+ } catch (\Exception $e) {
+ $this->_logger->error($e->getMessage());
+ throw new LocalizedException(__('Cron settings can\'t be saved'));
+ }
+
+ return parent::afterSave();
+ }
+}
diff --git a/app/code/Magento/Analytics/Model/Config/Backend/Enabled.php b/app/code/Magento/Analytics/Model/Config/Backend/Enabled.php
new file mode 100644
index 0000000000000..ac97f2a843e61
--- /dev/null
+++ b/app/code/Magento/Analytics/Model/Config/Backend/Enabled.php
@@ -0,0 +1,84 @@
+subscriptionHandler = $subscriptionHandler;
+ parent::__construct($context, $registry, $config, $cacheTypeList, $resource, $resourceCollection, $data);
+ }
+
+ /**
+ * Add additional handling after config value was saved.
+ *
+ * @return Value
+ * @throws LocalizedException
+ */
+ public function afterSave()
+ {
+ try {
+ if ($this->isValueChanged()) {
+ $enabled = $this->getData('value');
+
+ if ($enabled) {
+ $this->subscriptionHandler->processEnabled();
+ } else {
+ $this->subscriptionHandler->processDisabled();
+ }
+ }
+ } catch (\Exception $e) {
+ $this->_logger->error($e->getMessage());
+ throw new LocalizedException(__('There was an error save new configuration value.'));
+ }
+
+ return parent::afterSave();
+ }
+}
diff --git a/app/code/Magento/Analytics/Model/Config/Backend/Enabled/SubscriptionHandler.php b/app/code/Magento/Analytics/Model/Config/Backend/Enabled/SubscriptionHandler.php
new file mode 100644
index 0000000000000..4b125949948c6
--- /dev/null
+++ b/app/code/Magento/Analytics/Model/Config/Backend/Enabled/SubscriptionHandler.php
@@ -0,0 +1,172 @@
+configWriter = $configWriter;
+ $this->flagManager = $flagManager;
+ $this->analyticsToken = $analyticsToken;
+ $this->reinitableConfig = $reinitableConfig;
+ }
+
+ /**
+ * Processing of activation MBI subscription.
+ *
+ * Activate process of subscription handling if Analytics token is not received.
+ *
+ * @return bool
+ */
+ public function processEnabled()
+ {
+ if (!$this->analyticsToken->isTokenExist()) {
+ $this->setCronSchedule();
+ $this->setAttemptsFlag();
+ $this->reinitableConfig->reinit();
+ }
+
+ return true;
+ }
+
+ /**
+ * Set cron schedule setting into config for activation of subscription process.
+ *
+ * @return bool
+ */
+ private function setCronSchedule()
+ {
+ $this->configWriter->save(self::CRON_STRING_PATH, join(' ', self::CRON_EXPR_ARRAY));
+ return true;
+ }
+
+ /**
+ * Set flag as reserve counter of attempts subscription operation.
+ *
+ * @return bool
+ */
+ private function setAttemptsFlag()
+ {
+ return $this->flagManager
+ ->saveFlag(self::ATTEMPTS_REVERSE_COUNTER_FLAG_CODE, $this->attemptsInitValue);
+ }
+
+ /**
+ * Processing of deactivation MBI subscription.
+ *
+ * Disable data collection
+ * and interrupt subscription handling if Analytics token is not received.
+ *
+ * @return bool
+ */
+ public function processDisabled()
+ {
+ $this->disableCollectionData();
+
+ if (!$this->analyticsToken->isTokenExist()) {
+ $this->unsetAttemptsFlag();
+ }
+
+ return true;
+ }
+
+ /**
+ * Unset flag of attempts subscription operation.
+ *
+ * @return bool
+ */
+ private function unsetAttemptsFlag()
+ {
+ return $this->flagManager
+ ->deleteFlag(self::ATTEMPTS_REVERSE_COUNTER_FLAG_CODE);
+ }
+
+ /**
+ * Unset schedule of collection data cron.
+ *
+ * @return bool
+ */
+ private function disableCollectionData()
+ {
+ $this->configWriter->delete(CollectionTime::CRON_SCHEDULE_PATH);
+
+ return true;
+ }
+}
diff --git a/app/code/Magento/Analytics/Model/Config/Backend/Vertical.php b/app/code/Magento/Analytics/Model/Config/Backend/Vertical.php
new file mode 100644
index 0000000000000..1aabbb91ddf87
--- /dev/null
+++ b/app/code/Magento/Analytics/Model/Config/Backend/Vertical.php
@@ -0,0 +1,32 @@
+getValue())) {
+ throw new LocalizedException(__('Please select a vertical.'));
+ }
+
+ return $this;
+ }
+}
diff --git a/app/code/Magento/Analytics/Model/Config/Mapper.php b/app/code/Magento/Analytics/Model/Config/Mapper.php
new file mode 100644
index 0000000000000..504690b8e4763
--- /dev/null
+++ b/app/code/Magento/Analytics/Model/Config/Mapper.php
@@ -0,0 +1,66 @@
+ [
+ * 'name' => 'file_name',
+ * 'providers' => [
+ * 'reportProvider' => [
+ * 'name' => 'report_provider_name',
+ * 'class' => 'Magento\Analytics\ReportXml\ReportProvider',
+ * 'parameters' =>[
+ * 'name' => 'report_name',
+ * ],
+ * ],
+ * 'customProvider' => [
+ * 'name' => 'custom_provider_name',
+ * 'class' => 'Magento\Analytics\Model\CustomProvider',
+ * ],
+ * ],
+ * ]
+ * ];
+ */
+ public function execute($configData)
+ {
+ if (!isset($configData['config'][0]['file'])) {
+ return [];
+ }
+
+ $files = [];
+ foreach ($configData['config'][0]['file'] as $fileData) {
+ /** just one set of providers is allowed by xsd */
+ $providers = reset($fileData['providers']);
+ foreach ($providers as $providerType => $providerDataSet) {
+ /** just one set of provider data is allowed by xsd */
+ $providerData = reset($providerDataSet);
+ /** just one set of parameters is allowed by xsd */
+ $providerData['parameters'] = !empty($providerData['parameters'])
+ ? reset($providerData['parameters'])
+ : [];
+ $providerData['parameters'] = array_map(
+ 'reset',
+ $providerData['parameters']
+ );
+ $providers[$providerType] = $providerData;
+ }
+ $files[$fileData['name']] = $fileData;
+ $files[$fileData['name']]['providers'] = $providers;
+ }
+ return $files;
+ }
+}
diff --git a/app/code/Magento/Analytics/Model/Config/Reader.php b/app/code/Magento/Analytics/Model/Config/Reader.php
new file mode 100644
index 0000000000000..8980e31627717
--- /dev/null
+++ b/app/code/Magento/Analytics/Model/Config/Reader.php
@@ -0,0 +1,52 @@
+mapper = $mapper;
+ $this->readers = $readers;
+ }
+
+ /**
+ * Read configuration scope.
+ *
+ * @param string|null $scope
+ * @return array
+ */
+ public function read($scope = null)
+ {
+ $data = [];
+ foreach ($this->readers as $reader) {
+ $data = array_merge_recursive($data, $reader->read($scope));
+ }
+
+ return $this->mapper->execute($data);
+ }
+}
diff --git a/app/code/Magento/Analytics/Model/Config/Source/Vertical.php b/app/code/Magento/Analytics/Model/Config/Source/Vertical.php
new file mode 100644
index 0000000000000..c9d9582ea7c7a
--- /dev/null
+++ b/app/code/Magento/Analytics/Model/Config/Source/Vertical.php
@@ -0,0 +1,51 @@
+verticals = $verticals;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function toOptionArray()
+ {
+ $result = [
+ ['value' => '', 'label' => __('--Please Select--')]
+ ];
+
+ foreach ($this->verticals as $vertical) {
+ $result[] = ['value' => $vertical, 'label' => __($vertical)];
+ }
+
+ return $result;
+ }
+}
diff --git a/app/code/Magento/Analytics/Model/ConfigInterface.php b/app/code/Magento/Analytics/Model/ConfigInterface.php
new file mode 100644
index 0000000000000..caaa2e100c1c7
--- /dev/null
+++ b/app/code/Magento/Analytics/Model/ConfigInterface.php
@@ -0,0 +1,22 @@
+ 'command_class_name'.
+ *
+ * The list may be configured in each module via '/etc/di.xml'.
+ *
+ * @var string[]
+ */
+ private $commands;
+
+ /**
+ * @var ObjectManagerInterface
+ */
+ private $objectManager;
+
+ /**
+ * @param array $commands
+ * @param ObjectManagerInterface $objectManager
+ */
+ public function __construct(
+ array $commands,
+ ObjectManagerInterface $objectManager
+ ) {
+ $this->commands = $commands;
+ $this->objectManager = $objectManager;
+ }
+
+ /**
+ * Executes a command in accordance with the given name.
+ *
+ * @param string $commandName
+ * @return bool
+ * @throws NotFoundException if the command is not found.
+ */
+ public function execute($commandName)
+ {
+ if (!array_key_exists($commandName, $this->commands)) {
+ throw new NotFoundException(__('Command was not found.'));
+ }
+
+ /** @var \Magento\Analytics\Model\Connector\CommandInterface $command */
+ $command = $this->objectManager->create($this->commands[$commandName]);
+
+ return $command->execute();
+ }
+}
diff --git a/app/code/Magento/Analytics/Model/Connector/CommandInterface.php b/app/code/Magento/Analytics/Model/Connector/CommandInterface.php
new file mode 100644
index 0000000000000..7a8774fe3dba9
--- /dev/null
+++ b/app/code/Magento/Analytics/Model/Connector/CommandInterface.php
@@ -0,0 +1,21 @@
+curlFactory = $curlFactory;
+ $this->responseFactory = $responseFactory;
+ $this->converter = $converter;
+ $this->logger = $logger;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function request($method, $url, array $body = [], array $headers = [], $version = '1.1')
+ {
+ $response = new \Zend_Http_Response(0, []);
+
+ try {
+ $curl = $this->curlFactory->create();
+ $headers = $this->applyContentTypeHeaderFromConverter($headers);
+
+ $curl->write($method, $url, $version, $headers, $this->converter->toBody($body));
+
+ $result = $curl->read();
+
+ if ($curl->getErrno()) {
+ $this->logger->critical(
+ new \Exception(
+ sprintf(
+ 'MBI service CURL connection error #%s: %s',
+ $curl->getErrno(),
+ $curl->getError()
+ )
+ )
+ );
+
+ return $response;
+ }
+
+ $response = $this->responseFactory->create($result);
+ } catch (\Exception $e) {
+ $this->logger->critical($e);
+ }
+
+ return $response;
+ }
+
+ /**
+ * @param array $headers
+ *
+ * @return array
+ */
+ private function applyContentTypeHeaderFromConverter(array $headers)
+ {
+ $contentTypeHeaderKey = array_search($this->converter->getContentTypeHeader(), $headers);
+ if ($contentTypeHeaderKey === false) {
+ $headers[] = $this->converter->getContentTypeHeader();
+ }
+
+ return $headers;
+ }
+}
diff --git a/app/code/Magento/Analytics/Model/Connector/Http/ClientInterface.php b/app/code/Magento/Analytics/Model/Connector/Http/ClientInterface.php
new file mode 100644
index 0000000000000..a1e1f057684f6
--- /dev/null
+++ b/app/code/Magento/Analytics/Model/Connector/Http/ClientInterface.php
@@ -0,0 +1,29 @@
+converter = $converter;
+ $this->responseHandlers = $responseHandlers;
+ }
+
+ /**
+ * @param \Zend_Http_Response $response
+ *
+ * @return bool|string
+ */
+ public function getResult(\Zend_Http_Response $response)
+ {
+ $result = false;
+ $responseBody = $this->converter->fromBody($response->getBody());
+ if (array_key_exists($response->getStatus(), $this->responseHandlers)) {
+ $result = $this->responseHandlers[$response->getStatus()]->handleResponse($responseBody);
+ }
+
+ return $result;
+ }
+}
diff --git a/app/code/Magento/Analytics/Model/Connector/NotifyDataChangedCommand.php b/app/code/Magento/Analytics/Model/Connector/NotifyDataChangedCommand.php
new file mode 100644
index 0000000000000..f1a8ea6460f9d
--- /dev/null
+++ b/app/code/Magento/Analytics/Model/Connector/NotifyDataChangedCommand.php
@@ -0,0 +1,93 @@
+analyticsToken = $analyticsToken;
+ $this->httpClient = $httpClient;
+ $this->config = $config;
+ $this->responseResolver = $responseResolver;
+ $this->logger = $logger;
+ }
+
+ /**
+ * Notify MBI about that data collection was finished
+ *
+ * @return bool
+ */
+ public function execute()
+ {
+ $result = false;
+ if ($this->analyticsToken->isTokenExist()) {
+ $response = $this->httpClient->request(
+ ZendClient::POST,
+ $this->config->getValue($this->notifyDataChangedUrlPath),
+ [
+ "access-token" => $this->analyticsToken->getToken(),
+ "url" => $this->config->getValue(Store::XML_PATH_SECURE_BASE_URL),
+ ]
+ );
+ $result = $this->responseResolver->getResult($response);
+ }
+ return (bool)$result;
+ }
+}
diff --git a/app/code/Magento/Analytics/Model/Connector/OTPRequest.php b/app/code/Magento/Analytics/Model/Connector/OTPRequest.php
new file mode 100644
index 0000000000000..dfa283e10d070
--- /dev/null
+++ b/app/code/Magento/Analytics/Model/Connector/OTPRequest.php
@@ -0,0 +1,115 @@
+analyticsToken = $analyticsToken;
+ $this->httpClient = $httpClient;
+ $this->config = $config;
+ $this->responseResolver = $responseResolver;
+ $this->logger = $logger;
+ }
+
+ /**
+ * Performs obtaining of an OTP from the MBI service.
+ *
+ * Returns received OTP or FALSE in case of failure.
+ *
+ * @return string|false
+ */
+ public function call()
+ {
+ $result = false;
+
+ if ($this->analyticsToken->isTokenExist()) {
+ $response = $this->httpClient->request(
+ ZendClient::POST,
+ $this->config->getValue($this->otpUrlConfigPath),
+ [
+ "access-token" => $this->analyticsToken->getToken(),
+ "url" => $this->config->getValue(Store::XML_PATH_SECURE_BASE_URL),
+ ]
+ );
+
+ $result = $this->responseResolver->getResult($response);
+ if (!$result) {
+ $this->logger->warning(
+ sprintf(
+ 'Obtaining of an OTP from the MBI service has been failed: %s',
+ !empty($response->getBody()) ? $response->getBody() : 'Response body is empty.'
+ )
+ );
+ }
+ }
+
+ return $result;
+ }
+}
diff --git a/app/code/Magento/Analytics/Model/Connector/ResponseHandler/OTP.php b/app/code/Magento/Analytics/Model/Connector/ResponseHandler/OTP.php
new file mode 100644
index 0000000000000..d9a672e81f43d
--- /dev/null
+++ b/app/code/Magento/Analytics/Model/Connector/ResponseHandler/OTP.php
@@ -0,0 +1,24 @@
+analyticsToken = $analyticsToken;
+ $this->subscriptionHandler = $subscriptionHandler;
+ $this->subscriptionStatusProvider = $subscriptionStatusProvider;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function handleResponse(array $responseBody)
+ {
+ if ($this->subscriptionStatusProvider->getStatus() === SubscriptionStatusProvider::ENABLED) {
+ $this->analyticsToken->storeToken(null);
+ $this->subscriptionHandler->processEnabled();
+ }
+ return false;
+ }
+}
diff --git a/app/code/Magento/Analytics/Model/Connector/ResponseHandler/SignUp.php b/app/code/Magento/Analytics/Model/Connector/ResponseHandler/SignUp.php
new file mode 100644
index 0000000000000..b2261e418abc7
--- /dev/null
+++ b/app/code/Magento/Analytics/Model/Connector/ResponseHandler/SignUp.php
@@ -0,0 +1,51 @@
+analyticsToken = $analyticsToken;
+ $this->converter = $converter;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function handleResponse(array $body)
+ {
+ if (isset($body['access-token']) && !empty($body['access-token'])) {
+ $this->analyticsToken->storeToken($body['access-token']);
+ return $body['access-token'];
+ }
+
+ return false;
+ }
+}
diff --git a/app/code/Magento/Analytics/Model/Connector/ResponseHandler/Update.php b/app/code/Magento/Analytics/Model/Connector/ResponseHandler/Update.php
new file mode 100644
index 0000000000000..73fc575ae2821
--- /dev/null
+++ b/app/code/Magento/Analytics/Model/Connector/ResponseHandler/Update.php
@@ -0,0 +1,24 @@
+analyticsToken = $analyticsToken;
+ $this->integrationManager = $integrationManager;
+ $this->config = $config;
+ $this->httpClient = $httpClient;
+ $this->logger = $logger;
+ $this->responseResolver = $responseResolver;
+ }
+
+ /**
+ * Executes signUp command
+ *
+ * During this call Magento generates or retrieves access token for the integration user
+ * In case successful generation Magento activates user and sends access token to MA
+ * As the response, Magento receives a token to MA
+ * Magento stores this token in System Configuration
+ *
+ * This method returns true in case of success
+ *
+ * @return bool
+ */
+ public function execute()
+ {
+ $result = false;
+ $integrationToken = $this->integrationManager->generateToken();
+ if ($integrationToken) {
+ $this->integrationManager->activateIntegration();
+ $response = $this->httpClient->request(
+ ZendClient::POST,
+ $this->config->getValue($this->signUpUrlPath),
+ [
+ "token" => $integrationToken->getData('token'),
+ "url" => $this->config->getValue(Store::XML_PATH_SECURE_BASE_URL),
+ ]
+ );
+
+ $result = $this->responseResolver->getResult($response);
+ if (!$result) {
+ $this->logger->warning(
+ sprintf(
+ 'Subscription for MBI service has been failed. An error occurred during token exchange: %s',
+ !empty($response->getBody()) ? $response->getBody() : 'Response body is empty.'
+ )
+ );
+ }
+ }
+
+ return (bool)$result;
+ }
+}
diff --git a/app/code/Magento/Analytics/Model/Connector/UpdateCommand.php b/app/code/Magento/Analytics/Model/Connector/UpdateCommand.php
new file mode 100644
index 0000000000000..8f05f1107e87e
--- /dev/null
+++ b/app/code/Magento/Analytics/Model/Connector/UpdateCommand.php
@@ -0,0 +1,114 @@
+analyticsToken = $analyticsToken;
+ $this->httpClient = $httpClient;
+ $this->config = $config;
+ $this->logger = $logger;
+ $this->flagManager = $flagManager;
+ $this->responseResolver = $responseResolver;
+ }
+
+ /**
+ * Executes update request to MBI api in case store url was changed
+ *
+ * @return bool
+ */
+ public function execute()
+ {
+ $result = false;
+ if ($this->analyticsToken->isTokenExist()) {
+ $response = $this->httpClient->request(
+ ZendClient::PUT,
+ $this->config->getValue($this->updateUrlPath),
+ [
+ "url" => $this->flagManager
+ ->getFlagData(SubscriptionUpdateHandler::PREVIOUS_BASE_URL_FLAG_CODE),
+ "new-url" => $this->config->getValue(Store::XML_PATH_SECURE_BASE_URL),
+ "access-token" => $this->analyticsToken->getToken(),
+ ]
+ );
+ $result = $this->responseResolver->getResult($response);
+ if (!$result) {
+ $this->logger->warning(
+ sprintf(
+ 'Update of the subscription for MBI service has been failed: %s',
+ !empty($response->getBody()) ? $response->getBody() : 'Response body is empty.'
+ )
+ );
+ }
+ }
+
+ return (bool)$result;
+ }
+}
diff --git a/app/code/Magento/Analytics/Model/Cryptographer.php b/app/code/Magento/Analytics/Model/Cryptographer.php
new file mode 100644
index 0000000000000..6905eee372ae2
--- /dev/null
+++ b/app/code/Magento/Analytics/Model/Cryptographer.php
@@ -0,0 +1,130 @@
+analyticsToken = $analyticsToken;
+ $this->encodedContextFactory = $encodedContextFactory;
+ }
+
+ /**
+ * Encrypt input data.
+ *
+ * @param string $source
+ * @return EncodedContext
+ * @throws LocalizedException
+ */
+ public function encode($source)
+ {
+ if (!is_string($source)) {
+ try {
+ $source = (string)$source;
+ } catch (\Exception $e) {
+ throw new LocalizedException(__('Input data must be string or convertible into string.'));
+ }
+ } elseif (!$source) {
+ throw new LocalizedException(__('Input data must be non-empty string.'));
+ }
+ if (!$this->validateCipherMethod($this->cipherMethod)) {
+ throw new LocalizedException(__('Not valid cipher method.'));
+ }
+ $initializationVector = $this->getInitializationVector();
+
+ $encodedContext = $this->encodedContextFactory->create([
+ 'content' => openssl_encrypt(
+ $source,
+ $this->cipherMethod,
+ $this->getKey(),
+ OPENSSL_RAW_DATA,
+ $initializationVector
+ ),
+ 'initializationVector' => $initializationVector,
+ ]);
+
+ return $encodedContext;
+ }
+
+ /**
+ * Return key for encryption.
+ *
+ * @return string
+ * @throws LocalizedException
+ */
+ private function getKey()
+ {
+ $token = $this->analyticsToken->getToken();
+ if (!$token) {
+ throw new LocalizedException(__('Encryption key can\'t be empty.'));
+ }
+ return hash('sha256', $token);
+ }
+
+ /**
+ * Return established cipher method.
+ *
+ * @return string
+ */
+ private function getCipherMethod()
+ {
+ return $this->cipherMethod;
+ }
+
+ /**
+ * Return each time generated random initialization vector which depends on the cipher method.
+ *
+ * @return string
+ */
+ private function getInitializationVector()
+ {
+ $ivSize = openssl_cipher_iv_length($this->getCipherMethod());
+ return openssl_random_pseudo_bytes($ivSize);
+ }
+
+ /**
+ * Check that cipher method is allowed for encryption.
+ *
+ * @param string $cipherMethod
+ * @return bool
+ */
+ private function validateCipherMethod($cipherMethod)
+ {
+ $methods = openssl_get_cipher_methods();
+ return (false !== array_search($cipherMethod, $methods));
+ }
+}
diff --git a/app/code/Magento/Analytics/Model/EncodedContext.php b/app/code/Magento/Analytics/Model/EncodedContext.php
new file mode 100644
index 0000000000000..5fb2d0c15aef7
--- /dev/null
+++ b/app/code/Magento/Analytics/Model/EncodedContext.php
@@ -0,0 +1,52 @@
+content = $content;
+ $this->initializationVector = $initializationVector;
+ }
+
+ /**
+ * @return string
+ */
+ public function getContent()
+ {
+ return $this->content;
+ }
+
+ /**
+ * @return string
+ */
+ public function getInitializationVector()
+ {
+ return $this->initializationVector;
+ }
+}
diff --git a/app/code/Magento/Analytics/Model/Exception/State/SubscriptionUpdateException.php b/app/code/Magento/Analytics/Model/Exception/State/SubscriptionUpdateException.php
new file mode 100644
index 0000000000000..5d127037afea9
--- /dev/null
+++ b/app/code/Magento/Analytics/Model/Exception/State/SubscriptionUpdateException.php
@@ -0,0 +1,17 @@
+filesystem = $filesystem;
+ $this->archive = $archive;
+ $this->reportWriter = $reportWriter;
+ $this->cryptographer = $cryptographer;
+ $this->fileRecorder = $fileRecorder;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function prepareExportData()
+ {
+ try {
+ $tmpDirectory = $this->filesystem->getDirectoryWrite(DirectoryList::SYS_TMP);
+
+ $this->prepareDirectory($tmpDirectory, $this->getTmpFilesDirRelativePath());
+ $this->reportWriter->write($tmpDirectory, $this->getTmpFilesDirRelativePath());
+
+ $tmpFilesDirectoryAbsolutePath = $this->validateSource($tmpDirectory, $this->getTmpFilesDirRelativePath());
+ $archiveAbsolutePath = $this->prepareFileDirectory($tmpDirectory, $this->getArchiveRelativePath());
+ $this->pack(
+ $tmpFilesDirectoryAbsolutePath,
+ $archiveAbsolutePath
+ );
+
+ $this->validateSource($tmpDirectory, $this->getArchiveRelativePath());
+ $this->fileRecorder->recordNewFile(
+ $this->cryptographer->encode($tmpDirectory->readFile($this->getArchiveRelativePath()))
+ );
+ } finally {
+ $tmpDirectory->delete($this->getTmpFilesDirRelativePath());
+ $tmpDirectory->delete($this->getArchiveRelativePath());
+ }
+
+ return true;
+ }
+
+ /**
+ * Return relative path to a directory for temporary files with reports data.
+ *
+ * @return string
+ */
+ private function getTmpFilesDirRelativePath()
+ {
+ return $this->subdirectoryPath . 'tmp/';
+ }
+
+ /**
+ * Return relative path to a directory for an archive.
+ *
+ * @return string
+ */
+ private function getArchiveRelativePath()
+ {
+ return $this->subdirectoryPath . $this->archiveName;
+ }
+
+ /**
+ * Clean up a directory.
+ *
+ * @param WriteInterface $directory
+ * @param string $path
+ * @return string
+ */
+ private function prepareDirectory(WriteInterface $directory, $path)
+ {
+ $directory->delete($path);
+
+ return $directory->getAbsolutePath($path);
+ }
+
+ /**
+ * Remove a file and a create parent directory a file.
+ *
+ * @param WriteInterface $directory
+ * @param string $path
+ * @return string
+ */
+ private function prepareFileDirectory(WriteInterface $directory, $path)
+ {
+ $directory->delete($path);
+ if (dirname($path) !== '.') {
+ $directory->create(dirname($path));
+ }
+
+ return $directory->getAbsolutePath($path);
+ }
+
+ /**
+ * Packing data into an archive.
+ *
+ * @param string $source
+ * @param string $destination
+ * @return bool
+ */
+ private function pack($source, $destination)
+ {
+ $this->archive->pack(
+ $source,
+ $destination,
+ is_dir($source) ?: false
+ );
+
+ return true;
+ }
+
+ /**
+ * Validate that data source exist.
+ *
+ * Return absolute path in a validated data source.
+ *
+ * @param WriteInterface $directory
+ * @param string $path
+ * @return string
+ * @throws LocalizedException If source is not exist.
+ */
+ private function validateSource(WriteInterface $directory, $path)
+ {
+ if (!$directory->isExist($path)) {
+ throw new LocalizedException(__('Source "%1" is not exist', $directory->getAbsolutePath($path)));
+ }
+
+ return $directory->getAbsolutePath($path);
+ }
+}
diff --git a/app/code/Magento/Analytics/Model/ExportDataHandlerInterface.php b/app/code/Magento/Analytics/Model/ExportDataHandlerInterface.php
new file mode 100644
index 0000000000000..65efb33659c89
--- /dev/null
+++ b/app/code/Magento/Analytics/Model/ExportDataHandlerInterface.php
@@ -0,0 +1,19 @@
+exportDataHandler = $exportDataHandler;
+ $this->analyticsConnector = $connector;
+ }
+
+ /**
+ * {@inheritdoc}
+ * Execute notification command.
+ *
+ * @return bool
+ */
+ public function prepareExportData()
+ {
+ $result = $this->exportDataHandler->prepareExportData();
+ $this->analyticsConnector->execute('notifyDataChanged');
+ return $result;
+ }
+}
diff --git a/app/code/Magento/Analytics/Model/FileInfo.php b/app/code/Magento/Analytics/Model/FileInfo.php
new file mode 100644
index 0000000000000..19bdaf21b2a20
--- /dev/null
+++ b/app/code/Magento/Analytics/Model/FileInfo.php
@@ -0,0 +1,52 @@
+path = $path;
+ $this->initializationVector = $initializationVector;
+ }
+
+ /**
+ * @return string
+ */
+ public function getPath()
+ {
+ return $this->path;
+ }
+
+ /**
+ * @return string
+ */
+ public function getInitializationVector()
+ {
+ return $this->initializationVector;
+ }
+}
diff --git a/app/code/Magento/Analytics/Model/FileInfoManager.php b/app/code/Magento/Analytics/Model/FileInfoManager.php
new file mode 100644
index 0000000000000..e37700e665420
--- /dev/null
+++ b/app/code/Magento/Analytics/Model/FileInfoManager.php
@@ -0,0 +1,123 @@
+flagManager = $flagManager;
+ $this->fileInfoFactory = $fileInfoFactory;
+ }
+
+ /**
+ * Save FileInfo object.
+ *
+ * @param FileInfo $fileInfo
+ * @return bool
+ * @throws LocalizedException
+ */
+ public function save(FileInfo $fileInfo)
+ {
+ $parameters = [];
+ $parameters['initializationVector'] = $fileInfo->getInitializationVector();
+ $parameters['path'] = $fileInfo->getPath();
+
+ $emptyParameters = array_diff($parameters, array_filter($parameters));
+ if ($emptyParameters) {
+ throw new LocalizedException(
+ __('These arguments can\'t be empty "%1"', implode(', ', array_keys($emptyParameters)))
+ );
+ }
+
+ foreach ($this->encodedParameters as $encodedParameter) {
+ $parameters[$encodedParameter] = $this->encodeValue($parameters[$encodedParameter]);
+ }
+
+ $this->flagManager->saveFlag($this->flagCode, $parameters);
+
+ return true;
+ }
+
+ /**
+ * Load FileInfo object.
+ *
+ * @return FileInfo
+ */
+ public function load()
+ {
+ $parameters = $this->flagManager->getFlagData($this->flagCode) ?: [];
+
+ $encodedParameters = array_intersect($this->encodedParameters, array_keys($parameters));
+ foreach ($encodedParameters as $encodedParameter) {
+ $parameters[$encodedParameter] = $this->decodeValue($parameters[$encodedParameter]);
+ }
+
+ $fileInfo = $this->fileInfoFactory->create($parameters);
+
+ return $fileInfo;
+ }
+
+ /**
+ * Encode value.
+ *
+ * @param string $value
+ * @return string
+ */
+ private function encodeValue($value)
+ {
+ return base64_encode($value);
+ }
+
+ /**
+ * Decode value.
+ *
+ * @param string $value
+ * @return string
+ */
+ private function decodeValue($value)
+ {
+ return base64_decode($value);
+ }
+}
diff --git a/app/code/Magento/Analytics/Model/FileRecorder.php b/app/code/Magento/Analytics/Model/FileRecorder.php
new file mode 100644
index 0000000000000..70438a98d56f1
--- /dev/null
+++ b/app/code/Magento/Analytics/Model/FileRecorder.php
@@ -0,0 +1,136 @@
+fileInfoManager = $fileInfoManager;
+ $this->fileInfoFactory = $fileInfoFactory;
+ $this->filesystem = $filesystem;
+ }
+
+ /**
+ * Save new encrypted file, register it and remove old registered file.
+ *
+ * @param EncodedContext $encodedContext
+ * @return bool
+ */
+ public function recordNewFile(EncodedContext $encodedContext)
+ {
+ $directory = $this->filesystem->getDirectoryWrite(DirectoryList::MEDIA);
+
+ $fileRelativePath = $this->getFileRelativePath();
+ $directory->writeFile($fileRelativePath, $encodedContext->getContent());
+
+ $fileInfo = $this->fileInfoManager->load();
+ $this->registerFile($encodedContext, $fileRelativePath);
+ $this->removeOldFile($fileInfo, $directory);
+
+ return true;
+ }
+
+ /**
+ * Return relative path to encoded file.
+ *
+ * @return string
+ */
+ private function getFileRelativePath()
+ {
+ return $this->fileSubdirectoryPath . hash('sha256', time())
+ . '/' . $this->encodedFileName;
+ }
+
+ /**
+ * Register encoded file.
+ *
+ * @param EncodedContext $encodedContext
+ * @param string $fileRelativePath
+ * @return bool
+ */
+ private function registerFile(EncodedContext $encodedContext, $fileRelativePath)
+ {
+ $newFileInfo = $this->fileInfoFactory->create(
+ [
+ 'path' => $fileRelativePath,
+ 'initializationVector' => $encodedContext->getInitializationVector(),
+ ]
+ );
+ $this->fileInfoManager->save($newFileInfo);
+
+ return true;
+ }
+
+ /**
+ * Remove previously registered file.
+ *
+ * @param FileInfo $fileInfo
+ * @param WriteInterface $directory
+ * @return bool
+ */
+ private function removeOldFile(FileInfo $fileInfo, WriteInterface $directory)
+ {
+ if (!$fileInfo->getPath()) {
+ return true;
+ }
+
+ $directory->delete($fileInfo->getPath());
+
+ $directoryName = dirname($fileInfo->getPath());
+ if ($directoryName !== '.') {
+ $directory->delete($directoryName);
+ }
+
+ return true;
+ }
+}
diff --git a/app/code/Magento/Analytics/Model/IntegrationManager.php b/app/code/Magento/Analytics/Model/IntegrationManager.php
new file mode 100644
index 0000000000000..61a40a955e026
--- /dev/null
+++ b/app/code/Magento/Analytics/Model/IntegrationManager.php
@@ -0,0 +1,126 @@
+integrationService = $integrationService;
+ $this->config = $config;
+ $this->oauthService = $oauthService;
+ }
+
+ /**
+ * Activate predefined integration user
+ *
+ * @return bool
+ * @throws NoSuchEntityException
+ */
+ public function activateIntegration()
+ {
+ $integration = $this->integrationService->findByName(
+ $this->config->getConfigDataValue('analytics/integration_name')
+ );
+ if (!$integration->getId()) {
+ throw new NoSuchEntityException(__('Cannot find predefined integration user!'));
+ }
+ $integrationData = $this->getIntegrationData(Integration::STATUS_ACTIVE);
+ $integrationData['integration_id'] = $integration->getId();
+ $this->integrationService->update($integrationData);
+ return true;
+ }
+
+ /**
+ * This method execute Generate Token command and enable integration
+ *
+ * @return bool|\Magento\Integration\Model\Oauth\Token
+ */
+ public function generateToken()
+ {
+ $consumerId = $this->generateIntegration()->getConsumerId();
+ $accessToken = $this->oauthService->getAccessToken($consumerId);
+ if (!$accessToken && $this->oauthService->createAccessToken($consumerId, true)) {
+ $accessToken = $this->oauthService->getAccessToken($consumerId);
+ }
+ return $accessToken;
+ }
+
+ /**
+ * Returns consumer Id for MA integration user
+ *
+ * @return \Magento\Integration\Model\Integration
+ */
+ private function generateIntegration()
+ {
+ $integration = $this->integrationService->findByName(
+ $this->config->getConfigDataValue('analytics/integration_name')
+ );
+ if (!$integration->getId()) {
+ $integration = $this->integrationService->create($this->getIntegrationData());
+ }
+ return $integration;
+ }
+
+ /**
+ * Returns default attributes for MA integration user
+ *
+ * @param int $status
+ * @return array
+ */
+ private function getIntegrationData($status = Integration::STATUS_INACTIVE)
+ {
+ $integrationData = [
+ 'name' => $this->config->getConfigDataValue('analytics/integration_name'),
+ 'status' => $status,
+ 'all_resources' => false,
+ 'resource' => [
+ 'Magento_Analytics::analytics',
+ 'Magento_Analytics::analytics_api'
+ ],
+ ];
+ return $integrationData;
+ }
+}
diff --git a/app/code/Magento/Analytics/Model/Link.php b/app/code/Magento/Analytics/Model/Link.php
new file mode 100644
index 0000000000000..4a40796df4fd0
--- /dev/null
+++ b/app/code/Magento/Analytics/Model/Link.php
@@ -0,0 +1,54 @@
+url = $url;
+ $this->initializationVector = $initializationVector;
+ }
+
+ /**
+ * @return string
+ */
+ public function getUrl()
+ {
+ return $this->url;
+ }
+
+ /**
+ * @return string
+ */
+ public function getInitializationVector()
+ {
+ return $this->initializationVector;
+ }
+}
diff --git a/app/code/Magento/Analytics/Model/LinkProvider.php b/app/code/Magento/Analytics/Model/LinkProvider.php
new file mode 100644
index 0000000000000..2474653f4916c
--- /dev/null
+++ b/app/code/Magento/Analytics/Model/LinkProvider.php
@@ -0,0 +1,87 @@
+linkFactory = $linkFactory;
+ $this->fileInfoManager = $fileInfoManager;
+ $this->storeManager = $storeManager;
+ }
+
+ /**
+ * Returns base url to file according to store configuration
+ *
+ * @param FileInfo $fileInfo
+ * @return string
+ */
+ private function getBaseUrl(FileInfo $fileInfo)
+ {
+ return $this->storeManager->getStore()->getBaseUrl(UrlInterface::URL_TYPE_MEDIA) . $fileInfo->getPath();
+ }
+
+ /**
+ * Verify is requested file ready
+ *
+ * @param FileInfo $fileInfo
+ * @return bool
+ */
+ private function isFileReady(FileInfo $fileInfo)
+ {
+ return $fileInfo->getPath() && $fileInfo->getInitializationVector();
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function get()
+ {
+ $fileInfo = $this->fileInfoManager->load();
+ if (!$this->isFileReady($fileInfo)) {
+ throw new NoSuchEntityException(__('File is not ready yet.'));
+ }
+ return $this->linkFactory->create(
+ [
+ 'url' => $this->getBaseUrl($fileInfo),
+ 'initializationVector' => base64_encode($fileInfo->getInitializationVector())
+ ]
+ );
+ }
+}
diff --git a/app/code/Magento/Analytics/Model/Plugin/BaseUrlConfigPlugin.php b/app/code/Magento/Analytics/Model/Plugin/BaseUrlConfigPlugin.php
new file mode 100644
index 0000000000000..174272614fb19
--- /dev/null
+++ b/app/code/Magento/Analytics/Model/Plugin/BaseUrlConfigPlugin.php
@@ -0,0 +1,61 @@
+subscriptionUpdateHandler = $subscriptionUpdateHandler;
+ }
+
+ /**
+ * Add additional handling after config value was saved.
+ *
+ * @param Value $subject
+ * @param Value $result
+ * @return Value
+ * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+ */
+ public function afterAfterSave(
+ Value $subject,
+ Value $result
+ ) {
+ if ($this->isPluginApplicable($result)) {
+ $this->subscriptionUpdateHandler->processUrlUpdate($result->getOldValue());
+ }
+
+ return $result;
+ }
+
+ /**
+ * @param Value $result
+ * @return bool
+ */
+ private function isPluginApplicable(Value $result)
+ {
+ return $result->isValueChanged()
+ && ($result->getPath() === Store::XML_PATH_SECURE_BASE_URL)
+ && ($result->getScope() === ScopeConfigInterface::SCOPE_TYPE_DEFAULT);
+ }
+}
diff --git a/app/code/Magento/Analytics/Model/ProviderFactory.php b/app/code/Magento/Analytics/Model/ProviderFactory.php
new file mode 100644
index 0000000000000..3a23430fca077
--- /dev/null
+++ b/app/code/Magento/Analytics/Model/ProviderFactory.php
@@ -0,0 +1,39 @@
+objectManager = $objectManager;
+ }
+
+ /**
+ * @param string $providerName
+ * @return object
+ */
+ public function create($providerName)
+ {
+ return $this->objectManager->get($providerName);
+ }
+}
diff --git a/app/code/Magento/Analytics/Model/ReportUrlProvider.php b/app/code/Magento/Analytics/Model/ReportUrlProvider.php
new file mode 100644
index 0000000000000..e7fdf6f9e8132
--- /dev/null
+++ b/app/code/Magento/Analytics/Model/ReportUrlProvider.php
@@ -0,0 +1,94 @@
+analyticsToken = $analyticsToken;
+ $this->otpRequest = $otpRequest;
+ $this->config = $config;
+ $this->flagManager = $flagManager;
+ }
+
+ /**
+ * Provide URL on resource with reports.
+ *
+ * @return string
+ * @throws SubscriptionUpdateException
+ */
+ public function getUrl()
+ {
+ if ($this->flagManager->getFlagData(SubscriptionUpdateHandler::PREVIOUS_BASE_URL_FLAG_CODE)) {
+ throw new SubscriptionUpdateException(__(
+ 'Your Base URL has been changed and your reports are being updated. '
+ . 'Advanced Reporting will be available once this change has been processed. Please try again later.'
+ ));
+ }
+
+ $url = $this->config->getValue($this->urlReportConfigPath);
+ if ($this->analyticsToken->isTokenExist()) {
+ $otp = $this->otpRequest->call();
+ if ($otp) {
+ $query = http_build_query(['otp' => $otp], '', '&');
+ $url .= '?' . $query;
+ }
+ }
+
+ return $url;
+ }
+}
diff --git a/app/code/Magento/Analytics/Model/ReportWriter.php b/app/code/Magento/Analytics/Model/ReportWriter.php
new file mode 100644
index 0000000000000..7128658947908
--- /dev/null
+++ b/app/code/Magento/Analytics/Model/ReportWriter.php
@@ -0,0 +1,101 @@
+config = $config;
+ $this->reportValidator = $reportValidator;
+ $this->providerFactory = $providerFactory;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function write(WriteInterface $directory, $path)
+ {
+ $errorsList = [];
+ foreach ($this->config->get() as $file) {
+ $provider = reset($file['providers']);
+ if (isset($provider['parameters']['name'])) {
+ $error = $this->reportValidator->validate($provider['parameters']['name']);
+ if ($error) {
+ $errorsList[] = $error;
+ continue;
+ }
+ }
+ /** @var $providerObject */
+ $providerObject = $this->providerFactory->create($provider['class']);
+ $fileName = $provider['parameters'] ? $provider['parameters']['name'] : $provider['name'];
+ $fileFullPath = $path . $fileName . '.csv';
+ $fileData = $providerObject->getReport(...array_values($provider['parameters']));
+ $stream = $directory->openFile($fileFullPath, 'w+');
+ $stream->lock();
+ $headers = [];
+ foreach ($fileData as $row) {
+ if (!$headers) {
+ $headers = array_keys($row);
+ $stream->writeCsv($headers);
+ }
+ $stream->writeCsv($row);
+ }
+ $stream->unlock();
+ $stream->close();
+ }
+ if ($errorsList) {
+ $errorStream = $directory->openFile($path . $this->errorsFileName, 'w+');
+ foreach ($errorsList as $error) {
+ $errorStream->lock();
+ $errorStream->writeCsv($error);
+ $errorStream->unlock();
+ }
+ $errorStream->close();
+ }
+
+ return true;
+ }
+}
diff --git a/app/code/Magento/Analytics/Model/ReportWriterInterface.php b/app/code/Magento/Analytics/Model/ReportWriterInterface.php
new file mode 100644
index 0000000000000..a611095a47ae4
--- /dev/null
+++ b/app/code/Magento/Analytics/Model/ReportWriterInterface.php
@@ -0,0 +1,28 @@
+moduleManager = $moduleManager;
+ }
+
+ /**
+ * Returns module with module status
+ *
+ * @return array
+ */
+ public function current()
+ {
+ $current = parent::current();
+ if (is_array($current) && isset($current['module_name'])) {
+ $current['status'] =
+ $this->moduleManager->isEnabled($current['module_name']) == 1 ? 'Enabled' : "Disabled";
+ }
+ return $current;
+ }
+}
diff --git a/app/code/Magento/Analytics/Model/StoreConfigurationProvider.php b/app/code/Magento/Analytics/Model/StoreConfigurationProvider.php
new file mode 100644
index 0000000000000..0d226a9de7dc2
--- /dev/null
+++ b/app/code/Magento/Analytics/Model/StoreConfigurationProvider.php
@@ -0,0 +1,102 @@
+scopeConfig = $scopeConfig;
+ $this->configPaths = $configPaths;
+ $this->storeManager = $storeManager;
+ }
+
+ /**
+ * Generates report using config paths from di.xml
+ * For each website and store
+ * @return \IteratorIterator
+ */
+ public function getReport()
+ {
+ $configReport = $this->generateReportForScope(ScopeConfigInterface::SCOPE_TYPE_DEFAULT, 0);
+
+ /** @var WebsiteInterface $website */
+ foreach ($this->storeManager->getWebsites() as $website) {
+ $configReport = array_merge(
+ $this->generateReportForScope(ScopeInterface::SCOPE_WEBSITES, $website->getId()),
+ $configReport
+ );
+ }
+
+ /** @var StoreInterface $store */
+ foreach ($this->storeManager->getStores() as $store) {
+ $configReport = array_merge(
+ $this->generateReportForScope(ScopeInterface::SCOPE_STORES, $store->getId()),
+ $configReport
+ );
+ }
+ return new \IteratorIterator(new \ArrayIterator($configReport));
+ }
+
+ /**
+ * Creates report from config for scope type and scope id.
+ *
+ * @param string $scope
+ * @param int $scopeId
+ * @return array
+ */
+ private function generateReportForScope($scope, $scopeId)
+ {
+ $report = [];
+ foreach ($this->configPaths as $configPath) {
+ $report[] = [
+ "config_path" => $configPath,
+ "scope" => $scope,
+ "scope_id" => $scopeId,
+ "value" => $this->scopeConfig->getValue(
+ $configPath,
+ $scope,
+ $scopeId
+ )
+ ];
+ }
+ return $report;
+ }
+}
diff --git a/app/code/Magento/Analytics/Model/SubscriptionStatusProvider.php b/app/code/Magento/Analytics/Model/SubscriptionStatusProvider.php
new file mode 100644
index 0000000000000..1dd831a672faa
--- /dev/null
+++ b/app/code/Magento/Analytics/Model/SubscriptionStatusProvider.php
@@ -0,0 +1,120 @@
+scopeConfig = $scopeConfig;
+ $this->analyticsToken = $analyticsToken;
+ $this->flagManager = $flagManager;
+ }
+
+ /**
+ * Retrieve subscription status to Magento BI Advanced Reporting.
+ *
+ * Statuses:
+ * Enabled - if subscription is enabled and MA token was received;
+ * Pending - if subscription is enabled and MA token was not received;
+ * Disabled - if subscription is not enabled.
+ * Failed - if subscription is enabled and token was not received after attempts ended.
+ *
+ * @return string
+ */
+ public function getStatus()
+ {
+ $isSubscriptionEnabledInConfig = $this->scopeConfig->getValue('analytics/subscription/enabled');
+ if ($isSubscriptionEnabledInConfig) {
+ return $this->getStatusForEnabledSubscription();
+ }
+
+ return $this->getStatusForDisabledSubscription();
+ }
+
+ /**
+ * Retrieve status for subscription that enabled in config.
+ *
+ * @return string
+ */
+ public function getStatusForEnabledSubscription()
+ {
+ $status = static::ENABLED;
+ if ($this->flagManager->getFlagData(SubscriptionUpdateHandler::PREVIOUS_BASE_URL_FLAG_CODE)) {
+ $status = self::PENDING;
+ }
+
+ if (!$this->analyticsToken->isTokenExist()) {
+ $status = static::PENDING;
+ if ($this->flagManager->getFlagData(SubscriptionHandler::ATTEMPTS_REVERSE_COUNTER_FLAG_CODE) === null) {
+ $status = static::FAILED;
+ }
+ }
+
+ return $status;
+ }
+
+ /**
+ * Retrieve status for subscription that disabled in config.
+ *
+ * @return string
+ */
+ public function getStatusForDisabledSubscription()
+ {
+ return static::DISABLED;
+ }
+}
diff --git a/app/code/Magento/Analytics/Model/System/Message/NotificationAboutFailedSubscription.php b/app/code/Magento/Analytics/Model/System/Message/NotificationAboutFailedSubscription.php
new file mode 100644
index 0000000000000..9aaa2ebb3b56f
--- /dev/null
+++ b/app/code/Magento/Analytics/Model/System/Message/NotificationAboutFailedSubscription.php
@@ -0,0 +1,80 @@
+subscriptionStatusProvider = $subscriptionStatusProvider;
+ $this->urlBuilder = $urlBuilder;
+ }
+
+ /**
+ * @inheritdoc
+ *
+ * @codeCoverageIgnore
+ */
+ public function getIdentity()
+ {
+ return hash('sha256', 'ANALYTICS_NOTIFICATION');
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function isDisplayed()
+ {
+ return $this->subscriptionStatusProvider->getStatus() === SubscriptionStatusProvider::FAILED;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getText()
+ {
+ $messageDetails = '';
+
+ $messageDetails .= __('Failed to synchronize data to the Magento Business Intelligence service. ');
+ $messageDetails .= __(
+ 'Retry Synchronization ',
+ $this->urlBuilder->getUrl('analytics/subscription/retry')
+ );
+
+ return $messageDetails;
+ }
+
+ /**
+ * @inheritdoc
+ *
+ * @codeCoverageIgnore
+ */
+ public function getSeverity()
+ {
+ return self::SEVERITY_MAJOR;
+ }
+}
diff --git a/app/code/Magento/Analytics/README.md b/app/code/Magento/Analytics/README.md
new file mode 100644
index 0000000000000..7ec64abcd9b86
--- /dev/null
+++ b/app/code/Magento/Analytics/README.md
@@ -0,0 +1,41 @@
+# Magento_Analytics Module
+
+The Magento_Analytics module integrates your Magento instance with the [Magento Business Intelligence (MBI)](https://magento.com/products/business-intelligence) to use [Advanced Reporting](http://devdocs.magento.com/guides/v2.2/advanced-reporting/modules.html) functionality.
+
+The module implements the following functionality:
+
+* enabling subscription to the MBI and automatic re-subscription
+* changing the base URL with the same MBI account remained
+* declaring the configuration schemas for report data collection
+* collecting the Magento instance data as reports for the MBI
+* introducing API that provides the collected data
+* extending Magento configuration with the module parameters:
+ * subscription status (enabled/disabled)
+ * industry (a business area in which the instance website works)
+ * time of data collection (time of the day when the module collects data)
+
+## Structure
+
+Beyond the [usual module file structure](http://devdocs.magento.com/guides/v2.2/architecture/archi_perspectives/components/modules/mod_intro.html) the module contains a directory `ReportXml`.
+[Report XML](http://devdocs.magento.com/guides/v2.2/advanced-reporting/report-xml.html) is a markup language used to build reports for Advanced Reporting.
+The language declares SQL queries using XML declaration.
+
+## Subscription Process
+
+The subscription to the MBI service is enabled during the installation process of the Analytics module. Each administrator will be notified of these new features upon their initial login to the Admin Panel.
+
+## Analytics Settings
+
+Configuration settings for the Analytics module can be modified in the Admin Panel on the Stores > Configuration page under the General > Advanced Reporting tab.
+
+The following options can be adjusted:
+* Advanced Reporting Service (Enabled/Disabled)
+ * Alters the status of the Advanced Reporting subscription
+* Time of day to send data (Hour/Minute/Second in the store's time zone)
+ * Defines when the data collection process for the Advanced Reporting service occurs
+* Industry
+ * Defines the industry of the store in order to create a personalized Advanced Reporting experience
+
+## Extensibility
+
+We do not recommend to extend the Magento_Analytics module. It introduces an API that is purposed to transfer the collected data. Note that the API cannot be used for other needs.
diff --git a/app/code/Magento/Analytics/ReportXml/Config.php b/app/code/Magento/Analytics/ReportXml/Config.php
new file mode 100644
index 0000000000000..f50dcf941bf50
--- /dev/null
+++ b/app/code/Magento/Analytics/ReportXml/Config.php
@@ -0,0 +1,43 @@
+data = $data;
+ }
+
+ /**
+ * Returns config value by name
+ *
+ * @param string $queryName
+ * @return array
+ */
+ public function get($queryName)
+ {
+ return $this->data->get($queryName);
+ }
+}
diff --git a/app/code/Magento/Analytics/ReportXml/Config/Converter/Xml.php b/app/code/Magento/Analytics/ReportXml/Config/Converter/Xml.php
new file mode 100644
index 0000000000000..9e0b20a6ad414
--- /dev/null
+++ b/app/code/Magento/Analytics/ReportXml/Config/Converter/Xml.php
@@ -0,0 +1,61 @@
+hasAttributes()) {
+ $attrs = $source->attributes;
+ foreach ($attrs as $attr) {
+ $result[$attr->name] = $attr->value;
+ }
+ }
+ if ($source->hasChildNodes()) {
+ $children = $source->childNodes;
+ if ($children->length == 1) {
+ $child = $children->item(0);
+ if ($child->nodeType == XML_TEXT_NODE) {
+ $result['_value'] = $child->nodeValue;
+ return count($result) == 1 ? $result['_value'] : $result;
+ }
+ }
+ foreach ($children as $child) {
+ if ($child instanceof \DOMCharacterData) {
+ continue;
+ }
+ $result[$child->nodeName][] = $this->convertNode($child);
+ }
+ }
+ return $result;
+ }
+
+ /**
+ * Converts XML document into corresponding array.
+ *
+ * @param \DOMDocument $source
+ * @return array
+ */
+ public function convert($source)
+ {
+ return $this->convertNode($source);
+ }
+}
diff --git a/app/code/Magento/Analytics/ReportXml/Config/Mapper.php b/app/code/Magento/Analytics/ReportXml/Config/Mapper.php
new file mode 100644
index 0000000000000..4dda8f3c733a6
--- /dev/null
+++ b/app/code/Magento/Analytics/ReportXml/Config/Mapper.php
@@ -0,0 +1,37 @@
+readers = $readers;
+ $this->mapper = $mapper;
+ }
+
+ /**
+ * Reads configuration according to the given scope.
+ *
+ * @param string|null $scope
+ * @return array
+ */
+ public function read($scope = null)
+ {
+ $data = [];
+ foreach ($this->readers as $reader) {
+ $data = array_merge_recursive($data, $reader->read($scope));
+ }
+ return $this->mapper->execute($data);
+ }
+}
diff --git a/app/code/Magento/Analytics/ReportXml/ConfigInterface.php b/app/code/Magento/Analytics/ReportXml/ConfigInterface.php
new file mode 100644
index 0000000000000..ec03ddf429c06
--- /dev/null
+++ b/app/code/Magento/Analytics/ReportXml/ConfigInterface.php
@@ -0,0 +1,23 @@
+resourceConnection = $resourceConnection;
+ $this->objectManager = $objectManager;
+ }
+
+ /**
+ * Creates one-time connection for export
+ *
+ * @param string $connectionName
+ * @return AdapterInterface
+ */
+ public function getConnection($connectionName)
+ {
+ $connection = $this->resourceConnection->getConnection($connectionName);
+ $connectionClassName = get_class($connection);
+ $configData = $connection->getConfig();
+ $configData['use_buffered_query'] = false;
+ unset($configData['persistent']);
+ return $this->objectManager->create(
+ $connectionClassName,
+ [
+ 'config' => $configData
+ ]
+ );
+ }
+}
diff --git a/app/code/Magento/Analytics/ReportXml/DB/Assembler/AssemblerInterface.php b/app/code/Magento/Analytics/ReportXml/DB/Assembler/AssemblerInterface.php
new file mode 100644
index 0000000000000..083b4843c185a
--- /dev/null
+++ b/app/code/Magento/Analytics/ReportXml/DB/Assembler/AssemblerInterface.php
@@ -0,0 +1,27 @@
+conditionResolver = $conditionResolver;
+ $this->nameResolver = $nameResolver;
+ }
+
+ /**
+ * Assembles WHERE conditions
+ *
+ * @param SelectBuilder $selectBuilder
+ * @param array $queryConfig
+ * @return SelectBuilder
+ */
+ public function assemble(SelectBuilder $selectBuilder, $queryConfig)
+ {
+ if (!isset($queryConfig['source']['filter'])) {
+ return $selectBuilder;
+ }
+ $filters = $this->conditionResolver->getFilter(
+ $selectBuilder,
+ $queryConfig['source']['filter'],
+ $this->nameResolver->getAlias($queryConfig['source'])
+ );
+ $selectBuilder->setFilters(array_merge_recursive($selectBuilder->getFilters(), [$filters]));
+ return $selectBuilder;
+ }
+}
diff --git a/app/code/Magento/Analytics/ReportXml/DB/Assembler/FromAssembler.php b/app/code/Magento/Analytics/ReportXml/DB/Assembler/FromAssembler.php
new file mode 100644
index 0000000000000..811119ace221b
--- /dev/null
+++ b/app/code/Magento/Analytics/ReportXml/DB/Assembler/FromAssembler.php
@@ -0,0 +1,69 @@
+nameResolver = $nameResolver;
+ $this->columnsResolver = $columnsResolver;
+ $this->resourceConnection = $resourceConnection;
+ }
+
+ /**
+ * Assembles FROM condition
+ *
+ * @param SelectBuilder $selectBuilder
+ * @param array $queryConfig
+ * @return SelectBuilder
+ */
+ public function assemble(SelectBuilder $selectBuilder, $queryConfig)
+ {
+ $selectBuilder->setFrom(
+ [
+ $this->nameResolver->getAlias($queryConfig['source']) =>
+ $this->resourceConnection
+ ->getTableName($this->nameResolver->getName($queryConfig['source'])),
+ ]
+ );
+ $columns = $this->columnsResolver->getColumns($selectBuilder, $queryConfig['source']);
+ $selectBuilder->setColumns(array_merge($selectBuilder->getColumns(), $columns));
+ return $selectBuilder;
+ }
+}
diff --git a/app/code/Magento/Analytics/ReportXml/DB/Assembler/JoinAssembler.php b/app/code/Magento/Analytics/ReportXml/DB/Assembler/JoinAssembler.php
new file mode 100644
index 0000000000000..f3c6540a25171
--- /dev/null
+++ b/app/code/Magento/Analytics/ReportXml/DB/Assembler/JoinAssembler.php
@@ -0,0 +1,113 @@
+conditionResolver = $conditionResolver;
+ $this->nameResolver = $nameResolver;
+ $this->columnsResolver = $columnsResolver;
+ $this->resourceConnection = $resourceConnection;
+ }
+
+ /**
+ * Assembles JOIN conditions
+ *
+ * @param SelectBuilder $selectBuilder
+ * @param array $queryConfig
+ * @return SelectBuilder
+ */
+ public function assemble(SelectBuilder $selectBuilder, $queryConfig)
+ {
+ if (!isset($queryConfig['source']['link-source'])) {
+ return $selectBuilder;
+ }
+ $joins = [];
+ $filters = $selectBuilder->getFilters();
+
+ $sourceAlias = $this->nameResolver->getAlias($queryConfig['source']);
+
+ foreach ($queryConfig['source']['link-source'] as $join) {
+ $joinAlias = $this->nameResolver->getAlias($join);
+
+ $joins[$joinAlias] = [
+ 'link-type' => isset($join['link-type']) ? $join['link-type'] : 'left',
+ 'table' => [
+ $joinAlias => $this->resourceConnection
+ ->getTableName($this->nameResolver->getName($join)),
+ ],
+ 'condition' => $this->conditionResolver->getFilter(
+ $selectBuilder,
+ $join['using'],
+ $joinAlias,
+ $sourceAlias
+ )
+ ];
+ if (isset($join['filter'])) {
+ $filters = array_merge(
+ $filters,
+ [
+ $this->conditionResolver->getFilter(
+ $selectBuilder,
+ $join['filter'],
+ $joinAlias,
+ $sourceAlias
+ )
+ ]
+ );
+ }
+ $columns = $this->columnsResolver->getColumns($selectBuilder, isset($join['attribute']) ? $join : []);
+ $selectBuilder->setColumns(array_merge($selectBuilder->getColumns(), $columns));
+ }
+ $selectBuilder->setFilters($filters);
+ $selectBuilder->setJoins(array_merge($selectBuilder->getJoins(), $joins));
+ return $selectBuilder;
+ }
+}
diff --git a/app/code/Magento/Analytics/ReportXml/DB/ColumnsResolver.php b/app/code/Magento/Analytics/ReportXml/DB/ColumnsResolver.php
new file mode 100644
index 0000000000000..e6474d4c5dc6d
--- /dev/null
+++ b/app/code/Magento/Analytics/ReportXml/DB/ColumnsResolver.php
@@ -0,0 +1,100 @@
+nameResolver = $nameResolver;
+ $this->resourceConnection = $resourceConnection;
+ }
+
+ /**
+ * Returns connection
+ *
+ * @return \Magento\Framework\DB\Adapter\AdapterInterface
+ */
+ private function getConnection()
+ {
+ if (!$this->connection) {
+ $this->connection = $this->resourceConnection->getConnection();
+ }
+ return $this->connection;
+ }
+
+ /**
+ * Set columns list to SelectBuilder
+ *
+ * @param SelectBuilder $selectBuilder
+ * @param array $entityConfig
+ * @return array
+ */
+ public function getColumns(SelectBuilder $selectBuilder, $entityConfig)
+ {
+ if (!isset($entityConfig['attribute'])) {
+ return [];
+ }
+ $group = [];
+ $columns = $selectBuilder->getColumns();
+ foreach ($entityConfig['attribute'] as $attributeData) {
+ $columnAlias = $this->nameResolver->getAlias($attributeData);
+ $tableAlias = $this->nameResolver->getAlias($entityConfig);
+ $columnName = $this->nameResolver->getName($attributeData);
+ if (isset($attributeData['function'])) {
+ $prefix = '';
+ if (isset($attributeData['distinct']) && $attributeData['distinct'] == true) {
+ $prefix = ' DISTINCT ';
+ }
+ $expression = new ColumnValueExpression(
+ strtoupper($attributeData['function']) . '(' . $prefix
+ . $this->getConnection()->quoteIdentifier($tableAlias . '.' . $columnName)
+ . ')'
+ );
+ } else {
+ $expression = $tableAlias . '.' . $columnName;
+ }
+ $columns[$columnAlias] = $expression;
+ if (isset($attributeData['group'])) {
+ $group[$columnAlias] = $expression;
+ }
+ }
+ $selectBuilder->setGroup(array_merge($selectBuilder->getGroup(), $group));
+ return $columns;
+ }
+}
diff --git a/app/code/Magento/Analytics/ReportXml/DB/ConditionResolver.php b/app/code/Magento/Analytics/ReportXml/DB/ConditionResolver.php
new file mode 100644
index 0000000000000..773b96959e794
--- /dev/null
+++ b/app/code/Magento/Analytics/ReportXml/DB/ConditionResolver.php
@@ -0,0 +1,166 @@
+ '%1$s = %2$s',
+ 'neq' => '%1$s != %2$s',
+ 'like' => '%1$s LIKE %2$s',
+ 'nlike' => '%1$s NOT LIKE %2$s',
+ 'in' => '%1$s IN(%2$s)',
+ 'nin' => '%1$s NOT IN(%2$s)',
+ 'notnull' => '%1$s IS NOT NULL',
+ 'null' => '%1$s IS NULL',
+ 'gt' => '%1$s > %2$s',
+ 'lt' => '%1$s < %2$s',
+ 'gteq' => '%1$s >= %2$s',
+ 'lteq' => '%1$s <= %2$s',
+ 'finset' => 'FIND_IN_SET(%2$s, %1$s)'
+ ];
+
+ /**
+ * @var \Magento\Framework\DB\Adapter\AdapterInterface
+ */
+ private $connection;
+
+ /**
+ * @var ResourceConnection
+ */
+ private $resourceConnection;
+
+ /**
+ * ConditionResolver constructor.
+ * @param ResourceConnection $resourceConnection
+ */
+ public function __construct(
+ ResourceConnection $resourceConnection
+ ) {
+ $this->resourceConnection = $resourceConnection;
+ }
+
+ /**
+ * Returns connection
+ *
+ * @return \Magento\Framework\DB\Adapter\AdapterInterface
+ */
+ private function getConnection()
+ {
+ if (!$this->connection) {
+ $this->connection = $this->resourceConnection->getConnection();
+ }
+ return $this->connection;
+ }
+
+ /**
+ * Returns value for condition
+ *
+ * @param string $condition
+ * @param string $referencedEntity
+ * @return mixed|null|string|\Zend_Db_Expr
+ */
+ private function getValue($condition, $referencedEntity)
+ {
+ $value = null;
+ $argument = isset($condition['_value']) ? $condition['_value'] : null;
+ if (!isset($condition['type'])) {
+ $condition['type'] = 'value';
+ }
+
+ switch ($condition['type']) {
+ case "value":
+ $value = $this->getConnection()->quote($argument);
+ break;
+ case "variable":
+ $value = new Expression($argument);
+ break;
+ case "identifier":
+ $value = $this->getConnection()->quoteIdentifier(
+ $referencedEntity ? $referencedEntity . '.' . $argument : $argument
+ );
+ break;
+ }
+ return $value;
+ }
+
+ /**
+ * Returns condition for WHERE
+ *
+ * @param SelectBuilder $selectBuilder
+ * @param string $tableName
+ * @param array $condition
+ * @param null|string $referencedEntity
+ * @return string
+ */
+ private function getCondition(SelectBuilder $selectBuilder, $tableName, $condition, $referencedEntity = null)
+ {
+ $columns = $selectBuilder->getColumns();
+ if (isset($columns[$condition['attribute']])
+ && $columns[$condition['attribute']] instanceof Expression
+ ) {
+ $expression = $columns[$condition['attribute']];
+ } else {
+ $expression = $this->getConnection()->quoteIdentifier($tableName . '.' . $condition['attribute']);
+ }
+ return sprintf(
+ $this->conditionMap[$condition['operator']],
+ $expression,
+ $this->getValue($condition, $referencedEntity)
+ );
+ }
+
+ /**
+ * Build WHERE condition
+ *
+ * @param SelectBuilder $selectBuilder
+ * @param array $filterConfig
+ * @param string $aliasName
+ * @param null|string $referencedAlias
+ * @return array
+ */
+ public function getFilter(SelectBuilder $selectBuilder, $filterConfig, $aliasName, $referencedAlias = null)
+ {
+ $filtersParts = [];
+ foreach ($filterConfig as $filter) {
+ $glue = $filter['glue'];
+ $parts = [];
+ foreach ($filter['condition'] as $condition) {
+ if (isset($condition['type']) && $condition['type'] == 'variable') {
+ $selectBuilder->setParams(array_merge($selectBuilder->getParams(), [$condition['_value']]));
+ }
+ $parts[] = $this->getCondition(
+ $selectBuilder,
+ $aliasName,
+ $condition,
+ $referencedAlias
+ );
+ }
+ if (isset($filter['filter'])) {
+ $parts[] = '(' . $this->getFilter(
+ $selectBuilder,
+ $filter['filter'],
+ $aliasName,
+ $referencedAlias
+ ) . ')';
+ }
+ $filtersParts[] = '(' . implode(' ' . strtoupper($glue) . ' ', $parts) . ')';
+ }
+ return implode(' OR ', $filtersParts);
+ }
+}
diff --git a/app/code/Magento/Analytics/ReportXml/DB/NameResolver.php b/app/code/Magento/Analytics/ReportXml/DB/NameResolver.php
new file mode 100644
index 0000000000000..c9543002eb272
--- /dev/null
+++ b/app/code/Magento/Analytics/ReportXml/DB/NameResolver.php
@@ -0,0 +1,40 @@
+getName($elementConfig);
+ if (isset($elementConfig['alias'])) {
+ $alias = $elementConfig['alias'];
+ }
+ return $alias;
+ }
+}
diff --git a/app/code/Magento/Analytics/ReportXml/DB/ReportValidator.php b/app/code/Magento/Analytics/ReportXml/DB/ReportValidator.php
new file mode 100644
index 0000000000000..21a641f0a71c7
--- /dev/null
+++ b/app/code/Magento/Analytics/ReportXml/DB/ReportValidator.php
@@ -0,0 +1,64 @@
+connectionFactory = $connectionFactory;
+ $this->queryFactory = $queryFactory;
+ }
+
+ /**
+ * Tries to do query for provided report with limit 0 and return error information if it failed
+ *
+ * @param string $name
+ * @param SearchCriteriaInterface $criteria
+ * @return array
+ * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+ */
+ public function validate($name, SearchCriteriaInterface $criteria = null)
+ {
+ $query = $this->queryFactory->create($name);
+ $connection = $this->connectionFactory->getConnection($query->getConnectionName());
+ $query->getSelect()->limit(0);
+ try {
+ $connection->query($query->getSelect());
+ } catch (\Zend_Db_Statement_Exception $e) {
+ return [$name, $e->getMessage()];
+ }
+
+ return [];
+ }
+}
diff --git a/app/code/Magento/Analytics/ReportXml/DB/SelectBuilder.php b/app/code/Magento/Analytics/ReportXml/DB/SelectBuilder.php
new file mode 100644
index 0000000000000..17f2392758de8
--- /dev/null
+++ b/app/code/Magento/Analytics/ReportXml/DB/SelectBuilder.php
@@ -0,0 +1,305 @@
+resourceConnection = $resourceConnection;
+ }
+
+ /**
+ * Get join condition
+ *
+ * @return array
+ */
+ public function getJoins()
+ {
+ return $this->joins;
+ }
+
+ /**
+ * Set joins conditions
+ *
+ * @param array $joins
+ * @return $this
+ */
+ public function setJoins($joins)
+ {
+ $this->joins = $joins;
+
+ return $this;
+ }
+
+ /**
+ * Get connection name
+ *
+ * @return string
+ */
+ public function getConnectionName()
+ {
+ return $this->connectionName;
+ }
+
+ /**
+ * Set connection name
+ *
+ * @param string $connectionName
+ * @return $this
+ */
+ public function setConnectionName($connectionName)
+ {
+ $this->connectionName = $connectionName;
+
+ return $this;
+ }
+
+ /**
+ * Get columns
+ *
+ * @return array
+ */
+ public function getColumns()
+ {
+ return $this->columns;
+ }
+
+ /**
+ * Set columns
+ *
+ * @param array $columns
+ * @return $this
+ */
+ public function setColumns($columns)
+ {
+ $this->columns = $columns;
+
+ return $this;
+ }
+
+ /**
+ * Get filters
+ *
+ * @return array
+ */
+ public function getFilters()
+ {
+ return $this->filters;
+ }
+
+ /**
+ * Set filters
+ *
+ * @param array $filters
+ * @return $this
+ */
+ public function setFilters($filters)
+ {
+ $this->filters = $filters;
+
+ return $this;
+ }
+
+ /**
+ * Get from condition
+ *
+ * @return array
+ */
+ public function getFrom()
+ {
+ return $this->from;
+ }
+
+ /**
+ * Set from condition
+ *
+ * @param array $from
+ * @return $this
+ */
+ public function setFrom($from)
+ {
+ $this->from = $from;
+
+ return $this;
+ }
+
+ /**
+ * Process JOIN conditions
+ *
+ * @param Select $select
+ * @param array $joinConfig
+ * @return Select
+ */
+ private function processJoin(Select $select, $joinConfig)
+ {
+ switch ($joinConfig['link-type']) {
+ case 'left':
+ $select->joinLeft($joinConfig['table'], $joinConfig['condition'], []);
+ break;
+ case 'inner':
+ $select->joinInner($joinConfig['table'], $joinConfig['condition'], []);
+ break;
+ case 'right':
+ $select->joinRight($joinConfig['table'], $joinConfig['condition'], []);
+ break;
+ }
+ return $select;
+ }
+
+ /**
+ * Creates Select object
+ *
+ * @return Select
+ */
+ public function create()
+ {
+ $connection = $this->resourceConnection->getConnection($this->getConnectionName());
+ $select = $connection->select();
+ $select->from($this->getFrom(), []);
+ $select->columns($this->getColumns());
+ foreach ($this->getFilters() as $filter) {
+ $select->where($filter);
+ }
+ foreach ($this->getJoins() as $joinConfig) {
+ $select = $this->processJoin($select, $joinConfig);
+ }
+ if (!empty($this->getGroup())) {
+ $select->group(implode(', ', $this->getGroup()));
+ }
+ return $select;
+ }
+
+ /**
+ * Returns group
+ *
+ * @return array
+ */
+ public function getGroup()
+ {
+ return $this->group;
+ }
+
+ /**
+ * Set group
+ *
+ * @param array $group
+ * @return $this
+ */
+ public function setGroup($group)
+ {
+ $this->group = $group;
+
+ return $this;
+ }
+
+ /**
+ * Get parameters
+ *
+ * @return array
+ */
+ public function getParams()
+ {
+ return $this->params;
+ }
+
+ /**
+ * Set parameters
+ *
+ * @param array $params
+ * @return $this
+ */
+ public function setParams($params)
+ {
+ $this->params = $params;
+
+ return $this;
+ }
+
+ /**
+ * Get having condition
+ *
+ * @return array
+ */
+ public function getHaving()
+ {
+ return $this->having;
+ }
+
+ /**
+ * Set having condition
+ *
+ * @param array $having
+ * @return $this
+ */
+ public function setHaving($having)
+ {
+ $this->having = $having;
+
+ return $this;
+ }
+}
diff --git a/app/code/Magento/Analytics/ReportXml/DB/SelectBuilderFactory.php b/app/code/Magento/Analytics/ReportXml/DB/SelectBuilderFactory.php
new file mode 100644
index 0000000000000..1d88d4618efc5
--- /dev/null
+++ b/app/code/Magento/Analytics/ReportXml/DB/SelectBuilderFactory.php
@@ -0,0 +1,43 @@
+objectManager = $objectManager;
+ }
+
+ /**
+ * Create class instance with specified parameters
+ *
+ * @param array $data
+ * @return SelectBuilder
+ */
+ public function create(array $data = [])
+ {
+ return $this->objectManager->create(SelectBuilder::class, $data);
+ }
+}
diff --git a/app/code/Magento/Analytics/ReportXml/IteratorFactory.php b/app/code/Magento/Analytics/ReportXml/IteratorFactory.php
new file mode 100644
index 0000000000000..a196cef8b66dc
--- /dev/null
+++ b/app/code/Magento/Analytics/ReportXml/IteratorFactory.php
@@ -0,0 +1,61 @@
+objectManager = $objectManager;
+ $this->defaultIteratorName = $defaultIteratorName;
+ }
+
+ /**
+ * Creates instance of the result iterator with the query result as an input
+ * Result iterator can be changed through report configuration
+ *
+ * < ...
+ *
+ * Uses IteratorIterator by default
+ *
+ * @param \Traversable $result
+ * @param string|null $iteratorName
+ * @return \IteratorIterator
+ */
+ public function create(\Traversable $result, $iteratorName = null)
+ {
+ return $this->objectManager->create(
+ $iteratorName ?: $this->defaultIteratorName,
+ [
+ 'iterator' => $result
+ ]
+ );
+ }
+}
diff --git a/app/code/Magento/Analytics/ReportXml/Query.php b/app/code/Magento/Analytics/ReportXml/Query.php
new file mode 100644
index 0000000000000..46ec2fb494183
--- /dev/null
+++ b/app/code/Magento/Analytics/ReportXml/Query.php
@@ -0,0 +1,96 @@
+select = $select;
+ $this->connectionName = $connectionName;
+ $this->selectHydrator = $selectHydrator;
+ $this->config = $config;
+ }
+
+ /**
+ * @return Select
+ */
+ public function getSelect()
+ {
+ return $this->select;
+ }
+
+ /**
+ * @return string
+ */
+ public function getConnectionName()
+ {
+ return $this->connectionName;
+ }
+
+ /**
+ * @return array
+ */
+ public function getConfig()
+ {
+ return $this->config;
+ }
+
+ /**
+ * Specify data which should be serialized to JSON
+ * @link http://php.net/manual/en/jsonserializable.jsonserialize.php
+ * @return mixed data which can be serialized by json_encode ,
+ * which is a value of any type other than a resource.
+ * @since 5.4.0
+ */
+ public function jsonSerialize()
+ {
+ return [
+ 'connectionName' => $this->getConnectionName(),
+ 'select_parts' => $this->selectHydrator->extract($this->getSelect()),
+ 'config' => $this->getConfig()
+ ];
+ }
+}
diff --git a/app/code/Magento/Analytics/ReportXml/QueryFactory.php b/app/code/Magento/Analytics/ReportXml/QueryFactory.php
new file mode 100644
index 0000000000000..8ed7e767b28b3
--- /dev/null
+++ b/app/code/Magento/Analytics/ReportXml/QueryFactory.php
@@ -0,0 +1,142 @@
+config = $config;
+ $this->selectBuilderFactory = $selectBuilderFactory;
+ $this->assemblers = $assemblers;
+ $this->queryCache = $queryCache;
+ $this->objectManager = $objectManager;
+ $this->selectHydrator = $selectHydrator;
+ }
+
+ /**
+ * Returns query connection name according to configuration
+ *
+ * @param string $queryConfig
+ * @return string
+ */
+ private function getQueryConnectionName($queryConfig)
+ {
+ $connectionName = 'default';
+ if (isset($queryConfig['connection'])) {
+ $connectionName = $queryConfig['connection'];
+ }
+ return $connectionName;
+ }
+
+ /**
+ * Create query according to configuration settings
+ *
+ * @param string $queryName
+ * @return Query
+ */
+ private function constructQuery($queryName)
+ {
+ $queryConfig = $this->config->get($queryName);
+ $selectBuilder = $this->selectBuilderFactory->create();
+ $selectBuilder->setConnectionName($this->getQueryConnectionName($queryConfig));
+ foreach ($this->assemblers as $assembler) {
+ $selectBuilder = $assembler->assemble($selectBuilder, $queryConfig);
+ }
+ $select = $selectBuilder->create();
+ return $this->objectManager->create(
+ Query::class,
+ [
+ 'select' => $select,
+ 'selectHydrator' => $this->selectHydrator,
+ 'connectionName' => $selectBuilder->getConnectionName(),
+ 'config' => $queryConfig
+ ]
+ );
+ }
+
+ /**
+ * Creates query by name
+ *
+ * @param string $queryName
+ * @return Query
+ */
+ public function create($queryName)
+ {
+ $cached = $this->queryCache->load($queryName);
+ if ($cached) {
+ $queryData = json_decode($cached, true);
+ return $this->objectManager->create(
+ Query::class,
+ [
+ 'select' => $this->selectHydrator->recreate($queryData['select_parts']),
+ 'selectHydrator' => $this->selectHydrator,
+ 'connectionName' => $queryData['connectionName'],
+ 'config' => $queryData['config']
+ ]
+ );
+ }
+ $query = $this->constructQuery($queryName);
+ $this->queryCache->save(json_encode($query), $queryName);
+ return $query;
+ }
+}
diff --git a/app/code/Magento/Analytics/ReportXml/ReportProvider.php b/app/code/Magento/Analytics/ReportXml/ReportProvider.php
new file mode 100644
index 0000000000000..247be5fa8e4ca
--- /dev/null
+++ b/app/code/Magento/Analytics/ReportXml/ReportProvider.php
@@ -0,0 +1,76 @@
+queryFactory = $queryFactory;
+ $this->connectionFactory = $connectionFactory;
+ $this->iteratorFactory = $iteratorFactory;
+ }
+
+ /**
+ * Returns custom iterator name for report
+ * Null for default
+ *
+ * @param Query $query
+ * @return string|null
+ */
+ private function getIteratorName(Query $query)
+ {
+ $config = $query->getConfig();
+ return $config['iterator'] ?? null;
+ }
+
+ /**
+ * Returns report data by name and criteria
+ *
+ * @param string $name
+ * @return \IteratorIterator
+ */
+ public function getReport($name)
+ {
+ $query = $this->queryFactory->create($name);
+ $connection = $this->connectionFactory->getConnection($query->getConnectionName());
+ $statement = $connection->query($query->getSelect());
+ return $this->iteratorFactory->create($statement, $this->getIteratorName($query));
+ }
+}
diff --git a/app/code/Magento/Analytics/ReportXml/SelectHydrator.php b/app/code/Magento/Analytics/ReportXml/SelectHydrator.php
new file mode 100644
index 0000000000000..02cde2fd0598d
--- /dev/null
+++ b/app/code/Magento/Analytics/ReportXml/SelectHydrator.php
@@ -0,0 +1,143 @@
+resourceConnection = $resourceConnection;
+ $this->objectManager = $objectManager;
+ $this->selectParts = $selectParts;
+ }
+
+ /**
+ * @return array
+ */
+ private function getSelectParts()
+ {
+ return array_merge($this->predefinedSelectParts, $this->selectParts);
+ }
+
+ /**
+ * Extracts Select metadata parts
+ *
+ * @param Select $select
+ * @return array
+ * @throws \Zend_Db_Select_Exception
+ */
+ public function extract(Select $select)
+ {
+ $parts = [];
+ foreach ($this->getSelectParts() as $partName) {
+ $parts[$partName] = $select->getPart($partName);
+ }
+ return $parts;
+ }
+
+ /**
+ * @param array $selectParts
+ * @return Select
+ */
+ public function recreate(array $selectParts)
+ {
+ $select = $this->resourceConnection->getConnection()->select();
+
+ $select = $this->processColumns($select, $selectParts);
+
+ foreach ($selectParts as $partName => $partValue) {
+ $select->setPart($partName, $partValue);
+ }
+
+ return $select;
+ }
+
+ /**
+ * Process COLUMNS part values and add this part into select.
+ *
+ * If each column contains information about select expression
+ * an object with the type of this expression going to be created and assigned to this column.
+ *
+ * @param Select $select
+ * @param array $selectParts
+ * @return Select
+ */
+ private function processColumns(Select $select, array &$selectParts)
+ {
+ if (!empty($selectParts[Select::COLUMNS]) && is_array($selectParts[Select::COLUMNS])) {
+ $part = [];
+
+ foreach ($selectParts[Select::COLUMNS] as $columnEntry) {
+ list($correlationName, $column, $alias) = $columnEntry;
+ if (is_array($column) && !empty($column['class'])) {
+ $expression = $this->objectManager->create(
+ $column['class'],
+ isset($column['arguments']) ? $column['arguments'] : []
+ );
+ $part[] = [$correlationName, $expression, $alias];
+ } else {
+ $part[] = $columnEntry;
+ }
+ }
+
+ $select->setPart(Select::COLUMNS, $part);
+ unset($selectParts[Select::COLUMNS]);
+ }
+
+ return $select;
+ }
+}
diff --git a/app/code/Magento/Analytics/Setup/InstallData.php b/app/code/Magento/Analytics/Setup/InstallData.php
new file mode 100644
index 0000000000000..aaa619bbb0caa
--- /dev/null
+++ b/app/code/Magento/Analytics/Setup/InstallData.php
@@ -0,0 +1,53 @@
+getConnection()->insertMultiple(
+ $setup->getTable('core_config_data'),
+ [
+ [
+ 'scope' => 'default',
+ 'scope_id' => 0,
+ 'path' => 'analytics/subscription/enabled',
+ 'value' => 1
+ ],
+ [
+ 'scope' => 'default',
+ 'scope_id' => 0,
+ 'path' => SubscriptionHandler::CRON_STRING_PATH,
+ 'value' => join(' ', SubscriptionHandler::CRON_EXPR_ARRAY)
+ ]
+ ]
+ );
+
+ $setup->getConnection()->insert(
+ $setup->getTable('flag'),
+ [
+ 'flag_code' => SubscriptionHandler::ATTEMPTS_REVERSE_COUNTER_FLAG_CODE,
+ 'state' => 0,
+ 'flag_data' => 24,
+ ]
+ );
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Mftf/LICENSE.txt b/app/code/Magento/Analytics/Test/Mftf/LICENSE.txt
new file mode 100644
index 0000000000000..49525fd99da9c
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Mftf/LICENSE.txt
@@ -0,0 +1,48 @@
+
+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/Analytics/Test/Mftf/LICENSE_AFL.txt b/app/code/Magento/Analytics/Test/Mftf/LICENSE_AFL.txt
new file mode 100644
index 0000000000000..f39d641b18a19
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Mftf/LICENSE_AFL.txt
@@ -0,0 +1,48 @@
+
+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/Analytics/Test/Mftf/README.md b/app/code/Magento/Analytics/Test/Mftf/README.md
new file mode 100644
index 0000000000000..cdeb48941e6a4
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Mftf/README.md
@@ -0,0 +1,3 @@
+# Analytics Functional Tests
+
+The Functional Test Module for **Magento Analytics** module.
diff --git a/app/code/Magento/Analytics/Test/Unit/Block/Adminhtml/System/Config/AdditionalCommentTest.php b/app/code/Magento/Analytics/Test/Unit/Block/Adminhtml/System/Config/AdditionalCommentTest.php
new file mode 100644
index 0000000000000..407e323aeaae6
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Block/Adminhtml/System/Config/AdditionalCommentTest.php
@@ -0,0 +1,77 @@
+abstractElementMock = $this->getMockBuilder(AbstractElement::class)
+ ->setMethods(['getComment', 'getLabel'])
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->contextMock = $this->getMockBuilder(Context::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->formMock = $this->getMockBuilder(Form::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $objectManager = new ObjectManager($this);
+ $this->additionalComment = $objectManager->getObject(
+ AdditionalComment::class,
+ [
+ 'context' => $this->contextMock
+ ]
+ );
+ }
+
+ public function testRender()
+ {
+ $this->abstractElementMock->setForm($this->formMock);
+ $this->abstractElementMock->expects($this->any())
+ ->method('getComment')
+ ->willReturn('New comment');
+ $this->abstractElementMock->expects($this->any())
+ ->method('getLabel')
+ ->willReturn('Comment label');
+ $html = $this->additionalComment->render($this->abstractElementMock);
+ $this->assertRegExp(
+ "/New comment/",
+ $html
+ );
+ $this->assertRegExp(
+ "/Comment label/",
+ $html
+ );
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Block/Adminhtml/System/Config/CollectionTimeLabelTest.php b/app/code/Magento/Analytics/Test/Unit/Block/Adminhtml/System/Config/CollectionTimeLabelTest.php
new file mode 100644
index 0000000000000..54612076a757f
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Block/Adminhtml/System/Config/CollectionTimeLabelTest.php
@@ -0,0 +1,81 @@
+abstractElementMock = $this->getMockBuilder(AbstractElement::class)
+ ->setMethods(['getComment'])
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->contextMock = $this->getMockBuilder(Context::class)
+ ->setMethods(['getLocaleDate'])
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->formMock = $this->getMockBuilder(Form::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->timeZoneMock = $this->getMockBuilder(TimezoneInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->contextMock->expects($this->any())
+ ->method('getLocaleDate')
+ ->willReturn($this->timeZoneMock);
+
+ $objectManager = new ObjectManager($this);
+ $this->collectionTimeLabel = $objectManager->getObject(
+ CollectionTimeLabel::class,
+ [
+ 'context' => $this->contextMock
+ ]
+ );
+ }
+
+ public function testRender()
+ {
+ $timeZone = "America/New_York";
+ $this->abstractElementMock->setForm($this->formMock);
+ $this->timeZoneMock->expects($this->once())
+ ->method('getConfigTimezone')
+ ->willReturn($timeZone);
+ $this->abstractElementMock->expects($this->any())
+ ->method('getComment')
+ ->willReturn('Eastern Standard Time (America/New_York)');
+ $this->assertRegExp(
+ "/Eastern Standard Time \(America\/New_York\)/",
+ $this->collectionTimeLabel->render($this->abstractElementMock)
+ );
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Block/Adminhtml/System/Config/SubscriptionStatusLabelTest.php b/app/code/Magento/Analytics/Test/Unit/Block/Adminhtml/System/Config/SubscriptionStatusLabelTest.php
new file mode 100644
index 0000000000000..0806187ebac01
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Block/Adminhtml/System/Config/SubscriptionStatusLabelTest.php
@@ -0,0 +1,85 @@
+subscriptionStatusProviderMock = $this->getMockBuilder(SubscriptionStatusProvider::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->contextMock = $this->getMockBuilder(Context::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->abstractElementMock = $this->getMockBuilder(AbstractElement::class)
+ ->setMethods(['getComment'])
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->formMock = $this->getMockBuilder(Form::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $objectManager = new ObjectManager($this);
+ $this->subscriptionStatusLabel = $objectManager->getObject(
+ SubscriptionStatusLabel::class,
+ [
+ 'context' => $this->contextMock,
+ 'subscriptionStatusProvider' => $this->subscriptionStatusProviderMock
+ ]
+ );
+ }
+
+ public function testRender()
+ {
+ $this->abstractElementMock->setForm($this->formMock);
+ $this->subscriptionStatusProviderMock->expects($this->once())
+ ->method('getStatus')
+ ->willReturn('Enabled');
+ $this->abstractElementMock->expects($this->any())
+ ->method('getComment')
+ ->willReturn('Subscription status: Enabled');
+ $this->assertRegExp(
+ "/Subscription status: Enabled/",
+ $this->subscriptionStatusLabel->render($this->abstractElementMock)
+ );
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Block/Adminhtml/System/Config/VerticalTest.php b/app/code/Magento/Analytics/Test/Unit/Block/Adminhtml/System/Config/VerticalTest.php
new file mode 100644
index 0000000000000..6a0cecc781062
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Block/Adminhtml/System/Config/VerticalTest.php
@@ -0,0 +1,77 @@
+abstractElementMock = $this->getMockBuilder(AbstractElement::class)
+ ->setMethods(['getComment', 'getLabel', 'getHint'])
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->contextMock = $this->getMockBuilder(Context::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->formMock = $this->getMockBuilder(Form::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $objectManager = new ObjectManager($this);
+ $this->vertical = $objectManager->getObject(
+ Vertical::class,
+ [
+ 'context' => $this->contextMock
+ ]
+ );
+ }
+
+ public function testRender()
+ {
+ $this->abstractElementMock->setForm($this->formMock);
+ $this->abstractElementMock->expects($this->any())
+ ->method('getComment')
+ ->willReturn('New comment');
+ $this->abstractElementMock->expects($this->any())
+ ->method('getHint')
+ ->willReturn('New hint');
+ $html = $this->vertical->render($this->abstractElementMock);
+ $this->assertRegExp(
+ "/New comment/",
+ $html
+ );
+ $this->assertRegExp(
+ "/New hint/",
+ $html
+ );
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Controller/Adminhtml/BIEssentials/SignUpTest.php b/app/code/Magento/Analytics/Test/Unit/Controller/Adminhtml/BIEssentials/SignUpTest.php
new file mode 100644
index 0000000000000..6f613cdc4d639
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Controller/Adminhtml/BIEssentials/SignUpTest.php
@@ -0,0 +1,84 @@
+configMock = $this->getMockBuilder(ScopeConfigInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->resultRedirectFactoryMock = $this->getMockBuilder(RedirectFactory::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->redirectMock = $this->getMockBuilder(Redirect::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->objectManagerHelper = new ObjectManagerHelper($this);
+
+ $this->signUpController = $this->objectManagerHelper->getObject(
+ SignUp::class,
+ [
+ 'config' => $this->configMock,
+ 'resultRedirectFactory' => $this->resultRedirectFactoryMock
+ ]
+ );
+ }
+
+ /**
+ * @return void
+ */
+ public function testExecute()
+ {
+ $urlBIEssentialsConfigPath = 'analytics/url/bi_essentials';
+ $this->configMock->expects($this->once())
+ ->method('getValue')
+ ->with($urlBIEssentialsConfigPath)
+ ->willReturn('value');
+ $this->resultRedirectFactoryMock->expects($this->once())->method('create')->willReturn($this->redirectMock);
+ $this->redirectMock->expects($this->once())->method('setUrl')->with('value')->willReturnSelf();
+ $this->assertEquals($this->redirectMock, $this->signUpController->execute());
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Controller/Adminhtml/Reports/ShowTest.php b/app/code/Magento/Analytics/Test/Unit/Controller/Adminhtml/Reports/ShowTest.php
new file mode 100644
index 0000000000000..4f54ce5059965
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Controller/Adminhtml/Reports/ShowTest.php
@@ -0,0 +1,185 @@
+reportUrlProviderMock = $this->getMockBuilder(ReportUrlProvider::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->resultFactoryMock = $this->getMockBuilder(ResultFactory::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->redirectMock = $this->getMockBuilder(Redirect::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->messageManagerMock = $this->getMockBuilder(ManagerInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->objectManagerHelper = new ObjectManagerHelper($this);
+
+ $this->showController = $this->objectManagerHelper->getObject(
+ Show::class,
+ [
+ 'reportUrlProvider' => $this->reportUrlProviderMock,
+ 'resultFactory' => $this->resultFactoryMock,
+ 'messageManager' => $this->messageManagerMock,
+ ]
+ );
+ }
+
+ /**
+ * @return void
+ */
+ public function testExecute()
+ {
+ $otpUrl = 'http://example.com?otp=15vbjcfdvd15645';
+
+ $this->resultFactoryMock
+ ->expects($this->once())
+ ->method('create')
+ ->with(ResultFactory::TYPE_REDIRECT)
+ ->willReturn($this->redirectMock);
+ $this->reportUrlProviderMock
+ ->expects($this->once())
+ ->method('getUrl')
+ ->with()
+ ->willReturn($otpUrl);
+ $this->redirectMock
+ ->expects($this->once())
+ ->method('setUrl')
+ ->with($otpUrl)
+ ->willReturnSelf();
+ $this->assertSame($this->redirectMock, $this->showController->execute());
+ }
+
+ /**
+ * @dataProvider executeWithExceptionDataProvider
+ *
+ * @param \Exception $exception
+ */
+ public function testExecuteWithException(\Exception $exception)
+ {
+
+ $this->resultFactoryMock
+ ->expects($this->once())
+ ->method('create')
+ ->with(ResultFactory::TYPE_REDIRECT)
+ ->willReturn($this->redirectMock);
+ $this->reportUrlProviderMock
+ ->expects($this->once())
+ ->method('getUrl')
+ ->with()
+ ->willThrowException($exception);
+ if ($exception instanceof LocalizedException) {
+ $message = $exception->getMessage();
+ } else {
+ $message = __('Sorry, there has been an error processing your request. Please try again later.');
+ }
+ $this->messageManagerMock
+ ->expects($this->once())
+ ->method('addExceptionMessage')
+ ->with($exception, $message)
+ ->willReturnSelf();
+ $this->redirectMock
+ ->expects($this->once())
+ ->method('setPath')
+ ->with('adminhtml')
+ ->willReturnSelf();
+ $this->assertSame($this->redirectMock, $this->showController->execute());
+ }
+
+ /**
+ * @return array
+ */
+ public function executeWithExceptionDataProvider()
+ {
+ return [
+ 'ExecuteWithLocalizedException' => [new LocalizedException(__('TestMessage'))],
+ 'ExecuteWithException' => [new \Exception('TestMessage')],
+ ];
+ }
+
+ /**
+ * @return void
+ */
+ public function testExecuteWithSubscriptionUpdateException()
+ {
+ $exception = new SubscriptionUpdateException(__('TestMessage'));
+ $this->resultFactoryMock
+ ->expects($this->once())
+ ->method('create')
+ ->with(ResultFactory::TYPE_REDIRECT)
+ ->willReturn($this->redirectMock);
+ $this->reportUrlProviderMock
+ ->expects($this->once())
+ ->method('getUrl')
+ ->with()
+ ->willThrowException($exception);
+ $this->messageManagerMock
+ ->expects($this->once())
+ ->method('addNoticeMessage')
+ ->with($exception->getMessage())
+ ->willReturnSelf();
+ $this->redirectMock
+ ->expects($this->once())
+ ->method('setPath')
+ ->with('adminhtml')
+ ->willReturnSelf();
+ $this->assertSame($this->redirectMock, $this->showController->execute());
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Controller/Adminhtml/Subscription/RetryTest.php b/app/code/Magento/Analytics/Test/Unit/Controller/Adminhtml/Subscription/RetryTest.php
new file mode 100644
index 0000000000000..17c485a8df230
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Controller/Adminhtml/Subscription/RetryTest.php
@@ -0,0 +1,159 @@
+resultFactoryMock = $this->getMockBuilder(ResultFactory::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->resultRedirectMock = $this->getMockBuilder(Redirect::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->subscriptionHandlerMock = $this->getMockBuilder(SubscriptionHandler::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->messageManagerMock = $this->getMockBuilder(ManagerInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->objectManagerHelper = new ObjectManagerHelper($this);
+
+ $this->retryController = $this->objectManagerHelper->getObject(
+ Retry::class,
+ [
+ 'resultFactory' => $this->resultFactoryMock,
+ 'subscriptionHandler' => $this->subscriptionHandlerMock,
+ 'messageManager' => $this->messageManagerMock,
+ ]
+ );
+ }
+
+ /**
+ * @return void
+ */
+ public function testExecute()
+ {
+ $this->resultFactoryMock
+ ->expects($this->once())
+ ->method('create')
+ ->with(ResultFactory::TYPE_REDIRECT)
+ ->willReturn($this->resultRedirectMock);
+ $this->resultRedirectMock
+ ->expects($this->once())
+ ->method('setPath')
+ ->with('adminhtml')
+ ->willReturnSelf();
+ $this->subscriptionHandlerMock
+ ->expects($this->once())
+ ->method('processEnabled')
+ ->with()
+ ->willReturn(true);
+ $this->assertSame(
+ $this->resultRedirectMock,
+ $this->retryController->execute()
+ );
+ }
+
+ /**
+ * @dataProvider executeExceptionsDataProvider
+ *
+ * @param \Exception $exception
+ * @param Phrase $message
+ */
+ public function testExecuteWithException(\Exception $exception, Phrase $message)
+ {
+ $this->resultFactoryMock
+ ->expects($this->once())
+ ->method('create')
+ ->with(ResultFactory::TYPE_REDIRECT)
+ ->willReturn($this->resultRedirectMock);
+ $this->resultRedirectMock
+ ->expects($this->once())
+ ->method('setPath')
+ ->with('adminhtml')
+ ->willReturnSelf();
+ $this->subscriptionHandlerMock
+ ->expects($this->once())
+ ->method('processEnabled')
+ ->with()
+ ->willThrowException($exception);
+ $this->messageManagerMock
+ ->expects($this->once())
+ ->method('addExceptionMessage')
+ ->with($exception, $message);
+
+ $this->assertSame(
+ $this->resultRedirectMock,
+ $this->retryController->execute()
+ );
+ }
+
+ /**
+ * @return array
+ */
+ public function executeExceptionsDataProvider()
+ {
+ return [
+ [new LocalizedException(__('TestMessage')), __('TestMessage')],
+ [
+ new \Exception('TestMessage'),
+ __('Sorry, there has been an error processing your request. Please try again later.')
+ ],
+ ];
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Cron/CollectDataTest.php b/app/code/Magento/Analytics/Test/Unit/Cron/CollectDataTest.php
new file mode 100644
index 0000000000000..81c57d79033c8
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Cron/CollectDataTest.php
@@ -0,0 +1,91 @@
+exportDataHandlerMock = $this->getMockBuilder(ExportDataHandlerInterface::class)
+ ->getMockForAbstractClass();
+
+ $this->subscriptionStatusMock = $this->getMockBuilder(SubscriptionStatusProvider::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->objectManagerHelper = new ObjectManagerHelper($this);
+
+ $this->collectData = $this->objectManagerHelper->getObject(
+ CollectData::class,
+ [
+ 'exportDataHandler' => $this->exportDataHandlerMock,
+ 'subscriptionStatus' => $this->subscriptionStatusMock,
+ ]
+ );
+ }
+
+ /**
+ * @param string $status
+ * @return void
+ * @dataProvider executeDataProvider
+ */
+ public function testExecute($status)
+ {
+ $this->subscriptionStatusMock
+ ->expects($this->once())
+ ->method('getStatus')
+ ->with()
+ ->willReturn($status);
+ $this->exportDataHandlerMock
+ ->expects(($status === SubscriptionStatusProvider::ENABLED) ? $this->once() : $this->never())
+ ->method('prepareExportData')
+ ->with();
+
+ $this->assertTrue($this->collectData->execute());
+ }
+
+ /**
+ * @return array
+ */
+ public function executeDataProvider()
+ {
+ return [
+ 'Subscription is enabled' => [SubscriptionStatusProvider::ENABLED],
+ 'Subscription is disabled' => [SubscriptionStatusProvider::DISABLED],
+ ];
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Cron/SignUpTest.php b/app/code/Magento/Analytics/Test/Unit/Cron/SignUpTest.php
new file mode 100644
index 0000000000000..959a11f9e1058
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Cron/SignUpTest.php
@@ -0,0 +1,133 @@
+connectorMock = $this->getMockBuilder(Connector::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->configWriterMock = $this->getMockBuilder(WriterInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->flagManagerMock = $this->getMockBuilder(FlagManager::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->reinitableConfigMock = $this->getMockBuilder(ReinitableConfigInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->signUp = new SignUp(
+ $this->connectorMock,
+ $this->configWriterMock,
+ $this->flagManagerMock,
+ $this->reinitableConfigMock
+ );
+ }
+
+ public function testExecute()
+ {
+ $attemptsCount = 10;
+
+ $this->flagManagerMock->expects($this->once())
+ ->method('getFlagData')
+ ->with(SubscriptionHandler::ATTEMPTS_REVERSE_COUNTER_FLAG_CODE)
+ ->willReturn($attemptsCount);
+
+ $attemptsCount -= 1;
+ $this->flagManagerMock->expects($this->once())
+ ->method('saveFlag')
+ ->with(SubscriptionHandler::ATTEMPTS_REVERSE_COUNTER_FLAG_CODE, $attemptsCount);
+ $this->connectorMock->expects($this->once())
+ ->method('execute')
+ ->with('signUp')
+ ->willReturn(true);
+ $this->addDeleteAnalyticsCronExprAsserts();
+ $this->flagManagerMock->expects($this->once())
+ ->method('deleteFlag')
+ ->with(SubscriptionHandler::ATTEMPTS_REVERSE_COUNTER_FLAG_CODE);
+ $this->assertTrue($this->signUp->execute());
+ }
+
+ public function testExecuteFlagNotExist()
+ {
+ $this->flagManagerMock->expects($this->once())
+ ->method('getFlagData')
+ ->with(SubscriptionHandler::ATTEMPTS_REVERSE_COUNTER_FLAG_CODE)
+ ->willReturn(null);
+ $this->addDeleteAnalyticsCronExprAsserts();
+ $this->assertFalse($this->signUp->execute());
+ }
+
+ public function testExecuteZeroAttempts()
+ {
+ $attemptsCount = 0;
+ $this->flagManagerMock->expects($this->once())
+ ->method('getFlagData')
+ ->with(SubscriptionHandler::ATTEMPTS_REVERSE_COUNTER_FLAG_CODE)
+ ->willReturn($attemptsCount);
+ $this->addDeleteAnalyticsCronExprAsserts();
+ $this->flagManagerMock->expects($this->once())
+ ->method('deleteFlag')
+ ->with(SubscriptionHandler::ATTEMPTS_REVERSE_COUNTER_FLAG_CODE);
+ $this->assertFalse($this->signUp->execute());
+ }
+
+ /**
+ * Add assertions for method deleteAnalyticsCronExpr.
+ *
+ * @return void
+ */
+ private function addDeleteAnalyticsCronExprAsserts()
+ {
+ $this->configWriterMock
+ ->expects($this->once())
+ ->method('delete')
+ ->with(SubscriptionHandler::CRON_STRING_PATH)
+ ->willReturn(true);
+ $this->reinitableConfigMock
+ ->expects($this->once())
+ ->method('reinit')
+ ->willReturnSelf();
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Cron/UpdateTest.php b/app/code/Magento/Analytics/Test/Unit/Cron/UpdateTest.php
new file mode 100644
index 0000000000000..ede53d8783a7a
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Cron/UpdateTest.php
@@ -0,0 +1,214 @@
+connectorMock = $this->getMockBuilder(Connector::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->configWriterMock = $this->getMockBuilder(WriterInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->flagManagerMock = $this->getMockBuilder(FlagManager::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->reinitableConfigMock = $this->getMockBuilder(ReinitableConfigInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->analyticsTokenMock = $this->getMockBuilder(AnalyticsToken::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->update = new Update(
+ $this->connectorMock,
+ $this->configWriterMock,
+ $this->reinitableConfigMock,
+ $this->flagManagerMock,
+ $this->analyticsTokenMock
+ );
+ }
+
+ /**
+ * @return void
+ */
+ public function testExecuteWithoutToken()
+ {
+ $this->flagManagerMock
+ ->method('getFlagData')
+ ->with(SubscriptionUpdateHandler::SUBSCRIPTION_UPDATE_REVERSE_COUNTER_FLAG_CODE)
+ ->willReturn(10);
+ $this->connectorMock
+ ->expects($this->once())
+ ->method('execute')
+ ->with('update')
+ ->willReturn(false);
+ $this->analyticsTokenMock
+ ->expects($this->once())
+ ->method('isTokenExist')
+ ->willReturn(false);
+ $this->addFinalOutputAsserts();
+ $this->assertFalse($this->update->execute());
+ }
+
+ /**
+ * @param bool $isExecuted
+ */
+ private function addFinalOutputAsserts(bool $isExecuted = true)
+ {
+ $this->flagManagerMock
+ ->expects($this->exactly(2 * $isExecuted))
+ ->method('deleteFlag')
+ ->withConsecutive(
+ [SubscriptionUpdateHandler::SUBSCRIPTION_UPDATE_REVERSE_COUNTER_FLAG_CODE],
+ [SubscriptionUpdateHandler::PREVIOUS_BASE_URL_FLAG_CODE]
+ );
+ $this->configWriterMock
+ ->expects($this->exactly((int)$isExecuted))
+ ->method('delete')
+ ->with(SubscriptionUpdateHandler::UPDATE_CRON_STRING_PATH);
+ $this->reinitableConfigMock
+ ->expects($this->exactly((int)$isExecuted))
+ ->method('reinit')
+ ->with();
+ }
+
+ /**
+ * @param $counterData
+ * @return void
+ * @dataProvider executeWithEmptyReverseCounterDataProvider
+ */
+ public function testExecuteWithEmptyReverseCounter($counterData)
+ {
+ $this->flagManagerMock
+ ->method('getFlagData')
+ ->with(SubscriptionUpdateHandler::SUBSCRIPTION_UPDATE_REVERSE_COUNTER_FLAG_CODE)
+ ->willReturn($counterData);
+ $this->connectorMock
+ ->expects($this->never())
+ ->method('execute')
+ ->with('update')
+ ->willReturn(false);
+ $this->analyticsTokenMock
+ ->method('isTokenExist')
+ ->willReturn(true);
+ $this->addFinalOutputAsserts();
+ $this->assertFalse($this->update->execute());
+ }
+
+ /**
+ * Provides empty states of the reverse counter.
+ *
+ * @return array
+ */
+ public function executeWithEmptyReverseCounterDataProvider()
+ {
+ return [
+ [null],
+ [0]
+ ];
+ }
+
+ /**
+ * @param int $reverseCount
+ * @param bool $commandResult
+ * @param bool $finalConditionsIsExpected
+ * @param bool $functionResult
+ * @return void
+ * @dataProvider executeRegularScenarioDataProvider
+ */
+ public function testExecuteRegularScenario(
+ int $reverseCount,
+ bool $commandResult,
+ bool $finalConditionsIsExpected,
+ bool $functionResult
+ ) {
+ $this->flagManagerMock
+ ->method('getFlagData')
+ ->with(SubscriptionUpdateHandler::SUBSCRIPTION_UPDATE_REVERSE_COUNTER_FLAG_CODE)
+ ->willReturn($reverseCount);
+ $this->connectorMock
+ ->expects($this->once())
+ ->method('execute')
+ ->with('update')
+ ->willReturn($commandResult);
+ $this->analyticsTokenMock
+ ->method('isTokenExist')
+ ->willReturn(true);
+ $this->addFinalOutputAsserts($finalConditionsIsExpected);
+ $this->assertSame($functionResult, $this->update->execute());
+ }
+
+ /**
+ * @return array
+ */
+ public function executeRegularScenarioDataProvider()
+ {
+ return [
+ 'The last attempt with command execution result False' => [
+ 'Reverse count' => 1,
+ 'Command result' => false,
+ 'Executed final output conditions' => true,
+ 'Function result' => false,
+ ],
+ 'Not the last attempt with command execution result False' => [
+ 'Reverse count' => 10,
+ 'Command result' => false,
+ 'Executed final output conditions' => false,
+ 'Function result' => false,
+ ],
+ 'Command execution result True' => [
+ 'Reverse count' => 10,
+ 'Command result' => true,
+ 'Executed final output conditions' => true,
+ 'Function result' => true,
+ ],
+ ];
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Model/AnalyticsTokenTest.php b/app/code/Magento/Analytics/Test/Unit/Model/AnalyticsTokenTest.php
new file mode 100644
index 0000000000000..57315543bc32d
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Model/AnalyticsTokenTest.php
@@ -0,0 +1,129 @@
+reinitableConfigMock = $this->getMockBuilder(ReinitableConfigInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->configMock = $this->getMockBuilder(ScopeConfigInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->configWriterMock = $this->getMockBuilder(WriterInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->objectManagerHelper = new ObjectManagerHelper($this);
+
+ $this->tokenModel = $this->objectManagerHelper->getObject(
+ AnalyticsToken::class,
+ [
+ 'reinitableConfig' => $this->reinitableConfigMock,
+ 'config' => $this->configMock,
+ 'configWriter' => $this->configWriterMock,
+ 'tokenPath' => $this->tokenPath,
+ ]
+ );
+ }
+
+ /**
+ * @return void
+ */
+ public function testStoreToken()
+ {
+ $value = 'jjjj0000';
+
+ $this->configWriterMock
+ ->expects($this->once())
+ ->method('save')
+ ->with($this->tokenPath, $value);
+
+ $this->reinitableConfigMock
+ ->expects($this->once())
+ ->method('reinit')
+ ->willReturnSelf();
+
+ $this->assertTrue($this->tokenModel->storeToken($value));
+ }
+
+ /**
+ * @return void
+ */
+ public function testGetToken()
+ {
+ $value = 'jjjj0000';
+
+ $this->configMock
+ ->expects($this->once())
+ ->method('getValue')
+ ->with($this->tokenPath)
+ ->willReturn($value);
+
+ $this->assertSame($value, $this->tokenModel->getToken());
+ }
+
+ /**
+ * @return void
+ */
+ public function testIsTokenExist()
+ {
+ $this->assertFalse($this->tokenModel->isTokenExist());
+
+ $this->configMock
+ ->expects($this->once())
+ ->method('getValue')
+ ->with($this->tokenPath)
+ ->willReturn('0000');
+ $this->assertTrue($this->tokenModel->isTokenExist());
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Model/Config/Backend/Baseurl/SubscriptionUpdateHandlerTest.php b/app/code/Magento/Analytics/Test/Unit/Model/Config/Backend/Baseurl/SubscriptionUpdateHandlerTest.php
new file mode 100644
index 0000000000000..f5f721c038c57
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Model/Config/Backend/Baseurl/SubscriptionUpdateHandlerTest.php
@@ -0,0 +1,178 @@
+reinitableConfigMock = $this->getMockBuilder(ReinitableConfigInterface::class)
+ ->disableOriginalConstructor()
+ ->getMockForAbstractClass();
+
+ $this->analyticsTokenMock = $this->getMockBuilder(AnalyticsToken::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->flagManagerMock = $this->getMockBuilder(FlagManager::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->configWriterMock = $this->getMockBuilder(WriterInterface::class)
+ ->disableOriginalConstructor()
+ ->getMockForAbstractClass();
+
+ $this->objectManagerHelper = new ObjectManagerHelper($this);
+
+ $this->subscriptionUpdateHandler = $this->objectManagerHelper->getObject(
+ SubscriptionUpdateHandler::class,
+ [
+ 'reinitableConfig' => $this->reinitableConfigMock,
+ 'analyticsToken' => $this->analyticsTokenMock,
+ 'flagManager' => $this->flagManagerMock,
+ 'configWriter' => $this->configWriterMock,
+ ]
+ );
+ }
+
+ /**
+ * @return void
+ */
+ public function testTokenDoesNotExist()
+ {
+ $this->analyticsTokenMock
+ ->expects($this->once())
+ ->method('isTokenExist')
+ ->with()
+ ->willReturn(false);
+ $this->flagManagerMock
+ ->expects($this->never())
+ ->method('saveFlag');
+ $this->configWriterMock
+ ->expects($this->never())
+ ->method('save');
+ $this->assertTrue($this->subscriptionUpdateHandler->processUrlUpdate('http://store.com'));
+ }
+
+ /**
+ * @return void
+ */
+ public function testTokenAndPreviousBaseUrlExist()
+ {
+ $url = 'https://store.com';
+ $this->analyticsTokenMock
+ ->expects($this->once())
+ ->method('isTokenExist')
+ ->with()
+ ->willReturn(true);
+ $this->flagManagerMock
+ ->expects($this->once())
+ ->method('getFlagData')
+ ->with(SubscriptionUpdateHandler::PREVIOUS_BASE_URL_FLAG_CODE)
+ ->willReturn(true);
+ $this->flagManagerMock
+ ->expects($this->once())
+ ->method('saveFlag')
+ ->withConsecutive(
+ [SubscriptionUpdateHandler::SUBSCRIPTION_UPDATE_REVERSE_COUNTER_FLAG_CODE, $this->attemptsInitValue],
+ [SubscriptionUpdateHandler::PREVIOUS_BASE_URL_FLAG_CODE, $url]
+ );
+ $this->configWriterMock
+ ->expects($this->once())
+ ->method('save')
+ ->with(SubscriptionUpdateHandler::UPDATE_CRON_STRING_PATH, $this->cronExpression);
+ $this->reinitableConfigMock
+ ->expects($this->once())
+ ->method('reinit')
+ ->with();
+ $this->assertTrue($this->subscriptionUpdateHandler->processUrlUpdate($url));
+ }
+
+ /**
+ * @return void
+ */
+ public function testTokenExistAndWithoutPreviousBaseUrl()
+ {
+ $url = 'https://store.com';
+ $this->analyticsTokenMock
+ ->expects($this->once())
+ ->method('isTokenExist')
+ ->with()
+ ->willReturn(true);
+ $this->flagManagerMock
+ ->expects($this->once())
+ ->method('getFlagData')
+ ->with(SubscriptionUpdateHandler::PREVIOUS_BASE_URL_FLAG_CODE)
+ ->willReturn(false);
+ $this->flagManagerMock
+ ->expects($this->exactly(2))
+ ->method('saveFlag')
+ ->withConsecutive(
+ [SubscriptionUpdateHandler::PREVIOUS_BASE_URL_FLAG_CODE, $url],
+ [SubscriptionUpdateHandler::SUBSCRIPTION_UPDATE_REVERSE_COUNTER_FLAG_CODE, $this->attemptsInitValue]
+ );
+ $this->configWriterMock
+ ->expects($this->once())
+ ->method('save')
+ ->with(SubscriptionUpdateHandler::UPDATE_CRON_STRING_PATH, $this->cronExpression);
+ $this->reinitableConfigMock
+ ->expects($this->once())
+ ->method('reinit')
+ ->with();
+ $this->assertTrue($this->subscriptionUpdateHandler->processUrlUpdate($url));
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Model/Config/Backend/CollectionTimeTest.php b/app/code/Magento/Analytics/Test/Unit/Model/Config/Backend/CollectionTimeTest.php
new file mode 100644
index 0000000000000..071b96111ac8b
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Model/Config/Backend/CollectionTimeTest.php
@@ -0,0 +1,111 @@
+configWriterMock = $this->getMockBuilder(WriterInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->loggerMock = $this->getMockBuilder(LoggerInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->objectManagerHelper = new ObjectManagerHelper($this);
+
+ $this->collectionTime = $this->objectManagerHelper->getObject(
+ CollectionTime::class,
+ [
+ 'configWriter' => $this->configWriterMock,
+ '_logger' => $this->loggerMock,
+ ]
+ );
+ }
+
+ /**
+ * @return void
+ */
+ public function testAfterSave()
+ {
+ $this->collectionTime->setData('value', '05,04,03');
+
+ $this->configWriterMock
+ ->expects($this->once())
+ ->method('save')
+ ->with(CollectionTime::CRON_SCHEDULE_PATH, join(' ', ['04', '05', '*', '*', '*']));
+
+ $this->assertInstanceOf(
+ Value::class,
+ $this->collectionTime->afterSave()
+ );
+ }
+
+ /**
+ * @return void
+ * @expectedException \Magento\Framework\Exception\LocalizedException
+ */
+ public function testAfterSaveWrongValue()
+ {
+ $this->collectionTime->setData('value', '00,01');
+ $this->collectionTime->afterSave();
+ }
+
+ /**
+ * @return void
+ * @expectedException \Magento\Framework\Exception\LocalizedException
+ */
+ public function testAfterSaveWithLocalizedException()
+ {
+ $exception = new \Exception('Test message');
+ $this->collectionTime->setData('value', '05,04,03');
+
+ $this->configWriterMock
+ ->expects($this->once())
+ ->method('save')
+ ->with(CollectionTime::CRON_SCHEDULE_PATH, join(' ', ['04', '05', '*', '*', '*']))
+ ->willThrowException($exception);
+ $this->loggerMock
+ ->expects($this->once())
+ ->method('error')
+ ->with($exception->getMessage());
+ $this->collectionTime->afterSave();
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Model/Config/Backend/Enabled/SubscriptionHandlerTest.php b/app/code/Magento/Analytics/Test/Unit/Model/Config/Backend/Enabled/SubscriptionHandlerTest.php
new file mode 100644
index 0000000000000..82aa4dc72dfe0
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Model/Config/Backend/Enabled/SubscriptionHandlerTest.php
@@ -0,0 +1,152 @@
+flagManagerMock = $this->getMockBuilder(FlagManager::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->configWriterMock = $this->getMockBuilder(WriterInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->tokenMock = $this->getMockBuilder(AnalyticsToken::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->objectManagerHelper = new ObjectManagerHelper($this);
+
+ $this->subscriptionHandler = $this->objectManagerHelper->getObject(
+ SubscriptionHandler::class,
+ [
+ 'flagManager' => $this->flagManagerMock,
+ 'configWriter' => $this->configWriterMock,
+ 'attemptsInitValue' => $this->attemptsInitValue,
+ 'analyticsToken' => $this->tokenMock,
+ ]
+ );
+ }
+
+ public function testProcessEnabledTokenExist()
+ {
+ $this->tokenMock
+ ->expects($this->once())
+ ->method('isTokenExist')
+ ->willReturn(true);
+ $this->configWriterMock
+ ->expects($this->never())
+ ->method('save');
+ $this->flagManagerMock
+ ->expects($this->never())
+ ->method('saveFlag');
+ $this->assertTrue(
+ $this->subscriptionHandler->processEnabled()
+ );
+ }
+
+ public function testProcessEnabledTokenDoesNotExist()
+ {
+ $this->tokenMock
+ ->expects($this->once())
+ ->method('isTokenExist')
+ ->willReturn(false);
+ $this->configWriterMock
+ ->expects($this->once())
+ ->method('save')
+ ->with(SubscriptionHandler::CRON_STRING_PATH, "0 * * * *");
+ $this->flagManagerMock
+ ->expects($this->once())
+ ->method('saveFlag')
+ ->with(SubscriptionHandler::ATTEMPTS_REVERSE_COUNTER_FLAG_CODE, $this->attemptsInitValue)
+ ->willReturn(true);
+ $this->assertTrue(
+ $this->subscriptionHandler->processEnabled()
+ );
+ }
+
+ public function testProcessDisabledTokenDoesNotExist()
+ {
+ $this->configWriterMock
+ ->expects($this->once())
+ ->method('delete')
+ ->with(CollectionTime::CRON_SCHEDULE_PATH);
+ $this->tokenMock
+ ->expects($this->once())
+ ->method('isTokenExist')
+ ->willReturn(false);
+ $this->flagManagerMock
+ ->expects($this->once())
+ ->method('deleteFlag')
+ ->with(SubscriptionHandler::ATTEMPTS_REVERSE_COUNTER_FLAG_CODE)
+ ->willReturn(true);
+ $this->assertTrue(
+ $this->subscriptionHandler->processDisabled()
+ );
+ }
+
+ public function testProcessDisabledTokenExists()
+ {
+ $this->configWriterMock
+ ->expects($this->once())
+ ->method('delete')
+ ->with(CollectionTime::CRON_SCHEDULE_PATH);
+ $this->tokenMock
+ ->expects($this->once())
+ ->method('isTokenExist')
+ ->willReturn(true);
+ $this->flagManagerMock
+ ->expects($this->never())
+ ->method('deleteFlag');
+ $this->assertTrue(
+ $this->subscriptionHandler->processDisabled()
+ );
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Model/Config/Backend/EnabledTest.php b/app/code/Magento/Analytics/Test/Unit/Model/Config/Backend/EnabledTest.php
new file mode 100644
index 0000000000000..eea3193258bc6
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Model/Config/Backend/EnabledTest.php
@@ -0,0 +1,184 @@
+subscriptionHandlerMock = $this->getMockBuilder(SubscriptionHandler::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->loggerMock = $this->getMockBuilder(LoggerInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->configMock = $this->getMockBuilder(ScopeConfigInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->objectManagerHelper = new ObjectManagerHelper($this);
+
+ $this->enabledModel = $this->objectManagerHelper->getObject(
+ Enabled::class,
+ [
+ 'subscriptionHandler' => $this->subscriptionHandlerMock,
+ '_logger' => $this->loggerMock,
+ 'config' => $this->configMock,
+ ]
+ );
+ }
+
+ /**
+ * @return void
+ */
+ public function testAfterSaveSuccessEnabled()
+ {
+ $this->enabledModel->setData('value', $this->valueEnabled);
+
+ $this->configMock
+ ->expects($this->any())
+ ->method('getValue')
+ ->willReturn(!$this->valueEnabled);
+
+ $this->subscriptionHandlerMock
+ ->expects($this->once())
+ ->method('processEnabled')
+ ->with()
+ ->willReturn(true);
+
+ $this->assertInstanceOf(
+ Value::class,
+ $this->enabledModel->afterSave()
+ );
+ }
+
+ /**
+ * @return void
+ */
+ public function testAfterSaveSuccessDisabled()
+ {
+ $this->enabledModel->setData('value', $this->valueDisabled);
+
+ $this->configMock
+ ->expects($this->any())
+ ->method('getValue')
+ ->willReturn(!$this->valueDisabled);
+
+ $this->subscriptionHandlerMock
+ ->expects($this->once())
+ ->method('processDisabled')
+ ->with()
+ ->willReturn(true);
+
+ $this->assertInstanceOf(
+ Value::class,
+ $this->enabledModel->afterSave()
+ );
+ }
+
+ /**
+ * @return void
+ */
+ public function testAfterSaveSuccessValueNotChanged()
+ {
+ $this->enabledModel->setData('value', null);
+
+ $this->configMock
+ ->expects($this->any())
+ ->method('getValue')
+ ->willReturn(null);
+
+ $this->subscriptionHandlerMock
+ ->expects($this->never())
+ ->method('processEnabled')
+ ->with()
+ ->willReturn(true);
+ $this->subscriptionHandlerMock
+ ->expects($this->never())
+ ->method('processDisabled')
+ ->with()
+ ->willReturn(true);
+
+ $this->assertInstanceOf(
+ Value::class,
+ $this->enabledModel->afterSave()
+ );
+ }
+
+ /**
+ * @return void
+ * @expectedException \Magento\Framework\Exception\LocalizedException
+ */
+ public function testExecuteAfterSaveFailedWithLocalizedException()
+ {
+ $exception = new \Exception('Message');
+ $this->enabledModel->setData('value', $this->valueEnabled);
+
+ $this->subscriptionHandlerMock
+ ->expects($this->once())
+ ->method('processEnabled')
+ ->with()
+ ->willThrowException($exception);
+
+ $this->loggerMock
+ ->expects($this->once())
+ ->method('error')
+ ->with($exception->getMessage());
+
+ $this->enabledModel->afterSave();
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Model/Config/Backend/VerticalTest.php b/app/code/Magento/Analytics/Test/Unit/Model/Config/Backend/VerticalTest.php
new file mode 100644
index 0000000000000..6fe7d0aa93998
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Model/Config/Backend/VerticalTest.php
@@ -0,0 +1,59 @@
+objectManagerHelper =
+ new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this);
+
+ $this->subject = $this->objectManagerHelper->getObject(
+ \Magento\Analytics\Model\Config\Backend\Vertical::class
+ );
+ }
+
+ /**
+ * @return void
+ */
+ public function testBeforeSaveSuccess()
+ {
+ $this->subject->setValue('Apps and Games');
+
+ $this->assertInstanceOf(
+ \Magento\Analytics\Model\Config\Backend\Vertical::class,
+ $this->subject->beforeSave()
+ );
+ }
+
+ /**
+ * @return void
+ * @expectedException \Magento\Framework\Exception\LocalizedException
+ */
+ public function testBeforeSaveFailedWithLocalizedException()
+ {
+ $this->subject->setValue('');
+
+ $this->subject->beforeSave();
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Model/Config/MapperTest.php b/app/code/Magento/Analytics/Test/Unit/Model/Config/MapperTest.php
new file mode 100644
index 0000000000000..0b7f4870dbac8
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Model/Config/MapperTest.php
@@ -0,0 +1,142 @@
+objectManagerHelper = new ObjectManagerHelper($this);
+
+ $this->mapper = $this->objectManagerHelper->getObject(Mapper::class);
+ }
+
+ /**
+ * @param array $configData
+ * @param array $resultData
+ * @return void
+ *
+ * @dataProvider executingDataProvider
+ */
+ public function testExecution($configData, $resultData)
+ {
+ $this->assertSame($resultData, $this->mapper->execute($configData));
+ }
+
+ /**
+ * @return array
+ */
+ public function executingDataProvider()
+ {
+ return [
+ 'wrongConfig' => [
+ ['config' => ['files']],
+ []
+ ],
+ 'validConfigWithFileNodes' => [
+ [
+ 'config' => [
+ 0 => [
+ 'file' => [
+ 0 => [
+ 'name' => 'fileName',
+ 'providers' => [[]]
+ ]
+ ]
+ ]
+ ]
+ ],
+ [
+ 'fileName' => [
+ 'name' => 'fileName',
+ 'providers' => []
+ ]
+ ],
+ ],
+ 'validConfigWithProvidersNode' => [
+ [
+ 'config' => [
+ 0 => [
+ 'file' => [
+ 0 => [
+ 'name' => 'fileName',
+ 'providers' => [
+ 0 => [
+ 'reportProvider' => [0 => []]
+ ]
+ ]
+ ]
+ ]
+ ]
+ ]
+ ],
+ [
+ 'fileName' => [
+ 'name' => 'fileName',
+ 'providers' => [
+ 'reportProvider' => ['parameters' => []]
+ ]
+ ]
+ ],
+ ],
+ 'validConfigWithParametersNode' => [
+ [
+ 'config' => [
+ 0 => [
+ 'file' => [
+ 0 => [
+ 'name' => 'fileName',
+ 'providers' => [
+ 0 => [
+ 'reportProvider' => [
+ 0 => [
+ 'parameters' => [
+ 0 => ['name' => ['reportName']]
+ ]
+ ]
+ ]
+ ]
+ ]
+ ]
+ ]
+ ]
+ ]
+ ],
+ [
+ 'fileName' => [
+ 'name' => 'fileName',
+ 'providers' => [
+ 'reportProvider' => [
+ 'parameters' => [
+ 'name' => 'reportName'
+ ]
+ ]
+ ]
+ ]
+ ],
+ ],
+ ];
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Model/Config/ReaderTest.php b/app/code/Magento/Analytics/Test/Unit/Model/Config/ReaderTest.php
new file mode 100644
index 0000000000000..6aa9c7ef3106c
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Model/Config/ReaderTest.php
@@ -0,0 +1,108 @@
+mapperMock = $this->getMockBuilder(Mapper::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->readerXmlMock = $this->getMockBuilder(ReaderInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->readerDbMock = $this->getMockBuilder(ReaderInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->objectManagerHelper = new ObjectManagerHelper($this);
+
+ $this->reader = $this->objectManagerHelper->getObject(
+ Reader::class,
+ [
+ 'mapper' => $this->mapperMock,
+ 'readers' => [
+ $this->readerXmlMock,
+ $this->readerDbMock,
+ ],
+ ]
+ );
+ }
+
+ /**
+ * @return void
+ */
+ public function testRead()
+ {
+ $scope = 'store';
+ $xmlReaderResult = [
+ 'config' => ['node1' => ['node2' => 'node4']]
+ ];
+ $dbReaderResult = [
+ 'config' => ['node1' => ['node2' => 'node3']]
+ ];
+ $mapperResult = ['node2' => ['node3', 'node4']];
+
+ $this->readerXmlMock
+ ->expects($this->once())
+ ->method('read')
+ ->with($scope)
+ ->willReturn($xmlReaderResult);
+
+ $this->readerDbMock
+ ->expects($this->once())
+ ->method('read')
+ ->with($scope)
+ ->willReturn($dbReaderResult);
+
+ $this->mapperMock
+ ->expects($this->once())
+ ->method('execute')
+ ->with(array_merge_recursive($xmlReaderResult, $dbReaderResult))
+ ->willReturn($mapperResult);
+
+ $this->assertSame($mapperResult, $this->reader->read($scope));
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Model/Config/Source/VerticalTest.php b/app/code/Magento/Analytics/Test/Unit/Model/Config/Source/VerticalTest.php
new file mode 100644
index 0000000000000..c13205d34f25b
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Model/Config/Source/VerticalTest.php
@@ -0,0 +1,60 @@
+objectManagerHelper =
+ new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this);
+
+ $this->subject = $this->objectManagerHelper->getObject(
+ \Magento\Analytics\Model\Config\Source\Vertical::class,
+ [
+ 'verticals' => [
+ 'Apps and Games',
+ 'Athletic/Sporting Goods',
+ 'Art and Design'
+ ]
+ ]
+ );
+ }
+
+ /**
+ * @return void
+ */
+ public function testToOptionArray()
+ {
+ $expectedOptionsArray = [
+ ['value' => '', 'label' => __('--Please Select--')],
+ ['value' => 'Apps and Games', 'label' => __('Apps and Games')],
+ ['value' => 'Athletic/Sporting Goods', 'label' => __('Athletic/Sporting Goods')],
+ ['value' => 'Art and Design', 'label' => __('Art and Design')]
+ ];
+
+ $this->assertEquals(
+ $expectedOptionsArray,
+ $this->subject->toOptionArray()
+ );
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Model/ConfigTest.php b/app/code/Magento/Analytics/Test/Unit/Model/ConfigTest.php
new file mode 100644
index 0000000000000..8739219ebdf09
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Model/ConfigTest.php
@@ -0,0 +1,68 @@
+dataInterfaceMock = $this->getMockBuilder(DataInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->objectManagerHelper = new ObjectManagerHelper($this);
+
+ $this->config = $this->objectManagerHelper->getObject(
+ Config::class,
+ [
+ 'data' => $this->dataInterfaceMock,
+ ]
+ );
+ }
+
+ /**
+ * @return void
+ */
+ public function testGet()
+ {
+ $key = 'configKey';
+ $defaultValue = 'mock';
+ $configValue = 'emptyString';
+
+ $this->dataInterfaceMock
+ ->expects($this->once())
+ ->method('get')
+ ->with($key, $defaultValue)
+ ->willReturn($configValue);
+
+ $this->assertSame($configValue, $this->config->get($key, $defaultValue));
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Model/Connector/Http/Client/CurlTest.php b/app/code/Magento/Analytics/Test/Unit/Model/Connector/Http/Client/CurlTest.php
new file mode 100644
index 0000000000000..f8f3919b2489e
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Model/Connector/Http/Client/CurlTest.php
@@ -0,0 +1,218 @@
+curlMock = $this->getMockBuilder(
+ \Magento\Framework\HTTP\Adapter\Curl::class
+ )
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->loggerMock = $this->getMockBuilder(
+ \Psr\Log\LoggerInterface::class
+ )
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->curlFactoryMock = $this->getMockBuilder(CurlFactory::class)
+ ->setMethods(['create'])
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->curlFactoryMock->expects($this->any())
+ ->method('create')
+ ->willReturn($this->curlMock);
+
+ $this->responseFactoryMock = $this->getMockBuilder(
+ \Magento\Analytics\Model\Connector\Http\ResponseFactory::class
+ )
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->converterMock = $this->createJsonConverter();
+
+ $this->objectManagerHelper =
+ new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this);
+
+ $this->subject = $this->objectManagerHelper->getObject(
+ \Magento\Analytics\Model\Connector\Http\Client\Curl::class,
+ [
+ 'curlFactory' => $this->curlFactoryMock,
+ 'responseFactory' => $this->responseFactoryMock,
+ 'converter' => $this->converterMock,
+ 'logger' => $this->loggerMock,
+ ]
+ );
+ }
+
+ /**
+ * Returns test parameters for request.
+ *
+ * @return array
+ */
+ public function getTestData()
+ {
+ return [
+ [
+ 'data' => [
+ 'version' => '1.1',
+ 'body'=> ['name' => 'value'],
+ 'url' => 'http://www.mystore.com',
+ 'headers' => [JsonConverter::CONTENT_TYPE_HEADER],
+ 'method' => \Magento\Framework\HTTP\ZendClient::POST,
+ ]
+ ]
+ ];
+ }
+
+ /**
+ * @return void
+ * @dataProvider getTestData
+ */
+ public function testRequestSuccess(array $data)
+ {
+ $responseString = 'This is response.';
+ $response = new \Zend_Http_Response(201, [], $responseString);
+ $this->curlMock->expects($this->once())
+ ->method('write')
+ ->with(
+ $data['method'],
+ $data['url'],
+ $data['version'],
+ $data['headers'],
+ json_encode($data['body'])
+ );
+ $this->curlMock->expects($this->once())
+ ->method('read')
+ ->willReturn($responseString);
+ $this->curlMock->expects($this->any())
+ ->method('getErrno')
+ ->willReturn(0);
+
+ $this->responseFactoryMock->expects($this->any())
+ ->method('create')
+ ->with($responseString)
+ ->willReturn($response);
+
+ $this->assertEquals(
+ $response,
+ $this->subject->request(
+ $data['method'],
+ $data['url'],
+ $data['body'],
+ $data['headers'],
+ $data['version']
+ )
+ );
+ }
+
+ /**
+ * @return void
+ * @dataProvider getTestData
+ */
+ public function testRequestError(array $data)
+ {
+ $response = new \Zend_Http_Response(0, []);
+ $this->curlMock->expects($this->once())
+ ->method('write')
+ ->with(
+ $data['method'],
+ $data['url'],
+ $data['version'],
+ $data['headers'],
+ json_encode($data['body'])
+ );
+ $this->curlMock->expects($this->once())
+ ->method('read');
+ $this->curlMock->expects($this->atLeastOnce())
+ ->method('getErrno')
+ ->willReturn(1);
+ $this->curlMock->expects($this->atLeastOnce())
+ ->method('getError')
+ ->willReturn('CURL error.');
+
+ $this->loggerMock->expects($this->once())
+ ->method('critical')
+ ->with(
+ new \Exception(
+ 'MBI service CURL connection error #1: CURL error.'
+ )
+ );
+
+ $this->assertEquals(
+ $response,
+ $this->subject->request(
+ $data['method'],
+ $data['url'],
+ $data['body'],
+ $data['headers'],
+ $data['version']
+ )
+ );
+ }
+
+ /**
+ * @return \PHPUnit_Framework_MockObject_MockObject
+ */
+ private function createJsonConverter()
+ {
+ $converterMock = $this->getMockBuilder(ConverterInterface::class)
+ ->getMockForAbstractClass();
+ $converterMock->expects($this->any())->method('toBody')->willReturnCallback(function ($value) {
+ return json_encode($value);
+ });
+ $converterMock->expects($this->any())
+ ->method('getContentTypeHeader')
+ ->willReturn(JsonConverter::CONTENT_TYPE_HEADER);
+ return $converterMock;
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Model/Connector/Http/JsonConverterTest.php b/app/code/Magento/Analytics/Test/Unit/Model/Connector/Http/JsonConverterTest.php
new file mode 100644
index 0000000000000..60a19f3d5079e
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Model/Connector/Http/JsonConverterTest.php
@@ -0,0 +1,34 @@
+assertEquals(JsonConverter::CONTENT_TYPE_HEADER, $converter->getContentTypeHeader());
+ }
+
+ public function testConvertBody()
+ {
+ $body = '{"token": "secret-token"}';
+ $converter = new JsonConverter();
+ $this->assertEquals(json_decode($body, 1), $converter->fromBody($body));
+ }
+
+ public function testConvertData()
+ {
+ $data = ["token" => "secret-token"];
+ $converter = new JsonConverter();
+ $this->assertEquals(json_encode($data), $converter->toBody($data));
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Model/Connector/Http/ResponseResolverTest.php b/app/code/Magento/Analytics/Test/Unit/Model/Connector/Http/ResponseResolverTest.php
new file mode 100644
index 0000000000000..7c3c484843285
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Model/Connector/Http/ResponseResolverTest.php
@@ -0,0 +1,39 @@
+ 'testValue'];
+ $response = new \Zend_Http_Response(201, [], json_encode($expectedBody));
+ $responseHandlerMock = $this->getMockBuilder(ResponseHandlerInterface::class)
+ ->getMockForAbstractClass();
+ $responseHandlerMock->expects($this->once())
+ ->method('handleResponse')
+ ->with($expectedBody)
+ ->willReturn(true);
+ $notFoundResponseHandlerMock = $this->getMockBuilder(ResponseHandlerInterface::class)
+ ->getMockForAbstractClass();
+ $notFoundResponseHandlerMock->expects($this->never())->method('handleResponse');
+ $responseResolver = new ResponseResolver(
+ new JsonConverter(),
+ [
+ 201 => $responseHandlerMock,
+ 404 => $notFoundResponseHandlerMock,
+ ]
+ );
+ $this->assertTrue($responseResolver->getResult($response));
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Model/Connector/NotifyDataChangedCommandTest.php b/app/code/Magento/Analytics/Test/Unit/Model/Connector/NotifyDataChangedCommandTest.php
new file mode 100644
index 0000000000000..cee3877631c2e
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Model/Connector/NotifyDataChangedCommandTest.php
@@ -0,0 +1,107 @@
+analyticsTokenMock = $this->getMockBuilder(AnalyticsToken::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->httpClientMock = $this->getMockBuilder(ClientInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->configMock = $this->getMockBuilder(ScopeConfigInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->loggerMock = $this->getMockBuilder(LoggerInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $successHandler = $this->getMockBuilder(\Magento\Analytics\Model\Connector\Http\ResponseHandlerInterface::class)
+ ->getMockForAbstractClass();
+ $successHandler->method('handleResponse')
+ ->willReturn(true);
+
+ $this->notifyDataChangedCommand = new NotifyDataChangedCommand(
+ $this->analyticsTokenMock,
+ $this->httpClientMock,
+ $this->configMock,
+ new ResponseResolver(new JsonConverter(), [201 => $successHandler]),
+ $this->loggerMock
+ );
+ }
+
+ public function testExecuteSuccess()
+ {
+ $configVal = "Config val";
+ $token = "Secret token!";
+ $this->analyticsTokenMock->expects($this->once())
+ ->method('isTokenExist')
+ ->willReturn(true);
+ $this->configMock->expects($this->any())
+ ->method('getValue')
+ ->willReturn($configVal);
+ $this->analyticsTokenMock->expects($this->once())
+ ->method('getToken')
+ ->willReturn($token);
+ $this->httpClientMock->expects($this->once())
+ ->method('request')
+ ->with(
+ ZendClient::POST,
+ $configVal,
+ ['access-token' => $token, 'url' => $configVal]
+ )->willReturn(new \Zend_Http_Response(201, []));
+ $this->assertTrue($this->notifyDataChangedCommand->execute());
+ }
+
+ public function testExecuteWithoutToken()
+ {
+ $this->analyticsTokenMock->expects($this->once())
+ ->method('isTokenExist')
+ ->willReturn(false);
+ $this->httpClientMock->expects($this->never())
+ ->method('request');
+ $this->assertFalse($this->notifyDataChangedCommand->execute());
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Model/Connector/OTPRequestTest.php b/app/code/Magento/Analytics/Test/Unit/Model/Connector/OTPRequestTest.php
new file mode 100644
index 0000000000000..8a3f4efb15cf4
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Model/Connector/OTPRequestTest.php
@@ -0,0 +1,187 @@
+loggerMock = $this->getMockBuilder(LoggerInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->configMock = $this->getMockBuilder(ScopeConfigInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->httpClientMock = $this->getMockBuilder(ClientInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->analyticsTokenMock = $this->getMockBuilder(AnalyticsToken::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->responseResolverMock = $this->getMockBuilder(ResponseResolver::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->subject = new OTPRequest(
+ $this->analyticsTokenMock,
+ $this->httpClientMock,
+ $this->configMock,
+ $this->responseResolverMock,
+ $this->loggerMock
+ );
+ }
+
+ /**
+ * Returns test parameters for request.
+ *
+ * @return array
+ */
+ private function getTestData()
+ {
+ return [
+ 'otp' => 'thisisotp',
+ 'url' => 'http://www.mystore.com',
+ 'access-token' => 'thisisaccesstoken',
+ 'method' => \Magento\Framework\HTTP\ZendClient::POST,
+ 'body'=> ['access-token' => 'thisisaccesstoken','url' => 'http://www.mystore.com'],
+ ];
+ }
+
+ /**
+ * @return void
+ */
+ public function testCallSuccess()
+ {
+ $data = $this->getTestData();
+
+ $this->analyticsTokenMock->expects($this->once())
+ ->method('isTokenExist')
+ ->willReturn(true);
+ $this->analyticsTokenMock->expects($this->once())
+ ->method('getToken')
+ ->willReturn($data['access-token']);
+
+ $this->configMock->expects($this->any())
+ ->method('getValue')
+ ->willReturn($data['url']);
+
+ $this->httpClientMock->expects($this->once())
+ ->method('request')
+ ->with(
+ $data['method'],
+ $data['url'],
+ $data['body']
+ )
+ ->willReturn(new \Zend_Http_Response(201, []));
+ $this->responseResolverMock->expects($this->once())
+ ->method('getResult')
+ ->willReturn($data['otp']);
+
+ $this->assertEquals(
+ $data['otp'],
+ $this->subject->call()
+ );
+ }
+
+ /**
+ * @return void
+ */
+ public function testCallNoAccessToken()
+ {
+ $this->analyticsTokenMock->expects($this->once())
+ ->method('isTokenExist')
+ ->willReturn(false);
+
+ $this->httpClientMock->expects($this->never())
+ ->method('request');
+
+ $this->assertFalse($this->subject->call());
+ }
+
+ /**
+ * @return void
+ */
+ public function testCallNoOtp()
+ {
+ $data = $this->getTestData();
+
+ $this->analyticsTokenMock->expects($this->once())
+ ->method('isTokenExist')
+ ->willReturn(true);
+ $this->analyticsTokenMock->expects($this->once())
+ ->method('getToken')
+ ->willReturn($data['access-token']);
+
+ $this->configMock->expects($this->any())
+ ->method('getValue')
+ ->willReturn($data['url']);
+
+ $this->httpClientMock->expects($this->once())
+ ->method('request')
+ ->with(
+ $data['method'],
+ $data['url'],
+ $data['body']
+ )
+ ->willReturn(new \Zend_Http_Response(0, []));
+
+ $this->responseResolverMock->expects($this->once())
+ ->method('getResult')
+ ->willReturn(false);
+
+ $this->loggerMock->expects($this->once())
+ ->method('warning');
+
+ $this->assertFalse($this->subject->call());
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Model/Connector/ResponseHandler/OTPTest.php b/app/code/Magento/Analytics/Test/Unit/Model/Connector/ResponseHandler/OTPTest.php
new file mode 100644
index 0000000000000..0ff36cca5db2d
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Model/Connector/ResponseHandler/OTPTest.php
@@ -0,0 +1,22 @@
+assertFalse($OTPHandler->handleResponse([]));
+ $expectedOtp = 123;
+ $this->assertEquals($expectedOtp, $OTPHandler->handleResponse(['otp' => $expectedOtp]));
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Model/Connector/ResponseHandler/ReSignUpTest.php b/app/code/Magento/Analytics/Test/Unit/Model/Connector/ResponseHandler/ReSignUpTest.php
new file mode 100644
index 0000000000000..707003149bcfd
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Model/Connector/ResponseHandler/ReSignUpTest.php
@@ -0,0 +1,36 @@
+getMockBuilder(AnalyticsToken::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $analyticsToken->expects($this->once())
+ ->method('storeToken')
+ ->with(null);
+ $subscriptionHandler = $this->getMockBuilder(SubscriptionHandler::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $subscriptionStatusProvider = $this->getMockBuilder(SubscriptionStatusProvider::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $subscriptionStatusProvider->method('getStatus')->willReturn(SubscriptionStatusProvider::ENABLED);
+ $reSignUpHandler = new ReSignUp($analyticsToken, $subscriptionHandler, $subscriptionStatusProvider);
+ $this->assertFalse($reSignUpHandler->handleResponse([]));
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Model/Connector/ResponseHandler/SignUpTest.php b/app/code/Magento/Analytics/Test/Unit/Model/Connector/ResponseHandler/SignUpTest.php
new file mode 100644
index 0000000000000..81711cfc56950
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Model/Connector/ResponseHandler/SignUpTest.php
@@ -0,0 +1,30 @@
+getMockBuilder(AnalyticsToken::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $analyticsToken->expects($this->once())
+ ->method('storeToken')
+ ->with($accessToken);
+ $signUpHandler = new SignUp($analyticsToken, new JsonConverter());
+ $this->assertFalse($signUpHandler->handleResponse([]));
+ $this->assertEquals($accessToken, $signUpHandler->handleResponse(['access-token' => $accessToken]));
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Model/Connector/ResponseHandler/UpdateTest.php b/app/code/Magento/Analytics/Test/Unit/Model/Connector/ResponseHandler/UpdateTest.php
new file mode 100644
index 0000000000000..7779357e8bea7
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Model/Connector/ResponseHandler/UpdateTest.php
@@ -0,0 +1,20 @@
+assertTrue($updateHandler->handleResponse([]));
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Model/Connector/SignUpCommandTest.php b/app/code/Magento/Analytics/Test/Unit/Model/Connector/SignUpCommandTest.php
new file mode 100644
index 0000000000000..5593496a957b7
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Model/Connector/SignUpCommandTest.php
@@ -0,0 +1,174 @@
+analyticsTokenMock = $this->getMockBuilder(AnalyticsToken::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->integrationManagerMock = $this->getMockBuilder(IntegrationManager::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->integrationToken = $this->getMockBuilder(IntegrationToken::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->configMock = $this->getMockBuilder(ScopeConfigInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->httpClientMock = $this->getMockBuilder(ClientInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->loggerMock = $this->getMockBuilder(LoggerInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->responseResolverMock = $this->getMockBuilder(ResponseResolver::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->signUpCommand = new SignUpCommand(
+ $this->analyticsTokenMock,
+ $this->integrationManagerMock,
+ $this->configMock,
+ $this->httpClientMock,
+ $this->loggerMock,
+ $this->responseResolverMock
+ );
+ }
+
+ public function testExecuteSuccess()
+ {
+ $this->integrationManagerMock->expects($this->once())
+ ->method('generateToken')
+ ->willReturn($this->integrationToken);
+ $this->integrationManagerMock->expects($this->once())
+ ->method('activateIntegration')
+ ->willReturn(true);
+ $data = $this->getTestData();
+
+ $this->configMock->expects($this->any())
+ ->method('getValue')
+ ->willReturn($data['url']);
+ $this->integrationToken->expects($this->any())
+ ->method('getData')
+ ->with('token')
+ ->willReturn($data['integration-token']);
+ $httpResponse = new \Zend_Http_Response(201, [], '{"access-token": "' . $data['access-token'] . '"}');
+ $this->httpClientMock->expects($this->once())
+ ->method('request')
+ ->with(
+ $data['method'],
+ $data['url'],
+ $data['body']
+ )
+ ->willReturn($httpResponse);
+ $this->responseResolverMock->expects($this->any())
+ ->method('getResult')
+ ->with($httpResponse)
+ ->willReturn(true);
+ $this->assertTrue($this->signUpCommand->execute());
+ }
+
+ public function testExecuteFailureCannotGenerateToken()
+ {
+ $this->integrationManagerMock->expects($this->once())
+ ->method('generateToken')
+ ->willReturn(false);
+ $this->integrationManagerMock->expects($this->never())
+ ->method('activateIntegration');
+ $this->assertFalse($this->signUpCommand->execute());
+ }
+
+ public function testExecuteFailureResponseIsEmpty()
+ {
+ $this->integrationManagerMock->expects($this->once())
+ ->method('generateToken')
+ ->willReturn($this->integrationToken);
+ $this->integrationManagerMock->expects($this->once())
+ ->method('activateIntegration')
+ ->willReturn(true);
+ $httpResponse = new \Zend_Http_Response(0, []);
+ $this->httpClientMock->expects($this->once())
+ ->method('request')
+ ->willReturn($httpResponse);
+ $this->responseResolverMock->expects($this->any())
+ ->method('getResult')
+ ->willReturn(false);
+ $this->assertFalse($this->signUpCommand->execute());
+ }
+
+ /**
+ * Returns test parameters for request.
+ *
+ * @return array
+ */
+ private function getTestData()
+ {
+ return [
+ 'url' => 'http://www.mystore.com',
+ 'access-token' => 'thisisaccesstoken',
+ 'integration-token' => 'thisisintegrationtoken',
+ 'headers' => [JsonConverter::CONTENT_TYPE_HEADER],
+ 'method' => \Magento\Framework\HTTP\ZendClient::POST,
+ 'body'=> ['token' => 'thisisintegrationtoken','url' => 'http://www.mystore.com'],
+ ];
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Model/Connector/UpdateCommandTest.php b/app/code/Magento/Analytics/Test/Unit/Model/Connector/UpdateCommandTest.php
new file mode 100644
index 0000000000000..47253a13530e5
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Model/Connector/UpdateCommandTest.php
@@ -0,0 +1,143 @@
+analyticsTokenMock = $this->getMockBuilder(AnalyticsToken::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->httpClientMock = $this->getMockBuilder(ClientInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->configMock = $this->getMockBuilder(ScopeConfigInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->loggerMock = $this->getMockBuilder(LoggerInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->flagManagerMock = $this->getMockBuilder(FlagManager::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->responseResolverMock = $this->getMockBuilder(ResponseResolver::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->updateCommand = new UpdateCommand(
+ $this->analyticsTokenMock,
+ $this->httpClientMock,
+ $this->configMock,
+ $this->loggerMock,
+ $this->flagManagerMock,
+ $this->responseResolverMock
+ );
+ }
+
+ public function testExecuteSuccess()
+ {
+ $url = "old.localhost.com";
+ $configVal = "Config val";
+ $token = "Secret token!";
+ $this->analyticsTokenMock->expects($this->once())
+ ->method('isTokenExist')
+ ->willReturn(true);
+
+ $this->configMock->expects($this->any())
+ ->method('getValue')
+ ->willReturn($configVal);
+
+ $this->flagManagerMock->expects($this->once())
+ ->method('getFlagData')
+ ->with(SubscriptionUpdateHandler::PREVIOUS_BASE_URL_FLAG_CODE)
+ ->willReturn($url);
+
+ $this->analyticsTokenMock->expects($this->once())
+ ->method('getToken')
+ ->willReturn($token);
+
+ $this->httpClientMock->expects($this->once())
+ ->method('request')
+ ->with(
+ ZendClient::PUT,
+ $configVal,
+ [
+ 'url' => $url,
+ 'new-url' => $configVal,
+ 'access-token' => $token
+ ]
+ )->willReturn(new \Zend_Http_Response(200, []));
+
+ $this->responseResolverMock->expects($this->once())
+ ->method('getResult')
+ ->willReturn(true);
+
+ $this->assertTrue($this->updateCommand->execute());
+ }
+
+ public function testExecuteWithoutToken()
+ {
+ $this->analyticsTokenMock->expects($this->once())
+ ->method('isTokenExist')
+ ->willReturn(false);
+
+ $this->assertFalse($this->updateCommand->execute());
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Model/ConnectorTest.php b/app/code/Magento/Analytics/Test/Unit/Model/ConnectorTest.php
new file mode 100644
index 0000000000000..4414b81cbc183
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Model/ConnectorTest.php
@@ -0,0 +1,70 @@
+objectManagerMock = $this->getMockBuilder(ObjectManagerInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->signUpCommandMock = $this->getMockBuilder(SignUpCommand::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->commands = ['signUp' => SignUpCommand::class];
+ $this->connector = new Connector($this->commands, $this->objectManagerMock);
+ }
+
+ public function testExecute()
+ {
+ $commandName = 'signUp';
+ $this->objectManagerMock->expects($this->once())
+ ->method('create')
+ ->with($this->commands[$commandName])
+ ->willReturn($this->signUpCommandMock);
+ $this->signUpCommandMock->expects($this->once())
+ ->method('execute')
+ ->willReturn(true);
+ $this->assertTrue($this->connector->execute($commandName));
+ }
+
+ /**
+ * @expectedException \Magento\Framework\Exception\NotFoundException
+ */
+ public function testExecuteCommandNotFound()
+ {
+ $commandName = 'register';
+ $this->connector->execute($commandName);
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Model/CryptographerTest.php b/app/code/Magento/Analytics/Test/Unit/Model/CryptographerTest.php
new file mode 100644
index 0000000000000..a896c309b4007
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Model/CryptographerTest.php
@@ -0,0 +1,226 @@
+analyticsTokenMock = $this->getMockBuilder(AnalyticsToken::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->encodedContextFactoryMock = $this->getMockBuilder(EncodedContextFactory::class)
+ ->setMethods(['create'])
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->encodedContextMock = $this->getMockBuilder(EncodedContext::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->key = '';
+ $this->source = '';
+ $this->initializationVectors = [];
+
+ $this->objectManagerHelper = new ObjectManagerHelper($this);
+
+ $this->cryptographer = $this->objectManagerHelper->getObject(
+ Cryptographer::class,
+ [
+ 'analyticsToken' => $this->analyticsTokenMock,
+ 'encodedContextFactory' => $this->encodedContextFactoryMock,
+ 'cipherMethod' => $this->cipherMethod,
+ ]
+ );
+ }
+
+ /**
+ * @return void
+ */
+ public function testEncode()
+ {
+ $token = 'some-token-value';
+ $this->source = 'Some text';
+ $this->key = hash('sha256', $token);
+
+ $checkEncodedContext = function ($parameters) {
+ $emptyRequiredParameters =
+ array_diff(['content', 'initializationVector'], array_keys(array_filter($parameters)));
+ if ($emptyRequiredParameters) {
+ return false;
+ }
+
+ $encryptedData = openssl_encrypt(
+ $this->source,
+ $this->cipherMethod,
+ $this->key,
+ OPENSSL_RAW_DATA,
+ $parameters['initializationVector']
+ );
+
+ return ($encryptedData === $parameters['content']);
+ };
+
+ $this->analyticsTokenMock
+ ->expects($this->once())
+ ->method('getToken')
+ ->with()
+ ->willReturn($token);
+
+ $this->encodedContextFactoryMock
+ ->expects($this->once())
+ ->method('create')
+ ->with($this->callback($checkEncodedContext))
+ ->willReturn($this->encodedContextMock);
+
+ $this->assertSame($this->encodedContextMock, $this->cryptographer->encode($this->source));
+ }
+
+ /**
+ * @return void
+ */
+ public function testEncodeUniqueInitializationVector()
+ {
+ $this->source = 'Some text';
+ $token = 'some-token-value';
+
+ $registerInitializationVector = function ($parameters) {
+ if (empty($parameters['initializationVector'])) {
+ return false;
+ }
+
+ $this->initializationVectors[] = $parameters['initializationVector'];
+
+ return true;
+ };
+
+ $this->analyticsTokenMock
+ ->expects($this->exactly(2))
+ ->method('getToken')
+ ->with()
+ ->willReturn($token);
+
+ $this->encodedContextFactoryMock
+ ->expects($this->exactly(2))
+ ->method('create')
+ ->with($this->callback($registerInitializationVector))
+ ->willReturn($this->encodedContextMock);
+
+ $this->assertSame($this->encodedContextMock, $this->cryptographer->encode($this->source));
+ $this->assertSame($this->encodedContextMock, $this->cryptographer->encode($this->source));
+ $this->assertCount(2, array_unique($this->initializationVectors));
+ }
+
+ /**
+ * @expectedException \Magento\Framework\Exception\LocalizedException
+ * @dataProvider encodeNotValidSourceDataProvider
+ */
+ public function testEncodeNotValidSource($source)
+ {
+ $this->cryptographer->encode($source);
+ }
+
+ /**
+ * @return array
+ */
+ public function encodeNotValidSourceDataProvider()
+ {
+ return [
+ 'Array' => [[]],
+ 'Empty string' => [''],
+ ];
+ }
+
+ /**
+ * @expectedException \Magento\Framework\Exception\LocalizedException
+ */
+ public function testEncodeNotValidCipherMethod()
+ {
+ $source = 'Some string';
+ $cryptographer = $this->objectManagerHelper->getObject(
+ Cryptographer::class,
+ [
+ 'cipherMethod' => 'Wrong-method',
+ ]
+ );
+
+ $cryptographer->encode($source);
+ }
+
+ /**
+ * @expectedException \Magento\Framework\Exception\LocalizedException
+ */
+ public function testEncodeTokenNotValid()
+ {
+ $source = 'Some string';
+
+ $this->analyticsTokenMock
+ ->expects($this->once())
+ ->method('getToken')
+ ->with()
+ ->willReturn(null);
+
+ $this->cryptographer->encode($source);
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Model/EncodedContextTest.php b/app/code/Magento/Analytics/Test/Unit/Model/EncodedContextTest.php
new file mode 100644
index 0000000000000..a1a7c54510681
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Model/EncodedContextTest.php
@@ -0,0 +1,61 @@
+objectManagerHelper = new ObjectManagerHelper($this);
+ }
+
+ /**
+ * @param string $content
+ * @param string|null $initializationVector
+ * @return void
+ * @dataProvider constructDataProvider
+ */
+ public function testConstruct($content, $initializationVector)
+ {
+ $constructorArguments = [
+ 'content' => $content,
+ 'initializationVector' => $initializationVector,
+ ];
+ /** @var EncodedContext $encodedContext */
+ $encodedContext = $this->objectManagerHelper->getObject(
+ EncodedContext::class,
+ array_filter($constructorArguments)
+ );
+
+ $this->assertSame($content, $encodedContext->getContent());
+ $this->assertSame($initializationVector ?: '', $encodedContext->getInitializationVector());
+ }
+
+ /**
+ * @return array
+ */
+ public function constructDataProvider()
+ {
+ return [
+ 'Without Initialization Vector' => ['content text', null],
+ 'With Initialization Vector' => ['content text', 'c51sd3c4sd68c5sd'],
+ ];
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Model/ExportDataHandlerNotificationTest.php b/app/code/Magento/Analytics/Test/Unit/Model/ExportDataHandlerNotificationTest.php
new file mode 100644
index 0000000000000..1582c241bf45d
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Model/ExportDataHandlerNotificationTest.php
@@ -0,0 +1,74 @@
+objectManagerHelper = new ObjectManagerHelper($this);
+ }
+
+ /**
+ * @return void
+ */
+ public function testThatNotifyExecuted()
+ {
+ $expectedResult = true;
+ $notifyCommandName = 'notifyDataChanged';
+ $exportDataHandlerMockObject = $this->createExportDataHandlerMock();
+ $analyticsConnectorMockObject = $this->createAnalyticsConnectorMock();
+ /**
+ * @var $exportDataHandlerNotification ExportDataHandlerNotification
+ */
+ $exportDataHandlerNotification = $this->objectManagerHelper->getObject(
+ ExportDataHandlerNotification::class,
+ [
+ 'exportDataHandler' => $exportDataHandlerMockObject,
+ 'connector' => $analyticsConnectorMockObject,
+ ]
+ );
+ $exportDataHandlerMockObject->expects($this->once())
+ ->method('prepareExportData')
+ ->willReturn($expectedResult);
+ $analyticsConnectorMockObject->expects($this->once())
+ ->method('execute')
+ ->with($notifyCommandName);
+ $this->assertEquals($expectedResult, $exportDataHandlerNotification->prepareExportData());
+ }
+
+ /**
+ * @return \PHPUnit_Framework_MockObject_MockObject
+ */
+ private function createExportDataHandlerMock()
+ {
+ return $this->getMockBuilder(ExportDataHandler::class)->disableOriginalConstructor()->getMock();
+ }
+
+ /**
+ * @return \PHPUnit_Framework_MockObject_MockObject
+ */
+ private function createAnalyticsConnectorMock()
+ {
+ return $this->getMockBuilder(Connector::class)->disableOriginalConstructor()->getMock();
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Model/ExportDataHandlerTest.php b/app/code/Magento/Analytics/Test/Unit/Model/ExportDataHandlerTest.php
new file mode 100644
index 0000000000000..6ffb61d088567
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Model/ExportDataHandlerTest.php
@@ -0,0 +1,270 @@
+filesystemMock = $this->getMockBuilder(Filesystem::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->archiveMock = $this->getMockBuilder(Archive::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->reportWriterMock = $this->getMockBuilder(ReportWriterInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->cryptographerMock = $this->getMockBuilder(Cryptographer::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->fileRecorderMock = $this->getMockBuilder(FileRecorder::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->directoryMock = $this->getMockBuilder(WriteInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->encodedContextMock = $this->getMockBuilder(EncodedContext::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->objectManagerHelper = new ObjectManagerHelper($this);
+
+ $this->exportDataHandler = $this->objectManagerHelper->getObject(
+ ExportDataHandler::class,
+ [
+ 'filesystem' => $this->filesystemMock,
+ 'archive' => $this->archiveMock,
+ 'reportWriter' => $this->reportWriterMock,
+ 'cryptographer' => $this->cryptographerMock,
+ 'fileRecorder' => $this->fileRecorderMock,
+ 'subdirectoryPath' => $this->subdirectoryPath,
+ 'archiveName' => $this->archiveName,
+ ]
+ );
+ }
+
+ /**
+ * @param bool $isArchiveSourceDirectory
+ * @dataProvider prepareExportDataDataProvider
+ */
+ public function testPrepareExportData($isArchiveSourceDirectory)
+ {
+ $tmpFilesDirectoryPath = $this->subdirectoryPath . 'tmp/';
+ $archiveRelativePath = $this->subdirectoryPath . $this->archiveName;
+
+ $archiveSource = $isArchiveSourceDirectory ? (__DIR__) : '/tmp/' . $tmpFilesDirectoryPath;
+ $archiveAbsolutePath = '/tmp/' . $archiveRelativePath;
+
+ $this->filesystemMock
+ ->expects($this->once())
+ ->method('getDirectoryWrite')
+ ->with(DirectoryList::SYS_TMP)
+ ->willReturn($this->directoryMock);
+ $this->directoryMock
+ ->expects($this->exactly(4))
+ ->method('delete')
+ ->withConsecutive(
+ [$tmpFilesDirectoryPath],
+ [$archiveRelativePath]
+ );
+
+ $this->directoryMock
+ ->expects($this->exactly(4))
+ ->method('getAbsolutePath')
+ ->withConsecutive(
+ [$tmpFilesDirectoryPath],
+ [$tmpFilesDirectoryPath],
+ [$archiveRelativePath],
+ [$archiveRelativePath]
+ )
+ ->willReturnOnConsecutiveCalls(
+ $archiveSource,
+ $archiveSource,
+ $archiveAbsolutePath,
+ $archiveAbsolutePath
+ );
+
+ $this->reportWriterMock
+ ->expects($this->once())
+ ->method('write')
+ ->with($this->directoryMock, $tmpFilesDirectoryPath);
+
+ $this->directoryMock
+ ->expects($this->exactly(2))
+ ->method('isExist')
+ ->withConsecutive(
+ [$tmpFilesDirectoryPath],
+ [$archiveRelativePath]
+ )
+ ->willReturnOnConsecutiveCalls(
+ true,
+ true
+ );
+
+ $this->directoryMock
+ ->expects($this->once())
+ ->method('create')
+ ->with(dirname($archiveRelativePath));
+
+ $this->archiveMock
+ ->expects($this->once())
+ ->method('pack')
+ ->with(
+ $archiveSource,
+ $archiveAbsolutePath,
+ $isArchiveSourceDirectory ? true : false
+ );
+
+ $fileContent = 'Some text';
+ $this->directoryMock
+ ->expects($this->once())
+ ->method('readFile')
+ ->with($archiveRelativePath)
+ ->willReturn($fileContent);
+
+ $this->cryptographerMock
+ ->expects($this->once())
+ ->method('encode')
+ ->with($fileContent)
+ ->willReturn($this->encodedContextMock);
+
+ $this->fileRecorderMock
+ ->expects($this->once())
+ ->method('recordNewFile')
+ ->with($this->encodedContextMock);
+
+ $this->assertTrue($this->exportDataHandler->prepareExportData());
+ }
+
+ /**
+ * @return array
+ */
+ public function prepareExportDataDataProvider()
+ {
+ return [
+ 'Data source for archive is directory' => [true],
+ 'Data source for archive doesn\'t directory' => [false],
+ ];
+ }
+
+ /**
+ * @return void
+ * @expectedException \Magento\Framework\Exception\LocalizedException
+ */
+ public function testPrepareExportDataWithLocalizedException()
+ {
+ $tmpFilesDirectoryPath = $this->subdirectoryPath . 'tmp/';
+ $archivePath = $this->subdirectoryPath . $this->archiveName;
+
+ $this->filesystemMock
+ ->expects($this->once())
+ ->method('getDirectoryWrite')
+ ->with(DirectoryList::SYS_TMP)
+ ->willReturn($this->directoryMock);
+ $this->reportWriterMock
+ ->expects($this->once())
+ ->method('write')
+ ->with($this->directoryMock, $tmpFilesDirectoryPath);
+ $this->directoryMock
+ ->expects($this->exactly(3))
+ ->method('delete')
+ ->withConsecutive(
+ [$tmpFilesDirectoryPath],
+ [$tmpFilesDirectoryPath],
+ [$archivePath]
+ );
+ $this->directoryMock
+ ->expects($this->exactly(2))
+ ->method('getAbsolutePath')
+ ->with($tmpFilesDirectoryPath);
+ $this->directoryMock
+ ->expects($this->once())
+ ->method('isExist')
+ ->with($tmpFilesDirectoryPath)
+ ->willReturn(false);
+
+ $this->assertNull($this->exportDataHandler->prepareExportData());
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Model/FileInfoManagerTest.php b/app/code/Magento/Analytics/Test/Unit/Model/FileInfoManagerTest.php
new file mode 100644
index 0000000000000..da5f6af3ca4e1
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Model/FileInfoManagerTest.php
@@ -0,0 +1,194 @@
+flagManagerMock = $this->getMockBuilder(FlagManager::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->fileInfoFactoryMock = $this->getMockBuilder(FileInfoFactory::class)
+ ->setMethods(['create'])
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->fileInfoMock = $this->getMockBuilder(FileInfo::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->objectManagerHelper = new ObjectManagerHelper($this);
+
+ $this->fileInfoManager = $this->objectManagerHelper->getObject(
+ FileInfoManager::class,
+ [
+ 'flagManager' => $this->flagManagerMock,
+ 'fileInfoFactory' => $this->fileInfoFactoryMock,
+ 'flagCode' => $this->flagCode,
+ 'encodedParameters' => $this->encodedParameters,
+ ]
+ );
+ }
+
+ /**
+ * @return void
+ */
+ public function testSave()
+ {
+ $path = 'path/to/file';
+ $initializationVector = openssl_random_pseudo_bytes(16);
+ $parameters = [
+ 'path' => $path,
+ 'initializationVector' => $initializationVector,
+ ];
+
+ $this->fileInfoMock
+ ->expects($this->once())
+ ->method('getPath')
+ ->with()
+ ->willReturn($path);
+ $this->fileInfoMock
+ ->expects($this->once())
+ ->method('getInitializationVector')
+ ->with()
+ ->willReturn($initializationVector);
+
+ foreach ($this->encodedParameters as $encodedParameter) {
+ $parameters[$encodedParameter] = base64_encode($parameters[$encodedParameter]);
+ }
+ $this->flagManagerMock
+ ->expects($this->once())
+ ->method('saveFlag')
+ ->with($this->flagCode, $parameters);
+
+ $this->assertTrue($this->fileInfoManager->save($this->fileInfoMock));
+ }
+
+ /**
+ * @param string|null $path
+ * @param string|null $initializationVector
+ * @dataProvider saveWithLocalizedExceptionDataProvider
+ * @expectedException \Magento\Framework\Exception\LocalizedException
+ */
+ public function testSaveWithLocalizedException($path, $initializationVector)
+ {
+ $this->fileInfoMock
+ ->expects($this->once())
+ ->method('getPath')
+ ->with()
+ ->willReturn($path);
+ $this->fileInfoMock
+ ->expects($this->once())
+ ->method('getInitializationVector')
+ ->with()
+ ->willReturn($initializationVector);
+
+ $this->fileInfoManager->save($this->fileInfoMock);
+ }
+
+ /**
+ * @return array
+ */
+ public function saveWithLocalizedExceptionDataProvider()
+ {
+ return [
+ 'Empty FileInfo' => [null, null],
+ 'FileInfo without IV' => ['path/to/file', null],
+ ];
+ }
+
+ /**
+ * @dataProvider loadDataProvider
+ * @param array|null $parameters
+ */
+ public function testLoad($parameters)
+ {
+ $this->flagManagerMock
+ ->expects($this->once())
+ ->method('getFlagData')
+ ->with($this->flagCode)
+ ->willReturn($parameters);
+
+ $processedParameters = $parameters ?: [];
+ $encodedParameters = array_intersect($this->encodedParameters, array_keys($processedParameters));
+ foreach ($encodedParameters as $encodedParameter) {
+ $processedParameters[$encodedParameter] = base64_decode($processedParameters[$encodedParameter]);
+ }
+
+ $this->fileInfoFactoryMock
+ ->expects($this->once())
+ ->method('create')
+ ->with($processedParameters)
+ ->willReturn($this->fileInfoMock);
+
+ $this->assertSame($this->fileInfoMock, $this->fileInfoManager->load());
+ }
+
+ /**
+ * @return array
+ */
+ public function loadDataProvider()
+ {
+ return [
+ 'Empty flag data' => [null],
+ 'Correct flag data' => [[
+ 'path' => 'path/to/file',
+ 'initializationVector' => 'xUJjl54MVke+FvMFSBpRSA==',
+ ]],
+ ];
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Model/FileInfoTest.php b/app/code/Magento/Analytics/Test/Unit/Model/FileInfoTest.php
new file mode 100644
index 0000000000000..43ce833f1f03f
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Model/FileInfoTest.php
@@ -0,0 +1,62 @@
+objectManagerHelper = new ObjectManagerHelper($this);
+ }
+
+ /**
+ * @param string|null $path
+ * @param string|null $initializationVector
+ * @return void
+ * @dataProvider constructDataProvider
+ */
+ public function testConstruct($path, $initializationVector)
+ {
+ $constructorArguments = [
+ 'path' => $path,
+ 'initializationVector' => $initializationVector,
+ ];
+ /** @var FileInfo $fileInfo */
+ $fileInfo = $this->objectManagerHelper->getObject(
+ FileInfo::class,
+ array_filter($constructorArguments)
+ );
+
+ $this->assertSame($path ?: '', $fileInfo->getPath());
+ $this->assertSame($initializationVector ?: '', $fileInfo->getInitializationVector());
+ }
+
+ /**
+ * @return array
+ */
+ public function constructDataProvider()
+ {
+ return [
+ 'Degenerate object' => [null, null],
+ 'Without Initialization Vector' => ['content text', null],
+ 'With Initialization Vector' => ['content text', 'c51sd3c4sd68c5sd'],
+ ];
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Model/FileRecorderTest.php b/app/code/Magento/Analytics/Test/Unit/Model/FileRecorderTest.php
new file mode 100644
index 0000000000000..3c9520bdd995b
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Model/FileRecorderTest.php
@@ -0,0 +1,209 @@
+fileInfoManagerMock = $this->getMockBuilder(FileInfoManager::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->fileInfoFactoryMock = $this->getMockBuilder(FileInfoFactory::class)
+ ->setMethods(['create'])
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->filesystemMock = $this->getMockBuilder(Filesystem::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->fileInfoMock = $this->getMockBuilder(FileInfo::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->directoryMock = $this->getMockBuilder(WriteInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->encodedContextMock = $this->getMockBuilder(EncodedContext::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->objectManagerHelper = new ObjectManagerHelper($this);
+
+ $this->fileRecorder = $this->objectManagerHelper->getObject(
+ FileRecorder::class,
+ [
+ 'fileInfoManager' => $this->fileInfoManagerMock,
+ 'fileInfoFactory' => $this->fileInfoFactoryMock,
+ 'filesystem' => $this->filesystemMock,
+ 'fileSubdirectoryPath' => $this->fileSubdirectoryPath,
+ 'encodedFileName' => $this->encodedFileName,
+ ]
+ );
+ }
+
+ /**
+ * @param string $pathToExistingFile
+ * @dataProvider recordNewFileDataProvider
+ */
+ public function testRecordNewFile($pathToExistingFile)
+ {
+ $content = openssl_random_pseudo_bytes(200);
+
+ $this->filesystemMock
+ ->expects($this->once())
+ ->method('getDirectoryWrite')
+ ->with(DirectoryList::MEDIA)
+ ->willReturn($this->directoryMock);
+
+ $this->encodedContextMock
+ ->expects($this->once())
+ ->method('getContent')
+ ->with()
+ ->willReturn($content);
+
+ $hashLength = 64;
+ $fileRelativePathPattern = '#' . preg_quote($this->fileSubdirectoryPath, '#')
+ . '.{' . $hashLength . '}/' . preg_quote($this->encodedFileName, '#') . '#';
+ $this->directoryMock
+ ->expects($this->once())
+ ->method('writeFile')
+ ->with($this->matchesRegularExpression($fileRelativePathPattern), $content)
+ ->willReturn($this->directoryMock);
+
+ $this->fileInfoManagerMock
+ ->expects($this->once())
+ ->method('load')
+ ->with()
+ ->willReturn($this->fileInfoMock);
+
+ $this->encodedContextMock
+ ->expects($this->once())
+ ->method('getInitializationVector')
+ ->with()
+ ->willReturn('init_vector***');
+
+ /** register file */
+ $this->fileInfoFactoryMock
+ ->expects($this->once())
+ ->method('create')
+ ->with($this->callback(
+ function ($parameters) {
+ return !empty($parameters['path']) && ('init_vector***' === $parameters['initializationVector']);
+ }
+ ))
+ ->willReturn($this->fileInfoMock);
+ $this->fileInfoManagerMock
+ ->expects($this->once())
+ ->method('save')
+ ->with($this->fileInfoMock);
+
+ /** remove old file */
+ $this->fileInfoMock
+ ->expects($this->exactly($pathToExistingFile ? 3 : 1))
+ ->method('getPath')
+ ->with()
+ ->willReturn($pathToExistingFile);
+ $directoryName = dirname($pathToExistingFile);
+ if ($directoryName === '.') {
+ $this->directoryMock
+ ->expects($this->once())
+ ->method('delete')
+ ->with($pathToExistingFile);
+ } elseif ($directoryName) {
+ $this->directoryMock
+ ->expects($this->exactly(2))
+ ->method('delete')
+ ->withConsecutive(
+ [$pathToExistingFile],
+ [$directoryName]
+ );
+ }
+
+ $this->assertTrue($this->fileRecorder->recordNewFile($this->encodedContextMock));
+ }
+
+ /**
+ * @return array
+ */
+ public function recordNewFileDataProvider()
+ {
+ return [
+ 'File doesn\'t exist' => [''],
+ 'Existing file into subdirectory' => ['dir_name/file.txt'],
+ 'Existing file doesn\'t into subdirectory' => ['file.txt'],
+ ];
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Model/IntegrationManagerTest.php b/app/code/Magento/Analytics/Test/Unit/Model/IntegrationManagerTest.php
new file mode 100644
index 0000000000000..3076a22c85be4
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Model/IntegrationManagerTest.php
@@ -0,0 +1,228 @@
+integrationServiceMock = $this->getMockBuilder(IntegrationServiceInterface::class)
+ ->getMock();
+ $this->configMock = $this->getMockBuilder(Config::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->oauthServiceMock = $this->getMockBuilder(OauthServiceInterface::class)
+ ->getMock();
+ $this->integrationMock = $this->getMockBuilder(Integration::class)
+ ->disableOriginalConstructor()
+ ->setMethods([
+ 'getId',
+ 'getConsumerId'
+ ])
+ ->getMock();
+ $this->integrationManager = $objectManagerHelper->getObject(
+ IntegrationManager::class,
+ [
+ 'integrationService' => $this->integrationServiceMock,
+ 'oauthService' => $this->oauthServiceMock,
+ 'config' => $this->configMock
+ ]
+ );
+ }
+
+ /**
+ * @param string $status
+ *
+ * @return array
+ */
+ private function getIntegrationUserData($status)
+ {
+ return [
+ 'name' => 'ma-integration-user',
+ 'status' => $status,
+ 'all_resources' => false,
+ 'resource' => [
+ 'Magento_Analytics::analytics',
+ 'Magento_Analytics::analytics_api'
+ ],
+ ];
+ }
+
+ /**
+ * @return void
+ */
+ public function testActivateIntegrationSuccess()
+ {
+ $this->integrationServiceMock->expects($this->once())
+ ->method('findByName')
+ ->with('ma-integration-user')
+ ->willReturn($this->integrationMock);
+ $this->integrationMock->expects($this->exactly(2))
+ ->method('getId')
+ ->willReturn(100500);
+ $integrationData = $this->getIntegrationUserData(Integration::STATUS_ACTIVE);
+ $integrationData['integration_id'] = 100500;
+ $this->configMock->expects($this->exactly(2))
+ ->method('getConfigDataValue')
+ ->with('analytics/integration_name', null, null)
+ ->willReturn('ma-integration-user');
+ $this->integrationServiceMock->expects($this->once())
+ ->method('update')
+ ->with($integrationData);
+ $this->assertTrue($this->integrationManager->activateIntegration());
+ }
+
+ /**
+ * @expectedException \Magento\Framework\Exception\NoSuchEntityException
+ */
+ public function testActivateIntegrationFailureNoSuchEntity()
+ {
+ $this->integrationServiceMock->expects($this->once())
+ ->method('findByName')
+ ->with('ma-integration-user')
+ ->willReturn($this->integrationMock);
+ $this->integrationMock->expects($this->once())
+ ->method('getId')
+ ->willReturn(null);
+ $this->configMock->expects($this->once())
+ ->method('getConfigDataValue')
+ ->with('analytics/integration_name', null, null)
+ ->willReturn('ma-integration-user');
+ $this->integrationServiceMock->expects($this->never())
+ ->method('update');
+ $this->integrationManager->activateIntegration();
+ }
+
+ /**
+ * @dataProvider integrationIdDataProvider
+ *
+ * @param int|null $integrationId If null integration is absent.
+ * @return void
+ */
+ public function testGetTokenNewIntegration($integrationId)
+ {
+ $this->configMock->expects($this->atLeastOnce())
+ ->method('getConfigDataValue')
+ ->with('analytics/integration_name', null, null)
+ ->willReturn('ma-integration-user');
+ $this->integrationServiceMock->expects($this->once())
+ ->method('findByName')
+ ->with('ma-integration-user')
+ ->willReturn($this->integrationMock);
+ $this->integrationMock->expects($this->once())
+ ->method('getConsumerId')
+ ->willReturn(100500);
+ $this->integrationMock->expects($this->once())
+ ->method('getId')
+ ->willReturn($integrationId);
+ if (!$integrationId) {
+ $this->integrationServiceMock
+ ->expects($this->once())
+ ->method('create')
+ ->with($this->getIntegrationUserData(Integration::STATUS_INACTIVE))
+ ->willReturn($this->integrationMock);
+ }
+ $this->oauthServiceMock->expects($this->at(0))
+ ->method('getAccessToken')
+ ->with(100500)
+ ->willReturn(false);
+ $this->oauthServiceMock->expects($this->at(2))
+ ->method('getAccessToken')
+ ->with(100500)
+ ->willReturn('IntegrationToken');
+ $this->oauthServiceMock->expects($this->once())
+ ->method('createAccessToken')
+ ->with(100500, true)
+ ->willReturn(true);
+ $this->assertEquals('IntegrationToken', $this->integrationManager->generateToken());
+ }
+
+ /**
+ * @dataProvider integrationIdDataProvider
+ *
+ * @param int|null $integrationId If null integration is absent.
+ * @return void
+ */
+ public function testGetTokenExistingIntegration($integrationId)
+ {
+ $this->configMock->expects($this->atLeastOnce())
+ ->method('getConfigDataValue')
+ ->with('analytics/integration_name', null, null)
+ ->willReturn('ma-integration-user');
+ $this->integrationServiceMock->expects($this->once())
+ ->method('findByName')
+ ->with('ma-integration-user')
+ ->willReturn($this->integrationMock);
+ $this->integrationMock->expects($this->once())
+ ->method('getConsumerId')
+ ->willReturn(100500);
+ $this->integrationMock->expects($this->once())
+ ->method('getId')
+ ->willReturn($integrationId);
+ if (!$integrationId) {
+ $this->integrationServiceMock
+ ->expects($this->once())
+ ->method('create')
+ ->with($this->getIntegrationUserData(Integration::STATUS_INACTIVE))
+ ->willReturn($this->integrationMock);
+ }
+ $this->oauthServiceMock->expects($this->once())
+ ->method('getAccessToken')
+ ->with(100500)
+ ->willReturn('IntegrationToken');
+ $this->oauthServiceMock->expects($this->never())
+ ->method('createAccessToken');
+ $this->assertEquals('IntegrationToken', $this->integrationManager->generateToken());
+ }
+
+ /**
+ * @return array
+ */
+ public function integrationIdDataProvider()
+ {
+ return [
+ [1],
+ [null],
+ ];
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Model/LinkProviderTest.php b/app/code/Magento/Analytics/Test/Unit/Model/LinkProviderTest.php
new file mode 100644
index 0000000000000..c7aa2219d1eee
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Model/LinkProviderTest.php
@@ -0,0 +1,166 @@
+linkInterfaceFactoryMock = $this->getMockBuilder(LinkInterfaceFactory::class)
+ ->disableOriginalConstructor()
+ ->setMethods(['create'])
+ ->getMock();
+ $this->fileInfoManagerMock = $this->getMockBuilder(FileInfoManager::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->storeManagerInterfaceMock = $this->getMockBuilder(StoreManagerInterface::class)
+ ->getMockForAbstractClass();
+ $this->linkInterfaceMock = $this->getMockBuilder(LinkInterface::class)
+ ->getMockForAbstractClass();
+ $this->fileInfoMock = $this->getMockBuilder(FileInfo::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->storeMock = $this->getMockBuilder(Store::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->objectManagerHelper = new ObjectManagerHelper($this);
+ $this->linkProvider = $this->objectManagerHelper->getObject(
+ LinkProvider::class,
+ [
+ 'linkFactory' => $this->linkInterfaceFactoryMock,
+ 'fileInfoManager' => $this->fileInfoManagerMock,
+ 'storeManager' => $this->storeManagerInterfaceMock
+ ]
+ );
+ }
+
+ public function testGet()
+ {
+ $baseUrl = 'http://magento.local/pub/media/';
+ $fileInfoPath = 'analytics/data.tgz';
+ $fileInitializationVector = 'er312esq23eqq';
+ $this->fileInfoManagerMock->expects($this->once())
+ ->method('load')
+ ->willReturn($this->fileInfoMock);
+ $this->linkInterfaceFactoryMock->expects($this->once())
+ ->method('create')
+ ->with(
+ [
+ 'initializationVector' => base64_encode($fileInitializationVector),
+ 'url' => $baseUrl . $fileInfoPath
+ ]
+ )
+ ->willReturn($this->linkInterfaceMock);
+ $this->storeManagerInterfaceMock->expects($this->once())
+ ->method('getStore')->willReturn($this->storeMock);
+ $this->storeMock->expects($this->once())
+ ->method('getBaseUrl')
+ ->with(
+ UrlInterface::URL_TYPE_MEDIA
+ )
+ ->willReturn($baseUrl);
+ $this->fileInfoMock->expects($this->atLeastOnce())
+ ->method('getPath')
+ ->willReturn($fileInfoPath);
+ $this->fileInfoMock->expects($this->atLeastOnce())
+ ->method('getInitializationVector')
+ ->willReturn($fileInitializationVector);
+ $this->assertEquals($this->linkInterfaceMock, $this->linkProvider->get());
+ }
+
+ /**
+ * @param string|null $fileInfoPath
+ * @param string|null $fileInitializationVector
+ *
+ * @dataProvider fileNotReadyDataProvider
+ * @expectedException \Magento\Framework\Exception\NoSuchEntityException
+ * @expectedExceptionMessage File is not ready yet.
+ */
+ public function testFileNotReady($fileInfoPath, $fileInitializationVector)
+ {
+ $this->fileInfoManagerMock->expects($this->once())
+ ->method('load')
+ ->willReturn($this->fileInfoMock);
+ $this->fileInfoMock->expects($this->once())
+ ->method('getPath')
+ ->willReturn($fileInfoPath);
+ $this->fileInfoMock->expects($this->any())
+ ->method('getInitializationVector')
+ ->willReturn($fileInitializationVector);
+ $this->linkProvider->get();
+ }
+
+ /**
+ * @return array
+ */
+ public function fileNotReadyDataProvider()
+ {
+ return [
+ [null, 'initVector'],
+ ['path', null],
+ ['', 'initVector'],
+ ['path', ''],
+ ['', ''],
+ [null, null]
+ ];
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Model/Plugin/BaseUrlConfigPluginTest.php b/app/code/Magento/Analytics/Test/Unit/Model/Plugin/BaseUrlConfigPluginTest.php
new file mode 100644
index 0000000000000..a89e06562383b
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Model/Plugin/BaseUrlConfigPluginTest.php
@@ -0,0 +1,147 @@
+subscriptionUpdateHandlerMock = $this->getMockBuilder(SubscriptionUpdateHandler::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->configValueMock = $this->getMockBuilder(Value::class)
+ ->disableOriginalConstructor()
+ ->setMethods(['isValueChanged', 'getPath', 'getScope', 'getOldValue'])
+ ->getMock();
+ $this->objectManagerHelper = new ObjectManagerHelper($this);
+ $this->plugin = $this->objectManagerHelper->getObject(
+ BaseUrlConfigPlugin::class,
+ [
+ 'subscriptionUpdateHandler' => $this->subscriptionUpdateHandlerMock,
+ ]
+ );
+ }
+
+ /**
+ * @param array $configValueData
+ * @return void
+ * @dataProvider afterSavePluginIsNotApplicableDataProvider
+ */
+ public function testAfterSavePluginIsNotApplicable(
+ array $configValueData
+ ) {
+ $this->configValueMock
+ ->method('isValueChanged')
+ ->willReturn($configValueData['isValueChanged']);
+ $this->configValueMock
+ ->method('getPath')
+ ->willReturn($configValueData['path']);
+ $this->configValueMock
+ ->method('getScope')
+ ->willReturn($configValueData['scope']);
+ $this->subscriptionUpdateHandlerMock
+ ->expects($this->never())
+ ->method('processUrlUpdate');
+
+ $this->assertEquals(
+ $this->configValueMock,
+ $this->plugin->afterAfterSave($this->configValueMock, $this->configValueMock)
+ );
+ }
+
+ /**
+ * @return array
+ */
+ public function afterSavePluginIsNotApplicableDataProvider()
+ {
+ return [
+ 'Value has not been changed' => [
+ 'Config Value Data' => [
+ 'isValueChanged' => false,
+ 'path' => Store::XML_PATH_SECURE_BASE_URL,
+ 'scope' => ScopeConfigInterface::SCOPE_TYPE_DEFAULT
+ ],
+ ],
+ 'Unsecure URL has been changed' => [
+ 'Config Value Data' => [
+ 'isValueChanged' => true,
+ 'path' => Store::XML_PATH_UNSECURE_BASE_URL,
+ 'scope' => ScopeConfigInterface::SCOPE_TYPE_DEFAULT
+ ],
+ ],
+ 'Secure URL has been changed not in the Default scope' => [
+ 'Config Value Data' => [
+ 'isValueChanged' => true,
+ 'path' => Store::XML_PATH_SECURE_BASE_URL,
+ 'scope' => ScopeInterface::SCOPE_STORES
+ ],
+ ],
+ ];
+ }
+
+ /**
+ * @return void
+ */
+ public function testAfterSavePluginIsApplicable()
+ {
+ $this->configValueMock
+ ->method('isValueChanged')
+ ->willReturn(true);
+ $this->configValueMock
+ ->method('getPath')
+ ->willReturn(Store::XML_PATH_SECURE_BASE_URL);
+ $this->configValueMock
+ ->method('getScope')
+ ->willReturn(ScopeConfigInterface::SCOPE_TYPE_DEFAULT);
+ $this->configValueMock
+ ->method('getOldValue')
+ ->willReturn('http://store.com');
+ $this->subscriptionUpdateHandlerMock
+ ->expects($this->once())
+ ->method('processUrlUpdate')
+ ->with('http://store.com');
+
+ $this->assertEquals(
+ $this->configValueMock,
+ $this->plugin->afterAfterSave($this->configValueMock, $this->configValueMock)
+ );
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Model/ReportUrlProviderTest.php b/app/code/Magento/Analytics/Test/Unit/Model/ReportUrlProviderTest.php
new file mode 100644
index 0000000000000..0607a977e5b68
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Model/ReportUrlProviderTest.php
@@ -0,0 +1,149 @@
+configMock = $this->getMockBuilder(ScopeConfigInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->analyticsTokenMock = $this->getMockBuilder(AnalyticsToken::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->otpRequestMock = $this->getMockBuilder(OTPRequest::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->flagManagerMock = $this->getMockBuilder(FlagManager::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->objectManagerHelper = new ObjectManagerHelper($this);
+
+ $this->reportUrlProvider = $this->objectManagerHelper->getObject(
+ ReportUrlProvider::class,
+ [
+ 'config' => $this->configMock,
+ 'analyticsToken' => $this->analyticsTokenMock,
+ 'otpRequest' => $this->otpRequestMock,
+ 'flagManager' => $this->flagManagerMock,
+ 'urlReportConfigPath' => $this->urlReportConfigPath,
+ ]
+ );
+ }
+
+ /**
+ * @param bool $isTokenExist
+ * @param string|null $otp If null OTP was not received.
+ *
+ * @dataProvider getUrlDataProvider
+ */
+ public function testGetUrl($isTokenExist, $otp)
+ {
+ $reportUrl = 'https://example.com/report';
+ $url = '';
+
+ $this->configMock
+ ->expects($this->once())
+ ->method('getValue')
+ ->with($this->urlReportConfigPath)
+ ->willReturn($reportUrl);
+ $this->analyticsTokenMock
+ ->expects($this->once())
+ ->method('isTokenExist')
+ ->with()
+ ->willReturn($isTokenExist);
+ $this->otpRequestMock
+ ->expects($isTokenExist ? $this->once() : $this->never())
+ ->method('call')
+ ->with()
+ ->willReturn($otp);
+ if ($isTokenExist && $otp) {
+ $url = $reportUrl . '?' . http_build_query(['otp' => $otp], '', '&');
+ }
+ $this->assertSame($url ?: $reportUrl, $this->reportUrlProvider->getUrl());
+ }
+
+ /**
+ * @return array
+ */
+ public function getUrlDataProvider()
+ {
+ return [
+ 'TokenDoesNotExist' => [false, null],
+ 'TokenExistAndOtpEmpty' => [true, null],
+ 'TokenExistAndOtpValid' => [true, '249e6b658877bde2a77bc4ab'],
+ ];
+ }
+
+ /**
+ * @return void
+ */
+ public function testGetUrlWhenSubscriptionUpdateRunning()
+ {
+ $this->flagManagerMock
+ ->expects($this->once())
+ ->method('getFlagData')
+ ->with(SubscriptionUpdateHandler::PREVIOUS_BASE_URL_FLAG_CODE)
+ ->willReturn('http://store.com');
+ $this->expectException(SubscriptionUpdateException::class);
+ $this->reportUrlProvider->getUrl();
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Model/ReportWriterTest.php b/app/code/Magento/Analytics/Test/Unit/Model/ReportWriterTest.php
new file mode 100644
index 0000000000000..d9b030b84d639
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Model/ReportWriterTest.php
@@ -0,0 +1,213 @@
+configInterfaceMock = $this->getMockBuilder(ConfigInterface::class)->getMockForAbstractClass();
+ $this->reportValidatorMock = $this->getMockBuilder(ReportValidator::class)
+ ->disableOriginalConstructor()->getMock();
+ $this->providerFactoryMock = $this->getMockBuilder(ProviderFactory::class)
+ ->disableOriginalConstructor()->getMock();
+ $this->reportProviderMock = $this->getMockBuilder(ReportProvider::class)
+ ->disableOriginalConstructor()->getMock();
+ $this->directoryMock = $this->getMockBuilder(WriteInterface::class)->getMockForAbstractClass();
+ $this->objectManagerHelper = new ObjectManagerHelper($this);
+
+ $this->reportWriter = $this->objectManagerHelper->getObject(
+ ReportWriter::class,
+ [
+ 'config' => $this->configInterfaceMock,
+ 'reportValidator' => $this->reportValidatorMock,
+ 'providerFactory' => $this->providerFactoryMock
+ ]
+ );
+ }
+
+ /**
+ * @param array $configData
+ * @return void
+ *
+ * @dataProvider configDataProvider
+ */
+ public function testWrite(array $configData)
+ {
+ $errors = [];
+ $fileData = [
+ ['number' => 1, 'type' => 'Shoes Usual']
+ ];
+ $this->configInterfaceMock
+ ->expects($this->once())
+ ->method('get')
+ ->with()
+ ->willReturn([$configData]);
+ $this->providerFactoryMock
+ ->expects($this->once())
+ ->method('create')
+ ->with($this->providerClass)
+ ->willReturn($this->reportProviderMock);
+ $parameterName = isset(reset($configData)[0]['parameters']['name'])
+ ? reset($configData)[0]['parameters']['name']
+ : '';
+ $this->reportProviderMock->expects($this->once())
+ ->method('getReport')
+ ->with($parameterName ?: null)
+ ->willReturn($fileData);
+ $errorStreamMock = $this->getMockBuilder(
+ \Magento\Framework\Filesystem\File\WriteInterface::class
+ )->getMockForAbstractClass();
+ $errorStreamMock
+ ->expects($this->once())
+ ->method('lock')
+ ->with();
+ $errorStreamMock
+ ->expects($this->exactly(2))
+ ->method('writeCsv')
+ ->withConsecutive(
+ [array_keys($fileData[0])],
+ [$fileData[0]]
+ );
+ $errorStreamMock->expects($this->once())->method('unlock');
+ $errorStreamMock->expects($this->once())->method('close');
+ if ($parameterName) {
+ $this->reportValidatorMock
+ ->expects($this->once())
+ ->method('validate')
+ ->with($parameterName)
+ ->willReturn($errors);
+ }
+ $this->directoryMock
+ ->expects($this->once())
+ ->method('openFile')
+ ->with(
+ $this->stringContains('/var/tmp' . $parameterName ?: $this->reportName),
+ 'w+'
+ )->willReturn($errorStreamMock);
+ $this->assertTrue($this->reportWriter->write($this->directoryMock, '/var/tmp'));
+ }
+
+ /**
+ * @param array $configData
+ * @return void
+ *
+ * @dataProvider configDataProvider
+ */
+ public function testWriteErrorFile($configData)
+ {
+ $errors = ['orders', 'SQL Error: test'];
+ $this->configInterfaceMock->expects($this->once())->method('get')->willReturn([$configData]);
+ $errorStreamMock = $this->getMockBuilder(
+ \Magento\Framework\Filesystem\File\WriteInterface::class
+ )->getMockForAbstractClass();
+ $errorStreamMock->expects($this->once())->method('lock');
+ $errorStreamMock->expects($this->once())->method('writeCsv')->with($errors);
+ $errorStreamMock->expects($this->once())->method('unlock');
+ $errorStreamMock->expects($this->once())->method('close');
+ $this->reportValidatorMock->expects($this->once())->method('validate')->willReturn($errors);
+ $this->directoryMock->expects($this->once())->method('openFile')->with('/var/tmp' . 'errors.csv', 'w+')
+ ->willReturn($errorStreamMock);
+ $this->assertTrue($this->reportWriter->write($this->directoryMock, '/var/tmp'));
+ }
+
+ /**
+ * @return void
+ */
+ public function testWriteEmptyReports()
+ {
+ $this->configInterfaceMock->expects($this->once())->method('get')->willReturn([]);
+ $this->reportValidatorMock->expects($this->never())->method('validate');
+ $this->directoryMock->expects($this->never())->method('openFile');
+ $this->assertTrue($this->reportWriter->write($this->directoryMock, '/var/tmp'));
+ }
+
+ /**
+ * @return array
+ */
+ public function configDataProvider()
+ {
+ return [
+ 'reportProvider' => [
+ [
+ 'providers' => [
+ [
+ 'name' => $this->providerName,
+ 'class' => $this->providerClass,
+ 'parameters' => [
+ 'name' => $this->reportName
+ ],
+ ]
+ ]
+ ]
+ ],
+ ];
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Model/ReportXml/ModuleIteratorTest.php b/app/code/Magento/Analytics/Test/Unit/Model/ReportXml/ModuleIteratorTest.php
new file mode 100644
index 0000000000000..f314d77f32b41
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Model/ReportXml/ModuleIteratorTest.php
@@ -0,0 +1,50 @@
+moduleManagerMock = $this->getMockBuilder(ModuleManager::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $objectManagerHelper = new ObjectManagerHelper($this);
+ $this->moduleIterator = $objectManagerHelper->getObject(
+ ModuleIterator::class,
+ [
+ 'moduleManager' => $this->moduleManagerMock,
+ 'iterator' => new \ArrayIterator([0 => ['module_name' => 'Coco_Module']])
+ ]
+ );
+ }
+
+ public function testCurrent()
+ {
+ $this->moduleManagerMock->expects($this->once())
+ ->method('isEnabled')
+ ->with('Coco_Module')
+ ->willReturn(true);
+ foreach ($this->moduleIterator as $item) {
+ $this->assertEquals(['module_name' => 'Coco_Module', 'status' => 'Enabled'], $item);
+ }
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Model/StoreConfigurationProviderTest.php b/app/code/Magento/Analytics/Test/Unit/Model/StoreConfigurationProviderTest.php
new file mode 100644
index 0000000000000..cc46d175543ad
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Model/StoreConfigurationProviderTest.php
@@ -0,0 +1,123 @@
+scopeConfigMock = $this->getMockBuilder(ScopeConfigInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->storeManagerMock = $this->getMockBuilder(StoreManagerInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->websiteMock = $this->getMockBuilder(WebsiteInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->storeMock = $this->getMockBuilder(StoreInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->configPaths = [
+ 'web/unsecure/base_url',
+ 'currency/options/base',
+ 'general/locale/timezone'
+ ];
+
+ $this->storeConfigurationProvider = new StoreConfigurationProvider(
+ $this->scopeConfigMock,
+ $this->storeManagerMock,
+ $this->configPaths
+ );
+ }
+
+ public function testGetReport()
+ {
+ $map = [
+ ['web/unsecure/base_url', 'default', 0, '127.0.0.1'],
+ ['currency/options/base', 'default', 0, 'USD'],
+ ['general/locale/timezone', 'default', 0, 'America/Dawson'],
+ ['web/unsecure/base_url', 'websites', 1, '127.0.0.2'],
+ ['currency/options/base', 'websites', 1, 'USD'],
+ ['general/locale/timezone', 'websites', 1, 'America/Belem'],
+ ['web/unsecure/base_url', 'stores', 2, '127.0.0.3'],
+ ['currency/options/base', 'stores', 2, 'USD'],
+ ['general/locale/timezone', 'stores', 2, 'America/Phoenix'],
+ ];
+
+ $this->scopeConfigMock
+ ->method('getValue')
+ ->will($this->returnValueMap($map));
+
+ $this->storeManagerMock->expects($this->once())
+ ->method('getWebsites')
+ ->willReturn([$this->websiteMock]);
+
+ $this->storeManagerMock->expects($this->once())
+ ->method('getStores')
+ ->willReturn([$this->storeMock]);
+
+ $this->websiteMock->expects($this->once())
+ ->method('getId')
+ ->willReturn(1);
+
+ $this->storeMock->expects($this->once())
+ ->method('getId')
+ ->willReturn(2);
+ $result = iterator_to_array($this->storeConfigurationProvider->getReport());
+ $resultValues = [];
+ foreach ($result as $item) {
+ $resultValues[] = array_values($item);
+ }
+ array_multisort($resultValues);
+ array_multisort($map);
+ $this->assertEquals($resultValues, $map);
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Model/SubscriptionStatusProviderTest.php b/app/code/Magento/Analytics/Test/Unit/Model/SubscriptionStatusProviderTest.php
new file mode 100644
index 0000000000000..d6b041ce03178
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Model/SubscriptionStatusProviderTest.php
@@ -0,0 +1,196 @@
+scopeConfigMock = $this->getMockBuilder(ScopeConfigInterface::class)
+ ->getMockForAbstractClass();
+
+ $this->analyticsTokenMock = $this->getMockBuilder(AnalyticsToken::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->flagManagerMock = $this->getMockBuilder(FlagManager::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->objectManagerHelper = new ObjectManagerHelper($this);
+
+ $this->statusProvider = $this->objectManagerHelper->getObject(
+ SubscriptionStatusProvider::class,
+ [
+ 'scopeConfig' => $this->scopeConfigMock,
+ 'analyticsToken' => $this->analyticsTokenMock,
+ 'flagManager' => $this->flagManagerMock,
+ ]
+ );
+ }
+
+ /**
+ * @param array $flagManagerData
+ * @dataProvider getStatusShouldBeFailedDataProvider
+ */
+ public function testGetStatusShouldBeFailed(array $flagManagerData)
+ {
+ $this->analyticsTokenMock->expects($this->once())
+ ->method('isTokenExist')
+ ->willReturn(false);
+ $this->scopeConfigMock->expects($this->once())
+ ->method('getValue')
+ ->with('analytics/subscription/enabled')
+ ->willReturn(true);
+
+ $this->expectFlagManagerReturn($flagManagerData);
+ $this->assertEquals(SubscriptionStatusProvider::FAILED, $this->statusProvider->getStatus());
+ }
+
+ /**
+ * @return array
+ */
+ public function getStatusShouldBeFailedDataProvider()
+ {
+ return [
+ 'Subscription update doesn\'t active' => [
+ 'Flag Manager data mapping' => [
+ [SubscriptionUpdateHandler::PREVIOUS_BASE_URL_FLAG_CODE, null],
+ [SubscriptionHandler::ATTEMPTS_REVERSE_COUNTER_FLAG_CODE, null]
+ ],
+ ],
+ 'Subscription update is active' => [
+ 'Flag Manager data mapping' => [
+ [SubscriptionUpdateHandler::PREVIOUS_BASE_URL_FLAG_CODE, 'http://store.com'],
+ [SubscriptionHandler::ATTEMPTS_REVERSE_COUNTER_FLAG_CODE, null]
+ ],
+ ],
+ ];
+ }
+
+ /**
+ * @param array $flagManagerData
+ * @param bool $isTokenExist
+ * @dataProvider getStatusShouldBePendingDataProvider
+ */
+ public function testGetStatusShouldBePending(array $flagManagerData, bool $isTokenExist)
+ {
+ $this->analyticsTokenMock->expects($this->once())
+ ->method('isTokenExist')
+ ->willReturn($isTokenExist);
+ $this->scopeConfigMock->expects($this->once())
+ ->method('getValue')
+ ->with('analytics/subscription/enabled')
+ ->willReturn(true);
+
+ $this->expectFlagManagerReturn($flagManagerData);
+ $this->assertEquals(SubscriptionStatusProvider::PENDING, $this->statusProvider->getStatus());
+ }
+
+ /**
+ * @return array
+ */
+ public function getStatusShouldBePendingDataProvider()
+ {
+ return [
+ 'Subscription update doesn\'t active and the token does not exist' => [
+ 'Flag Manager data mapping' => [
+ [SubscriptionUpdateHandler::PREVIOUS_BASE_URL_FLAG_CODE, null],
+ [SubscriptionHandler::ATTEMPTS_REVERSE_COUNTER_FLAG_CODE, 45]
+ ],
+ 'isTokenExist' => false,
+ ],
+ 'Subscription update is active and the token does not exist' => [
+ 'Flag Manager data mapping' => [
+ [SubscriptionUpdateHandler::PREVIOUS_BASE_URL_FLAG_CODE, 'http://store.com'],
+ [SubscriptionHandler::ATTEMPTS_REVERSE_COUNTER_FLAG_CODE, 45]
+ ],
+ 'isTokenExist' => false,
+ ],
+ 'Subscription update is active and token exist' => [
+ 'Flag Manager data mapping' => [
+ [SubscriptionUpdateHandler::PREVIOUS_BASE_URL_FLAG_CODE, 'http://store.com'],
+ [SubscriptionHandler::ATTEMPTS_REVERSE_COUNTER_FLAG_CODE, null]
+ ],
+ 'isTokenExist' => true,
+ ],
+ ];
+ }
+
+ public function testGetStatusShouldBeEnabled()
+ {
+ $this->flagManagerMock
+ ->method('getFlagData')
+ ->with(SubscriptionUpdateHandler::PREVIOUS_BASE_URL_FLAG_CODE)
+ ->willReturn(null);
+ $this->analyticsTokenMock->expects($this->once())
+ ->method('isTokenExist')
+ ->willReturn(true);
+ $this->scopeConfigMock->expects($this->once())
+ ->method('getValue')
+ ->with('analytics/subscription/enabled')
+ ->willReturn(true);
+ $this->assertEquals(SubscriptionStatusProvider::ENABLED, $this->statusProvider->getStatus());
+ }
+
+ public function testGetStatusShouldBeDisabled()
+ {
+ $this->scopeConfigMock->expects($this->once())
+ ->method('getValue')
+ ->with('analytics/subscription/enabled')
+ ->willReturn(false);
+ $this->assertEquals(SubscriptionStatusProvider::DISABLED, $this->statusProvider->getStatus());
+ }
+
+ /**
+ * @param array $mapping
+ */
+ private function expectFlagManagerReturn(array $mapping)
+ {
+ $this->flagManagerMock
+ ->method('getFlagData')
+ ->willReturnMap($mapping);
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Model/System/Message/NotificationAboutFailedSubscriptionTest.php b/app/code/Magento/Analytics/Test/Unit/Model/System/Message/NotificationAboutFailedSubscriptionTest.php
new file mode 100644
index 0000000000000..ad1d87488d751
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Model/System/Message/NotificationAboutFailedSubscriptionTest.php
@@ -0,0 +1,106 @@
+subscriptionStatusMock = $this->getMockBuilder(SubscriptionStatusProvider::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->urlBuilderMock = $this->getMockBuilder(UrlInterface::class)
+ ->getMockForAbstractClass();
+ $this->objectManagerHelper = new ObjectManagerHelper($this);
+ $this->notification = $this->objectManagerHelper->getObject(
+ NotificationAboutFailedSubscription::class,
+ [
+ 'subscriptionStatusProvider' => $this->subscriptionStatusMock,
+ 'urlBuilder' => $this->urlBuilderMock
+ ]
+ );
+ }
+
+ public function testIsDisplayedWhenMessageShouldBeDisplayed()
+ {
+ $this->subscriptionStatusMock->expects($this->once())
+ ->method('getStatus')
+ ->willReturn(
+ SubscriptionStatusProvider::FAILED
+ );
+ $this->assertTrue($this->notification->isDisplayed());
+ }
+
+ /**
+ * @dataProvider notDisplayedNotificationStatuses
+ *
+ * @param $status
+ */
+ public function testIsDisplayedWhenMessageShouldNotBeDisplayed($status)
+ {
+ $this->subscriptionStatusMock->expects($this->once())
+ ->method('getStatus')
+ ->willReturn($status);
+ $this->assertFalse($this->notification->isDisplayed());
+ }
+
+ public function testGetTextShouldBuildMessage()
+ {
+ $retryUrl = 'http://magento.dev/retryUrl';
+ $this->urlBuilderMock->expects($this->once())
+ ->method('getUrl')
+ ->with('analytics/subscription/retry')
+ ->willReturn($retryUrl);
+ $messageDetails = 'Failed to synchronize data to the Magento Business Intelligence service. ';
+ $messageDetails .= sprintf('Retry Synchronization ', $retryUrl);
+ $this->assertEquals($messageDetails, $this->notification->getText());
+ }
+
+ /**
+ * Provide statuses according to which message should not be displayed.
+ *
+ * @return array
+ */
+ public function notDisplayedNotificationStatuses()
+ {
+ return [
+ [SubscriptionStatusProvider::PENDING],
+ [SubscriptionStatusProvider::DISABLED],
+ [SubscriptionStatusProvider::ENABLED],
+ ];
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/ReportXml/Config/Converter/XmlTest.php b/app/code/Magento/Analytics/Test/Unit/ReportXml/Config/Converter/XmlTest.php
new file mode 100644
index 0000000000000..3f1ed9a5cf4c0
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/ReportXml/Config/Converter/XmlTest.php
@@ -0,0 +1,121 @@
+objectManagerHelper =
+ new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this);
+
+ $this->subject = $this->objectManagerHelper->getObject(
+ \Magento\Analytics\ReportXml\Config\Converter\Xml::class
+ );
+ }
+
+ /**
+ * @return void
+ */
+ public function testConvertNoElements()
+ {
+ $this->assertEmpty(
+ $this->subject->convert(new \DOMDocument())
+ );
+ }
+
+ /**
+ * @return void
+ */
+ public function testConvert()
+ {
+ $dom = new \DOMDocument();
+
+ $expectedArray = [
+ 'config' => [
+ [
+ 'noNamespaceSchemaLocation' => 'urn:magento:module:Magento_Analytics:etc/reports.xsd',
+ 'report' => [
+ [
+ 'name' => 'test_report_1',
+ 'connection' => 'sales',
+ 'source' => [
+ [
+ 'name' => 'sales_order',
+ 'alias' => 'orders',
+ 'attribute' => [
+ [
+ 'name' => 'entity_id',
+ 'alias' => 'identifier',
+ ]
+ ],
+ 'filter' => [
+ [
+ 'glue' => 'and',
+ 'condition' => [
+ [
+ 'attribute' => 'entity_id',
+ 'operator' => 'gt',
+ '_value' => '10'
+ ]
+ ]
+ ]
+ ]
+ ]
+ ]
+ ],
+ [
+ 'name' => 'test_report_2',
+ 'connection' => 'default',
+ 'source' => [
+ [
+ 'name' => 'customer_entity',
+ 'alias' => 'customers',
+ 'attribute' => [
+ [
+ 'name' => 'email'
+ ]
+ ],
+ 'filter' => [
+ [
+ 'glue' => 'and',
+ 'condition' => [
+ [
+ 'attribute' => 'dob',
+ 'operator' => 'null'
+ ]
+ ]
+ ]
+ ]
+ ]
+ ]
+ ]
+ ]
+ ]
+ ]
+ ];
+
+ $dom->loadXML(file_get_contents(__DIR__ . '/../_files/valid_reports.xml'));
+
+ $this->assertEquals($expectedArray, $this->subject->convert($dom));
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/ReportXml/Config/MapperTest.php b/app/code/Magento/Analytics/Test/Unit/ReportXml/Config/MapperTest.php
new file mode 100644
index 0000000000000..85343b6b301d6
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/ReportXml/Config/MapperTest.php
@@ -0,0 +1,47 @@
+mapper = new Mapper();
+ }
+
+ public function testExecute()
+ {
+ $configData['config'][0]['report'] = [
+ [
+ 'source' => ['product'],
+ 'name' => 'Product',
+ ]
+ ];
+ $expectedResult = [
+ 'Product' => [
+ 'source' => 'product',
+ 'name' => 'Product',
+ ]
+ ];
+ $this->assertEquals($this->mapper->execute($configData), $expectedResult);
+ }
+
+ public function testExecuteWithoutReports()
+ {
+ $configData = [];
+ $this->assertEquals($this->mapper->execute($configData), []);
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/ReportXml/Config/_files/valid_reports.xml b/app/code/Magento/Analytics/Test/Unit/ReportXml/Config/_files/valid_reports.xml
new file mode 100644
index 0000000000000..e04ee96163797
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/ReportXml/Config/_files/valid_reports.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+ 10
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Analytics/Test/Unit/ReportXml/ConfigTest.php b/app/code/Magento/Analytics/Test/Unit/ReportXml/ConfigTest.php
new file mode 100644
index 0000000000000..cbc9aa129d874
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/ReportXml/ConfigTest.php
@@ -0,0 +1,64 @@
+dataMock = $this->getMockBuilder(DataInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->objectManagerHelper = new ObjectManagerHelper($this);
+
+ $this->config = $this->objectManagerHelper->getObject(
+ Config::class,
+ [
+ 'data' => $this->dataMock,
+ ]
+ );
+ }
+
+ public function testGet()
+ {
+ $queryName = 'query string';
+ $queryResult = [ 'query' => 1 ];
+
+ $this->dataMock
+ ->expects($this->once())
+ ->method('get')
+ ->with($queryName)
+ ->willReturn($queryResult);
+
+ $this->assertSame($queryResult, $this->config->get($queryName));
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/ReportXml/ConnectionFactoryTest.php b/app/code/Magento/Analytics/Test/Unit/ReportXml/ConnectionFactoryTest.php
new file mode 100644
index 0000000000000..1e4ae9142c13d
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/ReportXml/ConnectionFactoryTest.php
@@ -0,0 +1,106 @@
+resourceConnectionMock = $this->getMockBuilder(ResourceConnection::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->objectManagerMock = $this->getMockBuilder(ObjectManagerInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->connectionMock = $this->getMockBuilder(MysqlPdoAdapter::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->connectionNewMock = $this->getMockBuilder(MysqlPdoAdapter::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->objectManagerHelper = new ObjectManagerHelper($this);
+
+ $this->connectionFactory = $this->objectManagerHelper->getObject(
+ ConnectionFactory::class,
+ [
+ 'resourceConnection' => $this->resourceConnectionMock,
+ 'objectManager' => $this->objectManagerMock,
+ ]
+ );
+ }
+
+ public function testGetConnection()
+ {
+ $connectionName = 'read';
+
+ $this->resourceConnectionMock
+ ->expects($this->once())
+ ->method('getConnection')
+ ->with($connectionName)
+ ->willReturn($this->connectionMock);
+
+ $this->connectionMock
+ ->expects($this->once())
+ ->method('getConfig')
+ ->with()
+ ->willReturn(['persistent' => 1]);
+
+ $this->objectManagerMock
+ ->expects($this->once())
+ ->method('create')
+ ->with(get_class($this->connectionMock), ['config' => ['use_buffered_query' => false]])
+ ->willReturn($this->connectionNewMock);
+
+ $this->assertSame($this->connectionNewMock, $this->connectionFactory->getConnection($connectionName));
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/ReportXml/DB/Assembler/FilterAssemblerTest.php b/app/code/Magento/Analytics/Test/Unit/ReportXml/DB/Assembler/FilterAssemblerTest.php
new file mode 100644
index 0000000000000..3b01105a8873b
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/ReportXml/DB/Assembler/FilterAssemblerTest.php
@@ -0,0 +1,143 @@
+nameResolverMock = $this->getMockBuilder(
+ \Magento\Analytics\ReportXml\DB\NameResolver::class
+ )
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->selectBuilderMock = $this->getMockBuilder(
+ \Magento\Analytics\ReportXml\DB\SelectBuilder::class
+ )
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->selectBuilderMock->expects($this->any())
+ ->method('getFilters')
+ ->willReturn([]);
+
+ $this->conditionResolverMock = $this->getMockBuilder(
+ \Magento\Analytics\ReportXml\DB\ConditionResolver::class
+ )
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->objectManagerHelper =
+ new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this);
+
+ $this->subject = $this->objectManagerHelper->getObject(
+ \Magento\Analytics\ReportXml\DB\Assembler\FilterAssembler::class,
+ [
+ 'conditionResolver' => $this->conditionResolverMock,
+ 'nameResolver' => $this->nameResolverMock
+ ]
+ );
+ }
+
+ /**
+ * @return void
+ */
+ public function testAssembleEmpty()
+ {
+ $queryConfigMock = [
+ 'source' => [
+ 'name' => 'sales_order',
+ 'alias' => 'sales'
+ ]
+ ];
+
+ $this->selectBuilderMock->expects($this->never())
+ ->method('setFilters');
+
+ $this->assertEquals(
+ $this->selectBuilderMock,
+ $this->subject->assemble($this->selectBuilderMock, $queryConfigMock)
+ );
+ }
+
+ /**
+ * @return void
+ */
+ public function testAssembleNotEmpty()
+ {
+ $queryConfigMock = [
+ 'source' => [
+ 'name' => 'sales_order',
+ 'alias' => 'sales',
+ 'filter' => [
+ [
+ 'glue' => 'and',
+ 'condition' => [
+ [
+ 'attribute' => 'entity_id',
+ 'operator' => 'null'
+ ]
+ ]
+ ]
+ ]
+ ]
+ ];
+
+ $this->nameResolverMock->expects($this->any())
+ ->method('getAlias')
+ ->with($queryConfigMock['source'])
+ ->willReturn($queryConfigMock['source']['alias']);
+
+ $this->conditionResolverMock->expects($this->once())
+ ->method('getFilter')
+ ->with(
+ $this->selectBuilderMock,
+ $queryConfigMock['source']['filter'],
+ $queryConfigMock['source']['alias']
+ )
+ ->willReturn('(sales.entity_id IS NULL)');
+
+ $this->selectBuilderMock->expects($this->once())
+ ->method('setFilters')
+ ->with(['(sales.entity_id IS NULL)']);
+
+ $this->assertEquals(
+ $this->selectBuilderMock,
+ $this->subject->assemble($this->selectBuilderMock, $queryConfigMock)
+ );
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/ReportXml/DB/Assembler/FromAssemblerTest.php b/app/code/Magento/Analytics/Test/Unit/ReportXml/DB/Assembler/FromAssemblerTest.php
new file mode 100644
index 0000000000000..575db94a7b7e1
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/ReportXml/DB/Assembler/FromAssemblerTest.php
@@ -0,0 +1,167 @@
+nameResolverMock = $this->getMockBuilder(
+ \Magento\Analytics\ReportXml\DB\NameResolver::class
+ )
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->selectBuilderMock = $this->getMockBuilder(
+ \Magento\Analytics\ReportXml\DB\SelectBuilder::class
+ )
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->selectBuilderMock->expects($this->any())
+ ->method('getColumns')
+ ->willReturn([]);
+
+ $this->columnsResolverMock = $this->getMockBuilder(
+ \Magento\Analytics\ReportXml\DB\ColumnsResolver::class
+ )
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->resourceConnection = $this->getMockBuilder(ResourceConnection::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->objectManagerHelper =
+ new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this);
+
+ $this->subject = $this->objectManagerHelper->getObject(
+ \Magento\Analytics\ReportXml\DB\Assembler\FromAssembler::class,
+ [
+ 'nameResolver' => $this->nameResolverMock,
+ 'columnsResolver' => $this->columnsResolverMock,
+ 'resourceConnection' => $this->resourceConnection,
+ ]
+ );
+ }
+
+ /**
+ * @dataProvider assembleDataProvider
+ * @param array $queryConfig
+ * @param string $tableName
+ * @return void
+ */
+ public function testAssemble(array $queryConfig, $tableName)
+ {
+ $this->nameResolverMock->expects($this->any())
+ ->method('getAlias')
+ ->with($queryConfig['source'])
+ ->willReturn($queryConfig['source']['alias']);
+
+ $this->nameResolverMock->expects($this->once())
+ ->method('getName')
+ ->with($queryConfig['source'])
+ ->willReturn($queryConfig['source']['name']);
+
+ $this->resourceConnection
+ ->expects($this->once())
+ ->method('getTableName')
+ ->with($queryConfig['source']['name'])
+ ->willReturn($tableName);
+
+ $this->selectBuilderMock->expects($this->once())
+ ->method('setFrom')
+ ->with([$queryConfig['source']['alias'] => $tableName]);
+
+ $this->columnsResolverMock->expects($this->once())
+ ->method('getColumns')
+ ->with($this->selectBuilderMock, $queryConfig['source'])
+ ->willReturn(['entity_id' => 'sales.entity_id']);
+
+ $this->selectBuilderMock->expects($this->once())
+ ->method('setColumns')
+ ->with(['entity_id' => 'sales.entity_id']);
+
+ $this->assertEquals(
+ $this->selectBuilderMock,
+ $this->subject->assemble($this->selectBuilderMock, $queryConfig)
+ );
+ }
+
+ /**
+ * @return array
+ */
+ public function assembleDataProvider()
+ {
+ return [
+ 'Tables without prefixes' => [
+ [
+ 'source' => [
+ 'name' => 'sales_order',
+ 'alias' => 'sales',
+ 'attribute' => [
+ [
+ 'name' => 'entity_id'
+ ]
+ ],
+ ],
+ ],
+ 'sales_order',
+ ],
+ 'Tables with prefixes' => [
+ [
+ 'source' => [
+ 'name' => 'sales_order',
+ 'alias' => 'sales',
+ 'attribute' => [
+ [
+ 'name' => 'entity_id'
+ ]
+ ],
+ ],
+ ],
+ 'pref_sales_order',
+ ]
+ ];
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/ReportXml/DB/Assembler/JoinAssemblerTest.php b/app/code/Magento/Analytics/Test/Unit/ReportXml/DB/Assembler/JoinAssemblerTest.php
new file mode 100644
index 0000000000000..aaafd731552a0
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/ReportXml/DB/Assembler/JoinAssemblerTest.php
@@ -0,0 +1,279 @@
+nameResolverMock = $this->getMockBuilder(
+ \Magento\Analytics\ReportXml\DB\NameResolver::class
+ )
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->selectBuilderMock = $this->getMockBuilder(
+ \Magento\Analytics\ReportXml\DB\SelectBuilder::class
+ )
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->selectBuilderMock->expects($this->any())
+ ->method('getFilters')
+ ->willReturn([]);
+ $this->selectBuilderMock->expects($this->any())
+ ->method('getColumns')
+ ->willReturn([]);
+ $this->selectBuilderMock->expects($this->any())
+ ->method('getJoins')
+ ->willReturn([]);
+
+ $this->columnsResolverMock = $this->getMockBuilder(
+ \Magento\Analytics\ReportXml\DB\ColumnsResolver::class
+ )
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->conditionResolverMock = $this->getMockBuilder(
+ \Magento\Analytics\ReportXml\DB\ConditionResolver::class
+ )
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->resourceConnection = $this->getMockBuilder(ResourceConnection::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->objectManagerHelper =
+ new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this);
+
+ $this->subject = $this->objectManagerHelper->getObject(
+ \Magento\Analytics\ReportXml\DB\Assembler\JoinAssembler::class,
+ [
+ 'conditionResolver' => $this->conditionResolverMock,
+ 'nameResolver' => $this->nameResolverMock,
+ 'columnsResolver' => $this->columnsResolverMock,
+ 'resourceConnection' => $this->resourceConnection,
+ ]
+ );
+ }
+
+ /**
+ * @return void
+ */
+ public function testAssembleEmpty()
+ {
+ $queryConfigMock = [
+ 'source' => [
+ 'name' => 'sales_order',
+ 'alias' => 'sales'
+ ]
+ ];
+
+ $this->selectBuilderMock->expects($this->never())
+ ->method('setColumns');
+ $this->selectBuilderMock->expects($this->never())
+ ->method('setFilters');
+ $this->selectBuilderMock->expects($this->never())
+ ->method('setJoins');
+
+ $this->assertEquals(
+ $this->selectBuilderMock,
+ $this->subject->assemble($this->selectBuilderMock, $queryConfigMock)
+ );
+ }
+
+ /**
+ * @param array $queryConfigMock
+ * @param array $joinsMock
+ * @param array $tablesMapping
+ * @return void
+ * @dataProvider assembleNotEmptyDataProvider
+ */
+ public function testAssembleNotEmpty(array $queryConfigMock, array $joinsMock, array $tablesMapping)
+ {
+ $filtersMock = [];
+
+ $this->nameResolverMock->expects($this->at(0))
+ ->method('getAlias')
+ ->with($queryConfigMock['source'])
+ ->willReturn($queryConfigMock['source']['alias']);
+ $this->nameResolverMock->expects($this->at(1))
+ ->method('getAlias')
+ ->with($queryConfigMock['source']['link-source'][0])
+ ->willReturn($queryConfigMock['source']['link-source'][0]['alias']);
+ $this->nameResolverMock->expects($this->once())
+ ->method('getName')
+ ->with($queryConfigMock['source']['link-source'][0])
+ ->willReturn($queryConfigMock['source']['link-source'][0]['name']);
+
+ $this->resourceConnection
+ ->expects($this->any())
+ ->method('getTableName')
+ ->willReturnOnConsecutiveCalls(...array_values($tablesMapping));
+
+ $this->conditionResolverMock->expects($this->at(0))
+ ->method('getFilter')
+ ->with(
+ $this->selectBuilderMock,
+ $queryConfigMock['source']['link-source'][0]['using'],
+ $queryConfigMock['source']['link-source'][0]['alias'],
+ $queryConfigMock['source']['alias']
+ )
+ ->willReturn('(billing.parent_id = `sales`.`entity_id`)');
+
+ if (isset($queryConfigMock['source']['link-source'][0]['filter'])) {
+ $filtersMock = ['(sales.entity_id IS NULL)'];
+
+ $this->conditionResolverMock->expects($this->at(1))
+ ->method('getFilter')
+ ->with(
+ $this->selectBuilderMock,
+ $queryConfigMock['source']['link-source'][0]['filter'],
+ $queryConfigMock['source']['link-source'][0]['alias'],
+ $queryConfigMock['source']['alias']
+ )
+ ->willReturn($filtersMock[0]);
+
+ $this->columnsResolverMock->expects($this->once())
+ ->method('getColumns')
+ ->with($this->selectBuilderMock, $queryConfigMock['source']['link-source'][0])
+ ->willReturn(
+ [
+ 'entity_id' => 'sales.entity_id',
+ 'billing_address_id' => 'billing.entity_id'
+ ]
+ );
+
+ $this->selectBuilderMock->expects($this->once())
+ ->method('setColumns')
+ ->with(
+ [
+ 'entity_id' => 'sales.entity_id',
+ 'billing_address_id' => 'billing.entity_id'
+ ]
+ );
+ }
+
+ $this->selectBuilderMock->expects($this->once())
+ ->method('setFilters')
+ ->with($filtersMock);
+ $this->selectBuilderMock->expects($this->once())
+ ->method('setJoins')
+ ->with($joinsMock);
+
+ $this->assertEquals(
+ $this->selectBuilderMock,
+ $this->subject->assemble($this->selectBuilderMock, $queryConfigMock)
+ );
+ }
+
+ /**
+ * @return array
+ */
+ public function assembleNotEmptyDataProvider()
+ {
+ return [
+ [
+ [
+ 'source' => [
+ 'name' => 'sales_order',
+ 'alias' => 'sales',
+ 'link-source' => [
+ [
+ 'name' => 'sales_order_address',
+ 'alias' => 'billing',
+ 'link-type' => 'left',
+ 'attribute' => [
+ [
+ 'alias' => 'billing_address_id',
+ 'name' => 'entity_id'
+ ]
+ ],
+ 'using' => [
+ [
+ 'glue' => 'and',
+ 'condition' => [
+ [
+ 'attribute' => 'parent_id',
+ 'operator' => 'eq',
+ 'type' => 'identifier',
+ '_value' => 'entity_id'
+ ]
+ ]
+ ]
+ ],
+ 'filter' => [
+ [
+ 'glue' => 'and',
+ 'condition' => [
+ [
+ 'attribute' => 'entity_id',
+ 'operator' => 'null'
+ ]
+ ]
+ ]
+ ]
+ ]
+ ]
+ ]
+ ],
+ [
+ 'billing' => [
+ 'link-type' => 'left',
+ 'table' => [
+ 'billing' => 'pref_sales_order_address'
+ ],
+ 'condition' => '(billing.parent_id = `sales`.`entity_id`)'
+ ]
+ ],
+ ['sales_order_address' => 'pref_sales_order_address']
+ ]
+ ];
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/ReportXml/DB/ColumnsResolverTest.php b/app/code/Magento/Analytics/Test/Unit/ReportXml/DB/ColumnsResolverTest.php
new file mode 100644
index 0000000000000..bdbe3d1d22c22
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/ReportXml/DB/ColumnsResolverTest.php
@@ -0,0 +1,150 @@
+selectBuilderMock = $this->getMockBuilder(SelectBuilder::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->resourceConnectionMock = $this->getMockBuilder(ResourceConnection::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->connectionMock = $this->getMockBuilder(AdapterInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $objectManager = new ObjectManagerHelper($this);
+ $this->columnsResolver = $objectManager->getObject(
+ ColumnsResolver::class,
+ [
+ 'nameResolver' => new NameResolver(),
+ 'resourceConnection' => $this->resourceConnectionMock
+ ]
+ );
+ }
+
+ public function testGetColumnsWithoutAttributes()
+ {
+ $this->assertEquals($this->columnsResolver->getColumns($this->selectBuilderMock, []), []);
+ }
+
+ /**
+ * @dataProvider getColumnsDataProvider
+ */
+ public function testGetColumnsWithFunction($expectedColumns, $expectedGroup, $entityConfig)
+ {
+ $this->resourceConnectionMock->expects($this->any())
+ ->method('getConnection')
+ ->willReturn($this->connectionMock);
+ $this->connectionMock->expects($this->any())
+ ->method('quoteIdentifier')
+ ->with('cpe.name')
+ ->willReturn('`cpe`.`name`');
+ $this->selectBuilderMock->expects($this->once())
+ ->method('getColumns')
+ ->willReturn([]);
+ $this->selectBuilderMock->expects($this->once())
+ ->method('getGroup')
+ ->willReturn([]);
+ $this->selectBuilderMock->expects($this->once())
+ ->method('setGroup')
+ ->with($expectedGroup);
+ $this->assertEquals(
+ $expectedColumns,
+ $this->columnsResolver->getColumns(
+ $this->selectBuilderMock,
+ $entityConfig
+ )
+ );
+ }
+
+ /**
+ * @return array
+ */
+ public function getColumnsDataProvider()
+ {
+ return [
+ 'COUNT( DISTINCT `cpe`.`name`) AS name' => [
+ 'expectedColumns' => [
+ 'name' => new ColumnValueExpression('COUNT( DISTINCT `cpe`.`name`)')
+ ],
+ 'expectedGroup' => [
+ 'name' => new ColumnValueExpression('COUNT( DISTINCT `cpe`.`name`)')
+ ],
+ 'entityConfig' =>
+ [
+ 'name' => 'catalog_product_entity',
+ 'alias' => 'cpe',
+ 'attribute' => [
+ [
+ 'name' => 'name',
+ 'function' => 'COUNT',
+ 'distinct' => true,
+ 'group' => true
+ ]
+ ],
+ ],
+ ],
+ 'AVG(`cpe`.`name`) AS avg_name' => [
+ 'expectedColumns' => [
+ 'avg_name' => new ColumnValueExpression('AVG(`cpe`.`name`)')
+ ],
+ 'expectedGroup' => [],
+ 'entityConfig' =>
+ [
+ 'name' => 'catalog_product_entity',
+ 'alias' => 'cpe',
+ 'attribute' => [
+ [
+ 'name' => 'name',
+ 'alias' => 'avg_name',
+ 'function' => 'AVG',
+ ]
+ ],
+ ],
+ ]
+ ];
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/ReportXml/DB/ConditionResolverTest.php b/app/code/Magento/Analytics/Test/Unit/ReportXml/DB/ConditionResolverTest.php
new file mode 100644
index 0000000000000..c8182d068fba5
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/ReportXml/DB/ConditionResolverTest.php
@@ -0,0 +1,108 @@
+resourceConnectionMock = $this->getMockBuilder(ResourceConnection::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->selectBuilderMock = $this->getMockBuilder(SelectBuilder::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->connectionMock = $this->getMockBuilder(AdapterInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->conditionResolver = new ConditionResolver($this->resourceConnectionMock);
+ }
+
+ public function testGetFilter()
+ {
+ $condition = ["type" => "variable", "_value" => "1", "attribute" => "id", "operator" => "neq"];
+ $valueCondition = ["type" => "value", "_value" => "2", "attribute" => "first_name", "operator" => "eq"];
+ $identifierCondition = [
+ "type" => "identifier",
+ "_value" => "other_field",
+ "attribute" => "last_name",
+ "operator" => "eq"];
+ $filter = [["glue" => "AND", "condition" => [$valueCondition]]];
+ $filterConfig = [
+ ["glue" => "OR", "condition" => [$condition], 'filter' => $filter],
+ ["glue" => "OR", "condition" => [$identifierCondition]],
+ ];
+ $aliasName = 'n';
+ $this->selectBuilderMock->expects($this->any())
+ ->method('setParams')
+ ->with(array_merge([], [$condition['_value']]));
+
+ $this->selectBuilderMock->expects($this->once())
+ ->method('getParams')
+ ->willReturn([]);
+
+ $this->selectBuilderMock->expects($this->any())
+ ->method('getColumns')
+ ->willReturn(['price' => new Expression("(n.price = 400)")]);
+
+ $this->resourceConnectionMock->expects($this->once())
+ ->method('getConnection')
+ ->willReturn($this->connectionMock);
+
+ $this->connectionMock->expects($this->any())
+ ->method('quote')
+ ->willReturn("'John'");
+ $this->connectionMock->expects($this->exactly(4))
+ ->method('quoteIdentifier')
+ ->willReturnMap([
+ ['n.id', false, '`n`.`id`'],
+ ['n.first_name', false, '`n`.`first_name`'],
+ ['n.last_name', false, '`n`.`last_name`'],
+ ['other_field', false, '`other_field`'],
+ ]);
+
+ $result = "(`n`.`id` != 1 OR ((`n`.`first_name` = 'John'))) OR (`n`.`last_name` = `other_field`)";
+ $this->assertEquals(
+ $result,
+ $this->conditionResolver->getFilter($this->selectBuilderMock, $filterConfig, $aliasName)
+ );
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/ReportXml/DB/NameResolverTest.php b/app/code/Magento/Analytics/Test/Unit/ReportXml/DB/NameResolverTest.php
new file mode 100644
index 0000000000000..4accd03aef3ea
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/ReportXml/DB/NameResolverTest.php
@@ -0,0 +1,90 @@
+nameResolverMock = $this->getMockBuilder(NameResolver::class)
+ ->disableOriginalConstructor()
+ ->setMethods(['getName'])
+ ->getMock();
+
+ $this->objectManagerHelper = new ObjectManagerHelper($this);
+ $this->nameResolver = $this->objectManagerHelper->getObject(NameResolver::class);
+ }
+
+ public function testGetName()
+ {
+ $elementConfigMock = [
+ 'name' => 'sales_order',
+ 'alias' => 'sales',
+ ];
+
+ $this->assertSame('sales_order', $this->nameResolver->getName($elementConfigMock));
+ }
+
+ /**
+ * @param array $elementConfig
+ * @param string|null $elementAlias
+ *
+ * @dataProvider getAliasDataProvider
+ */
+ public function testGetAlias($elementConfig, $elementAlias)
+ {
+ $elementName = 'elementName';
+
+ $this->nameResolverMock
+ ->expects($this->once())
+ ->method('getName')
+ ->with($elementConfig)
+ ->willReturn($elementName);
+
+ $this->assertSame($elementAlias ?: $elementName, $this->nameResolverMock->getAlias($elementConfig));
+ }
+
+ /**
+ * @return array
+ */
+ public function getAliasDataProvider()
+ {
+ return [
+ 'ElementConfigWithAliases' => [
+ ['alias' => 'sales', 'name' => 'sales_order'],
+ 'sales',
+ ],
+ 'ElementConfigWithoutAliases' => [
+ ['name' => 'sales_order'],
+ null,
+ ]
+ ];
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/ReportXml/DB/ReportValidatorTest.php b/app/code/Magento/Analytics/Test/Unit/ReportXml/DB/ReportValidatorTest.php
new file mode 100644
index 0000000000000..bbb9ca4b511b6
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/ReportXml/DB/ReportValidatorTest.php
@@ -0,0 +1,125 @@
+connectionFactoryMock = $this->getMockBuilder(ConnectionFactory::class)
+ ->disableOriginalConstructor()->getMock();
+ $this->queryFactoryMock = $this->getMockBuilder(QueryFactory::class)
+ ->disableOriginalConstructor()->getMock();
+ $this->queryMock = $this->getMockBuilder(Query::class)->disableOriginalConstructor()
+ ->getMock();
+ $this->connectionMock = $this->getMockBuilder(AdapterInterface::class)->getMockForAbstractClass();
+ $this->selectMock = $this->getMockBuilder(Select::class)->disableOriginalConstructor()
+ ->getMock();
+ $this->objectManagerHelper = new ObjectManagerHelper($this);
+
+ $this->reportValidator = $this->objectManagerHelper->getObject(
+ ReportValidator::class,
+ [
+ 'connectionFactory' => $this->connectionFactoryMock,
+ 'queryFactory' => $this->queryFactoryMock
+ ]
+ );
+ }
+
+ /**
+ * @dataProvider errorDataProvider
+ * @param string $reportName
+ * @param array $result
+ * @param \PHPUnit_Framework_MockObject_Stub $queryReturnStub
+ */
+ public function testValidate($reportName, $result, \PHPUnit_Framework_MockObject_Stub $queryReturnStub)
+ {
+ $connectionName = 'testConnection';
+ $this->queryFactoryMock->expects($this->once())
+ ->method('create')
+ ->willReturn($this->queryMock);
+ $this->queryMock->expects($this->once())->method('getConnectionName')->willReturn($connectionName);
+ $this->connectionFactoryMock->expects($this->once())->method('getConnection')
+ ->with($connectionName)
+ ->willReturn($this->connectionMock);
+ $this->queryMock->expects($this->atLeastOnce())->method('getSelect')->willReturn($this->selectMock);
+ $this->selectMock->expects($this->once())->method('limit')->with(0);
+ $this->connectionMock->expects($this->once())->method('query')->with($this->selectMock)->will($queryReturnStub);
+ $this->assertEquals($result, $this->reportValidator->validate($reportName));
+ }
+
+ /**
+ * Provide variations of the error returning
+ *
+ * @return array
+ */
+ public function errorDataProvider()
+ {
+ $reportName = 'test';
+ $errorMessage = 'SQL Error 42';
+ return [
+ [
+ $reportName,
+ 'expectedResult' => [],
+ 'queryReturnStub' => $this->returnValue(null)
+ ],
+ [
+ $reportName,
+ 'expectedResult' => [$reportName, $errorMessage],
+ 'queryReturnStub' => $this->throwException(new \Zend_Db_Statement_Exception($errorMessage))
+ ]
+ ];
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/ReportXml/DB/SelectBuilderTest.php b/app/code/Magento/Analytics/Test/Unit/ReportXml/DB/SelectBuilderTest.php
new file mode 100644
index 0000000000000..1c60822aca795
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/ReportXml/DB/SelectBuilderTest.php
@@ -0,0 +1,103 @@
+resourceConnectionMock = $this->getMockBuilder(ResourceConnection::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->connectionMock = $this->getMockBuilder(AdapterInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->selectMock = $this->getMockBuilder(Select::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->selectBuilder = new SelectBuilder($this->resourceConnectionMock);
+ }
+
+ public function testCreate()
+ {
+ $connectionName = 'MySql';
+ $from = ['customer c'];
+ $columns = ['id', 'name', 'price'];
+ $filter = 'filter';
+ $joins = [
+ ['link-type' => 'left', 'table' => 'customer', 'condition' => 'in'],
+ ['link-type' => 'inner', 'table' => 'price', 'condition' => 'eq'],
+ ['link-type' => 'right', 'table' => 'attribute', 'condition' => 'neq'],
+ ];
+ $groups = ['id', 'name'];
+ $this->selectBuilder->setConnectionName($connectionName)
+ ->setFrom($from)
+ ->setColumns($columns)
+ ->setFilters([$filter])
+ ->setJoins($joins)
+ ->setGroup($groups);
+ $this->resourceConnectionMock->expects($this->once())
+ ->method('getConnection')
+ ->with($connectionName)
+ ->willReturn($this->connectionMock);
+ $this->connectionMock->expects($this->once())
+ ->method('select')
+ ->willReturn($this->selectMock);
+ $this->selectMock->expects($this->once())
+ ->method('from')
+ ->with($from, []);
+ $this->selectMock->expects($this->once())
+ ->method('columns')
+ ->with($columns);
+ $this->selectMock->expects($this->once())
+ ->method('where')
+ ->with($filter);
+ $this->selectMock->expects($this->once())
+ ->method('joinLeft')
+ ->with($joins[0]['table'], $joins[0]['condition'], []);
+ $this->selectMock->expects($this->once())
+ ->method('joinInner')
+ ->with($joins[1]['table'], $joins[1]['condition'], []);
+ $this->selectMock->expects($this->once())
+ ->method('joinRight')
+ ->with($joins[2]['table'], $joins[2]['condition'], []);
+ $this->selectBuilder->create();
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/ReportXml/IteratorFactoryTest.php b/app/code/Magento/Analytics/Test/Unit/ReportXml/IteratorFactoryTest.php
new file mode 100644
index 0000000000000..1d3f293ed676a
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/ReportXml/IteratorFactoryTest.php
@@ -0,0 +1,59 @@
+objectManagerMock = $this->getMockBuilder(ObjectManagerInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->iteratorIteratorMock = $this->getMockBuilder(\IteratorIterator::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->iteratorFactory = new IteratorFactory(
+ $this->objectManagerMock
+ );
+ }
+
+ public function testCreate()
+ {
+ $arrayObject = new \ArrayIterator([1, 2, 3, 4, 5]);
+ $this->objectManagerMock->expects($this->once())
+ ->method('create')
+ ->with(\IteratorIterator::class, ['iterator' => $arrayObject])
+ ->willReturn($this->iteratorIteratorMock);
+
+ $this->assertEquals($this->iteratorFactory->create($arrayObject), $this->iteratorIteratorMock);
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/ReportXml/QueryFactoryTest.php b/app/code/Magento/Analytics/Test/Unit/ReportXml/QueryFactoryTest.php
new file mode 100644
index 0000000000000..9a3805a50f167
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/ReportXml/QueryFactoryTest.php
@@ -0,0 +1,239 @@
+queryMock = $this->getMockBuilder(
+ \Magento\Analytics\ReportXml\Query::class
+ )
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->configMock = $this->getMockBuilder(
+ \Magento\Analytics\ReportXml\Config::class
+ )
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->selectMock = $this->getMockBuilder(
+ \Magento\Framework\DB\Select::class
+ )
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->assemblerMock = $this->getMockBuilder(
+ \Magento\Analytics\ReportXml\DB\Assembler\AssemblerInterface::class
+ )
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->queryCacheMock = $this->getMockBuilder(
+ \Magento\Framework\App\CacheInterface::class
+ )
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->objectManagerMock = $this->getMockBuilder(
+ \Magento\Framework\ObjectManagerInterface::class
+ )
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->selectHydratorMock = $this->getMockBuilder(
+ \Magento\Analytics\ReportXml\SelectHydrator::class
+ )
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->selectBuilderFactoryMock = $this->getMockBuilder(
+ \Magento\Analytics\ReportXml\DB\SelectBuilderFactory::class
+ )
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->objectManagerHelper =
+ new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this);
+
+ $this->subject = $this->objectManagerHelper->getObject(
+ \Magento\Analytics\ReportXml\QueryFactory::class,
+ [
+ 'config' => $this->configMock,
+ 'selectBuilderFactory' => $this->selectBuilderFactoryMock,
+ 'assemblers' => [$this->assemblerMock],
+ 'queryCache' => $this->queryCacheMock,
+ 'objectManager' => $this->objectManagerMock,
+ 'selectHydrator' => $this->selectHydratorMock
+ ]
+ );
+ }
+
+ /**
+ * @return void
+ */
+ public function testCreateCached()
+ {
+ $queryName = 'test_query';
+
+ $this->queryCacheMock->expects($this->any())
+ ->method('load')
+ ->with($queryName)
+ ->willReturn('{"connectionName":"sales","config":{},"select_parts":{}}');
+
+ $this->selectHydratorMock->expects($this->any())
+ ->method('recreate')
+ ->with([])
+ ->willReturn($this->selectMock);
+
+ $this->objectManagerMock->expects($this->once())
+ ->method('create')
+ ->with(
+ \Magento\Analytics\ReportXml\Query::class,
+ [
+ 'select' => $this->selectMock,
+ 'selectHydrator' => $this->selectHydratorMock,
+ 'connectionName' => 'sales',
+ 'config' => []
+ ]
+ )
+ ->willReturn($this->queryMock);
+
+ $this->queryCacheMock->expects($this->never())
+ ->method('save');
+
+ $this->assertEquals(
+ $this->queryMock,
+ $this->subject->create($queryName)
+ );
+ }
+
+ /**
+ * @return void
+ */
+ public function testCreateNotCached()
+ {
+ $queryName = 'test_query';
+
+ $queryConfigMock = [
+ 'name' => 'test_query',
+ 'connection' => 'sales'
+ ];
+
+ $selectBuilderMock = $this->getMockBuilder(
+ \Magento\Analytics\ReportXml\DB\SelectBuilder::class
+ )
+ ->disableOriginalConstructor()
+ ->getMock();
+ $selectBuilderMock->expects($this->once())
+ ->method('setConnectionName')
+ ->with($queryConfigMock['connection']);
+ $selectBuilderMock->expects($this->any())
+ ->method('create')
+ ->willReturn($this->selectMock);
+ $selectBuilderMock->expects($this->any())
+ ->method('getConnectionName')
+ ->willReturn($queryConfigMock['connection']);
+
+ $this->queryCacheMock->expects($this->any())
+ ->method('load')
+ ->with($queryName)
+ ->willReturn(null);
+
+ $this->configMock->expects($this->any())
+ ->method('get')
+ ->with($queryName)
+ ->willReturn($queryConfigMock);
+
+ $this->selectBuilderFactoryMock->expects($this->any())
+ ->method('create')
+ ->willReturn($selectBuilderMock);
+
+ $this->assemblerMock->expects($this->once())
+ ->method('assemble')
+ ->with($selectBuilderMock, $queryConfigMock)
+ ->willReturn($selectBuilderMock);
+
+ $this->objectManagerMock->expects($this->once())
+ ->method('create')
+ ->with(
+ \Magento\Analytics\ReportXml\Query::class,
+ [
+ 'select' => $this->selectMock,
+ 'selectHydrator' => $this->selectHydratorMock,
+ 'connectionName' => $queryConfigMock['connection'],
+ 'config' => $queryConfigMock
+ ]
+ )
+ ->willReturn($this->queryMock);
+
+ $this->queryCacheMock->expects($this->once())
+ ->method('save')
+ ->with(json_encode($this->queryMock), $queryName);
+
+ $this->assertEquals(
+ $this->queryMock,
+ $this->subject->create($queryName)
+ );
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/ReportXml/QueryTest.php b/app/code/Magento/Analytics/Test/Unit/ReportXml/QueryTest.php
new file mode 100644
index 0000000000000..fd924abdd9f44
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/ReportXml/QueryTest.php
@@ -0,0 +1,90 @@
+selectMock = $this->getMockBuilder(Select::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->selectHydratorMock = $this->getMockBuilder(SelectHydrator::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->objectManagerHelper = new ObjectManagerHelper($this);
+
+ $this->query = $this->objectManagerHelper->getObject(
+ Query::class,
+ [
+ 'select' => $this->selectMock,
+ 'connectionName' => $this->connectionName,
+ 'selectHydrator' => $this->selectHydratorMock,
+ 'config' => []
+ ]
+ );
+ }
+
+ /**
+ * @return void
+ */
+ public function testJsonSerialize()
+ {
+ $selectParts = ['part' => 1];
+
+ $this->selectHydratorMock
+ ->expects($this->once())
+ ->method('extract')
+ ->with($this->selectMock)
+ ->willReturn($selectParts);
+
+ $expectedResult = [
+ 'connectionName' => $this->connectionName,
+ 'select_parts' => $selectParts,
+ 'config' => []
+ ];
+
+ $this->assertSame($expectedResult, $this->query->jsonSerialize());
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/ReportXml/ReportProviderTest.php b/app/code/Magento/Analytics/Test/Unit/ReportXml/ReportProviderTest.php
new file mode 100644
index 0000000000000..5f329993dd291
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/ReportXml/ReportProviderTest.php
@@ -0,0 +1,180 @@
+selectMock = $this->getMockBuilder(
+ \Magento\Framework\DB\Select::class
+ )
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->queryMock = $this->getMockBuilder(
+ \Magento\Analytics\ReportXml\Query::class
+ )
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->queryMock->expects($this->any())
+ ->method('getSelect')
+ ->willReturn($this->selectMock);
+
+ $this->iteratorMock = $this->getMockBuilder(
+ \IteratorIterator::class
+ )
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->statementMock = $this->getMockBuilder(
+ \Magento\Framework\DB\Statement\Pdo\Mysql::class
+ )
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->statementMock->expects($this->any())
+ ->method('getIterator')
+ ->willReturn($this->iteratorMock);
+
+ $this->connectionMock = $this->getMockBuilder(
+ \Magento\Framework\DB\Adapter\AdapterInterface::class
+ )
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->queryFactoryMock = $this->getMockBuilder(
+ \Magento\Analytics\ReportXml\QueryFactory::class
+ )
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->iteratorFactoryMock = $this->getMockBuilder(
+ \Magento\Analytics\ReportXml\IteratorFactory::class
+ )
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->iteratorMock = $this->getMockBuilder(
+ \IteratorIterator::class
+ )
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->objectManagerHelper =
+ new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this);
+
+ $this->connectionFactoryMock = $this->getMockBuilder(
+ \Magento\Analytics\ReportXml\ConnectionFactory::class
+ )
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->subject = $this->objectManagerHelper->getObject(
+ \Magento\Analytics\ReportXml\ReportProvider::class,
+ [
+ 'queryFactory' => $this->queryFactoryMock,
+ 'connectionFactory' => $this->connectionFactoryMock,
+ 'iteratorFactory' => $this->iteratorFactoryMock
+ ]
+ );
+ }
+
+ /**
+ * @return void
+ */
+ public function testGetReport()
+ {
+ $reportName = 'test_report';
+ $connectionName = 'sales';
+
+ $this->queryFactoryMock->expects($this->once())
+ ->method('create')
+ ->with($reportName)
+ ->willReturn($this->queryMock);
+
+ $this->connectionFactoryMock->expects($this->once())
+ ->method('getConnection')
+ ->with($connectionName)
+ ->willReturn($this->connectionMock);
+
+ $this->queryMock->expects($this->once())
+ ->method('getConnectionName')
+ ->willReturn($connectionName);
+
+ $this->queryMock->expects($this->once())
+ ->method('getConfig')
+ ->willReturn(
+ [
+ 'connection' => $connectionName
+ ]
+ );
+
+ $this->connectionMock->expects($this->once())
+ ->method('query')
+ ->with($this->selectMock)
+ ->willReturn($this->statementMock);
+
+ $this->iteratorFactoryMock->expects($this->once())
+ ->method('create')
+ ->with($this->statementMock, null)
+ ->willReturn($this->iteratorMock);
+ $this->assertEquals($this->iteratorMock, $this->subject->getReport($reportName));
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/ReportXml/SelectHydratorTest.php b/app/code/Magento/Analytics/Test/Unit/ReportXml/SelectHydratorTest.php
new file mode 100644
index 0000000000000..ce57a1eca3689
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/ReportXml/SelectHydratorTest.php
@@ -0,0 +1,257 @@
+resourceConnectionMock = $this->getMockBuilder(ResourceConnection::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->connectionMock = $this->getMockBuilder(AdapterInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->selectMock = $this->getMockBuilder(Select::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->objectManagerMock = $this->getMockBuilder(ObjectManagerInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->objectManagerHelper = new ObjectManagerHelper($this);
+
+ $this->selectHydrator = $this->objectManagerHelper->getObject(
+ SelectHydrator::class,
+ [
+ 'resourceConnection' => $this->resourceConnectionMock,
+ 'objectManager' => $this->objectManagerMock,
+ ]
+ );
+ }
+
+ public function testExtract()
+ {
+ $selectParts =
+ [
+ Select::DISTINCT,
+ Select::COLUMNS,
+ Select::UNION,
+ Select::FROM,
+ Select::WHERE,
+ Select::GROUP,
+ Select::HAVING,
+ Select::ORDER,
+ Select::LIMIT_COUNT,
+ Select::LIMIT_OFFSET,
+ Select::FOR_UPDATE
+ ];
+
+ $result = [];
+ foreach ($selectParts as $part) {
+ $result[$part] = "Part";
+ }
+ $this->selectMock->expects($this->any())
+ ->method('getPart')
+ ->willReturn("Part");
+ $this->assertEquals($this->selectHydrator->extract($this->selectMock), $result);
+ }
+
+ /**
+ * @dataProvider recreateWithoutExpressionDataProvider
+ * @param array $selectParts
+ * @param array $parts
+ * @param array $partValues
+ */
+ public function testRecreateWithoutExpression($selectParts, $parts, $partValues)
+ {
+ $this->resourceConnectionMock->expects($this->once())
+ ->method('getConnection')
+ ->willReturn($this->connectionMock);
+ $this->connectionMock->expects($this->once())
+ ->method('select')
+ ->willReturn($this->selectMock);
+ foreach ($parts as $key => $part) {
+ $this->selectMock->expects($this->at($key))
+ ->method('setPart')
+ ->with($part, $partValues[$key]);
+ }
+
+ $this->assertSame($this->selectMock, $this->selectHydrator->recreate($selectParts));
+ }
+
+ /**
+ * @return array
+ */
+ public function recreateWithoutExpressionDataProvider()
+ {
+ return [
+ 'Select without expressions' => [
+ [
+ Select::COLUMNS => [
+ [
+ 'table_name',
+ 'field_name',
+ 'alias',
+ ],
+ [
+ 'table_name',
+ 'field_name_2',
+ 'alias_2',
+ ],
+ ]
+ ],
+ [Select::COLUMNS],
+ [[
+ [
+ 'table_name',
+ 'field_name',
+ 'alias',
+ ],
+ [
+ 'table_name',
+ 'field_name_2',
+ 'alias_2',
+ ],
+ ]],
+ ],
+ ];
+ }
+
+ /**
+ * @dataProvider recreateWithExpressionDataProvider
+ * @param array $selectParts
+ * @param array $expectedParts
+ * @param \PHPUnit_Framework_MockObject_MockObject[] $expressionMocks
+ */
+ public function testRecreateWithExpression(
+ array $selectParts,
+ array $expectedParts,
+ array $expressionMocks
+ ) {
+ $this->objectManagerMock
+ ->expects($this->exactly(count($expressionMocks)))
+ ->method('create')
+ ->with($this->isType('string'), $this->isType('array'))
+ ->willReturnOnConsecutiveCalls(...$expressionMocks);
+ $this->resourceConnectionMock
+ ->expects($this->once())
+ ->method('getConnection')
+ ->with()
+ ->willReturn($this->connectionMock);
+ $this->connectionMock
+ ->expects($this->once())
+ ->method('select')
+ ->with()
+ ->willReturn($this->selectMock);
+ foreach (array_keys($selectParts) as $key => $partName) {
+ $this->selectMock
+ ->expects($this->at($key))
+ ->method('setPart')
+ ->with($partName, $expectedParts[$partName]);
+ }
+
+ $this->assertSame($this->selectMock, $this->selectHydrator->recreate($selectParts));
+ }
+
+ /**
+ * @return array
+ */
+ public function recreateWithExpressionDataProvider()
+ {
+ $expressionMock = $this->getMockBuilder(JsonSerializableExpression::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ return [
+ 'Select without expressions' => [
+ 'Parts' => [
+ Select::COLUMNS => [
+ [
+ 'table_name',
+ 'field_name',
+ 'alias',
+ ],
+ [
+ 'table_name',
+ [
+ 'class' => 'Some_class',
+ 'arguments' => [
+ 'expression' => ['some(expression)']
+ ]
+ ],
+ 'alias_2',
+ ],
+ ]
+ ],
+ 'expectedParts' => [
+ Select::COLUMNS => [
+ [
+ 'table_name',
+ 'field_name',
+ 'alias',
+ ],
+ [
+ 'table_name',
+ $expressionMock,
+ 'alias_2',
+ ],
+ ]
+ ],
+ 'expectedExpressions' => [
+ $expressionMock
+ ]
+ ],
+ ];
+ }
+}
diff --git a/app/code/Magento/Analytics/composer.json b/app/code/Magento/Analytics/composer.json
new file mode 100644
index 0000000000000..7edb72db45e52
--- /dev/null
+++ b/app/code/Magento/Analytics/composer.json
@@ -0,0 +1,26 @@
+{
+ "name": "magento/module-analytics",
+ "description": "N/A",
+ "require": {
+ "php": "~7.0.13|~7.1.0",
+ "magento/module-backend": "100.2.*",
+ "magento/module-config": "101.0.*",
+ "magento/module-integration": "100.2.*",
+ "magento/module-store": "100.2.*",
+ "magento/framework": "101.0.*"
+ },
+ "type": "magento2-module",
+ "version": "100.2.3",
+ "license": [
+ "OSL-3.0",
+ "AFL-3.0"
+ ],
+ "autoload": {
+ "files": [
+ "registration.php"
+ ],
+ "psr-4": {
+ "Magento\\Analytics\\": ""
+ }
+ }
+}
diff --git a/app/code/Magento/Analytics/docs/images/M2_MA_signup.png b/app/code/Magento/Analytics/docs/images/M2_MA_signup.png
new file mode 100644
index 0000000000000..78ed8fad92881
Binary files /dev/null and b/app/code/Magento/Analytics/docs/images/M2_MA_signup.png differ
diff --git a/app/code/Magento/Analytics/docs/images/analytics_modules.png b/app/code/Magento/Analytics/docs/images/analytics_modules.png
new file mode 100644
index 0000000000000..0bf6048b0d9cc
Binary files /dev/null and b/app/code/Magento/Analytics/docs/images/analytics_modules.png differ
diff --git a/app/code/Magento/Analytics/docs/images/data_transition.png b/app/code/Magento/Analytics/docs/images/data_transition.png
new file mode 100644
index 0000000000000..a75e97983e15d
Binary files /dev/null and b/app/code/Magento/Analytics/docs/images/data_transition.png differ
diff --git a/app/code/Magento/Analytics/docs/images/definition.png b/app/code/Magento/Analytics/docs/images/definition.png
new file mode 100644
index 0000000000000..16acc576320b0
Binary files /dev/null and b/app/code/Magento/Analytics/docs/images/definition.png differ
diff --git a/app/code/Magento/Analytics/docs/images/mbi_file_exchange.png b/app/code/Magento/Analytics/docs/images/mbi_file_exchange.png
new file mode 100644
index 0000000000000..f39d2e4900703
Binary files /dev/null and b/app/code/Magento/Analytics/docs/images/mbi_file_exchange.png differ
diff --git a/app/code/Magento/Analytics/docs/images/signup.png b/app/code/Magento/Analytics/docs/images/signup.png
new file mode 100644
index 0000000000000..561e18b3a351f
Binary files /dev/null and b/app/code/Magento/Analytics/docs/images/signup.png differ
diff --git a/app/code/Magento/Analytics/docs/images/update.png b/app/code/Magento/Analytics/docs/images/update.png
new file mode 100644
index 0000000000000..149f5b5d3f9bd
Binary files /dev/null and b/app/code/Magento/Analytics/docs/images/update.png differ
diff --git a/app/code/Magento/Analytics/docs/images/update_request.png b/app/code/Magento/Analytics/docs/images/update_request.png
new file mode 100644
index 0000000000000..7181251e3634e
Binary files /dev/null and b/app/code/Magento/Analytics/docs/images/update_request.png differ
diff --git a/app/code/Magento/Analytics/etc/acl.xml b/app/code/Magento/Analytics/etc/acl.xml
new file mode 100644
index 0000000000000..bf2251895f929
--- /dev/null
+++ b/app/code/Magento/Analytics/etc/acl.xml
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Analytics/etc/adminhtml/di.xml b/app/code/Magento/Analytics/etc/adminhtml/di.xml
new file mode 100644
index 0000000000000..5e305e70e5ad3
--- /dev/null
+++ b/app/code/Magento/Analytics/etc/adminhtml/di.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+ - Magento\Analytics\Model\System\Message\NotificationAboutFailedSubscription
+
+
+
+
diff --git a/app/code/Magento/Analytics/etc/adminhtml/menu.xml b/app/code/Magento/Analytics/etc/adminhtml/menu.xml
new file mode 100644
index 0000000000000..915211c4bb85e
--- /dev/null
+++ b/app/code/Magento/Analytics/etc/adminhtml/menu.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Analytics/etc/adminhtml/routes.xml b/app/code/Magento/Analytics/etc/adminhtml/routes.xml
new file mode 100644
index 0000000000000..0ae2762dacc5f
--- /dev/null
+++ b/app/code/Magento/Analytics/etc/adminhtml/routes.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Analytics/etc/adminhtml/system.xml b/app/code/Magento/Analytics/etc/adminhtml/system.xml
new file mode 100644
index 0000000000000..4e21648d00ce8
--- /dev/null
+++ b/app/code/Magento/Analytics/etc/adminhtml/system.xml
@@ -0,0 +1,50 @@
+
+
+
+
+
+ Advanced Reporting
+ general
+ Magento_Analytics::analytics_settings
+
+ Advanced Reporting
+ For more information, see our
+ terms and conditions .]]>
+
+ Advanced Reporting Service
+ Magento\Config\Model\Config\Source\Enabledisable
+ Magento\Analytics\Model\Config\Backend\Enabled
+ Magento\Analytics\Block\Adminhtml\System\Config\SubscriptionStatusLabel
+ analytics/subscription/enabled
+
+
+ Time of day to send data
+ Magento\Analytics\Block\Adminhtml\System\Config\CollectionTimeLabel
+ Magento\Analytics\Model\Config\Backend\CollectionTime
+
+
+ Industry Data
+ Industry
+ In order to personalize your Advanced Reporting experience, please select your industry.
+ Magento\Analytics\Model\Config\Source\Vertical
+ Magento\Analytics\Model\Config\Backend\Vertical
+ Magento\Analytics\Block\Adminhtml\System\Config\Vertical
+
+
+
+
+
+
diff --git a/app/code/Magento/Analytics/etc/analytics.xml b/app/code/Magento/Analytics/etc/analytics.xml
new file mode 100644
index 0000000000000..77ebe751a31cf
--- /dev/null
+++ b/app/code/Magento/Analytics/etc/analytics.xml
@@ -0,0 +1,50 @@
+
+
+
+
+
+
+
+ modules
+
+
+
+
+
+
+
+
+
+
+
+
+
+ stores
+
+
+
+
+
+
+
+
+ websites
+
+
+
+
+
+
+
+
+ groups
+
+
+
+
+
diff --git a/app/code/Magento/Analytics/etc/analytics.xsd b/app/code/Magento/Analytics/etc/analytics.xsd
new file mode 100644
index 0000000000000..2506e3d6a6a9a
--- /dev/null
+++ b/app/code/Magento/Analytics/etc/analytics.xsd
@@ -0,0 +1,80 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ File name attribute can has only [a-zA-Z0-9/_].
+
+
+
+
+
+
+
+
+
+ Value is required.
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Analytics/etc/config.xml b/app/code/Magento/Analytics/etc/config.xml
new file mode 100644
index 0000000000000..b6194ba12993f
--- /dev/null
+++ b/app/code/Magento/Analytics/etc/config.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+ https://advancedreporting.rjmetrics.com/signup
+ https://advancedreporting.rjmetrics.com/update
+ https://dashboard.rjmetrics.com/v2/magento/signup
+ https://advancedreporting.rjmetrics.com/otp
+ https://advancedreporting.rjmetrics.com/report
+ https://advancedreporting.rjmetrics.com/report
+
+ Magento Analytics user
+
+ 02,00,00
+
+
+
+
diff --git a/app/code/Magento/Analytics/etc/crontab.xml b/app/code/Magento/Analytics/etc/crontab.xml
new file mode 100644
index 0000000000000..a4beef0359540
--- /dev/null
+++ b/app/code/Magento/Analytics/etc/crontab.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Analytics/etc/di.xml b/app/code/Magento/Analytics/etc/di.xml
new file mode 100644
index 0000000000000..b9bb9cc9ff00c
--- /dev/null
+++ b/app/code/Magento/Analytics/etc/di.xml
@@ -0,0 +1,275 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ - Magento\Analytics\Model\Connector\SignUpCommand
+ - Magento\Analytics\Model\Connector\UpdateCommand
+ - Magento\Analytics\Model\Connector\NotifyDataChangedCommand
+
+
+
+
+
+
+ Magento\Analytics\ReportXml\Config\Data
+
+
+
+
+ Magento\Analytics\ReportXml\Config\Reader
+ Magento_Analytics_ReportXml_CacheId
+
+
+
+
+ urn:magento:module:Magento_Analytics:etc/reports.xsd
+
+
+
+
+ Magento\Analytics\ReportXml\Config\Converter\Xml
+ Magento\Analytics\ReportXml\Config\SchemaLocator
+ reports.xml
+
+ - name
+ -
+
- name
+ - alias
+
+ - name
+ - name
+
+ - glue
+ -
+
- attribute
+ - operator
+
+
+ - glue
+ -
+
- attribute
+ - operator
+
+
+ - glue
+ -
+
- attribute
+ - operator
+
+ - glue
+ -
+
- attribute
+ - operator
+
+
+
+
+
+
+
+ - Magento\Analytics\ReportXml\Config\Reader\Xml
+
+
+
+
+
+
+ Magento\Analytics\Model\Config\Data
+
+
+
+
+ Magento\Analytics\Model\Config\Reader
+ Magento_Analytics_CacheId
+
+
+
+
+ urn:magento:module:Magento_Analytics:etc/analytics.xsd
+
+
+
+
+ Magento\Analytics\ReportXml\Config\Converter\Xml
+ Magento\Analytics\Model\Config\SchemaLocator
+ analytics.xml
+
+ - name
+
+
+
+
+
+
+
+ - Magento\Analytics\ReportXml\DB\Assembler\FromAssembler
+ - Magento\Analytics\ReportXml\DB\Assembler\FilterAssembler
+ - Magento\Analytics\ReportXml\DB\Assembler\JoinAssembler
+
+
+
+
+
+
+ - Magento\Analytics\Model\Config\Reader\Xml
+
+
+
+
+
+
+ - web/unsecure/base_url
+ - currency/options/base
+ - general/locale/timezone
+ - general/country/default
+ - carriers/dhl/title
+ - carriers/dhl/active
+ - carriers/fedex/title
+ - carriers/fedex/active
+ - carriers/flatrate/title
+ - carriers/flatrate/active
+ - carriers/tablerate/title
+ - carriers/tablerate/active
+ - carriers/freeshipping/title
+ - carriers/freeshipping/active
+ - carriers/ups/title
+ - carriers/ups/active
+ - carriers/usps/title
+ - carriers/usps/active
+ - payment/free/title
+ - payment/free/active
+ - payment/checkmo/title
+ - payment/checkmo/active
+ - payment/purchaseorder/title
+ - payment/purchaseorder/active
+ - payment/banktransfer/title
+ - payment/banktransfer/active
+ - payment/cashondelivery/title
+ - payment/cashondelivery/active
+ - payment/authorizenet_directpost/title
+ - payment/authorizenet_directpost/active
+ - payment/paypal_billing_agreement/title
+ - payment/paypal_billing_agreement/active
+ - payment/braintree/title
+ - payment/braintree/active
+ - payment/braintree_paypal/title
+ - payment/braintree_paypal/active
+ - analytics/general/vertical
+
+
+
+
+
+
+ - Apps and Games
+ - Athletic/Sporting Goods
+ - Art and Design
+ - Auto Parts
+ - Baby/Children’s Apparel, Gear and Toys
+ - Beauty and Cosmetics
+ - Books, Music and Magazines
+ - Crafts and Stationery
+ - Consumer Electronics
+ - Deal Site
+ - Fashion Apparel and Accessories
+ - Food, Beverage and Grocery
+ - Home Goods and Furniture
+ - Home Improvement
+ - Jewelry and Watches
+ - Mass Merchant
+ - Office Supplies
+ - Outdoor and Camping Gear
+ - Pet Goods
+ - Pharma and Medical Devices
+ - Technology B2B
+ - Other
+
+
+
+
+
+
+
+
+
+ - \Magento\Analytics\Model\Connector\ResponseHandler\SignUp
+
+
+
+
+
+
+ - Magento\Analytics\Model\Connector\ResponseHandler\Update
+ - Magento\Analytics\Model\Connector\ResponseHandler\ReSignUp
+
+
+
+
+
+
+ - Magento\Analytics\Model\Connector\ResponseHandler\OTP
+ - Magento\Analytics\Model\Connector\ResponseHandler\ReSignUp
+
+
+
+
+
+
+ - Magento\Analytics\Model\Connector\ResponseHandler\ReSignUp
+
+
+
+
+
+ SignUpResponseResolver
+
+
+
+
+ UpdateResponseResolver
+
+
+
+
+ OtpResponseResolver
+
+
+
+
+ NotifyDataChangedResponseResolver
+
+
+
+
+
+ - 1
+ - 1
+ - 1
+ - 1
+ - 1
+ - 1
+ - 1
+
+
+ - 1
+ - 1
+ - 1
+
+
+
+
diff --git a/app/code/Magento/Analytics/etc/module.xml b/app/code/Magento/Analytics/etc/module.xml
new file mode 100644
index 0000000000000..32ee5d23a4d86
--- /dev/null
+++ b/app/code/Magento/Analytics/etc/module.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Analytics/etc/reports.xml b/app/code/Magento/Analytics/etc/reports.xml
new file mode 100644
index 0000000000000..8a43658670293
--- /dev/null
+++ b/app/code/Magento/Analytics/etc/reports.xml
@@ -0,0 +1,48 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/code/Magento/Analytics/etc/reports.xsd b/app/code/Magento/Analytics/etc/reports.xsd
new file mode 100644
index 0000000000000..d0ba4068244fe
--- /dev/null
+++ b/app/code/Magento/Analytics/etc/reports.xsd
@@ -0,0 +1,88 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Analytics/etc/webapi.xml b/app/code/Magento/Analytics/etc/webapi.xml
new file mode 100644
index 0000000000000..8252d039f1d03
--- /dev/null
+++ b/app/code/Magento/Analytics/etc/webapi.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Analytics/i18n/en_US.csv b/app/code/Magento/Analytics/i18n/en_US.csv
new file mode 100644
index 0000000000000..516c388feb823
--- /dev/null
+++ b/app/code/Magento/Analytics/i18n/en_US.csv
@@ -0,0 +1,84 @@
+"Subscription status","Subscription status"
+"Sorry, there has been an error processing your request. Please try again later.","Sorry, there has been an error processing your request. Please try again later."
+"Sorry, there was an error processing your registration request to Magento Analytics. Please try again later.","Sorry, there was an error processing your registration request to Magento Analytics. Please try again later."
+"Error occurred during postponement notification","Error occurred during postponement notification"
+"Time value has an unsupported format","Time value has an unsupported format"
+"Cron settings can't be saved","Cron settings can't be saved"
+"There was an error save new configuration value.","There was an error save new configuration value."
+"Please select a vertical.","Please select a vertical."
+"--Please Select--","--Please Select--"
+"Command was not found.","Command was not found."
+"Input data must be string or convertible into string.","Input data must be string or convertible into string."
+"Input data must be non-empty string.","Input data must be non-empty string."
+"Not valid cipher method.","Not valid cipher method."
+"Encryption key can't be empty.","Encryption key can't be empty."
+"Source ""%1"" is not exist","Source ""%1"" is not exist"
+"These arguments can't be empty ""%1""","These arguments can't be empty ""%1"""
+"Cannot find predefined integration user!","Cannot find predefined integration user!"
+"File is not ready yet.","File is not ready yet."
+"Your Base URL has been changed and your reports are being updated. Advanced Reporting will be available once this change has been processed. Please try again later.","Your Base URL has been changed and your reports are being updated. Advanced Reporting will be available once this change has been processed. Please try again later."
+"Failed to synchronize data to the Magento Business Intelligence service. ","Failed to synchronize data to the Magento Business Intelligence service. "
+"Retry Synchronization ","Retry Synchronization "
+TestMessage,TestMessage
+"Error message","Error message"
+"Apps and Games","Apps and Games"
+"Athletic/Sporting Goods","Athletic/Sporting Goods"
+"Art and Design","Art and Design"
+"Advanced Reporting","Advanced Reporting"
+"Gain new insights and take command of your business' performance, using our dynamic product, order, and customer reports tailored to your customer data.","Gain new insights and take command of your business' performance, using our dynamic product, order, and customer reports tailored to your customer data."
+"View details","View details"
+"Go to Advanced Reporting","Go to Advanced Reporting"
+"An error occurred while subscription process.","An error occurred while subscription process."
+Analytics,Analytics
+API,API
+Configuration,Configuration
+"Business Intelligence","Business Intelligence"
+"BI Essentials","BI Essentials"
+"This service provides a dynamic suite of reports with rich insights about your business.
+ Your reports can be accessed securely on a personalized dashboard outside of the admin panel by clicking on the
+ ""Go to Advanced Reporting"" link. For more information, see our
+ terms and conditions .
+ ","This service provides a dynamic suite of reports with rich insights about your business.
+ Your reports can be accessed securely on a personalized dashboard outside of the admin panel by clicking on the
+ ""Go to Advanced Reporting"" link. For more information, see our
+ terms and conditions ."
+"Advanced Reporting Service","Advanced Reporting Service"
+Industry,Industry
+"Time of day to send data","Time of day to send data"
+"Get more insights from Magento Business Intelligence ","Get more insights from Magento Business Intelligence "
+"Magento Business Intelligence provides you with a simple and clear path to
+ becoming more data driven. Learn more about BI Essentials tier.","Magento Business Intelligence provides you with a simple and clear path to
+ becoming more data driven. Learn more about BI Essentials tier."
+"Auto Parts","Auto Parts"
+"Baby/Children’s Apparel, Gear and Toys","Baby/Children’s Apparel, Gear and Toys"
+"Beauty and Cosmetics","Beauty and Cosmetics"
+"Books, Music and Magazines","Books, Music and Magazines"
+"Crafts and Stationery","Crafts and Stationery"
+"Consumer Electronics","Consumer Electronics"
+"Deal Site","Deal Site"
+"Fashion Apparel and Accessories","Fashion Apparel and Accessories"
+"Food, Beverage and Grocery","Food, Beverage and Grocery"
+"Home Goods and Furniture","Home Goods and Furniture"
+"Home Improvement","Home Improvement"
+"Jewelry and Watches","Jewelry and Watches"
+"Mass Merchant","Mass Merchant"
+"Office Supplies","Office Supplies"
+"Outdoor and Camping Gear","Outdoor and Camping Gear"
+"Pet Goods","Pet Goods"
+"Pharma and Medical Devices","Pharma and Medical Devices"
+"Technology B2B","Technology B2B"
+"Analytics Subscription","Analytics Subscription"
+"powered by Magento Business Intelligence","powered by Magento Business Intelligence"
+"Are you sure you want to opt out?","Are you sure you want to opt out?"
+Cancel,Cancel
+"Opt out","Opt out"
+"Advanced Reporting in included,
+ free of charge, in your Magento software. When you opt out, we collect no product, order, and
+ customer data to generate our dynamic reports.
To opt in later: You can always turn on Advanced
+ Reporting in you Admin Panel.
","Advanced Reporting in included,
+ free of charge, in your Magento software. When you opt out, we collect no product, order, and
+ customer data to generate our dynamic reports.
To opt in later: You can always turn on Advanced
+ Reporting in you Admin Panel.
"
+"In order to personalize your Advanced Reporting experience, please select your industry.","In order to personalize your Advanced Reporting experience, please select your industry."
diff --git a/app/code/Magento/Analytics/registration.php b/app/code/Magento/Analytics/registration.php
new file mode 100644
index 0000000000000..58d3688b7491d
--- /dev/null
+++ b/app/code/Magento/Analytics/registration.php
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Analytics/view/adminhtml/templates/dashboard/section.phtml b/app/code/Magento/Analytics/view/adminhtml/templates/dashboard/section.phtml
new file mode 100644
index 0000000000000..a22c603b2a8b3
--- /dev/null
+++ b/app/code/Magento/Analytics/view/adminhtml/templates/dashboard/section.phtml
@@ -0,0 +1,28 @@
+
+
+
+
+
+ = $block->escapeHtml(__('Advanced Reporting')) ?>
+
+
+ = $block->escapeHtml(__('Gain new insights and take command of your business\' performance,' .
+ ' using our dynamic product, order, and customer reports tailored to your customer data.')) ?>
+
+
+
+
diff --git a/app/code/Magento/Authorization/Test/Mftf/LICENSE.txt b/app/code/Magento/Authorization/Test/Mftf/LICENSE.txt
new file mode 100644
index 0000000000000..49525fd99da9c
--- /dev/null
+++ b/app/code/Magento/Authorization/Test/Mftf/LICENSE.txt
@@ -0,0 +1,48 @@
+
+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/Authorization/Test/Mftf/LICENSE_AFL.txt b/app/code/Magento/Authorization/Test/Mftf/LICENSE_AFL.txt
new file mode 100644
index 0000000000000..f39d641b18a19
--- /dev/null
+++ b/app/code/Magento/Authorization/Test/Mftf/LICENSE_AFL.txt
@@ -0,0 +1,48 @@
+
+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/Authorization/Test/Mftf/README.md b/app/code/Magento/Authorization/Test/Mftf/README.md
new file mode 100644
index 0000000000000..1d44ab2e73052
--- /dev/null
+++ b/app/code/Magento/Authorization/Test/Mftf/README.md
@@ -0,0 +1,3 @@
+# Authorization Functional Tests
+
+The Functional Test Module for **Magento Authorization** module.
diff --git a/app/code/Magento/Authorization/Test/Unit/Model/Acl/AclRetrieverTest.php b/app/code/Magento/Authorization/Test/Unit/Model/Acl/AclRetrieverTest.php
index bd1a3616a746e..58720ad9c9f5c 100644
--- a/app/code/Magento/Authorization/Test/Unit/Model/Acl/AclRetrieverTest.php
+++ b/app/code/Magento/Authorization/Test/Unit/Model/Acl/AclRetrieverTest.php
@@ -78,6 +78,9 @@ public function testGetAllowedResourcesByUser()
);
}
+ /**
+ * @return AclRetriever
+ */
protected function createAclRetriever()
{
$this->roleMock = $this->createPartialMock(\Magento\Authorization\Model\Role::class, ['getId', '__wakeup']);
diff --git a/app/code/Magento/Authorization/composer.json b/app/code/Magento/Authorization/composer.json
index a894193888100..35063d1516784 100644
--- a/app/code/Magento/Authorization/composer.json
+++ b/app/code/Magento/Authorization/composer.json
@@ -2,12 +2,12 @@
"name": "magento/module-authorization",
"description": "Authorization module provides access to Magento ACL functionality.",
"require": {
- "php": "7.0.2|7.0.4|~7.0.6|~7.1.0",
+ "php": "~7.0.13|~7.1.0",
"magento/module-backend": "100.2.*",
- "magento/framework": "100.2.*"
+ "magento/framework": "101.0.*"
},
"type": "magento2-module",
- "version": "100.2.0-dev",
+ "version": "100.2.2",
"license": [
"OSL-3.0",
"AFL-3.0"
diff --git a/app/code/Magento/Authorizenet/Model/Directpost.php b/app/code/Magento/Authorizenet/Model/Directpost.php
index 0f10fd633cb5b..9186acce83b4c 100644
--- a/app/code/Magento/Authorizenet/Model/Directpost.php
+++ b/app/code/Magento/Authorizenet/Model/Directpost.php
@@ -5,10 +5,9 @@
*/
namespace Magento\Authorizenet\Model;
-use Magento\Framework\HTTP\ZendClientFactory;
+use Magento\Framework\App\ObjectManager;
use Magento\Payment\Model\Method\ConfigInterface;
use Magento\Payment\Model\Method\TransparentInterface;
-use Magento\Sales\Model\Order\Email\Sender\OrderSender;
/**
* Authorize.net DirectPost payment method model.
@@ -102,7 +101,7 @@ class Directpost extends \Magento\Authorizenet\Model\Authorizenet implements Tra
protected $response;
/**
- * @var OrderSender
+ * @var \Magento\Sales\Model\Order\Email\Sender\OrderSender
*/
protected $orderSender;
@@ -123,6 +122,16 @@ class Directpost extends \Magento\Authorizenet\Model\Authorizenet implements Tra
*/
private $psrLogger;
+ /**
+ * @var \Magento\Sales\Api\PaymentFailuresInterface
+ */
+ private $paymentFailures;
+
+ /**
+ * @var \Magento\Sales\Model\Order
+ */
+ private $order;
+
/**
* @param \Magento\Framework\Model\Context $context
* @param \Magento\Framework\Registry $registry
@@ -141,11 +150,12 @@ class Directpost extends \Magento\Authorizenet\Model\Authorizenet implements Tra
* @param \Magento\Sales\Model\OrderFactory $orderFactory
* @param \Magento\Store\Model\StoreManagerInterface $storeManager
* @param \Magento\Quote\Api\CartRepositoryInterface $quoteRepository
- * @param OrderSender $orderSender
+ * @param \Magento\Sales\Model\Order\Email\Sender\OrderSender $orderSender
* @param \Magento\Sales\Api\TransactionRepositoryInterface $transactionRepository
* @param \Magento\Framework\Model\ResourceModel\AbstractResource $resource
* @param \Magento\Framework\Data\Collection\AbstractDb $resourceCollection
* @param array $data
+ * @param \Magento\Sales\Api\PaymentFailuresInterface|null $paymentFailures
* @SuppressWarnings(PHPMD.ExcessiveParameterList)
*/
public function __construct(
@@ -162,7 +172,7 @@ public function __construct(
\Magento\Authorizenet\Model\Directpost\Request\Factory $requestFactory,
\Magento\Authorizenet\Model\Directpost\Response\Factory $responseFactory,
TransactionService $transactionService,
- ZendClientFactory $httpClientFactory,
+ \Magento\Framework\HTTP\ZendClientFactory $httpClientFactory,
\Magento\Sales\Model\OrderFactory $orderFactory,
\Magento\Store\Model\StoreManagerInterface $storeManager,
\Magento\Quote\Api\CartRepositoryInterface $quoteRepository,
@@ -170,7 +180,8 @@ public function __construct(
\Magento\Sales\Api\TransactionRepositoryInterface $transactionRepository,
\Magento\Framework\Model\ResourceModel\AbstractResource $resource = null,
\Magento\Framework\Data\Collection\AbstractDb $resourceCollection = null,
- array $data = []
+ array $data = [],
+ \Magento\Sales\Api\PaymentFailuresInterface $paymentFailures = null
) {
$this->orderFactory = $orderFactory;
$this->storeManager = $storeManager;
@@ -179,6 +190,8 @@ public function __construct(
$this->orderSender = $orderSender;
$this->transactionRepository = $transactionRepository;
$this->_code = static::METHOD_CODE;
+ $this->paymentFailures = $paymentFailures ? : ObjectManager::getInstance()
+ ->get(\Magento\Sales\Api\PaymentFailuresInterface::class);
parent::__construct(
$context,
@@ -561,13 +574,10 @@ public function process(array $responseData)
$this->validateResponse();
$response = $this->getResponse();
- //operate with order
- $orderIncrementId = $response->getXInvoiceNum();
$responseText = $this->dataHelper->wrapGatewayError($response->getXResponseReasonText());
$isError = false;
- if ($orderIncrementId) {
- /* @var $order \Magento\Sales\Model\Order */
- $order = $this->orderFactory->create()->loadByIncrementId($orderIncrementId);
+ if ($this->getOrderIncrementId()) {
+ $order = $this->getOrderFromResponse();
//check payment method
$payment = $order->getPayment();
if (!$payment || $payment->getMethod() != $this->getCode()) {
@@ -632,9 +642,10 @@ public function checkResponseCode()
return true;
case self::RESPONSE_CODE_DECLINED:
case self::RESPONSE_CODE_ERROR:
- throw new \Magento\Framework\Exception\LocalizedException(
- $this->dataHelper->wrapGatewayError($this->getResponse()->getXResponseReasonText())
- );
+ $errorMessage = $this->dataHelper->wrapGatewayError($this->getResponse()->getXResponseReasonText());
+ $order = $this->getOrderFromResponse();
+ $this->paymentFailures->handle((int)$order->getQuoteId(), $errorMessage);
+ throw new \Magento\Framework\Exception\LocalizedException($errorMessage);
default:
throw new \Magento\Framework\Exception\LocalizedException(
__('There was a payment authorization error.')
@@ -803,10 +814,14 @@ protected function declineOrder(\Magento\Sales\Model\Order $order, $message = ''
{
try {
$response = $this->getResponse();
- if ($voidPayment && $response->getXTransId() && strtoupper($response->getXType())
- == self::REQUEST_TYPE_AUTH_ONLY
+ if ($voidPayment
+ && $response->getXTransId()
+ && strtoupper($response->getXType()) == self::REQUEST_TYPE_AUTH_ONLY
) {
- $order->getPayment()->setTransactionId(null)->setParentTransactionId($response->getXTransId())->void();
+ $order->getPayment()
+ ->setTransactionId(null)
+ ->setParentTransactionId($response->getXTransId())
+ ->void($response);
}
$order->registerCancellation($message)->save();
} catch (\Exception $e) {
@@ -988,12 +1003,40 @@ protected function getTransactionResponse($transactionId)
private function getPsrLogger()
{
if (null === $this->psrLogger) {
- $this->psrLogger = \Magento\Framework\App\ObjectManager::getInstance()
+ $this->psrLogger = ObjectManager::getInstance()
->get(\Psr\Log\LoggerInterface::class);
}
return $this->psrLogger;
}
+ /**
+ * Fetch order by increment id from response.
+ *
+ * @return \Magento\Sales\Model\Order
+ */
+ private function getOrderFromResponse(): \Magento\Sales\Model\Order
+ {
+ if (!$this->order) {
+ $this->order = $this->orderFactory->create();
+
+ if ($incrementId = $this->getOrderIncrementId()) {
+ $this->order = $this->order->loadByIncrementId($incrementId);
+ }
+ }
+
+ return $this->order;
+ }
+
+ /**
+ * Fetch order increment id from response.
+ *
+ * @return string
+ */
+ private function getOrderIncrementId(): string
+ {
+ return $this->getResponse()->getXInvoiceNum();
+ }
+
/**
* Checks if filter action is Report Only. Transactions that trigger this filter are processed as normal,
* but are also reported in the Merchant Interface as triggering this filter.
diff --git a/app/code/Magento/Authorizenet/Model/Directpost/Request.php b/app/code/Magento/Authorizenet/Model/Directpost/Request.php
index d9a403e5c991e..fc78d836b6080 100644
--- a/app/code/Magento/Authorizenet/Model/Directpost/Request.php
+++ b/app/code/Magento/Authorizenet/Model/Directpost/Request.php
@@ -112,50 +112,50 @@ public function setDataFromOrder(
sprintf('%.2F', $order->getBaseShippingAmount())
);
- //need to use strval() because NULL values IE6-8 decodes as "null" in JSON in JavaScript,
+ //need to use (string) because NULL values IE6-8 decodes as "null" in JSON in JavaScript,
//but we need "" for null values.
$billing = $order->getBillingAddress();
if (!empty($billing)) {
- $this->setXFirstName(strval($billing->getFirstname()))
- ->setXLastName(strval($billing->getLastname()))
- ->setXCompany(strval($billing->getCompany()))
- ->setXAddress(strval($billing->getStreetLine(1)))
- ->setXCity(strval($billing->getCity()))
- ->setXState(strval($billing->getRegion()))
- ->setXZip(strval($billing->getPostcode()))
- ->setXCountry(strval($billing->getCountryId()))
- ->setXPhone(strval($billing->getTelephone()))
- ->setXFax(strval($billing->getFax()))
- ->setXCustId(strval($billing->getCustomerId()))
- ->setXCustomerIp(strval($order->getRemoteIp()))
- ->setXCustomerTaxId(strval($billing->getTaxId()))
- ->setXEmail(strval($order->getCustomerEmail()))
- ->setXEmailCustomer(strval($paymentMethod->getConfigData('email_customer')))
- ->setXMerchantEmail(strval($paymentMethod->getConfigData('merchant_email')));
+ $this->setXFirstName((string)$billing->getFirstname())
+ ->setXLastName((string)$billing->getLastname())
+ ->setXCompany((string)$billing->getCompany())
+ ->setXAddress((string)$billing->getStreetLine(1))
+ ->setXCity((string)$billing->getCity())
+ ->setXState((string)$billing->getRegion())
+ ->setXZip((string)$billing->getPostcode())
+ ->setXCountry((string)$billing->getCountryId())
+ ->setXPhone((string)$billing->getTelephone())
+ ->setXFax((string)$billing->getFax())
+ ->setXCustId((string)$billing->getCustomerId())
+ ->setXCustomerIp((string)$order->getRemoteIp())
+ ->setXCustomerTaxId((string)$billing->getTaxId())
+ ->setXEmail((string)$order->getCustomerEmail())
+ ->setXEmailCustomer((string)$paymentMethod->getConfigData('email_customer'))
+ ->setXMerchantEmail((string)$paymentMethod->getConfigData('merchant_email'));
}
$shipping = $order->getShippingAddress();
if (!empty($shipping)) {
$this->setXShipToFirstName(
- strval($shipping->getFirstname())
+ (string)$shipping->getFirstname()
)->setXShipToLastName(
- strval($shipping->getLastname())
+ (string)$shipping->getLastname()
)->setXShipToCompany(
- strval($shipping->getCompany())
+ (string)$shipping->getCompany()
)->setXShipToAddress(
- strval($shipping->getStreetLine(1))
+ (string)$shipping->getStreetLine(1)
)->setXShipToCity(
- strval($shipping->getCity())
+ (string)$shipping->getCity()
)->setXShipToState(
- strval($shipping->getRegion())
+ (string)$shipping->getRegion()
)->setXShipToZip(
- strval($shipping->getPostcode())
+ (string)$shipping->getPostcode()
)->setXShipToCountry(
- strval($shipping->getCountryId())
+ (string)$shipping->getCountryId()
);
}
- $this->setXPoNum(strval($payment->getPoNumber()));
+ $this->setXPoNum((string)$payment->getPoNumber());
return $this;
}
diff --git a/app/code/Magento/Authorizenet/Test/Mftf/LICENSE.txt b/app/code/Magento/Authorizenet/Test/Mftf/LICENSE.txt
new file mode 100644
index 0000000000000..49525fd99da9c
--- /dev/null
+++ b/app/code/Magento/Authorizenet/Test/Mftf/LICENSE.txt
@@ -0,0 +1,48 @@
+
+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/Authorizenet/Test/Mftf/LICENSE_AFL.txt b/app/code/Magento/Authorizenet/Test/Mftf/LICENSE_AFL.txt
new file mode 100644
index 0000000000000..f39d641b18a19
--- /dev/null
+++ b/app/code/Magento/Authorizenet/Test/Mftf/LICENSE_AFL.txt
@@ -0,0 +1,48 @@
+
+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/Authorizenet/Test/Mftf/README.md b/app/code/Magento/Authorizenet/Test/Mftf/README.md
new file mode 100644
index 0000000000000..9391126a85c94
--- /dev/null
+++ b/app/code/Magento/Authorizenet/Test/Mftf/README.md
@@ -0,0 +1,3 @@
+# Authorizenet Functional Tests
+
+The Functional Test Module for **Magento Authorizenet** module.
diff --git a/app/code/Magento/Authorizenet/Test/Unit/Model/Directpost/ResponseTest.php b/app/code/Magento/Authorizenet/Test/Unit/Model/Directpost/ResponseTest.php
index 6e5d55e52675e..b4274e87401ca 100644
--- a/app/code/Magento/Authorizenet/Test/Unit/Model/Directpost/ResponseTest.php
+++ b/app/code/Magento/Authorizenet/Test/Unit/Model/Directpost/ResponseTest.php
@@ -37,6 +37,9 @@ public function testGenerateHash($merchantMd5, $merchantApiLogin, $amount, $amou
);
}
+ /**
+ * @return array
+ */
public function generateHashDataProvider()
{
return [
@@ -57,6 +60,14 @@ public function generateHashDataProvider()
];
}
+ /**
+ * @param $merchantMd5
+ * @param $merchantApiLogin
+ * @param $amount
+ * @param $transactionId
+ *
+ * @return string
+ */
protected function generateHash($merchantMd5, $merchantApiLogin, $amount, $transactionId)
{
return strtoupper(md5($merchantMd5 . $merchantApiLogin . $transactionId . $amount));
diff --git a/app/code/Magento/Authorizenet/Test/Unit/Model/DirectpostTest.php b/app/code/Magento/Authorizenet/Test/Unit/Model/DirectpostTest.php
index dbb6ac8333c14..26d96b9bc2d90 100644
--- a/app/code/Magento/Authorizenet/Test/Unit/Model/DirectpostTest.php
+++ b/app/code/Magento/Authorizenet/Test/Unit/Model/DirectpostTest.php
@@ -5,6 +5,7 @@
*/
namespace Magento\Authorizenet\Test\Unit\Model;
+use Magento\Sales\Api\PaymentFailuresInterface;
use Magento\Framework\Simplexml\Element;
use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper;
use Magento\Authorizenet\Model\Directpost;
@@ -74,6 +75,14 @@ class DirectpostTest extends \PHPUnit\Framework\TestCase
*/
protected $requestFactory;
+ /**
+ * @var PaymentFailuresInterface|\PHPUnit_Framework_MockObject_MockObject
+ */
+ private $paymentFailures;
+
+ /**
+ * @inheritdoc
+ */
protected function setUp()
{
$this->scopeConfigMock = $this->getMockBuilder(\Magento\Framework\App\Config\ScopeConfigInterface::class)
@@ -104,6 +113,12 @@ protected function setUp()
->setMethods(['getTransactionDetails'])
->getMock();
+ $this->paymentFailures = $this->getMockBuilder(
+ PaymentFailuresInterface::class
+ )
+ ->disableOriginalConstructor()
+ ->getMock();
+
$this->requestFactory = $this->getRequestFactoryMock();
$httpClientFactoryMock = $this->getHttpClientFactoryMock();
@@ -117,7 +132,8 @@ protected function setUp()
'responseFactory' => $this->responseFactoryMock,
'transactionRepository' => $this->transactionRepositoryMock,
'transactionService' => $this->transactionServiceMock,
- 'httpClientFactory' => $httpClientFactoryMock
+ 'httpClientFactory' => $httpClientFactoryMock,
+ 'paymentFailures' => $this->paymentFailures,
]
);
}
@@ -313,12 +329,16 @@ public function checkResponseCodeSuccessDataProvider()
}
/**
- * @param bool $responseCode
+ * Checks response failures behaviour.
+ *
+ * @param int $responseCode
+ * @param int $failuresHandlerCalls
+ * @return void
*
* @expectedException \Magento\Framework\Exception\LocalizedException
* @dataProvider checkResponseCodeFailureDataProvider
*/
- public function testCheckResponseCodeFailure($responseCode)
+ public function testCheckResponseCodeFailure(int $responseCode, int $failuresHandlerCalls)
{
$reasonText = 'reason text';
@@ -333,18 +353,35 @@ public function testCheckResponseCodeFailure($responseCode)
->with($reasonText)
->willReturn(__('Gateway error: %1', $reasonText));
+ $orderMock = $this->getMockBuilder(Order::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $orderMock->expects($this->exactly($failuresHandlerCalls))
+ ->method('getQuoteId')
+ ->willReturn(1);
+
+ $this->paymentFailures->expects($this->exactly($failuresHandlerCalls))
+ ->method('handle')
+ ->with(1);
+
+ $reflection = new \ReflectionClass($this->directpost);
+ $order = $reflection->getProperty('order');
+ $order->setAccessible(true);
+ $order->setValue($this->directpost, $orderMock);
+
$this->directpost->checkResponseCode();
}
/**
* @return array
*/
- public function checkResponseCodeFailureDataProvider()
+ public function checkResponseCodeFailureDataProvider(): array
{
return [
- ['responseCode' => Directpost::RESPONSE_CODE_DECLINED],
- ['responseCode' => Directpost::RESPONSE_CODE_ERROR],
- ['responseCode' => 999999]
+ ['responseCode' => Directpost::RESPONSE_CODE_DECLINED, 1],
+ ['responseCode' => Directpost::RESPONSE_CODE_ERROR, 1],
+ ['responseCode' => 999999, 0],
];
}
diff --git a/app/code/Magento/Authorizenet/composer.json b/app/code/Magento/Authorizenet/composer.json
index 2abfebc3c1cb7..90f19e36777b2 100644
--- a/app/code/Magento/Authorizenet/composer.json
+++ b/app/code/Magento/Authorizenet/composer.json
@@ -2,21 +2,21 @@
"name": "magento/module-authorizenet",
"description": "N/A",
"require": {
- "php": "7.0.2|7.0.4|~7.0.6|~7.1.0",
- "magento/module-sales": "100.2.*",
+ "php": "~7.0.13|~7.1.0",
+ "magento/module-sales": "101.0.*",
"magento/module-store": "100.2.*",
- "magento/module-quote": "100.2.*",
+ "magento/module-quote": "101.0.*",
"magento/module-checkout": "100.2.*",
"magento/module-backend": "100.2.*",
"magento/module-payment": "100.2.*",
- "magento/module-catalog": "101.1.*",
- "magento/framework": "100.2.*"
+ "magento/module-catalog": "102.0.*",
+ "magento/framework": "101.0.*"
},
"suggest": {
- "magento/module-config": "100.2.*"
+ "magento/module-config": "101.0.*"
},
"type": "magento2-module",
- "version": "100.2.0-dev",
+ "version": "100.2.2",
"license": [
"proprietary"
],
diff --git a/app/code/Magento/Authorizenet/view/frontend/requirejs-config.js b/app/code/Magento/Authorizenet/view/frontend/requirejs-config.js
index 8edc38dce6f60..8c4c90bf111de 100644
--- a/app/code/Magento/Authorizenet/view/frontend/requirejs-config.js
+++ b/app/code/Magento/Authorizenet/view/frontend/requirejs-config.js
@@ -6,7 +6,8 @@
var config = {
map: {
'*': {
- transparent: 'Magento_Payment/transparent'
+ transparent: 'Magento_Payment/js/transparent',
+ 'Magento_Payment/transparent': 'Magento_Payment/js/transparent'
}
}
};
diff --git a/app/code/Magento/Backend/App/AbstractAction.php b/app/code/Magento/Backend/App/AbstractAction.php
index 99ee86b2b6407..3f658ee90bf4e 100644
--- a/app/code/Magento/Backend/App/AbstractAction.php
+++ b/app/code/Magento/Backend/App/AbstractAction.php
@@ -217,6 +217,7 @@ public function dispatch(\Magento\Framework\App\RequestInterface $request)
$this->_view->loadLayout(['default', 'adminhtml_denied'], true, true, false);
$this->_view->renderLayout();
$this->_request->setDispatched(true);
+
return $this->_response;
}
@@ -226,6 +227,11 @@ public function dispatch(\Magento\Framework\App\RequestInterface $request)
$this->_processLocaleSettings();
+ // Need to preload isFirstPageAfterLogin (see https://github.com/magento/magento2/issues/15510)
+ if ($this->_auth->isLoggedIn()) {
+ $this->_auth->getAuthStorage()->isFirstPageAfterLogin();
+ }
+
return parent::dispatch($request);
}
diff --git a/app/code/Magento/Backend/App/Action/Plugin/Authentication.php b/app/code/Magento/Backend/App/Action/Plugin/Authentication.php
index 68506a521c1cf..4b25e9921e404 100644
--- a/app/code/Magento/Backend/App/Action/Plugin/Authentication.php
+++ b/app/code/Magento/Backend/App/Action/Plugin/Authentication.php
@@ -160,7 +160,7 @@ protected function _processNotLoggedInUser(\Magento\Framework\App\RequestInterfa
} else {
$this->_actionFlag->set('', \Magento\Framework\App\ActionInterface::FLAG_NO_DISPATCH, true);
$this->_response->setRedirect($this->_url->getCurrentUrl());
- $this->messageManager->addError(__('Invalid Form Key. Please refresh the page.'));
+ $this->messageManager->addErrorMessage(__('Invalid Form Key. Please refresh the page.'));
$isRedirectNeeded = true;
}
}
@@ -205,7 +205,7 @@ protected function _performLogin(\Magento\Framework\App\RequestInterface $reques
$this->_auth->login($username, $password);
} catch (AuthenticationException $e) {
if (!$request->getParam('messageSent')) {
- $this->messageManager->addError($e->getMessage());
+ $this->messageManager->addErrorMessage($e->getMessage());
$request->setParam('messageSent', true);
$outputValue = false;
}
diff --git a/app/code/Magento/Backend/App/Config.php b/app/code/Magento/Backend/App/Config.php
index bb60a1e69dd47..e0983139a690d 100644
--- a/app/code/Magento/Backend/App/Config.php
+++ b/app/code/Magento/Backend/App/Config.php
@@ -68,6 +68,6 @@ public function isSetFlag($path)
if ($path) {
$configPath .= '/' . $path;
}
- return (bool) $this->appConfig->get(System::CONFIG_TYPE, $configPath);
+ return (bool)$this->appConfig->get(System::CONFIG_TYPE, $configPath);
}
}
diff --git a/app/code/Magento/Backend/App/DefaultPath.php b/app/code/Magento/Backend/App/DefaultPath.php
index df8b718389741..b790a2edc3fab 100644
--- a/app/code/Magento/Backend/App/DefaultPath.php
+++ b/app/code/Magento/Backend/App/DefaultPath.php
@@ -42,6 +42,6 @@ public function __construct(\Magento\Backend\App\ConfigInterface $config)
*/
public function getPart($code)
{
- return isset($this->_parts[$code]) ? $this->_parts[$code] : null;
+ return $this->_parts[$code] ?? null;
}
}
diff --git a/app/code/Magento/Backend/Block/Cache.php b/app/code/Magento/Backend/Block/Cache.php
index e14358396aa70..82c36bf3a1fe4 100644
--- a/app/code/Magento/Backend/Block/Cache.php
+++ b/app/code/Magento/Backend/Block/Cache.php
@@ -22,24 +22,29 @@ protected function _construct()
$this->_headerText = __('Cache Storage Management');
parent::_construct();
$this->buttonList->remove('add');
- $this->buttonList->add(
- 'flush_magento',
- [
- 'label' => __('Flush Magento Cache'),
- 'onclick' => 'setLocation(\'' . $this->getFlushSystemUrl() . '\')',
- 'class' => 'primary flush-cache-magento'
- ]
- );
- $message = __('The cache storage may contain additional data. Are you sure that you want to flush it?');
- $this->buttonList->add(
- 'flush_system',
- [
- 'label' => __('Flush Cache Storage'),
- 'onclick' => 'confirmSetLocation(\'' . $message . '\', \'' . $this->getFlushStorageUrl() . '\')',
- 'class' => 'flush-cache-storage'
- ]
- );
+ if ($this->_authorization->isAllowed('Magento_Backend::flush_magento_cache')) {
+ $this->buttonList->add(
+ 'flush_magento',
+ [
+ 'label' => __('Flush Magento Cache'),
+ 'onclick' => 'setLocation(\'' . $this->getFlushSystemUrl() . '\')',
+ 'class' => 'primary flush-cache-magento'
+ ]
+ );
+ }
+
+ if ($this->_authorization->isAllowed('Magento_Backend::flush_cache_storage')) {
+ $message = __('The cache storage may contain additional data. Are you sure that you want to flush it?');
+ $this->buttonList->add(
+ 'flush_system',
+ [
+ 'label' => __('Flush Cache Storage'),
+ 'onclick' => 'confirmSetLocation(\'' . $message . '\', \'' . $this->getFlushStorageUrl() . '\')',
+ 'class' => 'flush-cache-storage'
+ ]
+ );
+ }
}
/**
diff --git a/app/code/Magento/Backend/Block/Cache/Permissions.php b/app/code/Magento/Backend/Block/Cache/Permissions.php
new file mode 100644
index 0000000000000..272a603145f09
--- /dev/null
+++ b/app/code/Magento/Backend/Block/Cache/Permissions.php
@@ -0,0 +1,62 @@
+authorization = $authorization;
+ }
+
+ /**
+ * @return bool
+ */
+ public function hasAccessToFlushCatalogImages()
+ {
+ return $this->authorization->isAllowed('Magento_Backend::flush_catalog_images');
+ }
+ /**
+ * @return bool
+ */
+ public function hasAccessToFlushJsCss()
+ {
+ return $this->authorization->isAllowed('Magento_Backend::flush_js_css');
+ }
+ /**
+ * @return bool
+ */
+ public function hasAccessToFlushStaticFiles()
+ {
+ return $this->authorization->isAllowed('Magento_Backend::flush_static_files');
+ }
+ /**
+ * @return bool
+ */
+ public function hasAccessToAdditionalActions()
+ {
+ return ($this->hasAccessToFlushCatalogImages()
+ || $this->hasAccessToFlushJsCss()
+ || $this->hasAccessToFlushStaticFiles());
+ }
+}
diff --git a/app/code/Magento/Backend/Block/Dashboard.php b/app/code/Magento/Backend/Block/Dashboard.php
index 8d0a061621fe3..e1e87d8d4c5a3 100644
--- a/app/code/Magento/Backend/Block/Dashboard.php
+++ b/app/code/Magento/Backend/Block/Dashboard.php
@@ -20,7 +20,7 @@ class Dashboard extends \Magento\Backend\Block\Template
/**
* @var string
*/
- protected $_template = 'dashboard/index.phtml';
+ protected $_template = 'Magento_Backend::dashboard/index.phtml';
/**
* @return void
diff --git a/app/code/Magento/Backend/Block/Dashboard/Bar.php b/app/code/Magento/Backend/Block/Dashboard/Bar.php
index 29557f12c1093..7ccb2d51ccd1b 100644
--- a/app/code/Magento/Backend/Block/Dashboard/Bar.php
+++ b/app/code/Magento/Backend/Block/Dashboard/Bar.php
@@ -38,14 +38,6 @@ public function getTotals()
*/
public function addTotal($label, $value, $isQuantity = false)
{
- /*if (!$isQuantity) {
- $value = $this->format($value);
- $decimals = substr($value, -2);
- $value = substr($value, 0, -2);
- } else {
- $value = ($value != '')?$value:0;
- $decimals = '';
- }*/
if (!$isQuantity) {
$value = $this->format($value);
}
diff --git a/app/code/Magento/Backend/Block/Dashboard/Graph.php b/app/code/Magento/Backend/Block/Dashboard/Graph.php
index cecd7b8050352..8e238ccab44cb 100644
--- a/app/code/Magento/Backend/Block/Dashboard/Graph.php
+++ b/app/code/Magento/Backend/Block/Dashboard/Graph.php
@@ -90,7 +90,7 @@ class Graph extends \Magento\Backend\Block\Dashboard\AbstractDashboard
/**
* @var string
*/
- protected $_template = 'dashboard/graph.phtml';
+ protected $_template = 'Magento_Backend::dashboard/graph.phtml';
/**
* Adminhtml dashboard data
@@ -421,6 +421,8 @@ public function getChartUrl($directUrl = true)
$tmpstring = implode('|', $this->_axisLabels[$idx]);
$valueBuffer[] = $indexid . ":|" . $tmpstring;
+ } elseif ($idx == 'y') {
+ $valueBuffer[] = $indexid . ":|" . implode('|', $yLabels);
}
$indexid++;
}
diff --git a/app/code/Magento/Backend/Block/Dashboard/Grid.php b/app/code/Magento/Backend/Block/Dashboard/Grid.php
index 602b5e414d538..f7f9a79f17eb0 100644
--- a/app/code/Magento/Backend/Block/Dashboard/Grid.php
+++ b/app/code/Magento/Backend/Block/Dashboard/Grid.php
@@ -17,7 +17,7 @@ class Grid extends \Magento\Backend\Block\Widget\Grid\Extended
/**
* @var string
*/
- protected $_template = 'dashboard/grid.phtml';
+ protected $_template = 'Magento_Backend::dashboard/grid.phtml';
/**
* Setting default for every grid on dashboard
diff --git a/app/code/Magento/Backend/Block/Dashboard/Orders/Grid.php b/app/code/Magento/Backend/Block/Dashboard/Orders/Grid.php
index 9d9409fba093b..50279786c0a5b 100644
--- a/app/code/Magento/Backend/Block/Dashboard/Orders/Grid.php
+++ b/app/code/Magento/Backend/Block/Dashboard/Orders/Grid.php
@@ -92,7 +92,7 @@ protected function _prepareCollection()
protected function _afterLoadCollection()
{
foreach ($this->getCollection() as $item) {
- $item->getCustomer() ?: $item->setCustomer('Guest');
+ $item->getCustomer() ?: $item->setCustomer($item->getBillingAddress()->getName());
}
return $this;
}
diff --git a/app/code/Magento/Backend/Block/Dashboard/Sales.php b/app/code/Magento/Backend/Block/Dashboard/Sales.php
index d0f056230bcd1..6d7a4d6458a8e 100644
--- a/app/code/Magento/Backend/Block/Dashboard/Sales.php
+++ b/app/code/Magento/Backend/Block/Dashboard/Sales.php
@@ -15,7 +15,7 @@ class Sales extends \Magento\Backend\Block\Dashboard\Bar
/**
* @var string
*/
- protected $_template = 'dashboard/salebar.phtml';
+ protected $_template = 'Magento_Backend::dashboard/salebar.phtml';
/**
* @var \Magento\Framework\Module\Manager
diff --git a/app/code/Magento/Backend/Block/Dashboard/Totals.php b/app/code/Magento/Backend/Block/Dashboard/Totals.php
index 96ae6dd636380..4dcda3677584c 100644
--- a/app/code/Magento/Backend/Block/Dashboard/Totals.php
+++ b/app/code/Magento/Backend/Block/Dashboard/Totals.php
@@ -16,7 +16,7 @@ class Totals extends \Magento\Backend\Block\Dashboard\Bar
/**
* @var string
*/
- protected $_template = 'dashboard/totalbar.phtml';
+ protected $_template = 'Magento_Backend::dashboard/totalbar.phtml';
/**
* @var \Magento\Framework\Module\Manager
diff --git a/app/code/Magento/Backend/Block/GlobalSearch.php b/app/code/Magento/Backend/Block/GlobalSearch.php
index f4a46283808f4..b45eb84cdaee9 100644
--- a/app/code/Magento/Backend/Block/GlobalSearch.php
+++ b/app/code/Magento/Backend/Block/GlobalSearch.php
@@ -3,19 +3,61 @@
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
+
namespace Magento\Backend\Block;
+use Magento\Backend\Model\GlobalSearch\SearchEntityFactory;
+use Magento\Backend\Model\GlobalSearch\SearchEntity;
+use Magento\Framework\App\ObjectManager;
+
/**
* @api
* @since 100.0.2
*/
class GlobalSearch extends \Magento\Backend\Block\Template
{
+ /**
+ * @var SearchEntityFactory
+ */
+ private $searchEntityFactory;
+
/**
* @var string
*/
protected $_template = 'Magento_Backend::system/search.phtml';
+ /**
+ * @var array
+ */
+ private $entityResources;
+
+ /**
+ * @var array
+ */
+ private $entityPaths;
+
+ /**
+ * @param Template\Context $context
+ * @param array $data
+ * @param array $entityResources
+ * @param array $entityPaths
+ * @param SearchEntityFactory|null $searchEntityFactory
+ */
+ public function __construct(
+ Template\Context $context,
+ array $data = [],
+ array $entityResources = [],
+ array $entityPaths = [],
+ SearchEntityFactory $searchEntityFactory = null
+ ) {
+ $this->entityResources = $entityResources;
+ $this->entityPaths = $entityPaths;
+ $this->searchEntityFactory = $searchEntityFactory ?: ObjectManager::getInstance()
+ ->get(SearchEntityFactory::class);
+
+ parent::__construct($context, $data);
+ }
+
/**
* Get components configuration
* @return array
@@ -31,7 +73,52 @@ public function getWidgetInitOptions()
'filterProperty' => 'name',
'preventClickPropagation' => false,
'minLength' => 2,
+ 'submitInputOnEnter' => false,
]
];
}
+
+ /**
+ * Get entities which are allowed to show.
+ *
+ * @return SearchEntity[]
+ */
+ public function getEntitiesToShow()
+ {
+ $allowedEntityTypes = [];
+ $entitiesToShow = [];
+
+ foreach ($this->entityResources as $entityType => $resource) {
+ if ($this->getAuthorization()->isAllowed($resource)) {
+ $allowedEntityTypes[] = $entityType;
+ }
+ }
+
+ foreach ($allowedEntityTypes as $entityType) {
+ $url = $this->getUrlEntityType($entityType);
+
+ $searchEntity = $this->searchEntityFactory->create();
+ $searchEntity->setId('searchPreview' . $entityType);
+ $searchEntity->setTitle('in ' . $entityType);
+ $searchEntity->setUrl($url);
+
+ $entitiesToShow[] = $searchEntity;
+ }
+
+ return $entitiesToShow;
+ }
+
+ /**
+ * Get url path by entity type.
+ *
+ * @param string $entityType
+ *
+ * @return string
+ */
+ private function getUrlEntityType(string $entityType)
+ {
+ $urlPath = $this->entityPaths[$entityType] ?? '';
+
+ return $this->getUrl($urlPath);
+ }
}
diff --git a/app/code/Magento/Backend/Block/Media/Uploader.php b/app/code/Magento/Backend/Block/Media/Uploader.php
index 4987cb248bd0b..7e1ad03470720 100644
--- a/app/code/Magento/Backend/Block/Media/Uploader.php
+++ b/app/code/Magento/Backend/Block/Media/Uploader.php
@@ -3,8 +3,14 @@
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
+declare(strict_types=1);
+
namespace Magento\Backend\Block\Media;
+use Magento\Framework\App\ObjectManager;
+use Magento\Framework\Serialize\Serializer\Json;
+use Magento\Framework\Image\Adapter\ConfigInterface;
+
/**
* Adminhtml media library uploader
* @api
@@ -27,17 +33,34 @@ class Uploader extends \Magento\Backend\Block\Widget
*/
protected $_fileSizeService;
+ /**
+ * @var Json
+ */
+ private $jsonEncoder;
+
+ /**
+ * @var ConfigInterface
+ */
+ private $imageConfig;
+
/**
* @param \Magento\Backend\Block\Template\Context $context
* @param \Magento\Framework\File\Size $fileSize
* @param array $data
+ * @param Json $jsonEncoder
+ * @param ConfigInterface $imageConfig
*/
public function __construct(
\Magento\Backend\Block\Template\Context $context,
\Magento\Framework\File\Size $fileSize,
- array $data = []
+ array $data = [],
+ Json $jsonEncoder = null,
+ ConfigInterface $imageConfig = null
) {
$this->_fileSizeService = $fileSize;
+ $this->jsonEncoder = $jsonEncoder ?: ObjectManager::getInstance()->get(Json::class);
+ $this->imageConfig = $imageConfig ?: ObjectManager::getInstance()->get(ConfigInterface::class);
+
parent::__construct($context, $data);
}
@@ -79,6 +102,26 @@ public function getFileSizeService()
return $this->_fileSizeService;
}
+ /**
+ * Get Image Upload Maximum Width Config
+ *
+ * @return int
+ */
+ public function getImageUploadMaxWidth()
+ {
+ return $this->imageConfig->getMaxWidth();
+ }
+
+ /**
+ * Get Image Upload Maximum Height Config
+ *
+ * @return int
+ */
+ public function getImageUploadMaxHeight()
+ {
+ return $this->imageConfig->getMaxHeight();
+ }
+
/**
* Prepares layout and set element renderer
*
@@ -107,7 +150,7 @@ public function getJsObjectName()
*/
public function getConfigJson()
{
- return $this->_coreData->jsonEncode($this->getConfig()->getData());
+ return $this->jsonEncoder->encode($this->getConfig()->getData());
}
/**
diff --git a/app/code/Magento/Backend/Block/Menu.php b/app/code/Magento/Backend/Block/Menu.php
index bb239d31b1779..eae066b07ac38 100644
--- a/app/code/Magento/Backend/Block/Menu.php
+++ b/app/code/Magento/Backend/Block/Menu.php
@@ -76,6 +76,11 @@ class Menu extends \Magento\Backend\Block\Template
*/
private $anchorRenderer;
+ /**
+ * @var ConfigInterface
+ */
+ private $routeConfig;
+
/**
* @param Template\Context $context
* @param \Magento\Backend\Model\UrlInterface $url
@@ -83,9 +88,11 @@ class Menu extends \Magento\Backend\Block\Template
* @param \Magento\Backend\Model\Auth\Session $authSession
* @param \Magento\Backend\Model\Menu\Config $menuConfig
* @param \Magento\Framework\Locale\ResolverInterface $localeResolver
+ * @param \Magento\Framework\App\Route\ConfigInterface $routeConfig
* @param array $data
* @param MenuItemChecker|null $menuItemChecker
* @param AnchorRenderer|null $anchorRenderer
+ * @SuppressWarnings(PHPMD.ExcessiveParameterList)
*/
public function __construct(
\Magento\Backend\Block\Template\Context $context,
@@ -96,7 +103,8 @@ public function __construct(
\Magento\Framework\Locale\ResolverInterface $localeResolver,
array $data = [],
MenuItemChecker $menuItemChecker = null,
- AnchorRenderer $anchorRenderer = null
+ AnchorRenderer $anchorRenderer = null,
+ \Magento\Framework\App\Route\ConfigInterface $routeConfig = null
) {
$this->_url = $url;
$this->_iteratorFactory = $iteratorFactory;
@@ -105,6 +113,9 @@ public function __construct(
$this->_localeResolver = $localeResolver;
$this->menuItemChecker = $menuItemChecker;
$this->anchorRenderer = $anchorRenderer;
+ $this->routeConfig = $routeConfig ?:
+ \Magento\Framework\App\ObjectManager::getInstance()
+ ->get(\Magento\Framework\App\Route\ConfigInterface::class);
parent::__construct($context, $data);
}
@@ -203,8 +214,9 @@ protected function _afterToHtml($html)
*/
protected function _callbackSecretKey($match)
{
+ $routeId = $this->routeConfig->getRouteByFrontName($match[1]);
return \Magento\Backend\Model\UrlInterface::SECRET_KEY_PARAM_NAME . '/' . $this->_url->getSecretKey(
- $match[1],
+ $routeId,
$match[2],
$match[3]
);
diff --git a/app/code/Magento/Backend/Block/Page/Copyright.php b/app/code/Magento/Backend/Block/Page/Copyright.php
index 062497d6a8304..a1b61352930b5 100644
--- a/app/code/Magento/Backend/Block/Page/Copyright.php
+++ b/app/code/Magento/Backend/Block/Page/Copyright.php
@@ -18,5 +18,5 @@ class Copyright extends \Magento\Backend\Block\Template
*
* @var string
*/
- protected $_template = 'page/copyright.phtml';
+ protected $_template = 'Magento_Backend::page/copyright.phtml';
}
diff --git a/app/code/Magento/Backend/Block/Page/Footer.php b/app/code/Magento/Backend/Block/Page/Footer.php
index 368869b79e15c..3d1570e5ddfe7 100644
--- a/app/code/Magento/Backend/Block/Page/Footer.php
+++ b/app/code/Magento/Backend/Block/Page/Footer.php
@@ -17,7 +17,7 @@ class Footer extends \Magento\Backend\Block\Template
/**
* @var string
*/
- protected $_template = 'page/footer.phtml';
+ protected $_template = 'Magento_Backend::page/footer.phtml';
/**
* @var \Magento\Framework\App\ProductMetadataInterface
diff --git a/app/code/Magento/Backend/Block/Page/Header.php b/app/code/Magento/Backend/Block/Page/Header.php
index b7ed05ce58e95..c2c5f7472b370 100644
--- a/app/code/Magento/Backend/Block/Page/Header.php
+++ b/app/code/Magento/Backend/Block/Page/Header.php
@@ -18,7 +18,7 @@ class Header extends \Magento\Backend\Block\Template
/**
* @var string
*/
- protected $_template = 'page/header.phtml';
+ protected $_template = 'Magento_Backend::page/header.phtml';
/**
* Backend data
diff --git a/app/code/Magento/Backend/Block/Store/Switcher/Form/Renderer/Fieldset.php b/app/code/Magento/Backend/Block/Store/Switcher/Form/Renderer/Fieldset.php
index 2f9b73f0ae037..6fe8416784c2e 100644
--- a/app/code/Magento/Backend/Block/Store/Switcher/Form/Renderer/Fieldset.php
+++ b/app/code/Magento/Backend/Block/Store/Switcher/Form/Renderer/Fieldset.php
@@ -25,7 +25,7 @@ class Fieldset extends \Magento\Backend\Block\Template implements RendererInterf
/**
* @var string
*/
- protected $_template = 'store/switcher/form/renderer/fieldset.phtml';
+ protected $_template = 'Magento_Backend::store/switcher/form/renderer/fieldset.phtml';
/**
* Retrieve an element
diff --git a/app/code/Magento/Backend/Block/Store/Switcher/Form/Renderer/Fieldset/Element.php b/app/code/Magento/Backend/Block/Store/Switcher/Form/Renderer/Fieldset/Element.php
index ddd1f1a9178cd..71d4db6849bd2 100644
--- a/app/code/Magento/Backend/Block/Store/Switcher/Form/Renderer/Fieldset/Element.php
+++ b/app/code/Magento/Backend/Block/Store/Switcher/Form/Renderer/Fieldset/Element.php
@@ -23,7 +23,7 @@ class Element extends \Magento\Backend\Block\Widget\Form\Renderer\Fieldset\Eleme
/**
* @var string
*/
- protected $_template = 'store/switcher/form/renderer/fieldset/element.phtml';
+ protected $_template = 'Magento_Backend::store/switcher/form/renderer/fieldset/element.phtml';
/**
* Retrieve an element
diff --git a/app/code/Magento/Backend/Block/System/Store/Edit/AbstractForm.php b/app/code/Magento/Backend/Block/System/Store/Edit/AbstractForm.php
index f19799d2e4939..034887c67d1ee 100644
--- a/app/code/Magento/Backend/Block/System/Store/Edit/AbstractForm.php
+++ b/app/code/Magento/Backend/Block/System/Store/Edit/AbstractForm.php
@@ -37,7 +37,7 @@ protected function _prepareForm()
['data' => ['id' => 'edit_form', 'action' => $this->getData('action'), 'method' => 'post']]
);
- $this->_prepareStoreFieldSet($form);
+ $this->_prepareStoreFieldset($form);
$form->addField(
'store_type',
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 59657f38465d7..3d7154eb20f92 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
@@ -27,6 +27,7 @@ public function render(\Magento\Framework\DataObject $row)
$this->getUrl('adminhtml/*/editGroup', ['group_id' => $row->getGroupId()]) .
'">' .
$this->escapeHtml($row->getData($this->getColumn()->getIndex())) .
- '';
+ ' '
+ . '(' . __('Code') . ': ' . $row->getGroupCode() . ')';
}
}
diff --git a/app/code/Magento/Backend/Block/System/Store/Grid/Render/Store.php b/app/code/Magento/Backend/Block/System/Store/Grid/Render/Store.php
index 23b2de683a958..9cfc8bfc52691 100644
--- a/app/code/Magento/Backend/Block/System/Store/Grid/Render/Store.php
+++ b/app/code/Magento/Backend/Block/System/Store/Grid/Render/Store.php
@@ -27,6 +27,7 @@ public function render(\Magento\Framework\DataObject $row)
$this->getUrl('adminhtml/*/editStore', ['store_id' => $row->getStoreId()]) .
'">' .
$this->escapeHtml($row->getData($this->getColumn()->getIndex())) .
- '';
+ ' ' .
+ '(' . __('Code') . ': ' . $row->getStoreCode() . ')';
}
}
diff --git a/app/code/Magento/Backend/Block/System/Store/Grid/Render/Website.php b/app/code/Magento/Backend/Block/System/Store/Grid/Render/Website.php
index 913e2c903d20c..487eb4f8acfda 100644
--- a/app/code/Magento/Backend/Block/System/Store/Grid/Render/Website.php
+++ b/app/code/Magento/Backend/Block/System/Store/Grid/Render/Website.php
@@ -24,6 +24,7 @@ public function render(\Magento\Framework\DataObject $row)
$this->getUrl('adminhtml/*/editWebsite', ['website_id' => $row->getWebsiteId()]) .
'">' .
$this->escapeHtml($row->getData($this->getColumn()->getIndex())) .
- '';
+ ' ' .
+ '(' . __('Code') . ': ' . $row->getCode() . ')';
}
}
diff --git a/app/code/Magento/Backend/Block/Template.php b/app/code/Magento/Backend/Block/Template.php
index d0f39b54c1492..477be0f82462b 100644
--- a/app/code/Magento/Backend/Block/Template.php
+++ b/app/code/Magento/Backend/Block/Template.php
@@ -17,10 +17,12 @@
* Example:
*
*
- * My\Module\ViewModel\Custom
+ * My\Module\ViewModel\Custom
*
*
*
+ * Your class object can then be accessed by doing $block->getViewModel()
+ *
* @api
* @SuppressWarnings(PHPMD.NumberOfChildren)
* @since 100.0.2
diff --git a/app/code/Magento/Backend/Block/Widget/Button/ButtonList.php b/app/code/Magento/Backend/Block/Widget/Button/ButtonList.php
index 94af9a1d7578f..e2d71d171cf15 100644
--- a/app/code/Magento/Backend/Block/Widget/Button/ButtonList.php
+++ b/app/code/Magento/Backend/Block/Widget/Button/ButtonList.php
@@ -127,12 +127,6 @@ public function getItems()
*/
public function sortButtons(Item $itemA, Item $itemB)
{
- $sortOrderA = intval($itemA->getSortOrder());
- $sortOrderB = intval($itemB->getSortOrder());
-
- if ($sortOrderA == $sortOrderB) {
- return 0;
- }
- return ($sortOrderA < $sortOrderB) ? -1 : 1;
+ return (int)$itemA->getSortOrder() <=> (int)$itemB->getSortOrder();
}
}
diff --git a/app/code/Magento/Backend/Block/Widget/Form/Element/Dependence.php b/app/code/Magento/Backend/Block/Widget/Form/Element/Dependence.php
index 723deab1e9f7e..eff49c3b75ab2 100644
--- a/app/code/Magento/Backend/Block/Widget/Form/Element/Dependence.php
+++ b/app/code/Magento/Backend/Block/Widget/Form/Element/Dependence.php
@@ -124,14 +124,18 @@ protected function _toHtml()
if (!$this->_depends) {
return '';
}
- return '';
+
+ $params = $this->_getDependsJson();
+
+ if ($this->_configOptions) {
+ $params .= ', ' . $this->_jsonEncoder->encode($this->_configOptions);
+ }
+
+ return "";
}
/**
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 40a5d92c56b6f..632603d389d21 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
@@ -127,7 +127,7 @@ public function getHtml()
/**
* @param string|null $index
- * @return string
+ * @return array|string|int|float|null
*/
public function getEscapedValue($index = null)
{
@@ -138,6 +138,11 @@ public function getEscapedValue($index = null)
$this->_localeDate->getDateFormat(\IntlDateFormatter::SHORT)
);
}
+
+ if (is_string($value)) {
+ return $this->escapeHtml($value);
+ }
+
return $value;
}
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 96b3471db845e..1d8d658267020 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
@@ -26,7 +26,6 @@ public function getValue($index = null)
{
if ($index) {
if ($data = $this->getData('value', 'orig_' . $index)) {
- // date('Y-m-d', strtotime($data));
return $data;
}
return null;
@@ -140,8 +139,8 @@ public function getHtml()
/**
* Return escaped value for calendar
*
- * @param string $index
- * @return string
+ * @param string|null $index
+ * @return array|string|int|float|null
*/
public function getEscapedValue($index = null)
{
@@ -150,6 +149,11 @@ public function getEscapedValue($index = null)
if ($value instanceof \DateTimeInterface) {
return $this->_localeDate->formatDateTime($value);
}
+
+ if (is_string($value)) {
+ return $this->escapeHtml($value);
+ }
+
return $value;
}
diff --git a/app/code/Magento/Backend/Block/Widget/Grid/Column/Filter/Radio.php b/app/code/Magento/Backend/Block/Widget/Grid/Column/Filter/Radio.php
index 2cbe264c5f396..479a2b6b20293 100644
--- a/app/code/Magento/Backend/Block/Widget/Grid/Column/Filter/Radio.php
+++ b/app/code/Magento/Backend/Block/Widget/Grid/Column/Filter/Radio.php
@@ -31,8 +31,7 @@ public function getCondition()
{
if ($this->getValue()) {
return $this->getColumn()->getValue();
- } else {
- return [['neq' => $this->getColumn()->getValue()], ['is' => new \Zend_Db_Expr('NULL')]];
}
+ return [['neq' => $this->getColumn()->getValue()], ['is' => new \Zend_Db_Expr('NULL')]];
}
}
diff --git a/app/code/Magento/Backend/Block/Widget/Grid/Column/Renderer/AbstractRenderer.php b/app/code/Magento/Backend/Block/Widget/Grid/Column/Renderer/AbstractRenderer.php
index b8a2e283b29a0..9df8c532adfdd 100644
--- a/app/code/Magento/Backend/Block/Widget/Grid/Column/Renderer/AbstractRenderer.php
+++ b/app/code/Magento/Backend/Block/Widget/Grid/Column/Renderer/AbstractRenderer.php
@@ -58,7 +58,7 @@ public function render(DataObject $row)
$result .= $this->getColumn()->getEditOnly() ? ''
: '' . $this->_getValue($row) . ' ';
- return $result . $this->_getInputValueElement($row) . '' ;
+ return $result . $this->_getInputValueElement($row) . '';
}
return $this->_getValue($row);
}
diff --git a/app/code/Magento/Backend/Block/Widget/Grid/Column/Renderer/Currency.php b/app/code/Magento/Backend/Block/Widget/Grid/Column/Renderer/Currency.php
index ff0399e4f507f..03566bce3fc34 100644
--- a/app/code/Magento/Backend/Block/Widget/Grid/Column/Renderer/Currency.php
+++ b/app/code/Magento/Backend/Block/Widget/Grid/Column/Renderer/Currency.php
@@ -68,10 +68,7 @@ public function __construct(
$this->_storeManager = $storeManager;
$this->_currencyLocator = $currencyLocator;
$this->_localeCurrency = $localeCurrency;
- $defaultBaseCurrencyCode = $this->_scopeConfig->getValue(
- \Magento\Directory\Model\Currency::XML_PATH_CURRENCY_BASE,
- 'default'
- );
+ $defaultBaseCurrencyCode = $currencyLocator->getDefaultCurrency($this->_request);
$this->_defaultBaseCurrency = $currencyFactory->create()->load($defaultBaseCurrencyCode);
}
@@ -85,7 +82,7 @@ public function render(\Magento\Framework\DataObject $row)
{
if ($data = (string)$this->_getValue($row)) {
$currency_code = $this->_getCurrencyCode($row);
- $data = floatval($data) * $this->_getRate($row);
+ $data = (float)$data * $this->_getRate($row);
$sign = (bool)(int)$this->getColumn()->getShowNumberSign() && $data > 0 ? '+' : '';
$data = sprintf("%f", $data);
$data = $this->_localeCurrency->getCurrency($currency_code)->toCurrency($data);
@@ -121,10 +118,10 @@ protected function _getCurrencyCode($row)
protected function _getRate($row)
{
if ($rate = $this->getColumn()->getRate()) {
- return floatval($rate);
+ return (float)$rate;
}
if ($rate = $row->getData($this->getColumn()->getRateField())) {
- return floatval($rate);
+ return (float)$rate;
}
return $this->_defaultBaseCurrency->getRate($this->_getCurrencyCode($row));
}
diff --git a/app/code/Magento/Backend/Block/Widget/Grid/Column/Renderer/Massaction.php b/app/code/Magento/Backend/Block/Widget/Grid/Column/Renderer/Massaction.php
index 320713f8b57c4..a611e91f32f00 100644
--- a/app/code/Magento/Backend/Block/Widget/Grid/Column/Renderer/Massaction.php
+++ b/app/code/Magento/Backend/Block/Widget/Grid/Column/Renderer/Massaction.php
@@ -65,7 +65,7 @@ public function render(\Magento\Framework\DataObject $row)
*/
protected function _getCheckboxHtml($value, $checked)
{
- $id = 'id_' . rand(0, 999);
+ $id = 'id_' . random_int(0, 999);
$html = '';
$html .= ' _getRate($row);
+ $data = (float)$data * $this->_getRate($row);
$data = sprintf("%f", $data);
$data = $this->_localeCurrency->getCurrency($currencyCode)->toCurrency($data);
return $data;
@@ -94,10 +94,10 @@ protected function _getCurrencyCode($row)
protected function _getRate($row)
{
if ($rate = $this->getColumn()->getRate()) {
- return floatval($rate);
+ return (float)$rate;
}
if ($rate = $row->getData($this->getColumn()->getRateField())) {
- return floatval($rate);
+ return (float)$rate;
}
return 1;
}
diff --git a/app/code/Magento/Backend/Block/Widget/Grid/Massaction.php b/app/code/Magento/Backend/Block/Widget/Grid/Massaction.php
index d9b00d2ba2503..662cbedaed8db 100644
--- a/app/code/Magento/Backend/Block/Widget/Grid/Massaction.php
+++ b/app/code/Magento/Backend/Block/Widget/Grid/Massaction.php
@@ -3,8 +3,15 @@
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
+
namespace Magento\Backend\Block\Widget\Grid;
+use Magento\Backend\Block\Template\Context;
+use Magento\Framework\App\ObjectManager;
+use Magento\Framework\AuthorizationInterface;
+use Magento\Framework\DataObject;
+use Magento\Framework\Json\EncoderInterface;
+
/**
* Grid widget massaction default block
*
@@ -14,4 +21,72 @@
*/
class Massaction extends \Magento\Backend\Block\Widget\Grid\Massaction\AbstractMassaction
{
+ /**
+ * @var AuthorizationInterface
+ */
+ private $authorization;
+
+ /**
+ * Map bind item id to a particular acl type
+ * itemId => acl
+ *
+ * @var array
+ */
+ private $restrictions = [
+ 'enable' => 'Magento_Backend::toggling_cache_type',
+ 'disable' => 'Magento_Backend::toggling_cache_type',
+ 'refresh' => 'Magento_Backend::refresh_cache_type',
+ ];
+
+ /**
+ * Massaction constructor.
+ *
+ * @param Context $context
+ * @param EncoderInterface $jsonEncoder
+ * @param array $data
+ * @param AuthorizationInterface $authorization
+ */
+ public function __construct(
+ Context $context,
+ EncoderInterface $jsonEncoder,
+ array $data = [],
+ AuthorizationInterface $authorization = null
+ ) {
+ $this->authorization = $authorization ?: ObjectManager::getInstance()->get(AuthorizationInterface::class);
+
+ parent::__construct($context, $jsonEncoder, $data);
+ }
+
+ /**
+ * {@inheritdoc}
+ *
+ * @param string $itemId
+ * @param array|DataObject $item
+ *
+ * @return $this
+ */
+ public function addItem($itemId, $item)
+ {
+ if (!$this->isRestricted($itemId)) {
+ parent::addItem($itemId, $item);
+ }
+
+ return $this;
+ }
+
+ /**
+ * Check if access to action restricted
+ *
+ * @param string $itemId
+ *
+ * @return bool
+ */
+ private function isRestricted(string $itemId): bool
+ {
+ if (!key_exists($itemId, $this->restrictions)) {
+ return false;
+ }
+
+ return !$this->authorization->isAllowed($this->restrictions[$itemId]);
+ }
}
diff --git a/app/code/Magento/Backend/Block/Widget/Grid/Massaction/AbstractMassaction.php b/app/code/Magento/Backend/Block/Widget/Grid/Massaction/AbstractMassaction.php
index 8252ed1a1e2f8..99b9bb41ba1a1 100644
--- a/app/code/Magento/Backend/Block/Widget/Grid/Massaction/AbstractMassaction.php
+++ b/app/code/Magento/Backend/Block/Widget/Grid/Massaction/AbstractMassaction.php
@@ -222,9 +222,8 @@ public function getSelectedJson()
if ($selected = $this->getRequest()->getParam($this->getFormFieldNameInternal())) {
$selected = explode(',', $selected);
return join(',', $selected);
- } else {
- return '';
}
+ return '';
}
/**
@@ -237,9 +236,8 @@ public function getSelected()
if ($selected = $this->getRequest()->getParam($this->getFormFieldNameInternal())) {
$selected = explode(',', $selected);
return $selected;
- } else {
- return [];
}
+ return [];
}
/**
@@ -278,13 +276,13 @@ public function getGridIdsJson()
}
/** @var \Magento\Framework\Data\Collection $allIdsCollection */
$allIdsCollection = clone $this->getParentBlock()->getCollection();
-
+
if ($this->getMassactionIdField()) {
$massActionIdField = $this->getMassactionIdField();
} else {
$massActionIdField = $this->getParentBlock()->getMassactionIdField();
}
-
+
$gridIds = $allIdsCollection->setPageSize(0)->getColumnValues($massActionIdField);
if (!empty($gridIds)) {
return join(",", $gridIds);
diff --git a/app/code/Magento/Backend/Block/Widget/Grid/Massaction/Extended.php b/app/code/Magento/Backend/Block/Widget/Grid/Massaction/Extended.php
index 42f5e61bf5fa8..ca9799f390120 100644
--- a/app/code/Magento/Backend/Block/Widget/Grid/Massaction/Extended.php
+++ b/app/code/Magento/Backend/Block/Widget/Grid/Massaction/Extended.php
@@ -218,9 +218,8 @@ public function getSelectedJson()
if ($selected = $this->getRequest()->getParam($this->getFormFieldNameInternal())) {
$selected = explode(',', $selected);
return join(',', $selected);
- } else {
- return '';
}
+ return '';
}
/**
@@ -233,9 +232,8 @@ public function getSelected()
if ($selected = $this->getRequest()->getParam($this->getFormFieldNameInternal())) {
$selected = explode(',', $selected);
return $selected;
- } else {
- return [];
}
+ return [];
}
/**
@@ -275,13 +273,13 @@ public function getGridIdsJson()
/** @var \Magento\Framework\Data\Collection $allIdsCollection */
$allIdsCollection = clone $this->getParentBlock()->getCollection();
-
+
if ($this->getMassactionIdField()) {
$massActionIdField = $this->getMassactionIdField();
} else {
$massActionIdField = $this->getParentBlock()->getMassactionIdField();
}
-
+
$gridIds = $allIdsCollection->setPageSize(0)->getColumnValues($massActionIdField);
if (!empty($gridIds)) {
diff --git a/app/code/Magento/Backend/Block/Widget/Tabs.php b/app/code/Magento/Backend/Block/Widget/Tabs.php
index ec3628d2fcb43..f913098378634 100644
--- a/app/code/Magento/Backend/Block/Widget/Tabs.php
+++ b/app/code/Magento/Backend/Block/Widget/Tabs.php
@@ -117,6 +117,7 @@ public function addTab($tabId, $tab)
if (empty($tabId)) {
throw new \Exception(__('Please correct the tab configuration and try again. Tab Id should be not empty'));
}
+
if (is_array($tab)) {
$this->_tabs[$tabId] = new \Magento\Framework\DataObject($tab);
} elseif ($tab instanceof \Magento\Framework\DataObject) {
@@ -126,6 +127,7 @@ public function addTab($tabId, $tab)
}
} elseif (is_string($tab)) {
$this->_addTabByName($tab, $tabId);
+
if (!$this->_tabs[$tabId] instanceof TabInterface) {
unset($this->_tabs[$tabId]);
return $this;
@@ -133,6 +135,7 @@ public function addTab($tabId, $tab)
} else {
throw new \Exception(__('Please correct the tab configuration and try again.'));
}
+
if ($this->_tabs[$tabId]->getUrl() === null) {
$this->_tabs[$tabId]->setUrl('#');
}
@@ -143,10 +146,7 @@ public function addTab($tabId, $tab)
$this->_tabs[$tabId]->setId($tabId);
$this->_tabs[$tabId]->setTabId($tabId);
-
- if ($this->_activeTab === null) {
- $this->_activeTab = $tabId;
- }
+
if (true === $this->_tabs[$tabId]->getActive()) {
$this->setActiveTab($tabId);
}
@@ -235,33 +235,108 @@ protected function _setActiveTab($tabId)
*/
protected function _beforeToHtml()
{
+ $this->_tabs = $this->reorderTabs();
+
if ($activeTab = $this->getRequest()->getParam('active_tab')) {
$this->setActiveTab($activeTab);
} elseif ($activeTabId = $this->_authSession->getActiveTabId()) {
$this->_setActiveTab($activeTabId);
}
- $_new = [];
+ if ($this->_activeTab === null && !empty($this->_tabs)) {
+ /** @var TabInterface $tab */
+ $this->_activeTab = (reset($this->_tabs))->getId();
+ }
+
+ $this->assign('tabs', $this->_tabs);
+ return parent::_beforeToHtml();
+ }
+
+ /**
+ * Reorder the tabs.
+ *
+ * @return array
+ */
+ private function reorderTabs()
+ {
+ $orderByIdentity = [];
+ $orderByPosition = [];
+ $position = 100;
+
+ /**
+ * Set the initial positions for each tab.
+ *
+ * @var string $key
+ * @var TabInterface $tab
+ */
foreach ($this->_tabs as $key => $tab) {
- foreach ($this->_tabs as $k => $t) {
- if ($t->getAfter() == $key) {
- $_new[$key] = $tab;
- $_new[$k] = $t;
- } else {
- if (!$tab->getAfter() || !in_array($tab->getAfter(), array_keys($this->_tabs))) {
- $_new[$key] = $tab;
- }
- }
- }
+ $tab->setPosition($position);
+
+ $orderByIdentity[$key] = $tab;
+ $orderByPosition[$position] = $tab;
+
+ $position += 100;
}
- $this->_tabs = $_new;
- unset($_new);
+ return $this->applyTabsCorrectOrder($orderByPosition, $orderByIdentity);
+ }
+
+ /**
+ * @param array $orderByPosition
+ * @param array $orderByIdentity
+ *
+ * @return array
+ */
+ private function applyTabsCorrectOrder(array $orderByPosition, array $orderByIdentity)
+ {
+ $positionFactor = 1;
+
+ /**
+ * Rearrange the positions by using the after tag for each tab.
+ *
+ * @var integer $position
+ * @var TabInterface $tab
+ */
+ foreach ($orderByPosition as $position => $tab) {
+ if (!$tab->getAfter() || !in_array($tab->getAfter(), array_keys($orderByIdentity))) {
+ $positionFactor = 1;
+ continue;
+ }
+
+ $grandPosition = $orderByIdentity[$tab->getAfter()]->getPosition();
+ $newPosition = $grandPosition + $positionFactor;
- $this->assign('tabs', $this->_tabs);
- return parent::_beforeToHtml();
+ unset($orderByPosition[$position]);
+ $orderByPosition[$newPosition] = $tab;
+ $tab->setPosition($newPosition);
+
+ $positionFactor++;
+ }
+
+ return $this->finalTabsSortOrder($orderByPosition);
}
+ /**
+ * Apply the last sort order to tabs.
+ *
+ * @param array $orderByPosition
+ *
+ * @return array
+ */
+ private function finalTabsSortOrder(array $orderByPosition)
+ {
+ ksort($orderByPosition);
+
+ $ordered = [];
+
+ /** @var TabInterface $tab */
+ foreach ($orderByPosition as $tab) {
+ $ordered[$tab->getId()] = $tab;
+ }
+
+ return $ordered;
+ }
+
/**
* @return string
*/
diff --git a/app/code/Magento/Backend/Controller/Adminhtml/Auth/Login.php b/app/code/Magento/Backend/Controller/Adminhtml/Auth/Login.php
index b34e4d9c84939..e1ea57f63035e 100644
--- a/app/code/Magento/Backend/Controller/Adminhtml/Auth/Login.php
+++ b/app/code/Magento/Backend/Controller/Adminhtml/Auth/Login.php
@@ -50,9 +50,8 @@ public function execute()
// redirect according to rewrite rule
if ($requestUrl != $backendUrl) {
return $this->getRedirect($backendUrl);
- } else {
- return $this->resultPageFactory->create();
}
+ return $this->resultPageFactory->create();
}
/**
diff --git a/app/code/Magento/Backend/Controller/Adminhtml/Auth/Logout.php b/app/code/Magento/Backend/Controller/Adminhtml/Auth/Logout.php
index 41e32c929287a..e55c449a0e5bb 100644
--- a/app/code/Magento/Backend/Controller/Adminhtml/Auth/Logout.php
+++ b/app/code/Magento/Backend/Controller/Adminhtml/Auth/Logout.php
@@ -16,7 +16,7 @@ class Logout extends \Magento\Backend\Controller\Adminhtml\Auth
public function execute()
{
$this->_auth->logout();
- $this->messageManager->addSuccess(__('You have logged out.'));
+ $this->messageManager->addSuccessMessage(__('You have logged out.'));
/** @var \Magento\Backend\Model\View\Result\Redirect $resultRedirect */
$resultRedirect = $this->resultRedirectFactory->create();
diff --git a/app/code/Magento/Backend/Controller/Adminhtml/Cache/CleanImages.php b/app/code/Magento/Backend/Controller/Adminhtml/Cache/CleanImages.php
index 1895bd08c464f..888ce11313b8a 100644
--- a/app/code/Magento/Backend/Controller/Adminhtml/Cache/CleanImages.php
+++ b/app/code/Magento/Backend/Controller/Adminhtml/Cache/CleanImages.php
@@ -11,6 +11,13 @@
class CleanImages extends \Magento\Backend\Controller\Adminhtml\Cache
{
+ /**
+ * Authorization level of a basic admin session
+ *
+ * @see _isAllowed()
+ */
+ const ADMIN_RESOURCE = 'Magento_Backend::flush_catalog_images';
+
/**
* Clean JS/css files cache
*
@@ -21,11 +28,11 @@ public function execute()
try {
$this->_objectManager->create(\Magento\Catalog\Model\Product\Image::class)->clearCache();
$this->_eventManager->dispatch('clean_catalog_images_cache_after');
- $this->messageManager->addSuccess(__('The image cache was cleaned.'));
+ $this->messageManager->addSuccessMessage(__('The image cache was cleaned.'));
} catch (LocalizedException $e) {
- $this->messageManager->addError($e->getMessage());
+ $this->messageManager->addErrorMessage($e->getMessage());
} catch (\Exception $e) {
- $this->messageManager->addException($e, __('An error occurred while clearing the image cache.'));
+ $this->messageManager->addExceptionMessage($e, __('An error occurred while clearing the image cache.'));
}
/** @var \Magento\Backend\Model\View\Result\Redirect $resultRedirect */
diff --git a/app/code/Magento/Backend/Controller/Adminhtml/Cache/CleanMedia.php b/app/code/Magento/Backend/Controller/Adminhtml/Cache/CleanMedia.php
index 521cb9806a135..5df0a7779c4c1 100644
--- a/app/code/Magento/Backend/Controller/Adminhtml/Cache/CleanMedia.php
+++ b/app/code/Magento/Backend/Controller/Adminhtml/Cache/CleanMedia.php
@@ -11,6 +11,13 @@
class CleanMedia extends \Magento\Backend\Controller\Adminhtml\Cache
{
+ /**
+ * Authorization level of a basic admin session
+ *
+ * @see _isAllowed()
+ */
+ const ADMIN_RESOURCE = 'Magento_Backend::flush_js_css';
+
/**
* Clean JS/css files cache
*
@@ -21,11 +28,12 @@ public function execute()
try {
$this->_objectManager->get(\Magento\Framework\View\Asset\MergeService::class)->cleanMergedJsCss();
$this->_eventManager->dispatch('clean_media_cache_after');
- $this->messageManager->addSuccess(__('The JavaScript/CSS cache has been cleaned.'));
+ $this->messageManager->addSuccessMessage(__('The JavaScript/CSS cache has been cleaned.'));
} catch (LocalizedException $e) {
- $this->messageManager->addError($e->getMessage());
+ $this->messageManager->addErrorMessage($e->getMessage());
} catch (\Exception $e) {
- $this->messageManager->addException($e, __('An error occurred while clearing the JavaScript/CSS cache.'));
+ $this->messageManager
+ ->addExceptionMessage($e, __('An error occurred while clearing the JavaScript/CSS cache.'));
}
/** @var \Magento\Backend\Model\View\Result\Redirect $resultRedirect */
diff --git a/app/code/Magento/Backend/Controller/Adminhtml/Cache/CleanStaticFiles.php b/app/code/Magento/Backend/Controller/Adminhtml/Cache/CleanStaticFiles.php
index adfbf7cfe63c8..489eb5799a5e7 100644
--- a/app/code/Magento/Backend/Controller/Adminhtml/Cache/CleanStaticFiles.php
+++ b/app/code/Magento/Backend/Controller/Adminhtml/Cache/CleanStaticFiles.php
@@ -10,6 +10,13 @@
class CleanStaticFiles extends \Magento\Backend\Controller\Adminhtml\Cache
{
+ /**
+ * Authorization level of a basic admin session
+ *
+ * @see _isAllowed()
+ */
+ const ADMIN_RESOURCE = 'Magento_Backend::flush_static_files';
+
/**
* Clean static files cache
*
@@ -19,7 +26,7 @@ public function execute()
{
$this->_objectManager->get(\Magento\Framework\App\State\CleanupFiles::class)->clearMaterializedViewFiles();
$this->_eventManager->dispatch('clean_static_files_cache_after');
- $this->messageManager->addSuccess(__('The static files cache has been cleaned.'));
+ $this->messageManager->addSuccessMessage(__('The static files cache has been cleaned.'));
/** @var \Magento\Backend\Model\View\Result\Redirect $resultRedirect */
$resultRedirect = $this->resultFactory->create(ResultFactory::TYPE_REDIRECT);
diff --git a/app/code/Magento/Backend/Controller/Adminhtml/Cache/FlushAll.php b/app/code/Magento/Backend/Controller/Adminhtml/Cache/FlushAll.php
index 2f9dc9ad6a7a5..a2f18b4baf53d 100644
--- a/app/code/Magento/Backend/Controller/Adminhtml/Cache/FlushAll.php
+++ b/app/code/Magento/Backend/Controller/Adminhtml/Cache/FlushAll.php
@@ -8,6 +8,13 @@
class FlushAll extends \Magento\Backend\Controller\Adminhtml\Cache
{
+ /**
+ * Authorization level of a basic admin session
+ *
+ * @see _isAllowed()
+ */
+ const ADMIN_RESOURCE = 'Magento_Backend::flush_cache_storage';
+
/**
* Flush cache storage
*
@@ -20,7 +27,7 @@ public function execute()
foreach ($this->_cacheFrontendPool as $cacheFrontend) {
$cacheFrontend->getBackend()->clean();
}
- $this->messageManager->addSuccess(__("You flushed the cache storage."));
+ $this->messageManager->addSuccessMessage(__("You flushed the cache storage."));
/** @var \Magento\Backend\Model\View\Result\Redirect $resultRedirect */
$resultRedirect = $this->resultRedirectFactory->create();
return $resultRedirect->setPath('adminhtml/*');
diff --git a/app/code/Magento/Backend/Controller/Adminhtml/Cache/FlushSystem.php b/app/code/Magento/Backend/Controller/Adminhtml/Cache/FlushSystem.php
index 0f55a59353d65..90ed3432fa87b 100644
--- a/app/code/Magento/Backend/Controller/Adminhtml/Cache/FlushSystem.php
+++ b/app/code/Magento/Backend/Controller/Adminhtml/Cache/FlushSystem.php
@@ -8,6 +8,13 @@
class FlushSystem extends \Magento\Backend\Controller\Adminhtml\Cache
{
+ /**
+ * Authorization level of a basic admin session
+ *
+ * @see _isAllowed()
+ */
+ const ADMIN_RESOURCE = 'Magento_Backend::flush_magento_cache';
+
/**
* Flush all magento cache
*
@@ -20,7 +27,7 @@ public function execute()
$cacheFrontend->clean();
}
$this->_eventManager->dispatch('adminhtml_cache_flush_system');
- $this->messageManager->addSuccess(__("The Magento cache storage has been flushed."));
+ $this->messageManager->addSuccessMessage(__("The Magento cache storage has been flushed."));
/** @var \Magento\Backend\Model\View\Result\Redirect $resultRedirect */
$resultRedirect = $this->resultRedirectFactory->create();
return $resultRedirect->setPath('adminhtml/*');
diff --git a/app/code/Magento/Backend/Controller/Adminhtml/Cache/MassDisable.php b/app/code/Magento/Backend/Controller/Adminhtml/Cache/MassDisable.php
index 204105852b9f1..03b88ca1d3f47 100644
--- a/app/code/Magento/Backend/Controller/Adminhtml/Cache/MassDisable.php
+++ b/app/code/Magento/Backend/Controller/Adminhtml/Cache/MassDisable.php
@@ -16,6 +16,13 @@
*/
class MassDisable extends \Magento\Backend\Controller\Adminhtml\Cache
{
+ /**
+ * Authorization level of a basic admin session
+ *
+ * @see _isAllowed()
+ */
+ const ADMIN_RESOURCE = 'Magento_Backend::toggling_cache_type';
+
/**
* @var State
*/
@@ -60,12 +67,12 @@ private function disableCache()
}
if ($updatedTypes > 0) {
$this->_cacheState->persist();
- $this->messageManager->addSuccess(__("%1 cache type(s) disabled.", $updatedTypes));
+ $this->messageManager->addSuccessMessage(__("%1 cache type(s) disabled.", $updatedTypes));
}
} catch (LocalizedException $e) {
- $this->messageManager->addError($e->getMessage());
+ $this->messageManager->addErrorMessage($e->getMessage());
} catch (\Exception $e) {
- $this->messageManager->addException($e, __('An error occurred while disabling cache.'));
+ $this->messageManager->addExceptionMessage($e, __('An error occurred while disabling cache.'));
}
}
diff --git a/app/code/Magento/Backend/Controller/Adminhtml/Cache/MassEnable.php b/app/code/Magento/Backend/Controller/Adminhtml/Cache/MassEnable.php
index 32acf47887c44..1b98a00d4bf35 100644
--- a/app/code/Magento/Backend/Controller/Adminhtml/Cache/MassEnable.php
+++ b/app/code/Magento/Backend/Controller/Adminhtml/Cache/MassEnable.php
@@ -16,6 +16,13 @@
*/
class MassEnable extends \Magento\Backend\Controller\Adminhtml\Cache
{
+ /**
+ * Authorization level of a basic admin session
+ *
+ * @see _isAllowed()
+ */
+ const ADMIN_RESOURCE = 'Magento_Backend::toggling_cache_type';
+
/**
* @var State
*/
@@ -59,12 +66,12 @@ private function enableCache()
}
if ($updatedTypes > 0) {
$this->_cacheState->persist();
- $this->messageManager->addSuccess(__("%1 cache type(s) enabled.", $updatedTypes));
+ $this->messageManager->addSuccessMessage(__("%1 cache type(s) enabled.", $updatedTypes));
}
} catch (LocalizedException $e) {
- $this->messageManager->addError($e->getMessage());
+ $this->messageManager->addErrorMessage($e->getMessage());
} catch (\Exception $e) {
- $this->messageManager->addException($e, __('An error occurred while enabling cache.'));
+ $this->messageManager->addExceptionMessage($e, __('An error occurred while enabling cache.'));
}
}
diff --git a/app/code/Magento/Backend/Controller/Adminhtml/Cache/MassRefresh.php b/app/code/Magento/Backend/Controller/Adminhtml/Cache/MassRefresh.php
index e18aa1555e11b..bde211debcf72 100644
--- a/app/code/Magento/Backend/Controller/Adminhtml/Cache/MassRefresh.php
+++ b/app/code/Magento/Backend/Controller/Adminhtml/Cache/MassRefresh.php
@@ -11,6 +11,13 @@
class MassRefresh extends \Magento\Backend\Controller\Adminhtml\Cache
{
+ /**
+ * Authorization level of a basic admin session
+ *
+ * @see _isAllowed()
+ */
+ const ADMIN_RESOURCE = 'Magento_Backend::refresh_cache_type';
+
/**
* Mass action for cache refresh
*
@@ -30,12 +37,12 @@ public function execute()
$updatedTypes++;
}
if ($updatedTypes > 0) {
- $this->messageManager->addSuccess(__("%1 cache type(s) refreshed.", $updatedTypes));
+ $this->messageManager->addSuccessMessage(__("%1 cache type(s) refreshed.", $updatedTypes));
}
} catch (LocalizedException $e) {
- $this->messageManager->addError($e->getMessage());
+ $this->messageManager->addErrorMessage($e->getMessage());
} catch (\Exception $e) {
- $this->messageManager->addException($e, __('An error occurred while refreshing cache.'));
+ $this->messageManager->addExceptionMessage($e, __('An error occurred while refreshing cache.'));
}
/** @var \Magento\Backend\Model\View\Result\Redirect $resultRedirect */
diff --git a/app/code/Magento/Backend/Controller/Adminhtml/Dashboard/RefreshStatistics.php b/app/code/Magento/Backend/Controller/Adminhtml/Dashboard/RefreshStatistics.php
index f831fa67f4bb0..c10d1a77997b7 100644
--- a/app/code/Magento/Backend/Controller/Adminhtml/Dashboard/RefreshStatistics.php
+++ b/app/code/Magento/Backend/Controller/Adminhtml/Dashboard/RefreshStatistics.php
@@ -34,9 +34,9 @@ public function execute()
foreach ($collectionsNames as $collectionName) {
$this->_objectManager->create($collectionName)->aggregate();
}
- $this->messageManager->addSuccess(__('We updated lifetime statistic.'));
+ $this->messageManager->addSuccessMessage(__('We updated lifetime statistic.'));
} catch (\Exception $e) {
- $this->messageManager->addError(__('We can\'t refresh lifetime statistics.'));
+ $this->messageManager->addErrorMessage(__('We can\'t refresh lifetime statistics.'));
$this->logger->critical($e);
}
diff --git a/app/code/Magento/Backend/Controller/Adminhtml/Noroute/Index.php b/app/code/Magento/Backend/Controller/Adminhtml/Noroute/Index.php
index f03d58b9a3eb7..87153c381c1f7 100644
--- a/app/code/Magento/Backend/Controller/Adminhtml/Noroute/Index.php
+++ b/app/code/Magento/Backend/Controller/Adminhtml/Noroute/Index.php
@@ -8,6 +8,13 @@
class Index extends \Magento\Backend\App\Action
{
+ /**
+ * Array of actions which can be processed without secret key validation
+ *
+ * @var string[]
+ */
+ protected $_publicActions = ['index'];
+
/**
* @var \Magento\Framework\View\Result\PageFactory
*/
@@ -34,7 +41,7 @@ public function execute()
{
/** @var \Magento\Backend\Model\View\Result\Page $resultPage */
$resultPage = $this->resultPageFactory->create();
- $resultPage->setStatusHeader(404, '1.1', 'Forbidden');
+ $resultPage->setStatusHeader(404, '1.1', 'Not Found');
$resultPage->setHeader('Status', '404 File not found');
$resultPage->addHandle('adminhtml_noroute');
return $resultPage;
diff --git a/app/code/Magento/Backend/Controller/Adminhtml/System/Account/Save.php b/app/code/Magento/Backend/Controller/Adminhtml/System/Account/Save.php
index c9bce1cbf3888..d95b0541c2c76 100644
--- a/app/code/Magento/Backend/Controller/Adminhtml/System/Account/Save.php
+++ b/app/code/Magento/Backend/Controller/Adminhtml/System/Account/Save.php
@@ -32,9 +32,8 @@ private function getSecurityCookie()
{
if (!($this->securityCookie instanceof SecurityCookie)) {
return \Magento\Framework\App\ObjectManager::getInstance()->get(SecurityCookie::class);
- } else {
- return $this->securityCookie;
}
+ return $this->securityCookie;
}
/**
@@ -54,9 +53,9 @@ public function execute()
$user = $this->_objectManager->create(\Magento\User\Model\User::class)->load($userId);
$user->setId($userId)
- ->setUsername($this->getRequest()->getParam('username', false))
- ->setFirstname($this->getRequest()->getParam('firstname', false))
- ->setLastname($this->getRequest()->getParam('lastname', false))
+ ->setUserName($this->getRequest()->getParam('username', false))
+ ->setFirstName($this->getRequest()->getParam('firstname', false))
+ ->setLastName($this->getRequest()->getParam('lastname', false))
->setEmail(strtolower($this->getRequest()->getParam('email', false)));
if ($this->_objectManager->get(\Magento\Framework\Validator\Locale::class)->isValid($interfaceLocale)) {
@@ -77,12 +76,12 @@ public function execute()
$errors = $user->validate();
if ($errors !== true && !empty($errors)) {
foreach ($errors as $error) {
- $this->messageManager->addError($error);
+ $this->messageManager->addErrorMessage($error);
}
} else {
$user->save();
$user->sendNotificationEmailsIfRequired();
- $this->messageManager->addSuccess(__('You saved the account.'));
+ $this->messageManager->addSuccessMessage(__('You saved the account.'));
}
} catch (UserLockedException $e) {
$this->_auth->logout();
@@ -92,12 +91,12 @@ public function execute()
} catch (ValidatorException $e) {
$this->messageManager->addMessages($e->getMessages());
if ($e->getMessage()) {
- $this->messageManager->addError($e->getMessage());
+ $this->messageManager->addErrorMessage($e->getMessage());
}
} catch (LocalizedException $e) {
- $this->messageManager->addError($e->getMessage());
+ $this->messageManager->addErrorMessage($e->getMessage());
} catch (\Exception $e) {
- $this->messageManager->addError(__('An error occurred while saving account.'));
+ $this->messageManager->addErrorMessage(__('An error occurred while saving account.'));
}
/** @var \Magento\Backend\Model\View\Result\Redirect $resultRedirect */
diff --git a/app/code/Magento/Backend/Controller/Adminhtml/System/Design/Delete.php b/app/code/Magento/Backend/Controller/Adminhtml/System/Design/Delete.php
index 76402169f269e..21f28188cf874 100644
--- a/app/code/Magento/Backend/Controller/Adminhtml/System/Design/Delete.php
+++ b/app/code/Magento/Backend/Controller/Adminhtml/System/Design/Delete.php
@@ -19,11 +19,11 @@ public function execute()
try {
$design->delete();
- $this->messageManager->addSuccess(__('You deleted the design change.'));
+ $this->messageManager->addSuccessMessage(__('You deleted the design change.'));
} catch (\Magento\Framework\Exception\LocalizedException $e) {
- $this->messageManager->addError($e->getMessage());
+ $this->messageManager->addErrorMessage($e->getMessage());
} catch (\Exception $e) {
- $this->messageManager->addException($e, __("You can't delete the design change."));
+ $this->messageManager->addExceptionMessage($e, __("You can't delete the design change."));
}
}
/** @var \Magento\Backend\Model\View\Result\Redirect $resultRedirect */
diff --git a/app/code/Magento/Backend/Controller/Adminhtml/System/Design/Save.php b/app/code/Magento/Backend/Controller/Adminhtml/System/Design/Save.php
index 1f478604ced7d..0228b48f7f11e 100644
--- a/app/code/Magento/Backend/Controller/Adminhtml/System/Design/Save.php
+++ b/app/code/Magento/Backend/Controller/Adminhtml/System/Design/Save.php
@@ -50,9 +50,9 @@ public function execute()
try {
$design->save();
$this->_eventManager->dispatch('theme_save_after');
- $this->messageManager->addSuccess(__('You saved the design change.'));
+ $this->messageManager->addSuccessMessage(__('You saved the design change.'));
} catch (\Exception $e) {
- $this->messageManager->addError($e->getMessage());
+ $this->messageManager->addErrorMessage($e->getMessage());
$this->_objectManager->get(\Magento\Backend\Model\Session::class)->setDesignData($data);
return $resultRedirect->setPath('adminhtml/*/', ['id' => $design->getId()]);
}
diff --git a/app/code/Magento/Backend/Controller/Adminhtml/System/Store.php b/app/code/Magento/Backend/Controller/Adminhtml/System/Store.php
index 4fbae6abb423a..0beeb5168b6d1 100644
--- a/app/code/Magento/Backend/Controller/Adminhtml/System/Store.php
+++ b/app/code/Magento/Backend/Controller/Adminhtml/System/Store.php
@@ -103,12 +103,12 @@ protected function _backupDatabase()
->setType('db')
->setPath($filesystem->getDirectoryRead(DirectoryList::VAR_DIR)->getAbsolutePath('backups'));
$backupDb->createBackup($backup);
- $this->messageManager->addSuccess(__('The database was backed up.'));
+ $this->messageManager->addSuccessMessage(__('The database was backed up.'));
} catch (\Magento\Framework\Exception\LocalizedException $e) {
- $this->messageManager->addError($e->getMessage());
+ $this->messageManager->addErrorMessage($e->getMessage());
return false;
} catch (\Exception $e) {
- $this->messageManager->addException(
+ $this->messageManager->addExceptionMessage(
$e,
__('We can\'t create a backup right now. Please try again later.')
);
@@ -125,7 +125,7 @@ protected function _backupDatabase()
*/
protected function _addDeletionNotice($typeTitle)
{
- $this->messageManager->addNotice(
+ $this->messageManager->addNoticeMessage(
__(
'Deleting a %1 will not delete the information associated with the %1 (e.g. categories, products, etc.)'
. ', but the %1 will not be able to be restored. It is suggested that you create a database backup '
diff --git a/app/code/Magento/Backend/Controller/Adminhtml/System/Store/DeleteGroup.php b/app/code/Magento/Backend/Controller/Adminhtml/System/Store/DeleteGroup.php
index 925ae4c69ee8e..4e323be709ae1 100644
--- a/app/code/Magento/Backend/Controller/Adminhtml/System/Store/DeleteGroup.php
+++ b/app/code/Magento/Backend/Controller/Adminhtml/System/Store/DeleteGroup.php
@@ -15,13 +15,13 @@ public function execute()
{
$itemId = $this->getRequest()->getParam('item_id', null);
if (!($model = $this->_objectManager->create(\Magento\Store\Model\Group::class)->load($itemId))) {
- $this->messageManager->addError(__('Something went wrong. Please try again.'));
+ $this->messageManager->addErrorMessage(__('Something went wrong. Please try again.'));
/** @var \Magento\Backend\Model\View\Result\Redirect $redirectResult */
$redirectResult = $this->resultRedirectFactory->create();
return $redirectResult->setPath('adminhtml/*/');
}
if (!$model->isCanDelete()) {
- $this->messageManager->addError(__('This store cannot be deleted.'));
+ $this->messageManager->addErrorMessage(__('This store cannot be deleted.'));
/** @var \Magento\Backend\Model\View\Result\Redirect $redirectResult */
$redirectResult = $this->resultRedirectFactory->create();
return $redirectResult->setPath('adminhtml/*/editGroup', ['group_id' => $itemId]);
diff --git a/app/code/Magento/Backend/Controller/Adminhtml/System/Store/DeleteGroupPost.php b/app/code/Magento/Backend/Controller/Adminhtml/System/Store/DeleteGroupPost.php
index b6fbd88c7669c..75d285b139483 100644
--- a/app/code/Magento/Backend/Controller/Adminhtml/System/Store/DeleteGroupPost.php
+++ b/app/code/Magento/Backend/Controller/Adminhtml/System/Store/DeleteGroupPost.php
@@ -7,25 +7,34 @@
namespace Magento\Backend\Controller\Adminhtml\System\Store;
use Magento\Framework\Controller\ResultFactory;
+use Magento\Framework\App\Request\Http as HttpRequest;
+use Magento\Framework\Exception\NotFoundException;
class DeleteGroupPost extends \Magento\Backend\Controller\Adminhtml\System\Store
{
/**
* @return \Magento\Backend\Model\View\Result\Redirect
+ * @throws NotFoundException
*/
public function execute()
{
- $itemId = $this->getRequest()->getParam('item_id');
-
+ /** @var HttpRequest $request */
+ $request = $this->getRequest();
/** @var \Magento\Backend\Model\View\Result\Redirect $redirectResult */
- $redirectResult = $this->resultFactory->create(ResultFactory::TYPE_REDIRECT);
+ $redirectResult = $this->resultFactory->create(
+ ResultFactory::TYPE_REDIRECT
+ );
+ if (!$request->isPost()) {
+ throw new NotFoundException(__('Page not found.'));
+ }
+ $itemId = $request->getParam('item_id');
if (!($model = $this->_objectManager->create(\Magento\Store\Model\Group::class)->load($itemId))) {
- $this->messageManager->addError(__('Something went wrong. Please try again.'));
+ $this->messageManager->addErrorMessage(__('Something went wrong. Please try again.'));
return $redirectResult->setPath('adminhtml/*/');
}
if (!$model->isCanDelete()) {
- $this->messageManager->addError(__('This store cannot be deleted.'));
+ $this->messageManager->addErrorMessage(__('This store cannot be deleted.'));
return $redirectResult->setPath('adminhtml/*/editGroup', ['group_id' => $model->getId()]);
}
@@ -35,12 +44,12 @@ public function execute()
try {
$model->delete();
- $this->messageManager->addSuccess(__('You deleted the store.'));
+ $this->messageManager->addSuccessMessage(__('You deleted the store.'));
return $redirectResult->setPath('adminhtml/*/');
} catch (\Magento\Framework\Exception\LocalizedException $e) {
- $this->messageManager->addError($e->getMessage());
+ $this->messageManager->addErrorMessage($e->getMessage());
} catch (\Exception $e) {
- $this->messageManager->addException($e, __('Unable to delete the store. Please try again later.'));
+ $this->messageManager->addExceptionMessage($e, __('Unable to delete the store. Please try again later.'));
}
return $redirectResult->setPath('adminhtml/*/editGroup', ['group_id' => $itemId]);
}
diff --git a/app/code/Magento/Backend/Controller/Adminhtml/System/Store/DeleteStore.php b/app/code/Magento/Backend/Controller/Adminhtml/System/Store/DeleteStore.php
index b31de6cacc5ff..c340b1ec53aa5 100644
--- a/app/code/Magento/Backend/Controller/Adminhtml/System/Store/DeleteStore.php
+++ b/app/code/Magento/Backend/Controller/Adminhtml/System/Store/DeleteStore.php
@@ -15,13 +15,13 @@ public function execute()
{
$itemId = $this->getRequest()->getParam('item_id', null);
if (!($model = $this->_objectManager->create(\Magento\Store\Model\Store::class)->load($itemId))) {
- $this->messageManager->addError(__('Something went wrong. Please try again.'));
+ $this->messageManager->addErrorMessage(__('Something went wrong. Please try again.'));
/** @var \Magento\Backend\Model\View\Result\Redirect $redirectResult */
$redirectResult = $this->resultRedirectFactory->create();
return $redirectResult->setPath('adminhtml/*/');
}
if (!$model->isCanDelete()) {
- $this->messageManager->addError(__('This store view cannot be deleted.'));
+ $this->messageManager->addErrorMessage(__('This store view cannot be deleted.'));
/** @var \Magento\Backend\Model\View\Result\Redirect $redirectResult */
$redirectResult = $this->resultRedirectFactory->create();
return $redirectResult->setPath('adminhtml/*/editStore', ['store_id' => $itemId]);
diff --git a/app/code/Magento/Backend/Controller/Adminhtml/System/Store/DeleteStorePost.php b/app/code/Magento/Backend/Controller/Adminhtml/System/Store/DeleteStorePost.php
index ac470238e588f..d39ad6e005d8c 100644
--- a/app/code/Magento/Backend/Controller/Adminhtml/System/Store/DeleteStorePost.php
+++ b/app/code/Magento/Backend/Controller/Adminhtml/System/Store/DeleteStorePost.php
@@ -6,7 +6,9 @@
*/
namespace Magento\Backend\Controller\Adminhtml\System\Store;
+use Magento\Framework\App\Request\Http as HttpRequest;
use Magento\Framework\Controller\ResultFactory;
+use Magento\Framework\Exception\NotFoundException;
class DeleteStorePost extends \Magento\Backend\Controller\Adminhtml\System\Store
{
@@ -14,19 +16,27 @@ class DeleteStorePost extends \Magento\Backend\Controller\Adminhtml\System\Store
* Delete store view post action
*
* @return \Magento\Backend\Model\View\Result\Redirect
+ * @throws NotFoundException
*/
public function execute()
{
- $itemId = $this->getRequest()->getParam('item_id');
-
+ /** @var HttpRequest $request */
+ $request = $this->getRequest();
/** @var \Magento\Backend\Model\View\Result\Redirect $redirectResult */
- $redirectResult = $this->resultFactory->create(ResultFactory::TYPE_REDIRECT);
+ $redirectResult = $this->resultFactory->create(
+ ResultFactory::TYPE_REDIRECT
+ );
+ if (!$request->isPost()) {
+ throw new NotFoundException(__('Page not found.'));
+ }
+
+ $itemId = $request->getParam('item_id');
if (!($model = $this->_objectManager->create(\Magento\Store\Model\Store::class)->load($itemId))) {
- $this->messageManager->addError(__('Something went wrong. Please try again.'));
+ $this->messageManager->addErrorMessage(__('Something went wrong. Please try again.'));
return $redirectResult->setPath('adminhtml/*/');
}
if (!$model->isCanDelete()) {
- $this->messageManager->addError(__('This store view cannot be deleted.'));
+ $this->messageManager->addErrorMessage(__('This store view cannot be deleted.'));
return $redirectResult->setPath('adminhtml/*/editStore', ['store_id' => $model->getId()]);
}
@@ -37,14 +47,13 @@ public function execute()
try {
$model->delete();
- $this->_eventManager->dispatch('store_delete', ['store' => $model]);
-
- $this->messageManager->addSuccess(__('You deleted the store view.'));
+ $this->messageManager->addSuccessMessage(__('You deleted the store view.'));
return $redirectResult->setPath('adminhtml/*/');
} catch (\Magento\Framework\Exception\LocalizedException $e) {
- $this->messageManager->addError($e->getMessage());
+ $this->messageManager->addErrorMessage($e->getMessage());
} catch (\Exception $e) {
- $this->messageManager->addException($e, __('Unable to delete the store view. Please try again later.'));
+ $this->messageManager
+ ->addExceptionMessage($e, __('Unable to delete the store view. Please try again later.'));
}
return $redirectResult->setPath('adminhtml/*/editStore', ['store_id' => $itemId]);
}
diff --git a/app/code/Magento/Backend/Controller/Adminhtml/System/Store/DeleteWebsite.php b/app/code/Magento/Backend/Controller/Adminhtml/System/Store/DeleteWebsite.php
index 1f2ec4b2ba4b1..d86f57daa396c 100644
--- a/app/code/Magento/Backend/Controller/Adminhtml/System/Store/DeleteWebsite.php
+++ b/app/code/Magento/Backend/Controller/Adminhtml/System/Store/DeleteWebsite.php
@@ -15,13 +15,13 @@ public function execute()
{
$itemId = $this->getRequest()->getParam('item_id', null);
if (!($model = $this->_objectManager->create(\Magento\Store\Model\Website::class)->load($itemId))) {
- $this->messageManager->addError(__('Something went wrong. Please try again.'));
+ $this->messageManager->addErrorMessage(__('Something went wrong. Please try again.'));
/** @var \Magento\Backend\Model\View\Result\Redirect $redirectResult */
$redirectResult = $this->resultRedirectFactory->create();
return $redirectResult->setPath('adminhtml/*/');
}
if (!$model->isCanDelete()) {
- $this->messageManager->addError(__('This website cannot be deleted.'));
+ $this->messageManager->addErrorMessage(__('This website cannot be deleted.'));
/** @var \Magento\Backend\Model\View\Result\Redirect $redirectResult */
$redirectResult = $this->resultRedirectFactory->create();
return $redirectResult->setPath('adminhtml/*/editWebsite', ['website_id' => $itemId]);
diff --git a/app/code/Magento/Backend/Controller/Adminhtml/System/Store/DeleteWebsitePost.php b/app/code/Magento/Backend/Controller/Adminhtml/System/Store/DeleteWebsitePost.php
index c2d24b8c41a8c..aa9ac5dc7a437 100644
--- a/app/code/Magento/Backend/Controller/Adminhtml/System/Store/DeleteWebsitePost.php
+++ b/app/code/Magento/Backend/Controller/Adminhtml/System/Store/DeleteWebsitePost.php
@@ -7,27 +7,36 @@
namespace Magento\Backend\Controller\Adminhtml\System\Store;
use Magento\Framework\Controller\ResultFactory;
+use Magento\Framework\App\Request\Http as HttpRequest;
+use Magento\Framework\Exception\NotFoundException;
class DeleteWebsitePost extends \Magento\Backend\Controller\Adminhtml\System\Store
{
/**
* @return \Magento\Backend\Model\View\Result\Redirect
+ * @throws NotFoundException
*/
public function execute()
{
- $itemId = $this->getRequest()->getParam('item_id');
- $model = $this->_objectManager->create(\Magento\Store\Model\Website::class);
- $model->load($itemId);
-
+ /** @var HttpRequest $request */
+ $request = $this->getRequest();
/** @var \Magento\Backend\Model\View\Result\Redirect $redirectResult */
- $redirectResult = $this->resultFactory->create(ResultFactory::TYPE_REDIRECT);
+ $redirectResult = $this->resultFactory->create(
+ ResultFactory::TYPE_REDIRECT
+ );
+ if (!$request->isPost()) {
+ throw new NotFoundException(__('Page not found.'));
+ }
+ $itemId = $request->getParam('item_id');
+ $model = $this->_objectManager->create(\Magento\Store\Model\Website::class);
+ $model->load($itemId);
if (!$model) {
- $this->messageManager->addError(__('Something went wrong. Please try again.'));
+ $this->messageManager->addErrorMessage(__('Something went wrong. Please try again.'));
return $redirectResult->setPath('adminhtml/*/');
}
if (!$model->isCanDelete()) {
- $this->messageManager->addError(__('This website cannot be deleted.'));
+ $this->messageManager->addErrorMessage(__('This website cannot be deleted.'));
return $redirectResult->setPath('adminhtml/*/editWebsite', ['website_id' => $model->getId()]);
}
@@ -37,12 +46,12 @@ public function execute()
try {
$model->delete();
- $this->messageManager->addSuccess(__('You deleted the website.'));
+ $this->messageManager->addSuccessMessage(__('You deleted the website.'));
return $redirectResult->setPath('adminhtml/*/');
} catch (\Magento\Framework\Exception\LocalizedException $e) {
- $this->messageManager->addError($e->getMessage());
+ $this->messageManager->addErrorMessage($e->getMessage());
} catch (\Exception $e) {
- $this->messageManager->addException($e, __('Unable to delete the website. Please try again later.'));
+ $this->messageManager->addExceptionMessage($e, __('Unable to delete the website. Please try again later.'));
}
return $redirectResult->setPath('*/*/editWebsite', ['website_id' => $itemId]);
}
diff --git a/app/code/Magento/Backend/Controller/Adminhtml/System/Store/EditStore.php b/app/code/Magento/Backend/Controller/Adminhtml/System/Store/EditStore.php
index cbc068a480865..a8651984cfa63 100644
--- a/app/code/Magento/Backend/Controller/Adminhtml/System/Store/EditStore.php
+++ b/app/code/Magento/Backend/Controller/Adminhtml/System/Store/EditStore.php
@@ -57,7 +57,7 @@ public function execute()
if ($model->getId() || $this->_coreRegistry->registry('store_action') == 'add') {
$this->_coreRegistry->register('store_data', $model);
if ($this->_coreRegistry->registry('store_action') == 'edit' && $codeBase && !$model->isReadOnly()) {
- $this->messageManager->addNotice($codeBase);
+ $this->messageManager->addNoticeMessage($codeBase);
}
$resultPage = $this->createPage();
if ($this->_coreRegistry->registry('store_action') == 'add') {
@@ -71,7 +71,7 @@ public function execute()
));
return $resultPage;
} else {
- $this->messageManager->addError($notExists);
+ $this->messageManager->addErrorMessage($notExists);
/** @var \Magento\Backend\Model\View\Result\Redirect $resultRedirect */
$resultRedirect = $this->resultRedirectFactory->create();
return $resultRedirect->setPath('adminhtml/*/');
diff --git a/app/code/Magento/Backend/Controller/Adminhtml/System/Store/Save.php b/app/code/Magento/Backend/Controller/Adminhtml/System/Store/Save.php
index 1d6862a6ff845..910511c2b275e 100644
--- a/app/code/Magento/Backend/Controller/Adminhtml/System/Store/Save.php
+++ b/app/code/Magento/Backend/Controller/Adminhtml/System/Store/Save.php
@@ -32,7 +32,7 @@ private function processWebsiteSave($postData)
}
$websiteModel->save();
- $this->messageManager->addSuccess(__('You saved the website.'));
+ $this->messageManager->addSuccessMessage(__('You saved the website.'));
return $postData;
}
@@ -46,7 +46,6 @@ private function processWebsiteSave($postData)
*/
private function processStoreSave($postData)
{
- $eventName = 'store_edit';
/** @var \Magento\Store\Model\Store $storeModel */
$storeModel = $this->_objectManager->create(\Magento\Store\Model\Store::class);
$postData['store']['name'] = $this->filterManager->removeTags($postData['store']['name']);
@@ -56,7 +55,6 @@ private function processStoreSave($postData)
$storeModel->setData($postData['store']);
if ($postData['store']['store_id'] == '') {
$storeModel->setId(null);
- $eventName = 'store_add';
}
$groupModel = $this->_objectManager->create(
\Magento\Store\Model\Group::class
@@ -70,9 +68,7 @@ private function processStoreSave($postData)
);
}
$storeModel->save();
- $this->_objectManager->get(\Magento\Store\Model\StoreManager::class)->reinitStores();
- $this->_eventManager->dispatch($eventName, ['store' => $storeModel]);
- $this->messageManager->addSuccess(__('You saved the store view.'));
+ $this->messageManager->addSuccessMessage(__('You saved the store view.'));
return $postData;
}
@@ -102,8 +98,7 @@ private function processGroupSave($postData)
);
}
$groupModel->save();
- $this->_eventManager->dispatch('store_group_save', ['group' => $groupModel]);
- $this->messageManager->addSuccess(__('You saved the store.'));
+ $this->messageManager->addSuccessMessage(__('You saved the store.'));
return $postData;
}
@@ -139,10 +134,10 @@ public function execute()
$redirectResult->setPath('adminhtml/*/');
return $redirectResult;
} catch (\Magento\Framework\Exception\LocalizedException $e) {
- $this->messageManager->addError($e->getMessage());
+ $this->messageManager->addErrorMessage($e->getMessage());
$this->_getSession()->setPostData($postData);
} catch (\Exception $e) {
- $this->messageManager->addException(
+ $this->messageManager->addExceptionMessage(
$e,
__('Something went wrong while saving. Please review the error log.')
);
diff --git a/app/code/Magento/Backend/Helper/Dashboard/Order.php b/app/code/Magento/Backend/Helper/Dashboard/Order.php
index 9fc2c2cdb4e6f..c19c28b6e33eb 100644
--- a/app/code/Magento/Backend/Helper/Dashboard/Order.php
+++ b/app/code/Magento/Backend/Helper/Dashboard/Order.php
@@ -13,7 +13,7 @@
* @api
* @since 100.0.2
*/
-class Order extends \Magento\Backend\Helper\Dashboard\AbstractDashboard
+class Order extends AbstractDashboard
{
/**
* @var \Magento\Reports\Model\ResourceModel\Order\Collection
@@ -29,32 +29,25 @@ class Order extends \Magento\Backend\Helper\Dashboard\AbstractDashboard
/**
* @param \Magento\Framework\App\Helper\Context $context
* @param \Magento\Reports\Model\ResourceModel\Order\Collection $orderCollection
+ * @param \Magento\Store\Model\StoreManagerInterface $storeManager
*/
public function __construct(
\Magento\Framework\App\Helper\Context $context,
- \Magento\Reports\Model\ResourceModel\Order\Collection $orderCollection
+ \Magento\Reports\Model\ResourceModel\Order\Collection $orderCollection,
+ \Magento\Store\Model\StoreManagerInterface $storeManager = null
) {
$this->_orderCollection = $orderCollection;
- parent::__construct($context);
- }
+ $this->_storeManager = $storeManager ?: ObjectManager::getInstance()
+ ->get(\Magento\Store\Model\StoreManagerInterface::class);
- /**
- * The getter function to get the new StoreManager dependency
- *
- * @return \Magento\Store\Model\StoreManagerInterface
- *
- * @deprecated 100.1.0
- */
- private function getStoreManager()
- {
- if ($this->_storeManager === null) {
- $this->_storeManager = ObjectManager::getInstance()->get(\Magento\Store\Model\StoreManagerInterface::class);
- }
- return $this->_storeManager;
+ parent::__construct($context);
}
/**
* @return void
+ *
+ * @throws \Magento\Framework\Exception\LocalizedException
+ * @throws \Magento\Framework\Exception\NoSuchEntityException
*/
protected function _initCollection()
{
@@ -65,15 +58,15 @@ protected function _initCollection()
if ($this->getParam('store')) {
$this->_collection->addFieldToFilter('store_id', $this->getParam('store'));
} elseif ($this->getParam('website')) {
- $storeIds = $this->getStoreManager()->getWebsite($this->getParam('website'))->getStoreIds();
+ $storeIds = $this->_storeManager->getWebsite($this->getParam('website'))->getStoreIds();
$this->_collection->addFieldToFilter('store_id', ['in' => implode(',', $storeIds)]);
} elseif ($this->getParam('group')) {
- $storeIds = $this->getStoreManager()->getGroup($this->getParam('group'))->getStoreIds();
+ $storeIds = $this->_storeManager->getGroup($this->getParam('group'))->getStoreIds();
$this->_collection->addFieldToFilter('store_id', ['in' => implode(',', $storeIds)]);
} elseif (!$this->_collection->isLive()) {
$this->_collection->addFieldToFilter(
'store_id',
- ['eq' => $this->getStoreManager()->getStore(\Magento\Store\Model\Store::ADMIN_CODE)->getId()]
+ ['eq' => $this->_storeManager->getStore(\Magento\Store\Model\Store::ADMIN_CODE)->getId()]
);
}
$this->_collection->load();
diff --git a/app/code/Magento/Backend/Model/Auth/StorageInterface.php b/app/code/Magento/Backend/Model/Auth/StorageInterface.php
index 52b2b089c71e1..e643165a93317 100644
--- a/app/code/Magento/Backend/Model/Auth/StorageInterface.php
+++ b/app/code/Magento/Backend/Model/Auth/StorageInterface.php
@@ -23,7 +23,7 @@ interface StorageInterface
public function processLogin();
/**
- * Perform login specific actions
+ * Perform logout specific actions
*
* @return $this
* @abstract
diff --git a/app/code/Magento/Backend/Model/GlobalSearch/SearchEntity.php b/app/code/Magento/Backend/Model/GlobalSearch/SearchEntity.php
new file mode 100644
index 0000000000000..18691802b9218
--- /dev/null
+++ b/app/code/Magento/Backend/Model/GlobalSearch/SearchEntity.php
@@ -0,0 +1,73 @@
+getData('id');
+ }
+
+ /**
+ * Get url.
+ *
+ * @return string
+ */
+ public function getUrl()
+ {
+ return $this->getData('url');
+ }
+
+ /**
+ * Get title.
+ *
+ * @return string
+ */
+ public function getTitle()
+ {
+ return $this->getData('title');
+ }
+
+ /**
+ * Set Id.
+ *
+ * @param string $value
+ */
+ public function setId(string $value)
+ {
+ $this->setData('id', $value);
+ }
+
+ /**
+ * Set url.
+ *
+ * @param string $value
+ */
+ public function setUrl(string $value)
+ {
+ $this->setData('url', $value);
+ }
+
+ /**
+ * Set title.
+ *
+ * @param string $value
+ */
+ public function setTitle(string $value)
+ {
+ $this->setData('title', $value);
+ }
+}
diff --git a/app/code/Magento/Backend/Model/Menu.php b/app/code/Magento/Backend/Model/Menu.php
index 22110cf52de2b..b86ea0c082956 100644
--- a/app/code/Magento/Backend/Model/Menu.php
+++ b/app/code/Magento/Backend/Model/Menu.php
@@ -83,7 +83,7 @@ public function add(Item $item, $parentId = null, $index = null)
}
$parentItem->getChildren()->add($item, null, $index);
} else {
- $index = intval($index);
+ $index = (int)$index;
if (!isset($this[$index])) {
$this->offsetSet($index, $item);
$this->_logger->info(
diff --git a/app/code/Magento/Backend/Model/Menu/Builder.php b/app/code/Magento/Backend/Model/Menu/Builder.php
index ae572deab53d9..1c6e900bc5cfc 100644
--- a/app/code/Magento/Backend/Model/Menu/Builder.php
+++ b/app/code/Magento/Backend/Model/Menu/Builder.php
@@ -102,6 +102,6 @@ public function getResult(\Magento\Backend\Model\Menu $menu)
*/
protected function _getParam($params, $paramName, $defaultValue = null)
{
- return isset($params[$paramName]) ? $params[$paramName] : $defaultValue;
+ return $params[$paramName] ?? $defaultValue;
}
}
diff --git a/app/code/Magento/Backend/Model/Menu/Item.php b/app/code/Magento/Backend/Model/Menu/Item.php
index fe6564d24e891..67c6216cbbc06 100644
--- a/app/code/Magento/Backend/Model/Menu/Item.php
+++ b/app/code/Magento/Backend/Model/Menu/Item.php
@@ -467,15 +467,15 @@ public function toArray()
{
return [
'parent_id' => $this->_parentId,
- 'module_name' => $this->_moduleName,
+ 'module' => $this->_moduleName,
'sort_index' => $this->_sortIndex,
- 'depends_on_config' => $this->_dependsOnConfig,
+ 'dependsOnConfig' => $this->_dependsOnConfig,
'id' => $this->_id,
'resource' => $this->_resource,
'path' => $this->_path,
'action' => $this->_action,
- 'depends_on_module' => $this->_dependsOnModule,
- 'tooltip' => $this->_tooltip,
+ 'dependsOnModule' => $this->_dependsOnModule,
+ 'toolTip' => $this->_tooltip,
'title' => $this->_title,
'target' => $this->target,
'sub_menu' => isset($this->_submenu) ? $this->_submenu->toArray() : null
@@ -492,23 +492,22 @@ public function toArray()
public function populateFromArray(array $data)
{
$this->_parentId = $this->_getArgument($data, 'parent_id');
- $this->_moduleName = $this->_getArgument($data, 'module_name', 'Magento_Backend');
+ $this->_moduleName = $this->_getArgument($data, 'module', 'Magento_Backend');
$this->_sortIndex = $this->_getArgument($data, 'sort_index');
- $this->_dependsOnConfig = $this->_getArgument($data, 'depends_on_config');
+ $this->_dependsOnConfig = $this->_getArgument($data, 'dependsOnConfig');
$this->_id = $this->_getArgument($data, 'id');
$this->_resource = $this->_getArgument($data, 'resource');
$this->_path = $this->_getArgument($data, 'path', '');
$this->_action = $this->_getArgument($data, 'action');
- $this->_dependsOnModule = $this->_getArgument($data, 'depends_on_module');
- $this->_tooltip = $this->_getArgument($data, 'tooltip', '');
+ $this->_dependsOnModule = $this->_getArgument($data, 'dependsOnModule');
+ $this->_tooltip = $this->_getArgument($data, 'toolTip');
$this->_title = $this->_getArgument($data, 'title');
$this->target = $this->_getArgument($data, 'target');
+ $this->_submenu = null;
if (isset($data['sub_menu'])) {
$menu = $this->_menuFactory->create();
$menu->populateFromArray($data['sub_menu']);
$this->_submenu = $menu;
- } else {
- $this->_submenu = null;
}
}
}
diff --git a/app/code/Magento/Backend/Model/Url.php b/app/code/Magento/Backend/Model/Url.php
index 48b443fe7ffd3..f199fd0fe7bf1 100644
--- a/app/code/Magento/Backend/Model/Url.php
+++ b/app/code/Magento/Backend/Model/Url.php
@@ -202,7 +202,7 @@ public function getUrl($routePath = null, $routeParams = null)
}
$cacheSecretKey = false;
- if (is_array($routeParams) && isset($routeParams['_cache_secret_key'])) {
+ if (isset($routeParams['_cache_secret_key'])) {
unset($routeParams['_cache_secret_key']);
$cacheSecretKey = true;
}
@@ -210,25 +210,28 @@ public function getUrl($routePath = null, $routeParams = null)
if (!$this->useSecretKey()) {
return $result;
}
+
+ $this->getRouteParamsResolver()->unsetData('route_params');
$this->_setRoutePath($routePath);
+ $extraParams = $this->getRouteParamsResolver()->getRouteParams();
$routeName = $this->_getRouteName('*');
$controllerName = $this->_getControllerName(self::DEFAULT_CONTROLLER_NAME);
$actionName = $this->_getActionName(self::DEFAULT_ACTION_NAME);
- if ($cacheSecretKey) {
- $secret = [self::SECRET_KEY_PARAM_NAME => "\${$routeName}/{$controllerName}/{$actionName}\$"];
- } else {
- $secret = [
- self::SECRET_KEY_PARAM_NAME => $this->getSecretKey($routeName, $controllerName, $actionName),
- ];
- }
- if (is_array($routeParams)) {
- $routeParams = array_merge($secret, $routeParams);
- } else {
- $routeParams = $secret;
+
+ if (!isset($routeParams[self::SECRET_KEY_PARAM_NAME])) {
+ if (!is_array($routeParams)) {
+ $routeParams = [];
+ }
+ $secretKey = $cacheSecretKey
+ ? "\${$routeName}/{$controllerName}/{$actionName}\$"
+ : $this->getSecretKey($routeName, $controllerName, $actionName);
+ $routeParams[self::SECRET_KEY_PARAM_NAME] = $secretKey;
}
- if (is_array($this->_getRouteParams())) {
- $routeParams = array_merge($this->_getRouteParams(), $routeParams);
+
+ if (!empty($extraParams)) {
+ $routeParams = array_merge($extraParams, $routeParams);
}
+
return parent::getUrl("{$routeName}/{$controllerName}/{$actionName}", $routeParams);
}
diff --git a/app/code/Magento/Backend/Model/Widget/Grid/Parser.php b/app/code/Magento/Backend/Model/Widget/Grid/Parser.php
index e2b77fb471acd..fbed7149aa565 100644
--- a/app/code/Magento/Backend/Model/Widget/Grid/Parser.php
+++ b/app/code/Magento/Backend/Model/Widget/Grid/Parser.php
@@ -30,8 +30,9 @@ public function parseExpression($expression)
$expression = trim($expression);
foreach ($this->_operations as $operation) {
$splittedExpr = preg_split('/\\' . $operation . '/', $expression, -1, PREG_SPLIT_DELIM_CAPTURE);
- if (count($splittedExpr) > 1) {
- for ($i = 0; $i < count($splittedExpr); $i++) {
+ $count = count($splittedExpr);
+ if ($count > 1) {
+ for ($i = 0; $i < $count; $i++) {
$stack = array_merge($stack, $this->parseExpression($splittedExpr[$i]));
if ($i > 0) {
$stack[] = $operation;
diff --git a/app/code/Magento/Backend/Test/Mftf/ActionGroup/BackendActionGroup.xml b/app/code/Magento/Backend/Test/Mftf/ActionGroup/BackendActionGroup.xml
new file mode 100644
index 0000000000000..93d2ea7fc8d2f
--- /dev/null
+++ b/app/code/Magento/Backend/Test/Mftf/ActionGroup/BackendActionGroup.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Backend/Test/Mftf/ActionGroup/ConfigurationActionGroup.xml b/app/code/Magento/Backend/Test/Mftf/ActionGroup/ConfigurationActionGroup.xml
new file mode 100644
index 0000000000000..1f6b992df61e6
--- /dev/null
+++ b/app/code/Magento/Backend/Test/Mftf/ActionGroup/ConfigurationActionGroup.xml
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Backend/Test/Mftf/ActionGroup/LoginActionGroup.xml b/app/code/Magento/Backend/Test/Mftf/ActionGroup/LoginActionGroup.xml
new file mode 100644
index 0000000000000..bcff329d79dad
--- /dev/null
+++ b/app/code/Magento/Backend/Test/Mftf/ActionGroup/LoginActionGroup.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Backend/Test/Mftf/ActionGroup/LoginAsAdminActionGroup.xml b/app/code/Magento/Backend/Test/Mftf/ActionGroup/LoginAsAdminActionGroup.xml
new file mode 100644
index 0000000000000..7a8b1244c1512
--- /dev/null
+++ b/app/code/Magento/Backend/Test/Mftf/ActionGroup/LoginAsAdminActionGroup.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/code/Magento/Backend/Test/Mftf/ActionGroup/LogoutActionGroup.xml b/app/code/Magento/Backend/Test/Mftf/ActionGroup/LogoutActionGroup.xml
new file mode 100644
index 0000000000000..aabc83bec50b7
--- /dev/null
+++ b/app/code/Magento/Backend/Test/Mftf/ActionGroup/LogoutActionGroup.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Backend/Test/Mftf/ActionGroup/SortByIdDescendingActionGroup.xml b/app/code/Magento/Backend/Test/Mftf/ActionGroup/SortByIdDescendingActionGroup.xml
new file mode 100644
index 0000000000000..b7b63c5d9a62e
--- /dev/null
+++ b/app/code/Magento/Backend/Test/Mftf/ActionGroup/SortByIdDescendingActionGroup.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Backend/Test/Mftf/Data/BackendData.xml b/app/code/Magento/Backend/Test/Mftf/Data/BackendData.xml
new file mode 100644
index 0000000000000..0eeca081fcbd2
--- /dev/null
+++ b/app/code/Magento/Backend/Test/Mftf/Data/BackendData.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
+ data
+
+
diff --git a/app/code/Magento/Backend/Test/Mftf/LICENSE.txt b/app/code/Magento/Backend/Test/Mftf/LICENSE.txt
new file mode 100644
index 0000000000000..49525fd99da9c
--- /dev/null
+++ b/app/code/Magento/Backend/Test/Mftf/LICENSE.txt
@@ -0,0 +1,48 @@
+
+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/Backend/Test/Mftf/LICENSE_AFL.txt b/app/code/Magento/Backend/Test/Mftf/LICENSE_AFL.txt
new file mode 100644
index 0000000000000..f39d641b18a19
--- /dev/null
+++ b/app/code/Magento/Backend/Test/Mftf/LICENSE_AFL.txt
@@ -0,0 +1,48 @@
+
+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/Backend/Test/Mftf/Page/AdminConfigurationGeneralSectionPage.xml b/app/code/Magento/Backend/Test/Mftf/Page/AdminConfigurationGeneralSectionPage.xml
new file mode 100644
index 0000000000000..c0c4f4bd9d3a5
--- /dev/null
+++ b/app/code/Magento/Backend/Test/Mftf/Page/AdminConfigurationGeneralSectionPage.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
diff --git a/app/code/Magento/Backend/Test/Mftf/Page/AdminConfigurationStoresPage.xml b/app/code/Magento/Backend/Test/Mftf/Page/AdminConfigurationStoresPage.xml
new file mode 100644
index 0000000000000..05073acff3ca9
--- /dev/null
+++ b/app/code/Magento/Backend/Test/Mftf/Page/AdminConfigurationStoresPage.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Backend/Test/Mftf/Page/AdminDashboardPage.xml b/app/code/Magento/Backend/Test/Mftf/Page/AdminDashboardPage.xml
new file mode 100644
index 0000000000000..8c258accdf06c
--- /dev/null
+++ b/app/code/Magento/Backend/Test/Mftf/Page/AdminDashboardPage.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Backend/Test/Mftf/Page/AdminLoginPage.xml b/app/code/Magento/Backend/Test/Mftf/Page/AdminLoginPage.xml
new file mode 100644
index 0000000000000..8eb7c3f897768
--- /dev/null
+++ b/app/code/Magento/Backend/Test/Mftf/Page/AdminLoginPage.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Backend/Test/Mftf/README.md b/app/code/Magento/Backend/Test/Mftf/README.md
new file mode 100644
index 0000000000000..ed8a3a3bc2c49
--- /dev/null
+++ b/app/code/Magento/Backend/Test/Mftf/README.md
@@ -0,0 +1,3 @@
+# Backend Functional Tests
+
+The Functional Test Module for **Magento Backend** module.
diff --git a/app/code/Magento/Backend/Test/Mftf/Section/AdminConfigurationGeneralSectionCountryOptionsGroupSection.xml b/app/code/Magento/Backend/Test/Mftf/Section/AdminConfigurationGeneralSectionCountryOptionsGroupSection.xml
new file mode 100644
index 0000000000000..e6f5e599c605d
--- /dev/null
+++ b/app/code/Magento/Backend/Test/Mftf/Section/AdminConfigurationGeneralSectionCountryOptionsGroupSection.xml
@@ -0,0 +1,11 @@
+
+
+
+
diff --git a/app/code/Magento/Backend/Test/Mftf/Section/AdminConfirmationModalSection.xml b/app/code/Magento/Backend/Test/Mftf/Section/AdminConfirmationModalSection.xml
new file mode 100644
index 0000000000000..dc512e66528ac
--- /dev/null
+++ b/app/code/Magento/Backend/Test/Mftf/Section/AdminConfirmationModalSection.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
diff --git a/app/code/Magento/Backend/Test/Mftf/Section/AdminGridTableSection.xml b/app/code/Magento/Backend/Test/Mftf/Section/AdminGridTableSection.xml
new file mode 100644
index 0000000000000..3e8f8a8f2e412
--- /dev/null
+++ b/app/code/Magento/Backend/Test/Mftf/Section/AdminGridTableSection.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
diff --git a/app/code/Magento/Backend/Test/Mftf/Section/AdminHeaderSection.xml b/app/code/Magento/Backend/Test/Mftf/Section/AdminHeaderSection.xml
new file mode 100644
index 0000000000000..92b06878ab87f
--- /dev/null
+++ b/app/code/Magento/Backend/Test/Mftf/Section/AdminHeaderSection.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
diff --git a/app/code/Magento/Backend/Test/Mftf/Section/AdminLoginFormSection.xml b/app/code/Magento/Backend/Test/Mftf/Section/AdminLoginFormSection.xml
new file mode 100644
index 0000000000000..b65a969e334c4
--- /dev/null
+++ b/app/code/Magento/Backend/Test/Mftf/Section/AdminLoginFormSection.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
diff --git a/app/code/Magento/Backend/Test/Mftf/Section/AdminMainActionsSection.xml b/app/code/Magento/Backend/Test/Mftf/Section/AdminMainActionsSection.xml
new file mode 100644
index 0000000000000..c56a8768b0adf
--- /dev/null
+++ b/app/code/Magento/Backend/Test/Mftf/Section/AdminMainActionsSection.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
diff --git a/app/code/Magento/Backend/Test/Mftf/Section/AdminMenuSection.xml b/app/code/Magento/Backend/Test/Mftf/Section/AdminMenuSection.xml
new file mode 100644
index 0000000000000..9d3182b6236a4
--- /dev/null
+++ b/app/code/Magento/Backend/Test/Mftf/Section/AdminMenuSection.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
diff --git a/app/code/Magento/Backend/Test/Mftf/Section/AdminMessagesSection.xml b/app/code/Magento/Backend/Test/Mftf/Section/AdminMessagesSection.xml
new file mode 100644
index 0000000000000..72a00ed6db9b6
--- /dev/null
+++ b/app/code/Magento/Backend/Test/Mftf/Section/AdminMessagesSection.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
diff --git a/app/code/Magento/Backend/Test/Mftf/Section/AdminPromptModalSection.xml b/app/code/Magento/Backend/Test/Mftf/Section/AdminPromptModalSection.xml
new file mode 100644
index 0000000000000..ec351e0d98fe5
--- /dev/null
+++ b/app/code/Magento/Backend/Test/Mftf/Section/AdminPromptModalSection.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
diff --git a/app/code/Magento/Backend/Test/Mftf/Test/AdminLoginTest.xml b/app/code/Magento/Backend/Test/Mftf/Test/AdminLoginTest.xml
new file mode 100644
index 0000000000000..1eb455937f89b
--- /dev/null
+++ b/app/code/Magento/Backend/Test/Mftf/Test/AdminLoginTest.xml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Backend/Test/Unit/App/Action/Plugin/AuthenticationTest.php b/app/code/Magento/Backend/Test/Unit/App/Action/Plugin/AuthenticationTest.php
index 7e4c426de9452..88b994a6b93b7 100644
--- a/app/code/Magento/Backend/Test/Unit/App/Action/Plugin/AuthenticationTest.php
+++ b/app/code/Magento/Backend/Test/Unit/App/Action/Plugin/AuthenticationTest.php
@@ -146,6 +146,9 @@ public function testProcessNotLoggedInUser($isIFrameParam, $isAjaxParam, $isForw
$this->assertEquals($expectedResult, $this->plugin->aroundDispatch($subject, $proceed, $request));
}
+ /**
+ * @return array
+ */
public function processNotLoggedInUserDataProvider()
{
return [
diff --git a/app/code/Magento/Backend/Test/Unit/App/Action/Plugin/MassactionKeyTest.php b/app/code/Magento/Backend/Test/Unit/App/Action/Plugin/MassactionKeyTest.php
index 2f808eaf2d1b8..d793a80cdeacf 100644
--- a/app/code/Magento/Backend/Test/Unit/App/Action/Plugin/MassactionKeyTest.php
+++ b/app/code/Magento/Backend/Test/Unit/App/Action/Plugin/MassactionKeyTest.php
@@ -74,6 +74,9 @@ public function testBeforeDispatchWhenMassactionPrepareKeyRequestExists($postDat
$this->plugin->beforeDispatch($this->subjectMock, $this->requestMock);
}
+ /**
+ * @return array
+ */
public function beforeDispatchDataProvider()
{
return [
diff --git a/app/code/Magento/Backend/Test/Unit/App/Action/Stub/ActionStub.php b/app/code/Magento/Backend/Test/Unit/App/Action/Stub/ActionStub.php
index 4eff6218961af..2d60bef3f3e8c 100644
--- a/app/code/Magento/Backend/Test/Unit/App/Action/Stub/ActionStub.php
+++ b/app/code/Magento/Backend/Test/Unit/App/Action/Stub/ActionStub.php
@@ -8,6 +8,9 @@
class ActionStub extends \Magento\Backend\App\Action
{
+ /**
+ * @return \Magento\Framework\App\ResponseInterface|\Magento\Framework\Controller\ResultInterface|void
+ */
public function execute()
{
// Empty method stub for test
diff --git a/app/code/Magento/Backend/Test/Unit/App/Area/FrontNameResolverTest.php b/app/code/Magento/Backend/Test/Unit/App/Area/FrontNameResolverTest.php
index bc7dce6f20bac..642c6283decae 100644
--- a/app/code/Magento/Backend/Test/Unit/App/Area/FrontNameResolverTest.php
+++ b/app/code/Magento/Backend/Test/Unit/App/Area/FrontNameResolverTest.php
@@ -118,6 +118,9 @@ public function testIsHostBackend($url, $host, $useCustomAdminUrl, $customAdminU
$this->assertEquals($this->model->isHostBackend(), $expectedValue);
}
+ /**
+ * @return array
+ */
public function hostsDataProvider()
{
return [
diff --git a/app/code/Magento/Backend/Test/Unit/App/ConfigTest.php b/app/code/Magento/Backend/Test/Unit/App/ConfigTest.php
index 114c57867badf..53640a81e722f 100644
--- a/app/code/Magento/Backend/Test/Unit/App/ConfigTest.php
+++ b/app/code/Magento/Backend/Test/Unit/App/ConfigTest.php
@@ -70,6 +70,9 @@ public function testIsSetFlag($configPath, $configValue, $expectedResult)
$this->assertEquals($expectedResult, $this->model->isSetFlag($configPath));
}
+ /**
+ * @return array
+ */
public function isSetFlagDataProvider()
{
return [
diff --git a/app/code/Magento/Backend/Test/Unit/Block/AnchorRendererTest.php b/app/code/Magento/Backend/Test/Unit/Block/AnchorRendererTest.php
index f52f4ab337712..eccb08e788a95 100644
--- a/app/code/Magento/Backend/Test/Unit/Block/AnchorRendererTest.php
+++ b/app/code/Magento/Backend/Test/Unit/Block/AnchorRendererTest.php
@@ -141,6 +141,9 @@ public function testRenderAnchorLevelIsNotOne($hasTarget)
);
}
+ /**
+ * @return array
+ */
public function targetDataProvider()
{
return [
diff --git a/app/code/Magento/Backend/Test/Unit/Block/Cache/AdditionalTest.php b/app/code/Magento/Backend/Test/Unit/Block/Cache/AdditionalTest.php
index 160cfe609f85f..7a56536eaef44 100644
--- a/app/code/Magento/Backend/Test/Unit/Block/Cache/AdditionalTest.php
+++ b/app/code/Magento/Backend/Test/Unit/Block/Cache/AdditionalTest.php
@@ -88,6 +88,9 @@ public function testIsInProductionMode($mode, $expected)
$this->assertEquals($expected, $this->additonalBlock->isInProductionMode());
}
+ /**
+ * @return array
+ */
public function isInProductionModeDataProvider()
{
return [
diff --git a/app/code/Magento/Backend/Test/Unit/Block/Cache/PermissionsTest.php b/app/code/Magento/Backend/Test/Unit/Block/Cache/PermissionsTest.php
new file mode 100644
index 0000000000000..6975b8ac092ad
--- /dev/null
+++ b/app/code/Magento/Backend/Test/Unit/Block/Cache/PermissionsTest.php
@@ -0,0 +1,76 @@
+objectManager = new ObjectManager($this);
+
+ $this->mockAuthorization = $this->getMockBuilder(Authorization::class)
+ ->disableOriginalConstructor()
+ ->setMethods(['isAllowed'])
+ ->getMock();
+
+ $this->permissions = new Permissions($this->mockAuthorization);
+ }
+
+ public function testHasAccessToFlushCatalogImages()
+ {
+ $this->mockAuthorization->expects($this->atLeastOnce())
+ ->method('isAllowed')
+ ->with('Magento_Backend::flush_catalog_images')
+ ->willReturn(true);
+
+ $this->assertTrue($this->permissions->hasAccessToFlushCatalogImages());
+ }
+
+ public function testHasAccessToFlushJsCss()
+ {
+ $this->mockAuthorization->expects($this->atLeastOnce())
+ ->method('isAllowed')
+ ->with('Magento_Backend::flush_js_css')
+ ->willReturn(true);
+
+ $this->assertTrue($this->permissions->hasAccessToFlushJsCss());
+ }
+
+ public function testHasAccessToFlushStaticFiles()
+ {
+ $this->mockAuthorization->expects($this->atLeastOnce())
+ ->method('isAllowed')
+ ->with('Magento_Backend::flush_static_files')
+ ->willReturn(true);
+
+ $this->assertTrue($this->permissions->hasAccessToFlushStaticFiles());
+ }
+}
diff --git a/app/code/Magento/Backend/Test/Unit/Block/GlobalSearchTest.php b/app/code/Magento/Backend/Test/Unit/Block/GlobalSearchTest.php
new file mode 100644
index 0000000000000..f08a0098578b9
--- /dev/null
+++ b/app/code/Magento/Backend/Test/Unit/Block/GlobalSearchTest.php
@@ -0,0 +1,126 @@
+ \Magento\Catalog\Controller\Adminhtml\Product::ADMIN_RESOURCE,
+ 'Orders' => \Magento\Sales\Controller\Adminhtml\Order::ADMIN_RESOURCE,
+ 'Customers' => \Magento\Customer\Controller\Adminhtml\Index::ADMIN_RESOURCE,
+ 'Pages' => \Magento\Cms\Controller\Adminhtml\Page\Index::ADMIN_RESOURCE,
+ ];
+
+ /**
+ * @var array
+ */
+ private $entityPaths = [
+ 'Products' => 'catalog/product/index/',
+ 'Orders' => 'sales/order/index/',
+ 'Customers' => 'customer/index/index',
+ 'Pages' => 'cms/page/index/',
+ ];
+
+ protected function setUp()
+ {
+ $objectManager = new ObjectManager($this);
+
+ $this->authorization = $this->createMock(\Magento\Framework\AuthorizationInterface::class);
+ $this->urlBuilder = $this->createMock(\Magento\Framework\UrlInterface::class);
+ $context = $this->createMock(\Magento\Backend\Block\Template\Context::class);
+
+ $context->expects($this->atLeastOnce())->method('getAuthorization')->willReturn($this->authorization);
+ $context->expects($this->atLeastOnce())->method('getUrlBuilder')->willReturn($this->urlBuilder);
+
+ $this->searchEntityFactory = $this->createMock(\Magento\Backend\Model\GlobalSearch\SearchEntityFactory::class);
+
+ $this->globalSearch = $objectManager->getObject(
+ GlobalSearch::class,
+ [
+ 'context' => $context,
+ 'searchEntityFactory' => $this->searchEntityFactory,
+ 'entityResources' => $this->entityResources,
+ 'entityPaths' => $this->entityPaths,
+ ]
+ );
+ }
+
+ /**
+ * @param array $results
+ * @param int $expectedEntitiesQty
+ *
+ * @dataProvider getEntitiesToShowDataProvider
+ */
+ public function testGetEntitiesToShow(array $results, int $expectedEntitiesQty)
+ {
+ $searchEntity = $this->createMock(SearchEntity::class);
+
+ $this->authorization->expects($this->exactly(count($results)))->method('isAllowed')
+ ->willReturnOnConsecutiveCalls($results[0], $results[1], $results[2], $results[3]);
+ $this->urlBuilder->expects($this->exactly($expectedEntitiesQty))
+ ->method('getUrl')->willReturn('some/url/is/here');
+ $this->searchEntityFactory->expects($this->exactly($expectedEntitiesQty))
+ ->method('create')->willReturn($searchEntity);
+
+ $searchEntity->expects($this->exactly($expectedEntitiesQty))->method('setId');
+ $searchEntity->expects($this->exactly($expectedEntitiesQty))->method('setTitle');
+ $searchEntity->expects($this->exactly($expectedEntitiesQty))->method('setUrl');
+
+ $this->assertSame($expectedEntitiesQty, count($this->globalSearch->getEntitiesToShow()));
+ }
+
+ /**
+ * @return array
+ */
+ public function getEntitiesToShowDataProvider()
+ {
+ return [
+ [
+ [true, false, true, false],
+ 2,
+ ],
+ [
+ [true, true, true, true],
+ 4,
+ ],
+ [
+ [false, false, false, false],
+ 0,
+ ],
+ ];
+ }
+}
diff --git a/app/code/Magento/Backend/Test/Unit/Block/MenuItemCheckerTest.php b/app/code/Magento/Backend/Test/Unit/Block/MenuItemCheckerTest.php
index a79050faeb84a..aca719b2e65e9 100644
--- a/app/code/Magento/Backend/Test/Unit/Block/MenuItemCheckerTest.php
+++ b/app/code/Magento/Backend/Test/Unit/Block/MenuItemCheckerTest.php
@@ -74,6 +74,9 @@ public function testIsItemActiveLevelNotZero()
);
}
+ /**
+ * @return array
+ */
public function dataProvider()
{
return [
diff --git a/app/code/Magento/Backend/Test/Unit/Block/Widget/ButtonTest.php b/app/code/Magento/Backend/Test/Unit/Block/Widget/ButtonTest.php
index bcf5d1adbc12b..e64d1a97af4ae 100644
--- a/app/code/Magento/Backend/Test/Unit/Block/Widget/ButtonTest.php
+++ b/app/code/Magento/Backend/Test/Unit/Block/Widget/ButtonTest.php
@@ -61,6 +61,9 @@ public function testGetAttributesHtml($data, $expect)
$this->assertRegExp($expect, $attributes);
}
+ /**
+ * @return array
+ */
public function getAttributesHtmlDataProvider()
{
return [
diff --git a/app/code/Magento/Backend/Test/Unit/Block/Widget/Grid/Column/Renderer/Radio/ExtendedTest.php b/app/code/Magento/Backend/Test/Unit/Block/Widget/Grid/Column/Renderer/Radio/ExtendedTest.php
index 35e21d7d194aa..81f104dbb636b 100644
--- a/app/code/Magento/Backend/Test/Unit/Block/Widget/Grid/Column/Renderer/Radio/ExtendedTest.php
+++ b/app/code/Magento/Backend/Test/Unit/Block/Widget/Grid/Column/Renderer/Radio/ExtendedTest.php
@@ -54,6 +54,9 @@ public function testRender(array $rowData, $expectedResult)
$this->assertEquals($expectedResult, $this->_object->render(new \Magento\Framework\DataObject($rowData)));
}
+ /**
+ * @return array
+ */
public function renderDataProvider()
{
return [
diff --git a/app/code/Magento/Backend/Test/Unit/Block/Widget/Grid/Column/Renderer/RadioTest.php b/app/code/Magento/Backend/Test/Unit/Block/Widget/Grid/Column/Renderer/RadioTest.php
index 67ead0ddd8f35..6f838634c6bed 100644
--- a/app/code/Magento/Backend/Test/Unit/Block/Widget/Grid/Column/Renderer/RadioTest.php
+++ b/app/code/Magento/Backend/Test/Unit/Block/Widget/Grid/Column/Renderer/RadioTest.php
@@ -63,6 +63,9 @@ public function testRender(array $rowData, $expectedResult)
$this->assertEquals($expectedResult, $this->_object->render(new \Magento\Framework\DataObject($rowData)));
}
+ /**
+ * @return array
+ */
public function renderDataProvider()
{
return [
diff --git a/app/code/Magento/Backend/Test/Unit/Block/Widget/Grid/ColumnSetTest.php b/app/code/Magento/Backend/Test/Unit/Block/Widget/Grid/ColumnSetTest.php
index be171a8ed40bf..df242a4cf6129 100644
--- a/app/code/Magento/Backend/Test/Unit/Block/Widget/Grid/ColumnSetTest.php
+++ b/app/code/Magento/Backend/Test/Unit/Block/Widget/Grid/ColumnSetTest.php
@@ -117,7 +117,7 @@ public function testSetFilterTypePropagatesFilterTypeToColumns()
public function testGetRowUrlIfUrlPathNotSet()
{
- $this->assertEquals('#', $this->_block->getRowUrl(new \StdClass()));
+ $this->assertEquals('#', $this->_block->getRowUrl(new \stdClass()));
}
public function testGetRowUrl()
diff --git a/app/code/Magento/Backend/Test/Unit/Block/Widget/Grid/ColumnTest.php b/app/code/Magento/Backend/Test/Unit/Block/Widget/Grid/ColumnTest.php
index da13af87b71ea..2e6bed4783e7f 100644
--- a/app/code/Magento/Backend/Test/Unit/Block/Widget/Grid/ColumnTest.php
+++ b/app/code/Magento/Backend/Test/Unit/Block/Widget/Grid/ColumnTest.php
@@ -86,6 +86,9 @@ public function testGetSortable($value)
$this->assertFalse($this->_block->getSortable());
}
+ /**
+ * @return array
+ */
public function getSortableDataProvider()
{
return ['zero' => ['0'], 'false' => [false], 'null' => [null]];
@@ -351,7 +354,7 @@ public function testSetGetGrid()
$this->_block->setFilter('StdClass');
- $grid = new \StdClass();
+ $grid = new \stdClass();
$this->_block->setGrid($grid);
$this->assertEquals($grid, $this->_block->getGrid());
}
@@ -374,6 +377,9 @@ public function testColumnIsGrouped($groupedData, $expected)
$this->assertEquals($expected, $block->isGrouped());
}
+ /**
+ * @return array
+ */
public function columnGroupedDataProvider()
{
return [[[], false], [['grouped' => 0], false], [['grouped' => 1], true]];
diff --git a/app/code/Magento/Backend/Test/Unit/Block/Widget/Grid/Massaction/ExtendedTest.php b/app/code/Magento/Backend/Test/Unit/Block/Widget/Grid/Massaction/ExtendedTest.php
index 4525de1fee542..f81928c4540ba 100644
--- a/app/code/Magento/Backend/Test/Unit/Block/Widget/Grid/Massaction/ExtendedTest.php
+++ b/app/code/Magento/Backend/Test/Unit/Block/Widget/Grid/Massaction/ExtendedTest.php
@@ -152,6 +152,9 @@ public function testGetGridIdsJsonWithUseSelectAll(array $items, $result)
$this->assertEquals($result, $this->_block->getGridIdsJson());
}
+ /**
+ * @return array
+ */
public function dataProviderGetGridIdsJsonWithUseSelectAll()
{
return [
diff --git a/app/code/Magento/Backend/Test/Unit/Block/Widget/Grid/MassactionTest.php b/app/code/Magento/Backend/Test/Unit/Block/Widget/Grid/MassactionTest.php
index bb389a996e1ed..e8143b5f6b43a 100644
--- a/app/code/Magento/Backend/Test/Unit/Block/Widget/Grid/MassactionTest.php
+++ b/app/code/Magento/Backend/Test/Unit/Block/Widget/Grid/MassactionTest.php
@@ -10,6 +10,7 @@
namespace Magento\Backend\Test\Unit\Block\Widget\Grid;
use Magento\Backend\Block\Widget\Grid\Massaction\VisibilityCheckerInterface as VisibilityChecker;
+use Magento\Framework\Authorization;
class MassactionTest extends \PHPUnit\Framework\TestCase
{
@@ -43,6 +44,11 @@ class MassactionTest extends \PHPUnit\Framework\TestCase
*/
protected $_requestMock;
+ /**
+ * @var Authorization|\PHPUnit_Framework_MockObject_MockObject
+ */
+ protected $_authorizationMock;
+
/**
* @var VisibilityChecker|\PHPUnit_Framework_MockObject_MockObject
*/
@@ -86,11 +92,17 @@ protected function setUp()
$this->visibilityCheckerMock = $this->getMockBuilder(VisibilityChecker::class)
->getMockForAbstractClass();
+ $this->_authorizationMock = $this->getMockBuilder(Authorization::class)
+ ->disableOriginalConstructor()
+ ->setMethods(['isAllowed'])
+ ->getMock();
+
$arguments = [
'layout' => $this->_layoutMock,
'request' => $this->_requestMock,
'urlBuilder' => $this->_urlModelMock,
- 'data' => ['massaction_id_field' => 'test_id', 'massaction_id_filter' => 'test_id']
+ 'data' => ['massaction_id_field' => 'test_id', 'massaction_id_filter' => 'test_id'],
+ 'authorization' => $this->_authorizationMock,
];
$objectManagerHelper = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this);
@@ -145,6 +157,10 @@ public function testItemsProcessing($itemId, $item, $expectedItem)
->method('getUrl')
->willReturnMap($urlReturnValueMap);
+ $this->_authorizationMock->expects($this->any())
+ ->method('isAllowed')
+ ->willReturn(true);
+
$this->_block->addItem($itemId, $item);
$this->assertEquals(1, $this->_block->getCount());
@@ -184,6 +200,28 @@ public function itemsProcessingDataProvider()
"id" => 'test_id2',
]
)
+ ],
+ [
+ 'enabled',
+ new \Magento\Framework\DataObject(["label" => "Test Item Enabled", "url" => "*/*/test2"]),
+ new \Magento\Framework\DataObject(
+ [
+ "label" => "Test Item Enabled",
+ "url" => "http://localhost/index.php/backend/admin/test/test2",
+ "id" => 'enabled',
+ ]
+ )
+ ],
+ [
+ 'refresh',
+ new \Magento\Framework\DataObject(["label" => "Test Item Refresh", "url" => "*/*/test2"]),
+ new \Magento\Framework\DataObject(
+ [
+ "label" => "Test Item Refresh",
+ "url" => "http://localhost/index.php/backend/admin/test/test2",
+ "id" => 'refresh',
+ ]
+ )
]
];
}
@@ -205,6 +243,9 @@ public function testSelected($param, $expectedJson, $expected)
$this->assertEquals($expected, $this->_block->getSelected());
}
+ /**
+ * @return array
+ */
public function selectedDataProvider()
{
return [
@@ -237,7 +278,7 @@ public function testGetGridIdsJsonWithoutUseSelectAll()
public function testGetGridIdsJsonWithUseSelectAll(array $items, $result)
{
$this->_block->setUseSelectAll(true);
-
+
if ($this->_block->getMassactionIdField()) {
$massActionIdField = $this->_block->getMassactionIdField();
} else {
@@ -290,14 +331,20 @@ public function dataProviderGetGridIdsJsonWithUseSelectAll()
* @param int $count
* @param bool $withVisibilityChecker
* @param bool $isVisible
+ * @param bool $isAllowed
+ *
* @dataProvider addItemDataProvider
*/
- public function testAddItem($itemId, $item, $count, $withVisibilityChecker, $isVisible)
+ public function testAddItem($itemId, $item, $count, $withVisibilityChecker, $isVisible, $isAllowed)
{
$this->visibilityCheckerMock->expects($this->any())
->method('isVisible')
->willReturn($isVisible);
+ $this->_authorizationMock->expects($this->any())
+ ->method('isAllowed')
+ ->willReturn($isAllowed);
+
if ($withVisibilityChecker) {
$item['visible'] = $this->visibilityCheckerMock;
}
@@ -311,7 +358,7 @@ public function testAddItem($itemId, $item, $count, $withVisibilityChecker, $isV
->willReturnMap($urlReturnValueMap);
$this->_block->addItem($itemId, $item);
- $this->assertEquals($count, $this->_block->getCount());
+ $this->assertEquals($count, $this->_block->getCount(), $itemId);
}
/**
@@ -325,7 +372,8 @@ public function addItemDataProvider()
'item' => ['label' => 'Test 1', 'url' => '*/*/test1'],
'count' => 1,
'withVisibilityChecker' => false,
- '$isVisible' => false,
+ 'isVisible' => false,
+ 'isAllowed' => true,
],
[
'itemId' => 'test2',
@@ -333,21 +381,56 @@ public function addItemDataProvider()
'count' => 1,
'withVisibilityChecker' => false,
'isVisible' => true,
+ 'isAllowed' => true,
],
[
- 'itemId' => 'test1',
- 'item' => ['label' => 'Test 1. Hide', 'url' => '*/*/test1'],
+ 'itemId' => 'test3',
+ 'item' => ['label' => 'Test 3. Hide', 'url' => '*/*/test3'],
'count' => 0,
'withVisibilityChecker' => true,
'isVisible' => false,
+ 'isAllowed' => true,
],
[
- 'itemId' => 'test2',
- 'item' => ['label' => 'Test 2. Does not hide', 'url' => '*/*/test2'],
+ 'itemId' => 'test4',
+ 'item' => ['label' => 'Test 4. Does not hide', 'url' => '*/*/test4'],
'count' => 1,
'withVisibilityChecker' => true,
'isVisible' => true,
- ]
+ 'isAllowed' => true,
+ ],
+ [
+ 'itemId' => 'enable',
+ 'item' => ['label' => 'Test 5. Not restricted', 'url' => '*/*/test5'],
+ 'count' => 1,
+ 'withVisibilityChecker' => true,
+ 'isVisible' => true,
+ 'isAllowed' => true,
+ ],
+ [
+ 'itemId' => 'enable',
+ 'item' => ['label' => 'Test 5. restricted', 'url' => '*/*/test5'],
+ 'count' => 0,
+ 'withVisibilityChecker' => true,
+ 'isVisible' => true,
+ 'isAllowed' => false,
+ ],
+ [
+ 'itemId' => 'refresh',
+ 'item' => ['label' => 'Test 6. Not Restricted', 'url' => '*/*/test6'],
+ 'count' => 1,
+ 'withVisibilityChecker' => true,
+ 'isVisible' => true,
+ 'isAllowed' => true,
+ ],
+ [
+ 'itemId' => 'refresh',
+ 'item' => ['label' => 'Test 6. Restricted', 'url' => '*/*/test6'],
+ 'count' => 0,
+ 'withVisibilityChecker' => true,
+ 'isVisible' => true,
+ 'isAllowed' => false,
+ ],
];
}
}
diff --git a/app/code/Magento/Backend/Test/Unit/Block/Widget/TabTest.php b/app/code/Magento/Backend/Test/Unit/Block/Widget/TabTest.php
index 1670233324f8e..ad7c6fa99afd2 100644
--- a/app/code/Magento/Backend/Test/Unit/Block/Widget/TabTest.php
+++ b/app/code/Magento/Backend/Test/Unit/Block/Widget/TabTest.php
@@ -34,6 +34,9 @@ public function testGetters($method, $field, $value, $expected)
$this->assertEquals($expected, $object->{$method}());
}
+ /**
+ * @return array
+ */
public function dataProvider()
{
return [
diff --git a/app/code/Magento/Backend/Test/Unit/Controller/Adminhtml/Cache/CleanMediaTest.php b/app/code/Magento/Backend/Test/Unit/Controller/Adminhtml/Cache/CleanMediaTest.php
index b1911da024227..ac0f4a2f467c8 100644
--- a/app/code/Magento/Backend/Test/Unit/Controller/Adminhtml/Cache/CleanMediaTest.php
+++ b/app/code/Magento/Backend/Test/Unit/Controller/Adminhtml/Cache/CleanMediaTest.php
@@ -38,7 +38,7 @@ public function testExecute()
$messageManagerParams = $helper->getConstructArguments(\Magento\Framework\Message\Manager::class);
$messageManagerParams['exceptionMessageFactory'] = $exceptionMessageFactory;
$messageManager = $this->getMockBuilder(\Magento\Framework\Message\Manager::class)
- ->setMethods(['addSuccess'])
+ ->setMethods(['addSuccessMessage'])
->setConstructorArgs($messageManagerParams)
->getMock();
@@ -86,7 +86,7 @@ public function testExecute()
$mergeService->expects($this->once())->method('cleanMergedJsCss');
$messageManager->expects($this->once())
- ->method('addSuccess')
+ ->method('addSuccessMessage')
->with('The JavaScript/CSS cache has been cleaned.');
$valueMap = [
diff --git a/app/code/Magento/Backend/Test/Unit/Controller/Adminhtml/Cache/CleanStaticFilesTest.php b/app/code/Magento/Backend/Test/Unit/Controller/Adminhtml/Cache/CleanStaticFilesTest.php
index 40d9ca1aa8996..fc457cd9681e6 100644
--- a/app/code/Magento/Backend/Test/Unit/Controller/Adminhtml/Cache/CleanStaticFilesTest.php
+++ b/app/code/Magento/Backend/Test/Unit/Controller/Adminhtml/Cache/CleanStaticFilesTest.php
@@ -76,7 +76,7 @@ public function testExecute()
->with('clean_static_files_cache_after');
$this->messageManagerMock->expects($this->once())
- ->method('addSuccess')
+ ->method('addSuccessMessage')
->with('The static files cache has been cleaned.');
$resultRedirect = $this->getMockBuilder(\Magento\Backend\Model\View\Result\Redirect::class)
diff --git a/app/code/Magento/Backend/Test/Unit/Controller/Adminhtml/Cache/MassDisableTest.php b/app/code/Magento/Backend/Test/Unit/Controller/Adminhtml/Cache/MassDisableTest.php
index 556db311748bd..9a42d0acdba89 100644
--- a/app/code/Magento/Backend/Test/Unit/Controller/Adminhtml/Cache/MassDisableTest.php
+++ b/app/code/Magento/Backend/Test/Unit/Controller/Adminhtml/Cache/MassDisableTest.php
@@ -155,7 +155,7 @@ public function testExecuteInvalidTypeCache()
->willReturn(['someCache']);
$this->messageManagerMock->expects($this->once())
- ->method('addError')
+ ->method('addErrorMessage')
->with('Specified cache type(s) don\'t exist: someCache')
->willReturnSelf();
@@ -175,7 +175,7 @@ public function testExecuteWithException()
->willThrowException($exception);
$this->messageManagerMock->expects($this->once())
- ->method('addException')
+ ->method('addExceptionMessage')
->with($exception, 'An error occurred while disabling cache.')
->willReturnSelf();
@@ -215,7 +215,7 @@ public function testExecuteSuccess()
->method('persist');
$this->messageManagerMock->expects($this->once())
- ->method('addSuccess')
+ ->method('addSuccessMessage')
->with('1 cache type(s) disabled.')
->willReturnSelf();
diff --git a/app/code/Magento/Backend/Test/Unit/Controller/Adminhtml/Cache/MassEnableTest.php b/app/code/Magento/Backend/Test/Unit/Controller/Adminhtml/Cache/MassEnableTest.php
index ad622ca69757a..23cf6a9a0a70a 100644
--- a/app/code/Magento/Backend/Test/Unit/Controller/Adminhtml/Cache/MassEnableTest.php
+++ b/app/code/Magento/Backend/Test/Unit/Controller/Adminhtml/Cache/MassEnableTest.php
@@ -155,7 +155,7 @@ public function testExecuteInvalidTypeCache()
->willReturn(['someCache']);
$this->messageManagerMock->expects($this->once())
- ->method('addError')
+ ->method('addErrorMessage')
->with('Specified cache type(s) don\'t exist: someCache')
->willReturnSelf();
@@ -175,7 +175,7 @@ public function testExecuteWithException()
->willThrowException($exception);
$this->messageManagerMock->expects($this->once())
- ->method('addException')
+ ->method('addExceptionMessage')
->with($exception, 'An error occurred while enabling cache.')
->willReturnSelf();
@@ -215,7 +215,7 @@ public function testExecuteSuccess()
->method('persist');
$this->messageManagerMock->expects($this->once())
- ->method('addSuccess')
+ ->method('addSuccessMessage')
->with('1 cache type(s) enabled.')
->willReturnSelf();
diff --git a/app/code/Magento/Backend/Test/Unit/Controller/Adminhtml/Dashboard/AbstractTestCase.php b/app/code/Magento/Backend/Test/Unit/Controller/Adminhtml/Dashboard/AbstractTestCase.php
index dbd1cbe5ec8ec..6049ef76fb183 100644
--- a/app/code/Magento/Backend/Test/Unit/Controller/Adminhtml/Dashboard/AbstractTestCase.php
+++ b/app/code/Magento/Backend/Test/Unit/Controller/Adminhtml/Dashboard/AbstractTestCase.php
@@ -21,8 +21,7 @@ protected function assertExecute($controllerName, $blockName)
{
$objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this);
$outPut = "data";
- $resultRawMock = $this->createPartialMock(\Magento\Framework\Controller\Result\Raw::class, ['setContents'])
- ;
+ $resultRawMock = $this->createPartialMock(\Magento\Framework\Controller\Result\Raw::class, ['setContents']);
$resultRawFactoryMock =
$this->createPartialMock(\Magento\Framework\Controller\Result\RawFactory::class, ['create']);
$layoutFactoryMock = $this->createPartialMock(\Magento\Framework\View\LayoutFactory::class, ['create']);
diff --git a/app/code/Magento/Backend/Test/Unit/Controller/Adminhtml/Dashboard/RefreshStatisticsTest.php b/app/code/Magento/Backend/Test/Unit/Controller/Adminhtml/Dashboard/RefreshStatisticsTest.php
index e8dcc00345fc6..a985681919f0b 100644
--- a/app/code/Magento/Backend/Test/Unit/Controller/Adminhtml/Dashboard/RefreshStatisticsTest.php
+++ b/app/code/Magento/Backend/Test/Unit/Controller/Adminhtml/Dashboard/RefreshStatisticsTest.php
@@ -107,7 +107,7 @@ public function testExecute()
$this->resultRedirectFactory->expects($this->any())->method('create')->willReturn($this->resultRedirect);
$this->messageManager->expects($this->once())
- ->method('addSuccess')
+ ->method('addSuccessMessage')
->with(__('We updated lifetime statistic.'));
$this->objectManager->expects($this->any())
diff --git a/app/code/Magento/Backend/Test/Unit/Controller/Adminhtml/System/Account/SaveTest.php b/app/code/Magento/Backend/Test/Unit/Controller/Adminhtml/System/Account/SaveTest.php
index 844a821df1c20..a8490d6ba2e58 100644
--- a/app/code/Magento/Backend/Test/Unit/Controller/Adminhtml/System/Account/SaveTest.php
+++ b/app/code/Magento/Backend/Test/Unit/Controller/Adminhtml/System/Account/SaveTest.php
@@ -71,7 +71,7 @@ protected function setUp()
->getMock();
$this->_messagesMock = $this->getMockBuilder(\Magento\Framework\Message\Manager::class)
->disableOriginalConstructor()
- ->setMethods(['addSuccess'])
+ ->setMethods(['addSuccessMessage'])
->getMockForAbstractClass();
$this->_authSessionMock = $this->getMockBuilder(\Magento\Backend\Model\Auth\Session::class)
@@ -221,7 +221,7 @@ public function testSaveAction()
$this->_requestMock->setParams($requestParams);
- $this->_messagesMock->expects($this->once())->method('addSuccess')->with($this->equalTo($testedMessage));
+ $this->_messagesMock->expects($this->once())->method('addSuccessMessage')->with($this->equalTo($testedMessage));
$this->_controller->execute();
}
diff --git a/app/code/Magento/Backend/Test/Unit/Helper/DataTest.php b/app/code/Magento/Backend/Test/Unit/Helper/DataTest.php
index b7a33ab883b69..50c3a8571b48f 100644
--- a/app/code/Magento/Backend/Test/Unit/Helper/DataTest.php
+++ b/app/code/Magento/Backend/Test/Unit/Helper/DataTest.php
@@ -60,6 +60,9 @@ public function testPrepareFilterStringValues(array $inputString, array $expecte
$this->assertEquals($expected, $actual);
}
+ /**
+ * @return array
+ */
public function getPrepareFilterStringValuesDataProvider()
{
return [
diff --git a/app/code/Magento/Backend/Test/Unit/Model/Auth/SessionTest.php b/app/code/Magento/Backend/Test/Unit/Model/Auth/SessionTest.php
index 391deac5a1f4e..f1a4bc355b08e 100644
--- a/app/code/Magento/Backend/Test/Unit/Model/Auth/SessionTest.php
+++ b/app/code/Magento/Backend/Test/Unit/Model/Auth/SessionTest.php
@@ -120,6 +120,9 @@ public function testRefreshAcl($isUserPassedViaParams)
$this->assertSame($aclMock, $this->session->getAcl());
}
+ /**
+ * @return array
+ */
public function refreshAclDataProvider()
{
return [
@@ -234,6 +237,9 @@ public function testIsAllowed($isUserDefined, $isAclDefined, $isAllowed, $expect
$this->assertEquals($expectedResult, $this->session->isAllowed('resource'));
}
+ /**
+ * @return array
+ */
public function isAllowedDataProvider()
{
return [
@@ -254,6 +260,9 @@ public function testFirstPageAfterLogin($isFirstPageAfterLogin)
$this->assertEquals($isFirstPageAfterLogin, $this->session->isFirstPageAfterLogin());
}
+ /**
+ * @return array
+ */
public function firstPageAfterLoginDataProvider()
{
return [
diff --git a/app/code/Magento/Backend/Test/Unit/Model/Config/SessionLifetime/BackendModelTest.php b/app/code/Magento/Backend/Test/Unit/Model/Config/SessionLifetime/BackendModelTest.php
index 31a13191750a3..4695ef00d5d05 100755
--- a/app/code/Magento/Backend/Test/Unit/Model/Config/SessionLifetime/BackendModelTest.php
+++ b/app/code/Magento/Backend/Test/Unit/Model/Config/SessionLifetime/BackendModelTest.php
@@ -20,13 +20,17 @@ public function testBeforeSave($value, $errorMessage = null)
\Magento\Backend\Model\Config\SessionLifetime\BackendModel::class
);
if ($errorMessage !== null) {
- $this->expectException(\Magento\Framework\Exception\LocalizedException::class, $errorMessage);
+ $this->expectException(\Magento\Framework\Exception\LocalizedException::class);
+ $this->expectExceptionMessage($errorMessage);
}
$model->setValue($value);
$object = $model->beforeSave();
$this->assertEquals($model, $object);
}
+ /**
+ * @return array
+ */
public function adminSessionLifetimeDataProvider()
{
return [
diff --git a/app/code/Magento/Backend/Test/Unit/Model/Menu/ConfigTest.php b/app/code/Magento/Backend/Test/Unit/Model/Menu/ConfigTest.php
index bc18bd44f4be4..a6b728ceb392f 100644
--- a/app/code/Magento/Backend/Test/Unit/Model/Menu/ConfigTest.php
+++ b/app/code/Magento/Backend/Test/Unit/Model/Menu/ConfigTest.php
@@ -140,6 +140,9 @@ public function testGetMenuExceptionLogged($expectedException)
$this->model->getMenu();
}
+ /**
+ * @return array
+ */
public function getMenuExceptionLoggedDataProvider()
{
return [
diff --git a/app/code/Magento/Backend/Test/Unit/Model/Menu/Item/ValidatorTest.php b/app/code/Magento/Backend/Test/Unit/Model/Menu/Item/ValidatorTest.php
index 3c1f1e43900be..dec85f4b98e3d 100644
--- a/app/code/Magento/Backend/Test/Unit/Model/Menu/Item/ValidatorTest.php
+++ b/app/code/Magento/Backend/Test/Unit/Model/Menu/Item/ValidatorTest.php
@@ -79,6 +79,9 @@ public function testValidateWithMissingRequiredParamThrowsException($requiredPar
}
}
+ /**
+ * @return array
+ */
public function requiredParamsProvider()
{
return [['id'], ['title'], ['resource']];
@@ -102,6 +105,9 @@ public function testValidateWithNonValidPrimitivesThrowsException($param, $inval
}
}
+ /**
+ * @return array
+ */
public function invalidParamsProvider()
{
return [
diff --git a/app/code/Magento/Backend/Test/Unit/Model/Menu/ItemTest.php b/app/code/Magento/Backend/Test/Unit/Model/Menu/ItemTest.php
index 74368537c39c7..ad172cbfbd165 100644
--- a/app/code/Magento/Backend/Test/Unit/Model/Menu/ItemTest.php
+++ b/app/code/Magento/Backend/Test/Unit/Model/Menu/ItemTest.php
@@ -56,9 +56,9 @@ class ItemTest extends \PHPUnit\Framework\TestCase
'title' => 'Item Title',
'action' => '/system/config',
'resource' => 'Magento_Config::config',
- 'depends_on_module' => 'Magento_Backend',
- 'depends_on_config' => 'system/config/isEnabled',
- 'tooltip' => 'Item tooltip',
+ 'dependsOnModule' => 'Magento_Backend',
+ 'dependsOnConfig' => 'system/config/isEnabled',
+ 'toolTip' => 'Item tooltip',
];
protected function setUp()
diff --git a/app/code/Magento/Backend/Test/Unit/Model/MenuBuilderTest.php b/app/code/Magento/Backend/Test/Unit/Model/MenuBuilderTest.php
index 23d1ed5da1425..5d026a2b1fc32 100644
--- a/app/code/Magento/Backend/Test/Unit/Model/MenuBuilderTest.php
+++ b/app/code/Magento/Backend/Test/Unit/Model/MenuBuilderTest.php
@@ -35,6 +35,9 @@ public function testAfterGetResult($isPub, $times)
);
}
+ /**
+ * @return array
+ */
public function afterGetResultDataProvider()
{
return [[true, 1], [false, 0],];
diff --git a/app/code/Magento/Backend/Test/Unit/Model/Session/AdminConfigTest.php b/app/code/Magento/Backend/Test/Unit/Model/Session/AdminConfigTest.php
index 49fcdf4fc8770..00ae8c2f44a69 100644
--- a/app/code/Magento/Backend/Test/Unit/Model/Session/AdminConfigTest.php
+++ b/app/code/Magento/Backend/Test/Unit/Model/Session/AdminConfigTest.php
@@ -136,6 +136,9 @@ public function testSetSessionSettingsByConstructor($secureRequest)
$this->assertSame($secureRequest, $adminConfig->getCookieSecure());
}
+ /**
+ * @return array
+ */
public function requestSecureDataProvider()
{
return [[true], [false]];
diff --git a/app/code/Magento/Backend/Test/Unit/Model/Widget/Grid/ParserTest.php b/app/code/Magento/Backend/Test/Unit/Model/Widget/Grid/ParserTest.php
index 569c5ffc16c9c..98f1965477b2c 100644
--- a/app/code/Magento/Backend/Test/Unit/Model/Widget/Grid/ParserTest.php
+++ b/app/code/Magento/Backend/Test/Unit/Model/Widget/Grid/ParserTest.php
@@ -58,6 +58,9 @@ public function testIsOperation($operation, $expected)
$this->assertEquals($expected, $this->_model->isOperation($operation));
}
+ /**
+ * @return array
+ */
public function isOperationDataProvider()
{
return [
diff --git a/app/code/Magento/Backend/Test/Unit/Model/_files/menu_item_constructor_data.php b/app/code/Magento/Backend/Test/Unit/Model/_files/menu_item_constructor_data.php
index b0c74461980a2..82f07e264b963 100644
--- a/app/code/Magento/Backend/Test/Unit/Model/_files/menu_item_constructor_data.php
+++ b/app/code/Magento/Backend/Test/Unit/Model/_files/menu_item_constructor_data.php
@@ -12,21 +12,21 @@
'title' => 'Item Title',
'action' => '/system/config',
'resource' => 'Magento_Config::config',
- 'depends_on_module' => 'Magento_Backend',
- 'depends_on_config' => 'system/config/isEnabled',
- 'tooltip' => 'Item tooltip',
+ 'dependsOnModule' => 'Magento_Backend',
+ 'dependsOnConfig' => 'system/config/isEnabled',
+ 'toolTip' => 'Item tooltip',
],
[
'parent_id' => null,
- 'module_name' => 'Magento_Backend',
+ 'module' => 'Magento_Backend',
'sort_index' => null,
- 'depends_on_config' => 'system/config/isEnabled',
+ 'dependsOnConfig' => 'system/config/isEnabled',
'id' => 'item',
'resource' => 'Magento_Config::config',
'path' => '',
'action' => '/system/config',
- 'depends_on_module' => 'Magento_Backend',
- 'tooltip' => 'Item tooltip',
+ 'dependsOnModule' => 'Magento_Backend',
+ 'toolTip' => 'Item tooltip',
'title' => 'Item Title',
'sub_menu' => null,
'target' => null
@@ -38,43 +38,43 @@
'title' => 'Item Title',
'action' => '/system/config',
'resource' => 'Magento_Config::config',
- 'depends_on_module' => 'Magento_Backend',
- 'depends_on_config' => 'system/config/isEnabled',
- 'tooltip' => 'Item tooltip',
+ 'dependsOnModule' => 'Magento_Backend',
+ 'dependsOnConfig' => 'system/config/isEnabled',
+ 'toolTip' => 'Item tooltip',
],
[
'parent_id' => '1',
- 'module_name' => 'Magento_Module1',
+ 'module' => 'Magento_Module1',
'sort_index' => '50',
- 'depends_on_config' => null,
+ 'dependsOnConfig' => null,
'id' => '5',
'resource' => null,
'path' => null,
'action' => null,
- 'depends_on_module' => null,
- 'tooltip' => null,
+ 'dependsOnModule' => null,
+ 'toolTip' => null,
'title' => null,
'sub_menu' => [
'id' => 'item',
'title' => 'Item Title',
'action' => '/system/config',
'resource' => 'Magento_Config::config',
- 'depends_on_module' => 'Magento_Backend',
- 'depends_on_config' => 'system/config/isEnabled',
- 'tooltip' => 'Item tooltip',
+ 'dependsOnModule' => 'Magento_Backend',
+ 'dependsOnConfig' => 'system/config/isEnabled',
+ 'toolTip' => 'Item tooltip',
],
],
[
'parent_id' => '1',
- 'module_name' => 'Magento_Module1',
+ 'module' => 'Magento_Module1',
'sort_index' => '50',
- 'depends_on_config' => null,
+ 'dependsOnConfig' => null,
'id' => '5',
'resource' => null,
'path' => '',
'action' => null,
- 'depends_on_module' => null,
- 'tooltip' => '',
+ 'dependsOnModule' => null,
+ 'toolTip' => '',
'title' => null,
'sub_menu' => ['submenuArray'],
'target' => null
@@ -83,51 +83,51 @@
'data with submenu to constructor' => [
[
'parent_id' => '1',
- 'module_name' => 'Magento_Module1',
+ 'module' => 'Magento_Module1',
'sort_index' => '50',
- 'depends_on_config' => null,
+ 'dependsOnConfig' => null,
'id' => '5',
'resource' => null,
'path' => null,
'action' => null,
- 'depends_on_module' => null,
- 'tooltip' => null,
+ 'dependsOnModule' => null,
+ 'toolTip' => null,
'title' => null,
'sub_menu' => [
'id' => 'item',
'title' => 'Item Title',
'action' => '/system/config',
'resource' => 'Magento_Config::config',
- 'depends_on_module' => 'Magento_Backend',
- 'depends_on_config' => 'system/config/isEnabled',
- 'tooltip' => 'Item tooltip',
+ 'dependsOnModule' => 'Magento_Backend',
+ 'dependsOnConfig' => 'system/config/isEnabled',
+ 'toolTip' => 'Item tooltip',
],
],
[
'parent_id' => '1',
- 'module_name' => 'Magento_Module1',
+ 'module' => 'Magento_Module1',
'sort_index' => '50',
'sub_menu' => [
'id' => 'item',
'title' => 'Item Title',
'action' => '/system/config',
'resource' => 'Magento_Config::config',
- 'depends_on_module' => 'Magento_Backend',
- 'depends_on_config' => 'system/config/isEnabled',
- 'tooltip' => 'Item tooltip',
+ 'dependsOnModule' => 'Magento_Backend',
+ 'dependsOnConfig' => 'system/config/isEnabled',
+ 'toolTip' => 'Item tooltip',
],
],
[
'parent_id' => '1',
- 'module_name' => 'Magento_Module1',
+ 'module' => 'Magento_Module1',
'sort_index' => '50',
- 'depends_on_config' => null,
+ 'dependsOnConfig' => null,
'id' => null,
'resource' => null,
'path' => '',
'action' => null,
- 'depends_on_module' => null,
- 'tooltip' => '',
+ 'dependsOnModule' => null,
+ 'toolTip' => '',
'title' => null,
'sub_menu' => ['submenuArray'],
'target' => null
diff --git a/app/code/Magento/Backend/Test/Unit/Model/_files/menu_item_data.php b/app/code/Magento/Backend/Test/Unit/Model/_files/menu_item_data.php
index 30a43b0158ae3..b1a310d7d440b 100644
--- a/app/code/Magento/Backend/Test/Unit/Model/_files/menu_item_data.php
+++ b/app/code/Magento/Backend/Test/Unit/Model/_files/menu_item_data.php
@@ -11,22 +11,22 @@
'title' => 'Item Title',
'action' => '/system/config',
'resource' => 'Magento_Config::config',
- 'depends_on_module' => 'Magento_Backend',
- 'depends_on_config' => 'system/config/isEnabled',
- 'tooltip' => 'Item tooltip',
+ 'dependsOnModule' => 'Magento_Backend',
+ 'dependsOnConfig' => 'system/config/isEnabled',
+ 'toolTip' => 'Item tooltip',
'sub_menu' => null,
],
[
'parent_id' => null,
- 'module_name' => 'Magento_Backend',
+ 'module' => 'Magento_Backend',
'sort_index' => null,
- 'depends_on_config' => 'system/config/isEnabled',
+ 'dependsOnConfig' => 'system/config/isEnabled',
'id' => 'item',
'resource' => 'Magento_Config::config',
'path' => '',
'action' => '/system/config',
- 'depends_on_module' => 'Magento_Backend',
- 'tooltip' => 'Item tooltip',
+ 'dependsOnModule' => 'Magento_Backend',
+ 'toolTip' => 'Item tooltip',
'title' => 'Item Title',
'sub_menu' => null,
'target' => null
@@ -35,46 +35,46 @@
'with submenu' => [
[
'parent_id' => '1',
- 'module_name' => 'Magento_Module1',
+ 'module' => 'Magento_Module1',
'sort_index' => '50',
- 'depends_on_config' => null,
+ 'dependsOnConfig' => null,
'id' => '5',
'resource' => null,
'path' => null,
'action' => null,
- 'depends_on_module' => null,
- 'tooltip' => null,
+ 'dependsOnModule' => null,
+ 'toolTip' => null,
'title' => null,
'sub_menu' => [
'id' => 'item',
'title' => 'Item Title',
'action' => '/system/config',
'resource' => 'Magento_Config::config',
- 'depends_on_module' => 'Magento_Backend',
- 'depends_on_config' => 'system/config/isEnabled',
- 'tooltip' => 'Item tooltip',
+ 'dependsOnModule' => 'Magento_Backend',
+ 'dependsOnConfig' => 'system/config/isEnabled',
+ 'toolTip' => 'Item tooltip',
],
],
[
'parent_id' => '1',
- 'module_name' => 'Magento_Module1',
+ 'module' => 'Magento_Module1',
'sort_index' => '50',
- 'depends_on_config' => null,
+ 'dependsOnConfig' => null,
'id' => '5',
'resource' => null,
'path' => null,
'action' => null,
- 'depends_on_module' => null,
- 'tooltip' => '',
+ 'dependsOnModule' => null,
+ 'toolTip' => '',
'title' => null,
'sub_menu' => [
'id' => 'item',
'title' => 'Item Title',
'action' => '/system/config',
'resource' => 'Magento_Config::config',
- 'depends_on_module' => 'Magento_Backend',
- 'depends_on_config' => 'system/config/isEnabled',
- 'tooltip' => 'Item tooltip',
+ 'dependsOnModule' => 'Magento_Backend',
+ 'dependsOnConfig' => 'system/config/isEnabled',
+ 'toolTip' => 'Item tooltip',
],
'target' => null
]
@@ -82,38 +82,38 @@
'small set of data' => [
[
'parent_id' => '1',
- 'module_name' => 'Magento_Module1',
+ 'module' => 'Magento_Module1',
'sort_index' => '50',
'sub_menu' => [
'id' => 'item',
'title' => 'Item Title',
'action' => '/system/config',
'resource' => 'Magento_Config::config',
- 'depends_on_module' => 'Magento_Backend',
- 'depends_on_config' => 'system/config/isEnabled',
- 'tooltip' => 'Item tooltip',
+ 'dependsOnModule' => 'Magento_Backend',
+ 'dependsOnConfig' => 'system/config/isEnabled',
+ 'toolTip' => 'Item tooltip',
],
],
[
'parent_id' => '1',
- 'module_name' => 'Magento_Module1',
+ 'module' => 'Magento_Module1',
'sort_index' => '50',
- 'depends_on_config' => null,
+ 'dependsOnConfig' => null,
'id' => null,
'resource' => null,
'path' => '',
'action' => null,
- 'depends_on_module' => null,
- 'tooltip' => '',
+ 'dependsOnModule' => null,
+ 'toolTip' => '',
'title' => null,
'sub_menu' => [
'id' => 'item',
'title' => 'Item Title',
'action' => '/system/config',
'resource' => 'Magento_Config::config',
- 'depends_on_module' => 'Magento_Backend',
- 'depends_on_config' => 'system/config/isEnabled',
- 'tooltip' => 'Item tooltip',
+ 'dependsOnModule' => 'Magento_Backend',
+ 'dependsOnConfig' => 'system/config/isEnabled',
+ 'toolTip' => 'Item tooltip',
],
'target' => null
]
diff --git a/app/code/Magento/Backend/composer.json b/app/code/Magento/Backend/composer.json
index d3c94c1e286e0..845bc4ec87402 100644
--- a/app/code/Magento/Backend/composer.json
+++ b/app/code/Magento/Backend/composer.json
@@ -2,29 +2,29 @@
"name": "magento/module-backend",
"description": "N/A",
"require": {
- "php": "7.0.2|7.0.4|~7.0.6|~7.1.0",
+ "php": "~7.0.13|~7.1.0",
"magento/module-store": "100.2.*",
"magento/module-directory": "100.2.*",
"magento/module-developer": "100.2.*",
- "magento/module-eav": "100.2.*",
+ "magento/module-eav": "101.0.*",
"magento/module-reports": "100.2.*",
- "magento/module-sales": "100.2.*",
- "magento/module-quote": "100.2.*",
- "magento/module-catalog": "101.1.*",
- "magento/module-user": "100.2.*",
+ "magento/module-sales": "101.0.*",
+ "magento/module-quote": "101.0.*",
+ "magento/module-catalog": "102.0.*",
+ "magento/module-user": "101.0.*",
"magento/module-security": "100.2.*",
"magento/module-backup": "100.2.*",
- "magento/module-customer": "100.2.*",
+ "magento/module-customer": "101.0.*",
"magento/module-translation": "100.2.*",
"magento/module-require-js": "100.2.*",
- "magento/module-config": "100.2.*",
- "magento/framework": "100.2.*"
+ "magento/module-config": "101.0.*",
+ "magento/framework": "101.0.*"
},
"suggest": {
"magento/module-theme": "100.2.*"
},
"type": "magento2-module",
- "version": "100.2.0-dev",
+ "version": "100.2.6",
"license": [
"OSL-3.0",
"AFL-3.0"
diff --git a/app/code/Magento/Backend/etc/acl.xml b/app/code/Magento/Backend/etc/acl.xml
index af4ab5856e94c..cf9471e75bed9 100644
--- a/app/code/Magento/Backend/etc/acl.xml
+++ b/app/code/Magento/Backend/etc/acl.xml
@@ -38,7 +38,21 @@
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Backend/etc/adminhtml/di.xml b/app/code/Magento/Backend/etc/adminhtml/di.xml
index 5154c4eb56c91..d6c3273a9a67f 100644
--- a/app/code/Magento/Backend/etc/adminhtml/di.xml
+++ b/app/code/Magento/Backend/etc/adminhtml/di.xml
@@ -139,12 +139,39 @@
-
+
- Magento\Config\Model\Config\Structure\ElementVisibilityInterface::HIDDEN
+
+
+
+
+
+
- Magento\Config\Model\Config\Structure\ElementVisibilityInterface::DISABLED
+
+
+ Magento\Backend\Block\Template
+
+
+
+
+
+ - Magento_Catalog::products
+ - Magento_Sales::sales_order
+ - Magento_Customer::manage
+ - Magento_Cms::page
+
+
+ - catalog/product/index/
+ - sales/order/index/
+ - customer/index/index
+ - cms/page/index/
+
+
+
diff --git a/app/code/Magento/Backend/etc/adminhtml/system.xml b/app/code/Magento/Backend/etc/adminhtml/system.xml
index 27fd16cc920dc..ecf11ac601861 100644
--- a/app/code/Magento/Backend/etc/adminhtml/system.xml
+++ b/app/code/Magento/Backend/etc/adminhtml/system.xml
@@ -120,7 +120,7 @@
Magento\Config\Model\Config\Source\Yesno
- Add Block Names to Hints
+ Add Block Class Type to Hints
Magento\Config\Model\Config\Source\Yesno
@@ -181,7 +181,7 @@
Image Processing Settings
-
+
Image Adapter
Magento\Config\Model\Config\Source\Image\Adapter
Magento\Config\Model\Config\Backend\Image\Adapter
@@ -215,9 +215,10 @@
European Union Countries
Magento\Directory\Model\Config\Source\Country
-
+
Top destinations
Magento\Directory\Model\Config\Source\Country
+ 1
@@ -298,11 +299,11 @@
Disable Email Communications
Magento\Config\Model\Config\Source\Yesno
-
+
Host
For Windows server only.
-
+
Port (25)
For Windows server only.
@@ -319,6 +320,19 @@
+
+ Images Upload Configuration
+
+ Maximum Width
+ validate-greater-than-zero validate-number required-entry
+ Maximum allowed width for uploaded image.
+
+
+ Maximum Height
+ validate-greater-than-zero validate-number required-entry
+ Maximum allowed height for uploaded image.
+
+
Admin
@@ -409,17 +423,17 @@
Web
general
Magento_Config::web
-
+
Url Options
Add Store Code to Urls
Magento\Config\Model\Config\Source\Yesno
Magento\Config\Model\Config\Backend\Store
- Warning! When using Store Code in URLs, in some cases system may not work properly if URLs without Store Codes are specified in the third party services (e.g. PayPal etc.).]]>
+ Warning! When using Store Code in URLs, in some cases system may not work properly if URLs without Store Codes are specified in the third-party services (e.g. PayPal etc.).]]>
-
+
Auto-redirect to Base URL
Magento\Config\Model\Config\Source\Web\Redirect
I.e. redirect from http://example.com/store/ to http://www.example.com/store/
@@ -432,7 +446,7 @@
Magento\Config\Model\Config\Source\Yesno
-
+
Base URLs
Any of the fields allow fully qualified URLs that end with '/' (slash) e.g. http://example.com/magento/
@@ -440,7 +454,7 @@
Magento\Config\Model\Config\Backend\Baseurl
Specify URL or {{base_url}} placeholder.
-
+
Base Link URL
Magento\Config\Model\Config\Backend\Baseurl
May start with {{unsecure_base_url}} placeholder.
@@ -450,13 +464,13 @@
Magento\Config\Model\Config\Backend\Baseurl
May be empty or start with {{unsecure_base_url}} placeholder.
-
+
Base URL for User Media Files
Magento\Config\Model\Config\Backend\Baseurl
May be empty or start with {{unsecure_base_url}} placeholder.
-
+
Base URLs (Secure)
Any of the fields allow fully qualified URLs that end with '/' (slash) e.g. https://example.com/magento/
@@ -464,7 +478,7 @@
Magento\Config\Model\Config\Backend\Baseurl
Specify URL or {{base_url}}, or {{unsecure_base_url}} placeholder.
-
+
Secure Base Link URL
Magento\Config\Model\Config\Backend\Baseurl
May start with {{secure_base_url}} or {{unsecure_base_url}} placeholder.
@@ -474,24 +488,24 @@
Magento\Config\Model\Config\Backend\Baseurl
May be empty or start with {{secure_base_url}}, or {{unsecure_base_url}} placeholder.
-
+
Secure Base URL for User Media Files
Magento\Config\Model\Config\Backend\Baseurl
May be empty or start with {{secure_base_url}}, or {{unsecure_base_url}} placeholder.
-
+
Use Secure URLs on Storefront
Magento\Config\Model\Config\Source\Yesno
Magento\Config\Model\Config\Backend\Secure
Enter https protocol to use Secure URLs on Storefront.
-
+
Use Secure URLs in Admin
Magento\Config\Model\Config\Source\Yesno
Magento\Config\Model\Config\Backend\Secure
Enter https protocol to use Secure URLs in Admin.
-
+
Enable HTTP Strict Transport Security (HSTS)
Magento\Config\Model\Config\Source\Yesno
Magento\Config\Model\Config\Backend\Secure
@@ -501,7 +515,7 @@
1
-
+
Upgrade Insecure Requests
Magento\Config\Model\Config\Source\Yesno
Magento\Config\Model\Config\Backend\Secure
diff --git a/app/code/Magento/Backend/etc/config.xml b/app/code/Magento/Backend/etc/config.xml
index b7aaf8bf20dba..45d283ad3ff22 100644
--- a/app/code/Magento/Backend/etc/config.xml
+++ b/app/code/Magento/Backend/etc/config.xml
@@ -28,6 +28,10 @@
1
+
+ 1920
+ 1200
+
diff --git a/app/code/Magento/Backend/etc/module.xml b/app/code/Magento/Backend/etc/module.xml
index 57e00489391f2..8e79c16a73d2d 100644
--- a/app/code/Magento/Backend/etc/module.xml
+++ b/app/code/Magento/Backend/etc/module.xml
@@ -9,6 +9,7 @@
+
diff --git a/app/code/Magento/Backend/i18n/en_US.csv b/app/code/Magento/Backend/i18n/en_US.csv
index 2730d4d92835b..ac3c36e0e8f82 100644
--- a/app/code/Magento/Backend/i18n/en_US.csv
+++ b/app/code/Magento/Backend/i18n/en_US.csv
@@ -214,10 +214,13 @@ YTD,YTD
"Admin session lifetime must be greater than or equal to 60 seconds","Admin session lifetime must be greater than or equal to 60 seconds"
Order,Order
"Order #%1","Order #%1"
-"Access denied","Access denied"
-"Please try to sign out and sign in again.","Please try to sign out and sign in again."
-"If you continue to receive this message, please contact the store owner.","If you continue to receive this message, please contact the store owner."
"You need more permissions to access this.","You need more permissions to access this."
+"Sorry, you need permissions to view this content.","Sorry, you need permissions to view this content."
+"Next steps","Next steps"
+"If you think this is an error, try signing out and signing in again.","If you think this is an error, try signing out and signing in again."
+"Contact a system administrator or store owner to gain permissions.","Contact a system administrator or store owner to gain permissions."
+"Return to","Return to"
+"previous page","previous page"
"Welcome, please sign in","Welcome, please sign in"
Username,Username
"user name","user name"
@@ -402,9 +405,9 @@ Web,Web
"Url Options","Url Options"
"Add Store Code to Urls","Add Store Code to Urls"
"
- Warning! When using Store Code in URLs, in some cases system may not work properly if URLs without Store Codes are specified in the third party services (e.g. PayPal etc.).
+ Warning! When using Store Code in URLs, in some cases system may not work properly if URLs without Store Codes are specified in the third-party services (e.g. PayPal etc.).
","
- Warning! When using Store Code in URLs, in some cases system may not work properly if URLs without Store Codes are specified in the third party services (e.g. PayPal etc.).
+ Warning! When using Store Code in URLs, in some cases system may not work properly if URLs without Store Codes are specified in the third-party services (e.g. PayPal etc.).
"
"Auto-redirect to Base URL","Auto-redirect to Base URL"
"Search Engine Optimization","Search Engine Optimization"
@@ -444,7 +447,7 @@ Tags,Tags
"404 Error Page not found.
","404 Error Page not found.
"
"Community Edition","Community Edition"
"Default Theme","Default Theme"
-"If no value is specified, the system default is used. The system default may be modified by third party extensions.","If no value is specified, the system default is used. The system default may be modified by third party extensions."
+"If no value is specified, the system default is used. The system default may be modified by third-party extensions.","If no value is specified, the system default is used. The system default may be modified by third-party extensions."
"Applied Theme","Applied Theme"
"Design Rule","Design Rule"
"User Agent Rules","User Agent Rules"
@@ -458,3 +461,7 @@ Pagination,Pagination
"Alternative text for the next pages link in the pagination menu. If empty, default arrow image is used.","Alternative text for the next pages link in the pagination menu. If empty, default arrow image is used."
"Anchor Text for Next","Anchor Text for Next"
"Theme Name","Theme Name"
+"In Products","In Products"
+"In Orders","In Orders"
+"In Customers","In Customers"
+"In Pages","In Pages"
diff --git a/app/code/Magento/Backend/view/adminhtml/layout/adminhtml_cache_block.xml b/app/code/Magento/Backend/view/adminhtml/layout/adminhtml_cache_block.xml
index f6a93fbd84099..50d210f71025b 100644
--- a/app/code/Magento/Backend/view/adminhtml/layout/adminhtml_cache_block.xml
+++ b/app/code/Magento/Backend/view/adminhtml/layout/adminhtml_cache_block.xml
@@ -42,7 +42,7 @@
0
-
+
Cache Type
cache_type
@@ -53,7 +53,7 @@
true
-
+
Description
description
@@ -63,7 +63,7 @@
true
-
+
Tags
tags
@@ -73,7 +73,7 @@
0
-
+
Status
status
diff --git a/app/code/Magento/Backend/view/adminhtml/layout/adminhtml_cache_index.xml b/app/code/Magento/Backend/view/adminhtml/layout/adminhtml_cache_index.xml
index ab5ddc414b51f..4bbe70b6cdb92 100644
--- a/app/code/Magento/Backend/view/adminhtml/layout/adminhtml_cache_index.xml
+++ b/app/code/Magento/Backend/view/adminhtml/layout/adminhtml_cache_index.xml
@@ -11,7 +11,11 @@
-
+
+
+ Magento\Backend\Block\Cache\Permissions
+
+
diff --git a/app/code/Magento/Backend/view/adminhtml/layout/adminhtml_system_design_grid_block.xml b/app/code/Magento/Backend/view/adminhtml/layout/adminhtml_system_design_grid_block.xml
index 52d88ce717043..b96614f4bd8db 100644
--- a/app/code/Magento/Backend/view/adminhtml/layout/adminhtml_system_design_grid_block.xml
+++ b/app/code/Magento/Backend/view/adminhtml/layout/adminhtml_system_design_grid_block.xml
@@ -28,7 +28,7 @@
-
+
Store
100px
@@ -38,7 +38,7 @@
store_id
-
+
Design
options
@@ -47,7 +47,7 @@
design
-
+
Date From
left
@@ -57,7 +57,7 @@
date_from
-
+
Date To
left
diff --git a/app/code/Magento/Backend/view/adminhtml/layout/adminhtml_system_store_grid_block.xml b/app/code/Magento/Backend/view/adminhtml/layout/adminhtml_system_store_grid_block.xml
index 8dcb6e07b3c4e..126de5eb4084f 100644
--- a/app/code/Magento/Backend/view/adminhtml/layout/adminhtml_system_store_grid_block.xml
+++ b/app/code/Magento/Backend/view/adminhtml/layout/adminhtml_system_store_grid_block.xml
@@ -18,7 +18,7 @@
storeGrid
-
+
Web Site
left
@@ -27,7 +27,7 @@
Magento\Backend\Block\System\Store\Grid\Render\Website
-
+
Store
left
@@ -36,7 +36,7 @@
Magento\Backend\Block\System\Store\Grid\Render\Group
-
+
Store View
left
diff --git a/app/code/Magento/Backend/view/adminhtml/templates/admin/access_denied.phtml b/app/code/Magento/Backend/view/adminhtml/templates/admin/access_denied.phtml
index 852ecd5a07962..843328fbf17d7 100644
--- a/app/code/Magento/Backend/view/adminhtml/templates/admin/access_denied.phtml
+++ b/app/code/Magento/Backend/view/adminhtml/templates/admin/access_denied.phtml
@@ -12,12 +12,23 @@
* @see \Magento\Backend\Block\Denied
*/
?>
-= /* @escapeNotVerified */ __('Access denied') ?>
-hasAvailableResources()): ?>
-
-= /* @escapeNotVerified */ __('Please try to sign out and sign in again.') ?>
-= /* @escapeNotVerified */ __('If you continue to receive this message, please contact the store owner.') ?>
-
-
-= /* @escapeNotVerified */ __('You need more permissions to access this.') ?>
-
+
+
+
= $block->escapeHtml(__('Sorry, you need permissions to view this content.')) ?>
+
= $block->escapeHtml(__('Next steps')) ?>
+
+
diff --git a/app/code/Magento/Backend/view/adminhtml/templates/admin/login.phtml b/app/code/Magento/Backend/view/adminhtml/templates/admin/login.phtml
index 805e9783f3f18..52d5dd6d114ee 100644
--- a/app/code/Magento/Backend/view/adminhtml/templates/admin/login.phtml
+++ b/app/code/Magento/Backend/view/adminhtml/templates/admin/login.phtml
@@ -43,7 +43,7 @@
data-validate="{required:true}"
value=""
placeholder="= /* @escapeNotVerified */ __('password') ?>"
- autocomplete="new-password"
+ autocomplete="off"
/>
diff --git a/app/code/Magento/Backend/view/adminhtml/templates/media/uploader.phtml b/app/code/Magento/Backend/view/adminhtml/templates/media/uploader.phtml
index 1e14dd837634a..966372773f295 100644
--- a/app/code/Magento/Backend/view/adminhtml/templates/media/uploader.phtml
+++ b/app/code/Magento/Backend/view/adminhtml/templates/media/uploader.phtml
@@ -13,8 +13,8 @@
data-mage-init='{
"Magento_Backend/js/media-uploader" : {
"maxFileSize": = /* @escapeNotVerified */ $block->getFileSizeService()->getMaxFileSize() ?>,
- "maxWidth":= /* @escapeNotVerified */ \Magento\Framework\File\Uploader::MAX_IMAGE_WIDTH ?> ,
- "maxHeight": = /* @escapeNotVerified */ \Magento\Framework\File\Uploader::MAX_IMAGE_HEIGHT ?>
+ "maxWidth":= /* @escapeNotVerified */ $block->getImageUploadMaxWidth() ?> ,
+ "maxHeight": = /* @escapeNotVerified */ $block->getImageUploadMaxHeight() ?>
}
}'
>
diff --git a/app/code/Magento/Backend/view/adminhtml/templates/page/header.phtml b/app/code/Magento/Backend/view/adminhtml/templates/page/header.phtml
index 40b7173f47417..f952001f5e2ff 100644
--- a/app/code/Magento/Backend/view/adminhtml/templates/page/header.phtml
+++ b/app/code/Magento/Backend/view/adminhtml/templates/page/header.phtml
@@ -17,7 +17,7 @@
= /* @escapeNotVerified */ $edition ?>
class="logo">
+ alt="= $block->escapeHtml(__('Magento Admin Panel')) ?>" title="= $block->escapeHtml(__('Magento Admin Panel')) ?>"/>
@@ -29,7 +29,7 @@
data-mage-init='{"dropdown":{}}'
data-toggle="dropdown">
- = $block->escapeHtml($block->getUser()->getUsername()) ?>
+ = $block->escapeHtml($block->getUser()->getUserName()) ?>
-
= /* @escapeNotVerified */ __('Next page') ?>
diff --git a/app/code/Magento/Backend/view/adminhtml/templates/widget/grid/extended.phtml b/app/code/Magento/Backend/view/adminhtml/templates/widget/grid/extended.phtml
index a31bf4d23abaa..f97db4ad993b1 100644
--- a/app/code/Magento/Backend/view/adminhtml/templates/widget/grid/extended.phtml
+++ b/app/code/Magento/Backend/view/adminhtml/templates/widget/grid/extended.phtml
@@ -72,7 +72,7 @@ $numColumns = sizeof($block->getColumns());
getPagerVisibility()): ?>
diff --git a/app/code/Magento/Bundle/Block/Adminhtml/Catalog/Product/Composite/Fieldset/Options/Type/Checkbox.php b/app/code/Magento/Bundle/Block/Adminhtml/Catalog/Product/Composite/Fieldset/Options/Type/Checkbox.php
index 0b3a938255de1..b220e2c98d77c 100644
--- a/app/code/Magento/Bundle/Block/Adminhtml/Catalog/Product/Composite/Fieldset/Options/Type/Checkbox.php
+++ b/app/code/Magento/Bundle/Block/Adminhtml/Catalog/Product/Composite/Fieldset/Options/Type/Checkbox.php
@@ -17,7 +17,7 @@ class Checkbox extends \Magento\Bundle\Block\Catalog\Product\View\Type\Bundle\Op
/**
* @var string
*/
- protected $_template = 'product/composite/fieldset/options/type/checkbox.phtml';
+ protected $_template = 'Magento_Bundle::product/composite/fieldset/options/type/checkbox.phtml';
/**
* @param string $elementId
diff --git a/app/code/Magento/Bundle/Block/Adminhtml/Catalog/Product/Composite/Fieldset/Options/Type/Multi.php b/app/code/Magento/Bundle/Block/Adminhtml/Catalog/Product/Composite/Fieldset/Options/Type/Multi.php
index 304b3a5cf34ed..a4b8c6bde73aa 100644
--- a/app/code/Magento/Bundle/Block/Adminhtml/Catalog/Product/Composite/Fieldset/Options/Type/Multi.php
+++ b/app/code/Magento/Bundle/Block/Adminhtml/Catalog/Product/Composite/Fieldset/Options/Type/Multi.php
@@ -17,7 +17,7 @@ class Multi extends \Magento\Bundle\Block\Catalog\Product\View\Type\Bundle\Optio
/**
* @var string
*/
- protected $_template = 'product/composite/fieldset/options/type/multi.phtml';
+ protected $_template = 'Magento_Bundle::product/composite/fieldset/options/type/multi.phtml';
/**
* @param string $elementId
diff --git a/app/code/Magento/Bundle/Block/Adminhtml/Catalog/Product/Composite/Fieldset/Options/Type/Radio.php b/app/code/Magento/Bundle/Block/Adminhtml/Catalog/Product/Composite/Fieldset/Options/Type/Radio.php
index e011ab36e8029..1519b3a67ac97 100644
--- a/app/code/Magento/Bundle/Block/Adminhtml/Catalog/Product/Composite/Fieldset/Options/Type/Radio.php
+++ b/app/code/Magento/Bundle/Block/Adminhtml/Catalog/Product/Composite/Fieldset/Options/Type/Radio.php
@@ -17,7 +17,7 @@ class Radio extends \Magento\Bundle\Block\Catalog\Product\View\Type\Bundle\Optio
/**
* @var string
*/
- protected $_template = 'product/composite/fieldset/options/type/radio.phtml';
+ protected $_template = 'Magento_Bundle::product/composite/fieldset/options/type/radio.phtml';
/**
* @param string $elementId
diff --git a/app/code/Magento/Bundle/Block/Adminhtml/Catalog/Product/Composite/Fieldset/Options/Type/Select.php b/app/code/Magento/Bundle/Block/Adminhtml/Catalog/Product/Composite/Fieldset/Options/Type/Select.php
index f1206db359b5c..502dfa32044a3 100644
--- a/app/code/Magento/Bundle/Block/Adminhtml/Catalog/Product/Composite/Fieldset/Options/Type/Select.php
+++ b/app/code/Magento/Bundle/Block/Adminhtml/Catalog/Product/Composite/Fieldset/Options/Type/Select.php
@@ -17,7 +17,7 @@ class Select extends \Magento\Bundle\Block\Catalog\Product\View\Type\Bundle\Opti
/**
* @var string
*/
- protected $_template = 'product/composite/fieldset/options/type/select.phtml';
+ protected $_template = 'Magento_Bundle::product/composite/fieldset/options/type/select.phtml';
/**
* @param string $elementId
diff --git a/app/code/Magento/Bundle/Block/Adminhtml/Catalog/Product/Edit/Tab/Attributes/Extend.php b/app/code/Magento/Bundle/Block/Adminhtml/Catalog/Product/Edit/Tab/Attributes/Extend.php
index 15808c9dd170d..0e21e566d5e75 100644
--- a/app/code/Magento/Bundle/Block/Adminhtml/Catalog/Product/Edit/Tab/Attributes/Extend.php
+++ b/app/code/Magento/Bundle/Block/Adminhtml/Catalog/Product/Edit/Tab/Attributes/Extend.php
@@ -142,7 +142,6 @@ public function getExtendedElement($switchAttributeCode)
[
'name' => "product[{$switchAttributeCode}]",
'values' => $this->getOptions(),
- 'value' => $switchAttributeCode,
'class' => 'required-entry next-toinput',
'no_span' => true,
'disabled' => $this->isDisabledField(),
diff --git a/app/code/Magento/Bundle/Block/Adminhtml/Catalog/Product/Edit/Tab/Bundle.php b/app/code/Magento/Bundle/Block/Adminhtml/Catalog/Product/Edit/Tab/Bundle.php
index f124740a766ab..8be512a3e6348 100644
--- a/app/code/Magento/Bundle/Block/Adminhtml/Catalog/Product/Edit/Tab/Bundle.php
+++ b/app/code/Magento/Bundle/Block/Adminhtml/Catalog/Product/Edit/Tab/Bundle.php
@@ -20,7 +20,7 @@ class Bundle extends \Magento\Backend\Block\Widget implements \Magento\Backend\B
/**
* @var string
*/
- protected $_template = 'product/edit/bundle.phtml';
+ protected $_template = 'Magento_Bundle::product/edit/bundle.phtml';
/**
* Core registry
diff --git a/app/code/Magento/Bundle/Block/Adminhtml/Catalog/Product/Edit/Tab/Bundle/Option.php b/app/code/Magento/Bundle/Block/Adminhtml/Catalog/Product/Edit/Tab/Bundle/Option.php
index 13c5dcc81afb3..19da6bc6244e5 100644
--- a/app/code/Magento/Bundle/Block/Adminhtml/Catalog/Product/Edit/Tab/Bundle/Option.php
+++ b/app/code/Magento/Bundle/Block/Adminhtml/Catalog/Product/Edit/Tab/Bundle/Option.php
@@ -26,7 +26,7 @@ class Option extends \Magento\Backend\Block\Widget
/**
* @var string
*/
- protected $_template = 'product/edit/bundle/option.phtml';
+ protected $_template = 'Magento_Bundle::product/edit/bundle/option.phtml';
/**
* Core registry
diff --git a/app/code/Magento/Bundle/Block/Adminhtml/Catalog/Product/Edit/Tab/Bundle/Option/Search.php b/app/code/Magento/Bundle/Block/Adminhtml/Catalog/Product/Edit/Tab/Bundle/Option/Search.php
index 5b73c22b5781a..cf4814d3cd778 100644
--- a/app/code/Magento/Bundle/Block/Adminhtml/Catalog/Product/Edit/Tab/Bundle/Option/Search.php
+++ b/app/code/Magento/Bundle/Block/Adminhtml/Catalog/Product/Edit/Tab/Bundle/Option/Search.php
@@ -15,7 +15,7 @@ class Search extends \Magento\Backend\Block\Widget
/**
* @var string
*/
- protected $_template = 'product/edit/bundle/option/search.phtml';
+ protected $_template = 'Magento_Bundle::product/edit/bundle/option/search.phtml';
/**
* @return void
diff --git a/app/code/Magento/Bundle/Block/Adminhtml/Catalog/Product/Edit/Tab/Bundle/Option/Selection.php b/app/code/Magento/Bundle/Block/Adminhtml/Catalog/Product/Edit/Tab/Bundle/Option/Selection.php
index 353808dc66a72..cf88f9b93d32f 100644
--- a/app/code/Magento/Bundle/Block/Adminhtml/Catalog/Product/Edit/Tab/Bundle/Option/Selection.php
+++ b/app/code/Magento/Bundle/Block/Adminhtml/Catalog/Product/Edit/Tab/Bundle/Option/Selection.php
@@ -15,7 +15,7 @@ class Selection extends \Magento\Backend\Block\Widget
/**
* @var string
*/
- protected $_template = 'product/edit/bundle/option/selection.phtml';
+ protected $_template = 'Magento_Bundle::product/edit/bundle/option/selection.phtml';
/**
* Catalog data
diff --git a/app/code/Magento/Bundle/Block/Adminhtml/Sales/Order/Items/Renderer.php b/app/code/Magento/Bundle/Block/Adminhtml/Sales/Order/Items/Renderer.php
index 4cb087df0e1a6..23fc2026ab111 100644
--- a/app/code/Magento/Bundle/Block/Adminhtml/Sales/Order/Items/Renderer.php
+++ b/app/code/Magento/Bundle/Block/Adminhtml/Sales/Order/Items/Renderer.php
@@ -95,9 +95,8 @@ public function getChildren($item)
if (isset($itemsArray[$item->getOrderItem()->getId()])) {
return $itemsArray[$item->getOrderItem()->getId()];
- } else {
- return null;
}
+ return null;
}
/**
@@ -219,9 +218,8 @@ public function getOrderItem()
{
if ($this->getItem() instanceof \Magento\Sales\Model\Order\Item) {
return $this->getItem();
- } else {
- return $this->getItem()->getOrderItem();
}
+ return $this->getItem()->getOrderItem();
}
/**
diff --git a/app/code/Magento/Bundle/Block/Catalog/Product/View/Type/Bundle.php b/app/code/Magento/Bundle/Block/Catalog/Product/View/Type/Bundle.php
index 6cb103fc86789..fc8706ce54d06 100644
--- a/app/code/Magento/Bundle/Block/Catalog/Product/View/Type/Bundle.php
+++ b/app/code/Magento/Bundle/Block/Catalog/Product/View/Type/Bundle.php
@@ -7,6 +7,7 @@
use Magento\Bundle\Model\Option;
use Magento\Catalog\Model\Product;
+use Magento\Framework\DataObject;
/**
* Catalog bundle product info block
@@ -56,6 +57,11 @@ class Bundle extends \Magento\Catalog\Block\Product\View\AbstractView
*/
private $catalogRuleProcessor;
+ /**
+ * @var array
+ */
+ private $optionsPosition = [];
+
/**
* @param \Magento\Catalog\Block\Product\Context $context
* @param \Magento\Framework\Stdlib\ArrayUtils $arrayUtils
@@ -161,7 +167,7 @@ public function getJsonConfig()
$defaultValues = [];
$preConfiguredFlag = $currentProduct->hasPreconfiguredValues();
- /** @var \Magento\Framework\DataObject|null $preConfiguredValues */
+ /** @var DataObject|null $preConfiguredValues */
$preConfiguredValues = $preConfiguredFlag ? $currentProduct->getPreconfiguredValues() : null;
$position = 0;
@@ -172,6 +178,7 @@ public function getJsonConfig()
}
$optionId = $optionItem->getId();
$options[$optionId] = $this->getOptionItemData($optionItem, $currentProduct, $position);
+ $this->optionsPosition[$position] = $optionId;
// Add attribute default value (if set)
if ($preConfiguredFlag) {
@@ -179,12 +186,13 @@ public function getJsonConfig()
if ($configValue) {
$defaultValues[$optionId] = $configValue;
}
+ $options = $this->processOptions($optionId, $options, $preConfiguredValues);
}
$position++;
}
$config = $this->getConfigData($currentProduct, $options);
- $configObj = new \Magento\Framework\DataObject(
+ $configObj = new DataObject(
[
'config' => $config,
]
@@ -228,18 +236,23 @@ private function getSelectionItemData(Product $product, Product $selection)
$qty = ($selection->getSelectionQty() * 1) ?: '1';
$optionPriceAmount = $product->getPriceInfo()
- ->getPrice('bundle_option')
+ ->getPrice(\Magento\Bundle\Pricing\Price\BundleOptionPrice::PRICE_CODE)
->getOptionSelectionAmount($selection);
$finalPrice = $optionPriceAmount->getValue();
$basePrice = $optionPriceAmount->getBaseAmount();
+ $oldPrice = $product->getPriceInfo()
+ ->getPrice(\Magento\Bundle\Pricing\Price\BundleOptionRegularPrice::PRICE_CODE)
+ ->getOptionSelectionAmount($selection)
+ ->getValue();
+
$selection = [
'qty' => $qty,
'customQty' => $selection->getSelectionCanChangeQty(),
'optionId' => $selection->getId(),
'prices' => [
'oldPrice' => [
- 'amount' => $basePrice
+ 'amount' => $oldPrice
],
'basePrice' => [
'amount' => $basePrice
@@ -363,6 +376,7 @@ private function getConfigData(Product $product, array $options)
$config = [
'options' => $options,
'selected' => $this->selectedOptions,
+ 'positions' => $this->optionsPosition,
'bundleId' => $product->getId(),
'priceFormat' => $this->localeFormat->getPriceFormat(),
'prices' => [
@@ -381,4 +395,30 @@ private function getConfigData(Product $product, array $options)
];
return $config;
}
+
+ /**
+ * Set preconfigured quantities and selections to options.
+ *
+ * @param string $optionId
+ * @param array $options
+ * @param DataObject $preConfiguredValues
+ * @return array
+ */
+ private function processOptions(string $optionId, array $options, DataObject $preConfiguredValues)
+ {
+ $preConfiguredQtys = $preConfiguredValues->getData("bundle_option_qty/${optionId}") ?? [];
+ $selections = $options[$optionId]['selections'];
+ array_walk($selections, function (&$selection, $selectionId) use ($preConfiguredQtys) {
+ if (is_array($preConfiguredQtys) && isset($preConfiguredQtys[$selectionId])) {
+ $selection['qty'] = $preConfiguredQtys[$selectionId];
+ } else {
+ if ((int)$preConfiguredQtys > 0) {
+ $selection['qty'] = $preConfiguredQtys;
+ }
+ }
+ });
+ $options[$optionId]['selections'] = $selections;
+
+ return $options;
+ }
}
diff --git a/app/code/Magento/Bundle/Block/Catalog/Product/View/Type/Bundle/Option.php b/app/code/Magento/Bundle/Block/Catalog/Product/View/Type/Bundle/Option.php
index dd62676612250..22849bcb20276 100644
--- a/app/code/Magento/Bundle/Block/Catalog/Product/View/Type/Bundle/Option.php
+++ b/app/code/Magento/Bundle/Block/Catalog/Product/View/Type/Bundle/Option.php
@@ -169,7 +169,9 @@ protected function _getSelectedOptions()
*/
protected function assignSelection(\Magento\Bundle\Model\Option $option, $selectionId)
{
- if ($selectionId && $option->getSelectionById($selectionId)) {
+ if (is_array($selectionId)) {
+ $this->_selectedOptions = $selectionId;
+ } else if ($selectionId && $option->getSelectionById($selectionId)) {
$this->_selectedOptions = $selectionId;
} elseif (!$option->getRequired()) {
$this->_selectedOptions = 'None';
@@ -191,9 +193,8 @@ public function isSelected($selection)
return in_array($selection->getSelectionId(), $selectedOptions);
} elseif ($selectedOptions == 'None') {
return false;
- } else {
- return $selection->getIsDefault() && $selection->isSaleable();
}
+ return $selection->getIsDefault() && $selection->isSaleable();
}
/**
diff --git a/app/code/Magento/Bundle/Block/Catalog/Product/View/Type/Bundle/Option/Checkbox.php b/app/code/Magento/Bundle/Block/Catalog/Product/View/Type/Bundle/Option/Checkbox.php
index 8ca0cf8a5159e..83730d4eae2bd 100644
--- a/app/code/Magento/Bundle/Block/Catalog/Product/View/Type/Bundle/Option/Checkbox.php
+++ b/app/code/Magento/Bundle/Block/Catalog/Product/View/Type/Bundle/Option/Checkbox.php
@@ -16,5 +16,5 @@ class Checkbox extends \Magento\Bundle\Block\Catalog\Product\View\Type\Bundle\Op
/**
* @var string
*/
- protected $_template = 'catalog/product/view/type/bundle/option/checkbox.phtml';
+ protected $_template = 'Magento_Bundle::catalog/product/view/type/bundle/option/checkbox.phtml';
}
diff --git a/app/code/Magento/Bundle/Block/Catalog/Product/View/Type/Bundle/Option/Multi.php b/app/code/Magento/Bundle/Block/Catalog/Product/View/Type/Bundle/Option/Multi.php
index 3319db8cff1d5..79e94a18a789e 100644
--- a/app/code/Magento/Bundle/Block/Catalog/Product/View/Type/Bundle/Option/Multi.php
+++ b/app/code/Magento/Bundle/Block/Catalog/Product/View/Type/Bundle/Option/Multi.php
@@ -16,7 +16,7 @@ class Multi extends \Magento\Bundle\Block\Catalog\Product\View\Type\Bundle\Optio
/**
* @var string
*/
- protected $_template = 'catalog/product/view/type/bundle/option/multi.phtml';
+ protected $_template = 'Magento_Bundle::catalog/product/view/type/bundle/option/multi.phtml';
/**
* @inheritdoc
diff --git a/app/code/Magento/Bundle/Block/Catalog/Product/View/Type/Bundle/Option/Radio.php b/app/code/Magento/Bundle/Block/Catalog/Product/View/Type/Bundle/Option/Radio.php
index 84a619dafab52..07c113bd8e4bb 100644
--- a/app/code/Magento/Bundle/Block/Catalog/Product/View/Type/Bundle/Option/Radio.php
+++ b/app/code/Magento/Bundle/Block/Catalog/Product/View/Type/Bundle/Option/Radio.php
@@ -16,5 +16,5 @@ class Radio extends \Magento\Bundle\Block\Catalog\Product\View\Type\Bundle\Optio
/**
* @var string
*/
- protected $_template = 'catalog/product/view/type/bundle/option/radio.phtml';
+ protected $_template = 'Magento_Bundle::catalog/product/view/type/bundle/option/radio.phtml';
}
diff --git a/app/code/Magento/Bundle/Block/Catalog/Product/View/Type/Bundle/Option/Select.php b/app/code/Magento/Bundle/Block/Catalog/Product/View/Type/Bundle/Option/Select.php
index d7f1cf41057a8..63f0d35bda0f0 100644
--- a/app/code/Magento/Bundle/Block/Catalog/Product/View/Type/Bundle/Option/Select.php
+++ b/app/code/Magento/Bundle/Block/Catalog/Product/View/Type/Bundle/Option/Select.php
@@ -16,5 +16,5 @@ class Select extends \Magento\Bundle\Block\Catalog\Product\View\Type\Bundle\Opti
/**
* @var string
*/
- protected $_template = 'catalog/product/view/type/bundle/option/select.phtml';
+ protected $_template = 'Magento_Bundle::catalog/product/view/type/bundle/option/select.phtml';
}
diff --git a/app/code/Magento/Bundle/Block/Sales/Order/Items/Renderer.php b/app/code/Magento/Bundle/Block/Sales/Order/Items/Renderer.php
index a29c93fc4e139..003ddba86ad75 100644
--- a/app/code/Magento/Bundle/Block/Sales/Order/Items/Renderer.php
+++ b/app/code/Magento/Bundle/Block/Sales/Order/Items/Renderer.php
@@ -142,9 +142,8 @@ public function getValueHtml($item)
if ($attributes = $this->getSelectionAttributes($item)) {
return sprintf('%d', $attributes['qty']) . ' x ' . $this->escapeHtml($item->getName()) . " "
. $this->getOrder()->formatPrice($attributes['price']);
- } else {
- return $this->escapeHtml($item->getName());
}
+ return $this->escapeHtml($item->getName());
}
/**
@@ -179,9 +178,8 @@ public function getChildren($item)
if (isset($itemsArray[$item->getOrderItem()->getId()])) {
return $itemsArray[$item->getOrderItem()->getId()];
- } else {
- return null;
}
+ return null;
}
/**
diff --git a/app/code/Magento/Bundle/Controller/Adminhtml/Product/Initialization/Helper/Plugin/Bundle.php b/app/code/Magento/Bundle/Controller/Adminhtml/Product/Initialization/Helper/Plugin/Bundle.php
index 6688648a3c4fd..3c9eac68eb9e4 100644
--- a/app/code/Magento/Bundle/Controller/Adminhtml/Product/Initialization/Helper/Plugin/Bundle.php
+++ b/app/code/Magento/Bundle/Controller/Adminhtml/Product/Initialization/Helper/Plugin/Bundle.php
@@ -105,8 +105,13 @@ public function afterInitialize(
if ($result['bundle_options'] && !$compositeReadonly) {
$product->setBundleOptionsData($result['bundle_options']);
}
+
$this->processBundleOptionsData($product);
$this->processDynamicOptionsData($product);
+ } elseif (!$compositeReadonly) {
+ $extension = $product->getExtensionAttributes();
+ $extension->setBundleProductOptions([]);
+ $product->setExtensionAttributes($extension);
}
$affectProductSelections = (bool)$this->request->getPost('affect_bundle_product_selections');
@@ -127,7 +132,7 @@ protected function processBundleOptionsData(\Magento\Catalog\Model\Product $prod
}
$options = [];
foreach ($bundleOptionsData as $key => $optionData) {
- if ((bool)$optionData['delete']) {
+ if (!empty($optionData['delete'])) {
continue;
}
diff --git a/app/code/Magento/Bundle/Model/OptionRepository.php b/app/code/Magento/Bundle/Model/OptionRepository.php
index 43e46ef7f1c89..39fcbdc62102f 100644
--- a/app/code/Magento/Bundle/Model/OptionRepository.php
+++ b/app/code/Magento/Bundle/Model/OptionRepository.php
@@ -208,7 +208,7 @@ public function save(
}
} else {
if (!$existingOption->getOptionId()) {
- throw new NoSuchEntityException('Requested option doesn\'t exist');
+ throw new NoSuchEntityException(__('Requested option doesn\'t exist'));
}
$option->setData(array_merge($existingOption->getData(), $option->getData()));
@@ -277,10 +277,11 @@ protected function updateOptionSelection(
* @param string $sku
* @return \Magento\Catalog\Api\Data\ProductInterface
* @throws \Magento\Framework\Exception\InputException
+ * @throws \Magento\Framework\Exception\NoSuchEntityException
*/
private function getProduct($sku)
{
- $product = $this->productRepository->get($sku, true);
+ $product = $this->productRepository->get($sku, true, null, true);
if ($product->getTypeId() != \Magento\Catalog\Model\Product\Type::TYPE_BUNDLE) {
throw new InputException(__('Only implemented for bundle product'));
}
diff --git a/app/code/Magento/Bundle/Model/Plugin/PriceBackend.php b/app/code/Magento/Bundle/Model/Plugin/PriceBackend.php
index f3c0548f76e5d..1914d5b5146c3 100644
--- a/app/code/Magento/Bundle/Model/Plugin/PriceBackend.php
+++ b/app/code/Magento/Bundle/Model/Plugin/PriceBackend.php
@@ -29,8 +29,7 @@ public function aroundValidate(
&& $object->getPriceType() == \Magento\Bundle\Model\Product\Price::PRICE_TYPE_DYNAMIC
) {
return true;
- } else {
- return $proceed($object);
}
+ return $proceed($object);
}
}
diff --git a/app/code/Magento/Bundle/Model/Product/CopyConstructor/Bundle.php b/app/code/Magento/Bundle/Model/Product/CopyConstructor/Bundle.php
index 61559df4d2cf6..20e4828835d06 100644
--- a/app/code/Magento/Bundle/Model/Product/CopyConstructor/Bundle.php
+++ b/app/code/Magento/Bundle/Model/Product/CopyConstructor/Bundle.php
@@ -27,7 +27,17 @@ public function build(Product $product, Product $duplicate)
$bundleOptions = $product->getExtensionAttributes()->getBundleProductOptions() ?: [];
$duplicatedBundleOptions = [];
foreach ($bundleOptions as $key => $bundleOption) {
- $duplicatedBundleOptions[$key] = clone $bundleOption;
+ $duplicatedBundleOption = clone $bundleOption;
+ /**
+ * Set option and selection ids to 'null' in order to create new option(selection) for duplicated product,
+ * but not modifying existing one, which led to lost of option(selection) in original product.
+ */
+ $productLinks = $duplicatedBundleOption->getProductLinks() ?: [];
+ foreach ($productLinks as $productLink) {
+ $productLink->setSelectionId(null);
+ }
+ $duplicatedBundleOption->setOptionId(null);
+ $duplicatedBundleOptions[$key] = $duplicatedBundleOption;
}
$duplicate->getExtensionAttributes()->setBundleProductOptions($duplicatedBundleOptions);
}
diff --git a/app/code/Magento/Bundle/Model/Product/SaveHandler.php b/app/code/Magento/Bundle/Model/Product/SaveHandler.php
index de11df62cbb05..09fce9222e63d 100644
--- a/app/code/Magento/Bundle/Model/Product/SaveHandler.php
+++ b/app/code/Magento/Bundle/Model/Product/SaveHandler.php
@@ -5,7 +5,6 @@
*/
namespace Magento\Bundle\Model\Product;
-use Magento\Catalog\Api\Data\ProductInterface;
use Magento\Bundle\Api\ProductOptionRepositoryInterface as OptionRepository;
use Magento\Bundle\Api\ProductLinkManagementInterface;
use Magento\Framework\App\ObjectManager;
@@ -53,50 +52,50 @@ public function __construct(
* @param object $entity
* @param array $arguments
* @return \Magento\Catalog\Api\Data\ProductInterface|object
+ * @throws \Magento\Framework\Exception\NoSuchEntityException
+ * @throws \Magento\Framework\Exception\InputException
+ * @throws \Magento\Framework\Exception\CouldNotSaveException
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
*/
public function execute($entity, $arguments = [])
{
/** @var \Magento\Bundle\Api\Data\OptionInterface[] $options */
- $options = $entity->getExtensionAttributes()->getBundleProductOptions() ?: [];
+ $bundleProductOptions = $entity->getExtensionAttributes()->getBundleProductOptions() ?: [];
- if ($entity->getTypeId() !== 'bundle' || empty($options)) {
+ if ($entity->getTypeId() !== Type::TYPE_CODE || empty($bundleProductOptions)) {
return $entity;
}
- if (!$entity->getCopyFromView()) {
- $updatedOptions = [];
- $oldOptions = $this->optionRepository->getList($entity->getSku());
-
- $metadata = $this->metadataPool->getMetadata(ProductInterface::class);
+ $existingBundleProductOptions = $this->optionRepository->getList($entity->getSku());
- $productId = $entity->getData($metadata->getLinkField());
+ $existingOptionsIds = !empty($existingBundleProductOptions)
+ ? $this->getOptionIds($existingBundleProductOptions)
+ : [];
+ $optionIds = !empty($bundleProductOptions)
+ ? $this->getOptionIds($bundleProductOptions)
+ : [];
- foreach ($options as $option) {
- $updatedOptions[$option->getOptionId()][$productId] = (bool)$option->getOptionId();
- }
-
- foreach ($oldOptions as $option) {
- if (!isset($updatedOptions[$option->getOptionId()][$productId])) {
- $option->setParentId($productId);
- $this->removeOptionLinks($entity->getSku(), $option);
- $this->optionRepository->delete($option);
- }
- }
- }
+ $options = $bundleProductOptions ?: [];
- foreach ($options as $option) {
- $this->optionRepository->save($entity, $option);
+ if (!$entity->getCopyFromView()) {
+ $this->processRemovedOptions($entity->getSku(), $existingOptionsIds, $optionIds);
+
+ $newOptionsIds = array_diff($optionIds, $existingOptionsIds);
+ $this->saveOptions($entity, $options, $newOptionsIds);
+ } else {
+ //save only labels and not selections + product links
+ $this->saveOptions($entity, $options);
+ $entity->setCopyFromView(false);
}
- $entity->setCopyFromView(false);
-
return $entity;
}
/**
* @param string $entitySku
* @param \Magento\Bundle\Api\Data\OptionInterface $option
+ * @throws \Magento\Framework\Exception\NoSuchEntityException
+ * @throws \Magento\Framework\Exception\InputException
* @return void
*/
protected function removeOptionLinks($entitySku, $option)
@@ -108,4 +107,67 @@ protected function removeOptionLinks($entitySku, $option)
}
}
}
+
+ /**
+ * Perform save for all options entities
+ *
+ * @param object $entity
+ * @param array $options
+ * @param array $newOptionsIds
+ * @throws \Magento\Framework\Exception\CouldNotSaveException
+ * @throws \Magento\Framework\Exception\InputException
+ * @return void
+ */
+ private function saveOptions($entity, array $options, array $newOptionsIds = [])
+ {
+ foreach ($options as $option) {
+ if (in_array($option->getOptionId(), $newOptionsIds, true)) {
+ $option->setOptionId(null);
+ }
+ $this->optionRepository->save($entity, $option);
+ }
+ }
+
+ /**
+ * Get options ids from array of the options entities
+ *
+ * @param array $options
+ * @return array
+ */
+ private function getOptionIds(array $options)
+ {
+ $optionIds = [];
+
+ if (empty($options)) {
+ return $optionIds;
+ }
+
+ /** @var \Magento\Bundle\Api\Data\OptionInterface $option */
+ foreach ($options as $option) {
+ if ($option->getOptionId()) {
+ $optionIds[] = $option->getOptionId();
+ }
+ }
+ return $optionIds;
+ }
+
+ /**
+ * Removes old options that no longer exists
+ *
+ * @param string $entitySku
+ * @param array $existingOptionsIds
+ * @param array $optionIds
+ * @throws \Magento\Framework\Exception\NoSuchEntityException
+ * @throws \Magento\Framework\Exception\InputException
+ * @throws \Magento\Framework\Exception\CouldNotSaveException
+ * @return void
+ */
+ private function processRemovedOptions($entitySku, array $existingOptionsIds, array $optionIds)
+ {
+ foreach (array_diff($existingOptionsIds, $optionIds) as $optionId) {
+ $option = $this->optionRepository->get($entitySku, $optionId);
+ $this->removeOptionLinks($entitySku, $option);
+ $this->optionRepository->delete($option);
+ }
+ }
}
diff --git a/app/code/Magento/Bundle/Model/Product/Type.php b/app/code/Magento/Bundle/Model/Product/Type.php
index bd261d287273e..afcb09f2f43fe 100644
--- a/app/code/Magento/Bundle/Model/Product/Type.php
+++ b/app/code/Magento/Bundle/Model/Product/Type.php
@@ -8,12 +8,13 @@
namespace Magento\Bundle\Model\Product;
-use Magento\Framework\App\ObjectManager;
+use Magento\Bundle\Model\ResourceModel\Selection\Collection as Selections;
+use Magento\Bundle\Model\ResourceModel\Selection\Collection\FilterApplier as SelectionCollectionFilterApplier;
use Magento\Catalog\Api\ProductRepositoryInterface;
+use Magento\Framework\App\ObjectManager;
+use Magento\Framework\EntityManager\MetadataPool;
use Magento\Framework\Pricing\PriceCurrencyInterface;
use Magento\Framework\Serialize\Serializer\Json;
-use Magento\Framework\EntityManager\MetadataPool;
-use Magento\Bundle\Model\ResourceModel\Selection\Collection\FilterApplier as SelectionCollectionFilterApplier;
/**
* Bundle Type Model
@@ -486,7 +487,9 @@ public function getSelectionsCollection($optionIds, $product)
\Magento\Catalog\Api\Data\ProductInterface::class
);
- $selectionsCollection = $this->_bundleCollection->create()
+ /** @var Selections $selectionsCollection */
+ $selectionsCollection = $this->_bundleCollection->create();
+ $selectionsCollection
->addAttributeToSelect($this->_config->getProductAttributes())
->addAttributeToSelect('tax_class_id') //used for calculation item taxes in Bundle with Dynamic Price
->setFlag('product_children', true)
@@ -533,12 +536,12 @@ public function updateQtyOption($options, \Magento\Framework\DataObject $option,
foreach ($selections as $selection) {
if ($selection->getProductId() == $optionProduct->getId()) {
- foreach ($options as &$option) {
- if ($option->getCode() == 'selection_qty_' . $selection->getSelectionId()) {
+ foreach ($options as $quoteItemOption) {
+ if ($quoteItemOption->getCode() == 'selection_qty_' . $selection->getSelectionId()) {
if ($optionUpdateFlag) {
- $option->setValue(intval($option->getValue()));
+ $quoteItemOption->setValue((int)$quoteItemOption->getValue());
} else {
- $option->setValue($value);
+ $quoteItemOption->setValue($value);
}
}
}
@@ -558,7 +561,7 @@ public function updateQtyOption($options, \Magento\Framework\DataObject $option,
*/
public function prepareQuoteItemQty($qty, $product)
{
- return intval($qty);
+ return (int)$qty;
}
/**
@@ -587,6 +590,7 @@ public function isSalable($product)
foreach ($this->getOptionsCollection($product) as $option) {
$hasSalable = false;
+ /** @var Selections $selectionsCollection */
$selectionsCollection = $this->_bundleCollection->create();
$selectionsCollection->addAttributeToSelect('status');
$selectionsCollection->addQuantityFilter();
@@ -855,8 +859,9 @@ public function getSelectionsByIds($selectionIds, $product)
if (!$usedSelections || $usedSelectionsIds !== $selectionIds) {
$storeId = $product->getStoreId();
- $usedSelections = $this->_bundleCollection
- ->create()
+ /** @var Selections $usedSelections */
+ $usedSelections = $this->_bundleCollection->create();
+ $usedSelections
->addAttributeToSelect('*')
->setFlag('product_children', true)
->addStoreFilter($this->getStoreFilter($product))
@@ -1008,11 +1013,8 @@ public function shakeSelections($firstItem, $secondItem)
$secondItem->getPosition(),
$secondItem->getSelectionId(),
];
- if ($aPosition == $bPosition) {
- return 0;
- } else {
- return $aPosition < $bPosition ? -1 : 1;
- }
+
+ return $aPosition <=> $bPosition;
}
/**
diff --git a/app/code/Magento/Bundle/Model/ResourceModel/Indexer/Price.php b/app/code/Magento/Bundle/Model/ResourceModel/Indexer/Price.php
index 401374db86fef..e42ab1c672604 100644
--- a/app/code/Magento/Bundle/Model/ResourceModel/Indexer/Price.php
+++ b/app/code/Magento/Bundle/Model/ResourceModel/Indexer/Price.php
@@ -6,20 +6,170 @@
namespace Magento\Bundle\Model\ResourceModel\Indexer;
use Magento\Catalog\Api\Data\ProductInterface;
+use Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\BasePriceModifier;
+use Magento\Framework\Indexer\DimensionalIndexerInterface;
+use Magento\Framework\EntityManager\MetadataPool;
+use Magento\Catalog\Model\Indexer\Product\Price\TableMaintainer;
+use Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\IndexTableStructureFactory;
+use Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\IndexTableStructure;
+use Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\Query\JoinAttributeProcessor;
+use Magento\Customer\Model\Indexer\CustomerGroupDimensionProvider;
+use Magento\Store\Model\Indexer\WebsiteDimensionProvider;
+use Magento\Catalog\Model\Product\Attribute\Source\Status;
/**
* Bundle products Price indexer resource model
*
- * @author Magento Core Team
+ * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
*/
-class Price extends \Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\DefaultPrice
+class Price implements DimensionalIndexerInterface
{
/**
- * @inheritdoc
+ * @var IndexTableStructureFactory
*/
- protected function reindex($entityIds = null)
+ private $indexTableStructureFactory;
+
+ /**
+ * @var TableMaintainer
+ */
+ private $tableMaintainer;
+
+ /**
+ * @var MetadataPool
+ */
+ private $metadataPool;
+
+ /**
+ * @var \Magento\Framework\App\ResourceConnection
+ */
+ private $resource;
+
+ /**
+ * @var bool
+ */
+ private $fullReindexAction;
+
+ /**
+ * @var string
+ */
+ private $connectionName;
+
+ /**
+ * @var \Magento\Framework\DB\Adapter\AdapterInterface
+ */
+ private $connection;
+
+ /**
+ * Mapping between dimensions and field in database
+ *
+ * @var array
+ */
+ private $dimensionToFieldMapper = [
+ WebsiteDimensionProvider::DIMENSION_NAME => 'pw.website_id',
+ CustomerGroupDimensionProvider::DIMENSION_NAME => 'cg.customer_group_id',
+ ];
+
+ /**
+ * @var BasePriceModifier
+ */
+ private $basePriceModifier;
+
+ /**
+ * @var JoinAttributeProcessor
+ */
+ private $joinAttributeProcessor;
+
+ /**
+ * @var \Magento\Framework\Event\ManagerInterface
+ */
+ private $eventManager;
+
+ /**
+ * @var \Magento\Framework\Module\Manager
+ */
+ private $moduleManager;
+
+ /**
+ * @param IndexTableStructureFactory $indexTableStructureFactory
+ * @param TableMaintainer $tableMaintainer
+ * @param MetadataPool $metadataPool
+ * @param \Magento\Framework\App\ResourceConnection $resource
+ * @param BasePriceModifier $basePriceModifier
+ * @param JoinAttributeProcessor $joinAttributeProcessor
+ * @param \Magento\Framework\Event\ManagerInterface $eventManager
+ * @param \Magento\Framework\Module\Manager $moduleManager
+ * @param bool $fullReindexAction
+ * @param string $connectionName
+ *
+ * @SuppressWarnings(PHPMD.ExcessiveParameterList)
+ */
+ public function __construct(
+ IndexTableStructureFactory $indexTableStructureFactory,
+ TableMaintainer $tableMaintainer,
+ MetadataPool $metadataPool,
+ \Magento\Framework\App\ResourceConnection $resource,
+ BasePriceModifier $basePriceModifier,
+ JoinAttributeProcessor $joinAttributeProcessor,
+ \Magento\Framework\Event\ManagerInterface $eventManager,
+ \Magento\Framework\Module\Manager $moduleManager,
+ $fullReindexAction = false,
+ $connectionName = 'indexer'
+ ) {
+ $this->indexTableStructureFactory = $indexTableStructureFactory;
+ $this->tableMaintainer = $tableMaintainer;
+ $this->connectionName = $connectionName;
+ $this->metadataPool = $metadataPool;
+ $this->resource = $resource;
+ $this->fullReindexAction = $fullReindexAction;
+ $this->basePriceModifier = $basePriceModifier;
+ $this->joinAttributeProcessor = $joinAttributeProcessor;
+ $this->eventManager = $eventManager;
+ $this->moduleManager = $moduleManager;
+ }
+
+ /**
+ * {@inheritdoc}
+ *
+ * @throws \Exception
+ */
+ public function executeByDimensions(array $dimensions, \Traversable $entityIds)
{
- $this->_prepareBundlePrice($entityIds);
+ $this->tableMaintainer->createMainTmpTable($dimensions);
+
+ $temporaryPriceTable = $this->indexTableStructureFactory->create([
+ 'tableName' => $this->tableMaintainer->getMainTmpTable($dimensions),
+ 'entityField' => 'entity_id',
+ 'customerGroupField' => 'customer_group_id',
+ 'websiteField' => 'website_id',
+ 'taxClassField' => 'tax_class_id',
+ 'originalPriceField' => 'price',
+ 'finalPriceField' => 'final_price',
+ 'minPriceField' => 'min_price',
+ 'maxPriceField' => 'max_price',
+ 'tierPriceField' => 'tier_price',
+ ]);
+
+ $entityIds = iterator_to_array($entityIds);
+
+ $this->prepareTierPriceIndex($dimensions, $entityIds);
+
+ $this->prepareBundlePriceTable();
+
+ $this->prepareBundlePriceByType(
+ \Magento\Bundle\Model\Product\Price::PRICE_TYPE_FIXED,
+ $dimensions,
+ $entityIds
+ );
+
+ $this->prepareBundlePriceByType(
+ \Magento\Bundle\Model\Product\Price::PRICE_TYPE_DYNAMIC,
+ $dimensions,
+ $entityIds
+ );
+
+ $this->calculateBundleOptionPrice($temporaryPriceTable, $dimensions);
+
+ $this->basePriceModifier->modifyPrice($temporaryPriceTable, $entityIds);
}
/**
@@ -27,9 +177,9 @@ protected function reindex($entityIds = null)
*
* @return string
*/
- protected function _getBundlePriceTable()
+ private function getBundlePriceTable()
{
- return $this->tableStrategy->getTableName('catalog_product_index_price_bundle');
+ return $this->getTable('catalog_product_index_price_bundle_tmp');
}
/**
@@ -37,9 +187,9 @@ protected function _getBundlePriceTable()
*
* @return string
*/
- protected function _getBundleSelectionTable()
+ private function getBundleSelectionTable()
{
- return $this->tableStrategy->getTableName('catalog_product_index_price_bundle_sel');
+ return $this->getTable('catalog_product_index_price_bundle_sel_tmp');
}
/**
@@ -47,9 +197,9 @@ protected function _getBundleSelectionTable()
*
* @return string
*/
- protected function _getBundleOptionTable()
+ private function getBundleOptionTable()
{
- return $this->tableStrategy->getTableName('catalog_product_index_price_bundle_opt');
+ return $this->getTable('catalog_product_index_price_bundle_opt_tmp');
}
/**
@@ -57,9 +207,9 @@ protected function _getBundleOptionTable()
*
* @return $this
*/
- protected function _prepareBundlePriceTable()
+ private function prepareBundlePriceTable()
{
- $this->getConnection()->delete($this->_getBundlePriceTable());
+ $this->getConnection()->delete($this->getBundlePriceTable());
return $this;
}
@@ -68,9 +218,9 @@ protected function _prepareBundlePriceTable()
*
* @return $this
*/
- protected function _prepareBundleSelectionTable()
+ private function prepareBundleSelectionTable()
{
- $this->getConnection()->delete($this->_getBundleSelectionTable());
+ $this->getConnection()->delete($this->getBundleSelectionTable());
return $this;
}
@@ -79,61 +229,68 @@ protected function _prepareBundleSelectionTable()
*
* @return $this
*/
- protected function _prepareBundleOptionTable()
+ private function prepareBundleOptionTable()
{
- $this->getConnection()->delete($this->_getBundleOptionTable());
+ $this->getConnection()->delete($this->getBundleOptionTable());
return $this;
}
/**
* Prepare temporary price index data for bundle products by price type
*
+ * @param array $dimensions
* @param int $priceType
* @param int|array $entityIds the entity ids limitation
- * @return $this
+ * @return void
+ * @throws \Exception
* @SuppressWarnings(PHPMD.ExcessiveMethodLength)
*/
- protected function _prepareBundlePriceByType($priceType, $entityIds = null)
+ private function prepareBundlePriceByType($priceType, array $dimensions, $entityIds = null)
{
$connection = $this->getConnection();
- $table = $this->_getBundlePriceTable();
-
$select = $connection->select()->from(
['e' => $this->getTable('catalog_product_entity')],
['entity_id']
- )->join(
+ )->joinInner(
['cg' => $this->getTable('customer_group')],
- '',
+ array_key_exists(CustomerGroupDimensionProvider::DIMENSION_NAME, $dimensions)
+ ? sprintf(
+ '%s = %s',
+ $this->dimensionToFieldMapper[CustomerGroupDimensionProvider::DIMENSION_NAME],
+ $dimensions[CustomerGroupDimensionProvider::DIMENSION_NAME]->getValue()
+ ) : '',
['customer_group_id']
- );
- $this->_addWebsiteJoinToSelect($select, true);
- $this->_addProductWebsiteJoinToSelect($select, 'cw.website_id', "e.entity_id");
- $select->columns(
- 'website_id',
- 'cw'
- )->join(
- ['cwd' => $this->_getWebsiteDateTable()],
- 'cw.website_id = cwd.website_id',
+ )->joinInner(
+ ['pw' => $this->getTable('catalog_product_website')],
+ 'pw.product_id = e.entity_id',
+ ['pw.website_id']
+ )->joinInner(
+ ['cwd' => $this->getTable('catalog_product_index_website')],
+ 'pw.website_id = cwd.website_id',
[]
- )->joinLeft(
- ['tp' => $this->_getTierPriceIndexTable()],
- 'tp.entity_id = e.entity_id AND tp.website_id = cw.website_id' .
+ );
+ $select->joinLeft(
+ ['tp' => $this->getTable('catalog_product_index_tier_price')],
+ 'tp.entity_id = e.entity_id AND tp.website_id = pw.website_id' .
' AND tp.customer_group_id = cg.customer_group_id',
[]
)->where(
'e.type_id=?',
- $this->getTypeId()
+ \Magento\Bundle\Ui\DataProvider\Product\Listing\Collector\BundlePrice::PRODUCT_TYPE
);
- // add enable products limitation
- $statusCond = $connection->quoteInto(
- '=?',
- \Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED
- );
- $linkField = $this->getMetadataPool()->getMetadata(ProductInterface::class)->getLinkField();
- $this->_addAttributeToSelect($select, 'status', "e.$linkField", 'cs.store_id', $statusCond, true);
+ foreach ($dimensions as $dimension) {
+ if (!isset($this->dimensionToFieldMapper[$dimension->getName()])) {
+ throw new \LogicException(
+ 'Provided dimension is not valid for Price indexer: ' . $dimension->getName()
+ );
+ }
+ $select->where($this->dimensionToFieldMapper[$dimension->getName()] . ' = ?', $dimension->getValue());
+ }
+
+ $this->joinAttributeProcessor->process($select, 'status', Status::STATUS_ENABLED);
if ($this->moduleManager->isEnabled('Magento_Tax')) {
- $taxClassId = $this->_addAttributeToSelect($select, 'tax_class_id', "e.$linkField", 'cs.store_id');
+ $taxClassId = $this->joinAttributeProcessor->process($select, 'tax_class_id');
} else {
$taxClassId = new \Zend_Db_Expr('0');
}
@@ -146,59 +303,49 @@ protected function _prepareBundlePriceByType($priceType, $entityIds = null)
);
}
- $priceTypeCond = $connection->quoteInto('=?', $priceType);
- $this->_addAttributeToSelect($select, 'price_type', "e.$linkField", 'cs.store_id', $priceTypeCond);
-
- $price = $this->_addAttributeToSelect($select, 'price', "e.$linkField", 'cs.store_id');
- $specialPrice = $this->_addAttributeToSelect($select, 'special_price', "e.$linkField", 'cs.store_id');
- $specialFrom = $this->_addAttributeToSelect($select, 'special_from_date', "e.$linkField", 'cs.store_id');
- $specialTo = $this->_addAttributeToSelect($select, 'special_to_date', "e.$linkField", 'cs.store_id');
- $curentDate = new \Zend_Db_Expr('cwd.website_date');
-
- $specialExpr = $connection->getCheckSql(
- $connection->getCheckSql(
- $specialFrom . ' IS NULL',
- '1',
- $connection->getCheckSql($specialFrom . ' <= ' . $curentDate, '1', '0')
- ) . " > 0 AND " . $connection->getCheckSql(
- $specialTo . ' IS NULL',
- '1',
- $connection->getCheckSql($specialTo . ' >= ' . $curentDate, '1', '0')
- ) . " > 0 AND {$specialPrice} > 0 AND {$specialPrice} < 100 ",
- $specialPrice,
- '0'
- );
+ $this->joinAttributeProcessor->process($select, 'price_type', $priceType);
+
+ $price = $this->joinAttributeProcessor->process($select, 'price');
+ $specialPrice = $this->joinAttributeProcessor->process($select, 'special_price');
+ $specialFrom = $this->joinAttributeProcessor->process($select, 'special_from_date');
+ $specialTo = $this->joinAttributeProcessor->process($select, 'special_to_date');
+ $currentDate = new \Zend_Db_Expr('cwd.website_date');
- $tierExpr = new \Zend_Db_Expr("tp.min_price");
+ $specialFromDate = $connection->getDatePartSql($specialFrom);
+ $specialToDate = $connection->getDatePartSql($specialTo);
+ $specialFromExpr = "{$specialFrom} IS NULL OR {$specialFromDate} <= {$currentDate}";
+ $specialToExpr = "{$specialTo} IS NULL OR {$specialToDate} >= {$currentDate}";
+ $specialExpr = "{$specialPrice} IS NOT NULL AND {$specialPrice} > 0 AND {$specialPrice} < 100"
+ . " AND {$specialFromExpr} AND {$specialToExpr}";
+ $tierExpr = new \Zend_Db_Expr('tp.min_price');
if ($priceType == \Magento\Bundle\Model\Product\Price::PRICE_TYPE_FIXED) {
- $finalPrice = $connection->getCheckSql(
- $specialExpr . ' > 0',
- 'ROUND(' . $price . ' * (' . $specialExpr . ' / 100), 4)',
- $price
+ $specialPriceExpr = $connection->getCheckSql(
+ $specialExpr,
+ 'ROUND(' . $price . ' * (' . $specialPrice . ' / 100), 4)',
+ 'NULL'
);
$tierPrice = $connection->getCheckSql(
$tierExpr . ' IS NOT NULL',
- 'ROUND(' . $price . ' - ' . '(' . $price . ' * (' . $tierExpr . ' / 100)), 4)',
+ 'ROUND((1 - ' . $tierExpr . ' / 100) * ' . $price . ', 4)',
'NULL'
);
-
- $finalPrice = $connection->getCheckSql(
- "{$tierPrice} < {$finalPrice}",
- $tierPrice,
- $finalPrice
- );
+ $finalPrice = $connection->getLeastSql([
+ $price,
+ $connection->getIfNullSql($specialPriceExpr, $price),
+ $connection->getIfNullSql($tierPrice, $price),
+ ]);
} else {
- $finalPrice = new \Zend_Db_Expr("0");
+ $finalPrice = new \Zend_Db_Expr('0');
$tierPrice = $connection->getCheckSql($tierExpr . ' IS NOT NULL', '0', 'NULL');
}
$select->columns(
[
'price_type' => new \Zend_Db_Expr($priceType),
- 'special_price' => $specialExpr,
+ 'special_price' => $connection->getCheckSql($specialExpr, $specialPrice, '0'),
'tier_percent' => $tierExpr,
- 'orig_price' => $connection->getCheckSql($price . ' IS NULL', '0', $price),
+ 'orig_price' => $connection->getIfNullSql($price, '0'),
'price' => $finalPrice,
'min_price' => $finalPrice,
'max_price' => $finalPrice,
@@ -214,107 +361,75 @@ protected function _prepareBundlePriceByType($priceType, $entityIds = null)
/**
* Add additional external limitation
*/
- $this->_eventManager->dispatch(
+ $this->eventManager->dispatch(
'catalog_product_prepare_index_select',
[
'select' => $select,
'entity_field' => new \Zend_Db_Expr('e.entity_id'),
- 'website_field' => new \Zend_Db_Expr('cw.website_id'),
- 'store_field' => new \Zend_Db_Expr('cs.store_id')
+ 'website_field' => new \Zend_Db_Expr('pw.website_id'),
+ 'store_field' => new \Zend_Db_Expr('cwd.default_store_id')
]
);
- $query = $select->insertFromSelect($table);
+ $query = $select->insertFromSelect($this->getBundlePriceTable());
$connection->query($query);
-
- return $this;
}
/**
* Calculate fixed bundle product selections price
*
- * @return $this
+ * @param IndexTableStructure $priceTable
+ * @param array $dimensions
+ *
+ * @return void
+ * @throws \Exception
*/
- protected function _calculateBundleOptionPrice()
+ private function calculateBundleOptionPrice($priceTable, $dimensions)
{
$connection = $this->getConnection();
- $this->_prepareBundleSelectionTable();
- $this->_calculateBundleSelectionPrice(\Magento\Bundle\Model\Product\Price::PRICE_TYPE_FIXED);
- $this->_calculateBundleSelectionPrice(\Magento\Bundle\Model\Product\Price::PRICE_TYPE_DYNAMIC);
+ $this->prepareBundleSelectionTable();
+ $this->calculateBundleSelectionPrice($dimensions, \Magento\Bundle\Model\Product\Price::PRICE_TYPE_FIXED);
+ $this->calculateBundleSelectionPrice($dimensions, \Magento\Bundle\Model\Product\Price::PRICE_TYPE_DYNAMIC);
- $this->_prepareBundleOptionTable();
+ $this->prepareBundleOptionTable();
$select = $connection->select()->from(
- ['i' => $this->_getBundleSelectionTable()],
+ $this->getBundleSelectionTable(),
['entity_id', 'customer_group_id', 'website_id', 'option_id']
)->group(
- ['entity_id', 'customer_group_id', 'website_id', 'option_id', 'is_required', 'group_type']
- )->columns(
- [
- 'min_price' => $connection->getCheckSql('i.is_required = 1', 'MIN(i.price)', '0'),
- 'alt_price' => $connection->getCheckSql('i.is_required = 0', 'MIN(i.price)', '0'),
- 'max_price' => $connection->getCheckSql('i.group_type = 1', 'SUM(i.price)', 'MAX(i.price)'),
- 'tier_price' => $connection->getCheckSql('i.is_required = 1', 'MIN(i.tier_price)', '0'),
- 'alt_tier_price' => $connection->getCheckSql('i.is_required = 0', 'MIN(i.tier_price)', '0'),
- ]
- );
-
- $query = $select->insertFromSelect($this->_getBundleOptionTable());
- $connection->query($query);
-
- $this->_prepareDefaultFinalPriceTable();
-
- $minPrice = new \Zend_Db_Expr(
- $connection->getCheckSql('SUM(io.min_price) = 0', 'MIN(io.alt_price)', 'SUM(io.min_price)') . ' + i.price'
- );
- $maxPrice = new \Zend_Db_Expr("SUM(io.max_price) + i.price");
- $tierPrice = $connection->getCheckSql(
- 'MIN(i.tier_percent) IS NOT NULL',
- $connection->getCheckSql(
- 'SUM(io.tier_price) = 0',
- 'SUM(io.alt_tier_price)',
- 'SUM(io.tier_price)'
- ) . ' + MIN(i.tier_price)',
- 'NULL'
+ ['entity_id', 'customer_group_id', 'website_id', 'option_id']
);
-
- $select = $connection->select()->from(
- ['io' => $this->_getBundleOptionTable()],
- ['entity_id', 'customer_group_id', 'website_id']
- )->join(
- ['i' => $this->_getBundlePriceTable()],
- 'i.entity_id = io.entity_id AND i.customer_group_id = io.customer_group_id' .
- ' AND i.website_id = io.website_id',
- []
- )->group(
- ['io.entity_id', 'io.customer_group_id', 'io.website_id', 'i.tax_class_id', 'i.orig_price', 'i.price']
- )->columns(
+ $minPrice = $connection->getCheckSql('is_required = 1', 'price', 'NULL');
+ $tierPrice = $connection->getCheckSql('is_required = 1', 'tier_price', 'NULL');
+ $select->columns(
[
- 'i.tax_class_id',
- 'orig_price' => 'i.orig_price',
- 'price' => 'i.price',
- 'min_price' => $minPrice,
- 'max_price' => $maxPrice,
- 'tier_price' => $tierPrice,
- 'base_tier' => 'MIN(i.base_tier)',
+ 'min_price' => new \Zend_Db_Expr('MIN(' . $minPrice . ')'),
+ 'alt_price' => new \Zend_Db_Expr('MIN(price)'),
+ 'max_price' => $connection->getCheckSql('group_type = 0', 'MAX(price)', 'SUM(price)'),
+ 'tier_price' => new \Zend_Db_Expr('MIN(' . $tierPrice . ')'),
+ 'alt_tier_price' => new \Zend_Db_Expr('MIN(tier_price)'),
]
);
- $query = $select->insertFromSelect($this->_getDefaultFinalPriceTable());
+ $query = $select->insertFromSelect($this->getBundleOptionTable());
$connection->query($query);
- return $this;
+ $this->getConnection()->delete($priceTable->getTableName());
+ $this->applyBundlePrice($priceTable);
+ $this->applyBundleOptionPrice($priceTable);
}
/**
* Calculate bundle product selections price by product type
*
+ * @param array $dimensions
* @param int $priceType
- * @return $this
+ * @return void
+ * @throws \Exception
* @SuppressWarnings(PHPMD.ExcessiveMethodLength)
*/
- protected function _calculateBundleSelectionPrice($priceType)
+ private function calculateBundleSelectionPrice($dimensions, $priceType)
{
$connection = $this->getConnection();
@@ -348,38 +463,39 @@ protected function _calculateBundleSelectionPrice($priceType)
'ROUND(i.base_tier - (i.base_tier * (' . $selectionPriceValue . ' / 100)),4)',
$connection->getCheckSql(
'i.tier_percent > 0',
- 'ROUND(' .
- $selectionPriceValue .
- ' - (' .
- $selectionPriceValue .
- ' * (i.tier_percent / 100)),4)',
+ 'ROUND((1 - i.tier_percent / 100) * ' . $selectionPriceValue . ',4)',
$selectionPriceValue
)
) . ' * bs.selection_qty',
'NULL'
);
- $priceExpr = new \Zend_Db_Expr(
- $connection->getCheckSql("{$tierExpr} < {$priceExpr}", $tierExpr, $priceExpr)
- );
+ $priceExpr = $connection->getLeastSql([
+ $priceExpr,
+ $connection->getIfNullSql($tierExpr, $priceExpr),
+ ]);
} else {
- $priceExpr = new \Zend_Db_Expr(
- $connection->getCheckSql(
- 'i.special_price > 0 AND i.special_price < 100',
- 'ROUND(idx.min_price * (i.special_price / 100), 4)',
- 'idx.min_price'
- ) . ' * bs.selection_qty'
+ $price = 'idx.min_price * bs.selection_qty';
+ $specialExpr = $connection->getCheckSql(
+ 'i.special_price > 0 AND i.special_price < 100',
+ 'ROUND(' . $price . ' * (i.special_price / 100), 4)',
+ $price
);
$tierExpr = $connection->getCheckSql(
- 'i.base_tier IS NOT NULL',
- 'ROUND(idx.min_price * (i.base_tier / 100), 4)* bs.selection_qty',
+ 'i.tier_percent IS NOT NULL',
+ 'ROUND((1 - i.tier_percent / 100) * ' . $price . ', 4)',
'NULL'
);
+ $priceExpr = $connection->getLeastSql([
+ $specialExpr,
+ $connection->getIfNullSql($tierExpr, $price),
+ ]);
}
- $linkField = $this->getMetadataPool()->getMetadata(ProductInterface::class)->getLinkField();
+ $metadata = $this->metadataPool->getMetadata(ProductInterface::class);
+ $linkField = $metadata->getLinkField();
$select = $connection->select()->from(
- ['i' => $this->_getBundlePriceTable()],
+ ['i' => $this->getBundlePriceTable()],
['entity_id', 'customer_group_id', 'website_id']
)->join(
['parent_product' => $this->getTable('catalog_product_entity')],
@@ -398,7 +514,7 @@ protected function _calculateBundleSelectionPrice($priceType)
'bs.selection_id = bsp.selection_id AND bsp.website_id = i.website_id',
['']
)->join(
- ['idx' => $this->getIdxTable()],
+ ['idx' => $this->getMainTable($dimensions)],
'bs.product_id = idx.entity_id AND i.customer_group_id = idx.customer_group_id' .
' AND i.website_id = idx.website_id',
[]
@@ -418,49 +534,26 @@ protected function _calculateBundleSelectionPrice($priceType)
]
);
- $query = $select->insertFromSelect($this->_getBundleSelectionTable());
+ $query = $select->insertFromSelect($this->getBundleSelectionTable());
$connection->query($query);
-
- return $this;
- }
-
- /**
- * Prepare temporary index price for bundle products
- *
- * @param int|array $entityIds the entity ids limitation
- * @return $this
- */
- protected function _prepareBundlePrice($entityIds = null)
- {
- if (!$this->hasEntity() && empty($entityIds)) {
- return $this;
- }
- $this->_prepareTierPriceIndex($entityIds);
- $this->_prepareBundlePriceTable();
- $this->_prepareBundlePriceByType(\Magento\Bundle\Model\Product\Price::PRICE_TYPE_FIXED, $entityIds);
- $this->_prepareBundlePriceByType(\Magento\Bundle\Model\Product\Price::PRICE_TYPE_DYNAMIC, $entityIds);
-
- $this->_calculateBundleOptionPrice();
- $this->_applyCustomOption();
-
- $this->_movePriceDataToIndexTable();
-
- return $this;
}
/**
* Prepare percentage tier price for bundle products
*
- * @param int|array $entityIds
- * @return $this
+ * @param array $dimensions
+ * @param array $entityIds
+ * @return void
+ * @throws \Exception
*/
- protected function _prepareTierPriceIndex($entityIds = null)
+ private function prepareTierPriceIndex($dimensions, $entityIds)
{
$connection = $this->getConnection();
- $linkField = $this->getMetadataPool()->getMetadata(ProductInterface::class)->getLinkField();
+ $metadata = $this->metadataPool->getMetadata(ProductInterface::class);
+ $linkField = $metadata->getLinkField();
// remove index by bundle products
$select = $connection->select()->from(
- ['i' => $this->_getTierPriceIndexTable()],
+ ['i' => $this->getTable('catalog_product_index_tier_price')],
null
)->join(
['e' => $this->getTable('catalog_product_entity')],
@@ -468,7 +561,7 @@ protected function _prepareTierPriceIndex($entityIds = null)
[]
)->where(
'e.type_id=?',
- $this->getTypeId()
+ \Magento\Bundle\Ui\DataProvider\Product\Listing\Collector\BundlePrice::PRODUCT_TYPE
);
$query = $select->deleteFromSelect('i');
$connection->query($query);
@@ -485,27 +578,140 @@ protected function _prepareTierPriceIndex($entityIds = null)
'tp.all_groups = 1 OR (tp.all_groups = 0 AND tp.customer_group_id = cg.customer_group_id)',
['customer_group_id']
)->join(
- ['cw' => $this->getTable('store_website')],
- 'tp.website_id = 0 OR tp.website_id = cw.website_id',
+ ['pw' => $this->getTable('store_website')],
+ 'tp.website_id = 0 OR tp.website_id = pw.website_id',
['website_id']
)->where(
- 'cw.website_id != 0'
+ 'pw.website_id != 0'
)->where(
'e.type_id=?',
- $this->getTypeId()
+ \Magento\Bundle\Ui\DataProvider\Product\Listing\Collector\BundlePrice::PRODUCT_TYPE
)->columns(
new \Zend_Db_Expr('MIN(tp.value)')
)->group(
- ['e.entity_id', 'cg.customer_group_id', 'cw.website_id']
+ ['e.entity_id', 'cg.customer_group_id', 'pw.website_id']
);
if (!empty($entityIds)) {
$select->where('e.entity_id IN(?)', $entityIds);
}
+ foreach ($dimensions as $dimension) {
+ if (!isset($this->dimensionToFieldMapper[$dimension->getName()])) {
+ throw new \LogicException(
+ 'Provided dimension is not valid for Price indexer: ' . $dimension->getName()
+ );
+ }
+ $select->where($this->dimensionToFieldMapper[$dimension->getName()] . ' = ?', $dimension->getValue());
+ }
- $query = $select->insertFromSelect($this->_getTierPriceIndexTable());
+ $query = $select->insertFromSelect($this->getTable('catalog_product_index_tier_price'));
$connection->query($query);
+ }
- return $this;
+ /**
+ * @param IndexTableStructure $priceTable
+ */
+ private function applyBundlePrice($priceTable)
+ {
+ $select = $this->getConnection()->select();
+ $select->from(
+ $this->getBundlePriceTable(),
+ [
+ 'entity_id',
+ 'customer_group_id',
+ 'website_id',
+ 'tax_class_id',
+ 'orig_price',
+ 'price',
+ 'min_price',
+ 'max_price',
+ 'tier_price',
+ ]
+ );
+
+ $query = $select->insertFromSelect($priceTable->getTableName());
+ $this->getConnection()->query($query);
+ }
+
+ /**
+ * @param IndexTableStructure $priceTable
+ */
+ private function applyBundleOptionPrice($priceTable)
+ {
+ $connection = $this->getConnection();
+
+ $subSelect = $connection->select()->from(
+ $this->getBundleOptionTable(),
+ [
+ 'entity_id',
+ 'customer_group_id',
+ 'website_id',
+ 'min_price' => new \Zend_Db_Expr('SUM(min_price)'),
+ 'alt_price' => new \Zend_Db_Expr('MIN(alt_price)'),
+ 'max_price' => new \Zend_Db_Expr('SUM(max_price)'),
+ 'tier_price' => new \Zend_Db_Expr('SUM(tier_price)'),
+ 'alt_tier_price' => new \Zend_Db_Expr('MIN(alt_tier_price)'),
+ ]
+ )->group(
+ ['entity_id', 'customer_group_id', 'website_id']
+ );
+
+ $minPrice = 'i.min_price + ' . $connection->getIfNullSql('io.min_price', '0');
+ $tierPrice = 'i.tier_price + ' . $connection->getIfNullSql('io.tier_price', '0');
+ $select = $connection->select()->join(
+ ['io' => $subSelect],
+ 'i.entity_id = io.entity_id AND i.customer_group_id = io.customer_group_id' .
+ ' AND i.website_id = io.website_id',
+ []
+ )->columns(
+ [
+ 'min_price' => $connection->getCheckSql("{$minPrice} = 0", 'io.alt_price', $minPrice),
+ 'max_price' => new \Zend_Db_Expr('io.max_price + i.max_price'),
+ 'tier_price' => $connection->getCheckSql("{$tierPrice} = 0", 'io.alt_tier_price', $tierPrice),
+ ]
+ );
+
+ $query = $select->crossUpdateFromSelect(['i' => $priceTable->getTableName()]);
+ $connection->query($query);
+ }
+
+ /**
+ * Get main table
+ *
+ * @param array $dimensions
+ * @return string
+ */
+ private function getMainTable($dimensions)
+ {
+ if ($this->fullReindexAction) {
+ return $this->tableMaintainer->getMainReplicaTable($dimensions);
+ }
+ return $this->tableMaintainer->getMainTable($dimensions);
+ }
+
+ /**
+ * Get connection
+ *
+ * return \Magento\Framework\DB\Adapter\AdapterInterface
+ * @throws \DomainException
+ */
+ private function getConnection(): \Magento\Framework\DB\Adapter\AdapterInterface
+ {
+ if ($this->connection === null) {
+ $this->connection = $this->resource->getConnection($this->connectionName);
+ }
+
+ return $this->connection;
+ }
+
+ /**
+ * Get table
+ *
+ * @param string $tableName
+ * @return string
+ */
+ private function getTable($tableName)
+ {
+ return $this->resource->getTableName($tableName, $this->connectionName);
}
}
diff --git a/app/code/Magento/Bundle/Model/ResourceModel/Option.php b/app/code/Magento/Bundle/Model/ResourceModel/Option.php
index 2ad7e57f522d6..6ba281cba10b4 100644
--- a/app/code/Magento/Bundle/Model/ResourceModel/Option.php
+++ b/app/code/Magento/Bundle/Model/ResourceModel/Option.php
@@ -86,7 +86,6 @@ protected function _afterSave(\Magento\Framework\Model\AbstractModel $object)
'store_id = ? OR store_id = 0' => $object->getStoreId(),
'parent_product_id = ?' => $object->getParentId()
];
-
$connection = $this->getConnection();
$connection->delete($this->getTable('catalog_product_bundle_option_value'), $condition);
@@ -99,12 +98,10 @@ protected function _afterSave(\Magento\Framework\Model\AbstractModel $object)
$connection->insert($this->getTable('catalog_product_bundle_option_value'), $data->getData());
/**
- * also saving default value if this store view scope
+ * also saving default fallback value
*/
-
- if ($object->getStoreId()) {
- $data->setStoreId(0);
- $data->setTitle($object->getDefaultTitle());
+ if (0 !== (int)$object->getStoreId()) {
+ $data->setStoreId(0)->setTitle($object->getDefaultTitle());
$connection->insert($this->getTable('catalog_product_bundle_option_value'), $data->getData());
}
diff --git a/app/code/Magento/Bundle/Model/ResourceModel/Selection/Collection.php b/app/code/Magento/Bundle/Model/ResourceModel/Selection/Collection.php
index 0216812199b50..5b88288ff72ca 100644
--- a/app/code/Magento/Bundle/Model/ResourceModel/Selection/Collection.php
+++ b/app/code/Magento/Bundle/Model/ResourceModel/Selection/Collection.php
@@ -5,10 +5,8 @@
*/
namespace Magento\Bundle\Model\ResourceModel\Selection;
-use Magento\Customer\Api\GroupManagementInterface;
use Magento\Framework\DataObject;
use Magento\Framework\DB\Select;
-use Magento\Framework\EntityManager\MetadataPool;
use Magento\Catalog\Model\ResourceModel\Product\Collection\ProductLimitationFactory;
use Magento\Framework\App\ObjectManager;
@@ -45,6 +43,95 @@ class Collection extends \Magento\Catalog\Model\ResourceModel\Product\Collection
*/
private $websiteScopePriceJoined = false;
+ /**
+ * @var \Magento\CatalogInventory\Model\ResourceModel\Stock\Item
+ */
+ private $stockItem;
+
+ /**
+ * Collection constructor.
+ * @param \Magento\Framework\Data\Collection\EntityFactory $entityFactory
+ * @param \Psr\Log\LoggerInterface $logger
+ * @param \Magento\Framework\Data\Collection\Db\FetchStrategyInterface $fetchStrategy
+ * @param \Magento\Framework\Event\ManagerInterface $eventManager
+ * @param \Magento\Eav\Model\Config $eavConfig
+ * @param \Magento\Framework\App\ResourceConnection $resource
+ * @param \Magento\Eav\Model\EntityFactory $eavEntityFactory
+ * @param \Magento\Catalog\Model\ResourceModel\Helper $resourceHelper
+ * @param \Magento\Framework\Validator\UniversalFactory $universalFactory
+ * @param \Magento\Store\Model\StoreManagerInterface $storeManager
+ * @param \Magento\Framework\Module\Manager $moduleManager
+ * @param \Magento\Catalog\Model\Indexer\Product\Flat\State $catalogProductFlatState
+ * @param \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig
+ * @param \Magento\Catalog\Model\Product\OptionFactory $productOptionFactory
+ * @param \Magento\Catalog\Model\ResourceModel\Url $catalogUrl
+ * @param \Magento\Framework\Stdlib\DateTime\TimezoneInterface $localeDate
+ * @param \Magento\Customer\Model\Session $customerSession
+ * @param \Magento\Framework\Stdlib\DateTime $dateTime
+ * @param \Magento\Customer\Api\GroupManagementInterface $groupManagement
+ * @param \Magento\Framework\DB\Adapter\AdapterInterface|null $connection
+ * @param ProductLimitationFactory|null $productLimitationFactory
+ * @param \Magento\Framework\EntityManager\MetadataPool|null $metadataPool
+ * @param \Magento\Catalog\Model\Indexer\Category\Product\TableMaintainer|null $tableMaintainer
+ * @param \Magento\CatalogInventory\Model\ResourceModel\Stock\Item|null $stockItem
+ * @SuppressWarnings(PHPMD.ExcessiveParameterList)
+ */
+ public function __construct(
+ \Magento\Framework\Data\Collection\EntityFactory $entityFactory,
+ \Psr\Log\LoggerInterface $logger,
+ \Magento\Framework\Data\Collection\Db\FetchStrategyInterface $fetchStrategy,
+ \Magento\Framework\Event\ManagerInterface $eventManager,
+ \Magento\Eav\Model\Config $eavConfig,
+ \Magento\Framework\App\ResourceConnection $resource,
+ \Magento\Eav\Model\EntityFactory $eavEntityFactory,
+ \Magento\Catalog\Model\ResourceModel\Helper $resourceHelper,
+ \Magento\Framework\Validator\UniversalFactory $universalFactory,
+ \Magento\Store\Model\StoreManagerInterface $storeManager,
+ \Magento\Framework\Module\Manager $moduleManager,
+ \Magento\Catalog\Model\Indexer\Product\Flat\State $catalogProductFlatState,
+ \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig,
+ \Magento\Catalog\Model\Product\OptionFactory $productOptionFactory,
+ \Magento\Catalog\Model\ResourceModel\Url $catalogUrl,
+ \Magento\Framework\Stdlib\DateTime\TimezoneInterface $localeDate,
+ \Magento\Customer\Model\Session $customerSession,
+ \Magento\Framework\Stdlib\DateTime $dateTime,
+ \Magento\Customer\Api\GroupManagementInterface $groupManagement,
+ \Magento\Framework\DB\Adapter\AdapterInterface $connection = null,
+ ProductLimitationFactory $productLimitationFactory = null,
+ \Magento\Framework\EntityManager\MetadataPool $metadataPool = null,
+ \Magento\Catalog\Model\Indexer\Category\Product\TableMaintainer $tableMaintainer = null,
+ \Magento\CatalogInventory\Model\ResourceModel\Stock\Item $stockItem = null
+ ) {
+ parent::__construct(
+ $entityFactory,
+ $logger,
+ $fetchStrategy,
+ $eventManager,
+ $eavConfig,
+ $resource,
+ $eavEntityFactory,
+ $resourceHelper,
+ $universalFactory,
+ $storeManager,
+ $moduleManager,
+ $catalogProductFlatState,
+ $scopeConfig,
+ $productOptionFactory,
+ $catalogUrl,
+ $localeDate,
+ $customerSession,
+ $dateTime,
+ $groupManagement,
+ $connection,
+ $productLimitationFactory,
+ $metadataPool,
+ $tableMaintainer
+ );
+
+ $this->stockItem = $stockItem
+ ?? ObjectManager::getInstance()->get(\Magento\CatalogInventory\Model\ResourceModel\Stock\Item::class);
+ }
+
/**
* Initialize collection
*
@@ -64,13 +151,7 @@ protected function _construct()
*/
public function _afterLoad()
{
- parent::_afterLoad();
- if ($this->getStoreId() && $this->_items) {
- foreach ($this->_items as $item) {
- $item->setStoreId($this->getStoreId());
- }
- }
- return $this;
+ return parent::_afterLoad();
}
/**
@@ -163,22 +244,38 @@ public function setPositionOrder()
}
/**
- * Add filtering of product then havent enoght stock
+ * Add filtering of products that have 0 items left.
*
* @return $this
* @since 100.2.0
*/
public function addQuantityFilter()
{
+ $manageStockExpr = $this->stockItem->getManageStockExpr('stock_item');
+ $backordersExpr = $this->stockItem->getBackordersExpr('stock_item');
+ $minQtyExpr = $this->getConnection()->getCheckSql(
+ 'selection.selection_can_change_qty',
+ $this->stockItem->getMinSaleQtyExpr('stock_item'),
+ 'selection.selection_qty'
+ );
+
+ $where = $manageStockExpr . ' = 0';
+ $where .= ' OR ('
+ . 'stock_item.is_in_stock = ' . \Magento\CatalogInventory\Model\Stock::STOCK_IN_STOCK
+ . ' AND ('
+ . $backordersExpr . ' != ' . \Magento\CatalogInventory\Model\Stock::BACKORDERS_NO
+ . ' OR '
+ . $minQtyExpr . ' <= stock_item.qty'
+ . ')'
+ . ')';
+
$this->getSelect()
->joinInner(
- ['stock' => $this->getTable('cataloginventory_stock_status')],
- 'selection.product_id = stock.product_id',
+ ['stock_item' => $this->stockItem->getMainTable()],
+ 'selection.product_id = stock_item.product_id',
[]
- )
- ->where(
- '(selection.selection_can_change_qty or selection.selection_qty <= stock.qty) and stock.stock_status'
- );
+ )->where($where);
+
return $this;
}
@@ -253,7 +350,10 @@ 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/Pdf/Items/AbstractItems.php b/app/code/Magento/Bundle/Model/Sales/Order/Pdf/Items/AbstractItems.php
index 2f81308f67f50..30e37e54a21db 100644
--- a/app/code/Magento/Bundle/Model/Sales/Order/Pdf/Items/AbstractItems.php
+++ b/app/code/Magento/Bundle/Model/Sales/Order/Pdf/Items/AbstractItems.php
@@ -92,9 +92,8 @@ public function getChildren($item)
if (isset($itemsArray[$item->getOrderItem()->getId()])) {
return $itemsArray[$item->getOrderItem()->getId()];
- } else {
- return null;
}
+ return null;
}
/**
@@ -244,9 +243,8 @@ public function getOrderItem()
{
if ($this->getItem() instanceof \Magento\Sales\Model\Order\Item) {
return $this->getItem();
- } else {
- return $this->getItem()->getOrderItem();
}
+ return $this->getItem()->getOrderItem();
}
/**
diff --git a/app/code/Magento/Bundle/Pricing/Adjustment/Calculator.php b/app/code/Magento/Bundle/Pricing/Adjustment/Calculator.php
index 9d035aece57bc..adb0777151b9e 100644
--- a/app/code/Magento/Bundle/Pricing/Adjustment/Calculator.php
+++ b/app/code/Magento/Bundle/Pricing/Adjustment/Calculator.php
@@ -271,9 +271,8 @@ public function calculateBundleAmount($basePriceValue, $bundleProduct, $selectio
{
if ($bundleProduct->getPriceType() == Price::PRICE_TYPE_FIXED) {
return $this->calculateFixedBundleAmount($basePriceValue, $bundleProduct, $selectionPriceList, $exclude);
- } else {
- return $this->calculateDynamicBundleAmount($basePriceValue, $bundleProduct, $selectionPriceList, $exclude);
}
+ return $this->calculateDynamicBundleAmount($basePriceValue, $bundleProduct, $selectionPriceList, $exclude);
}
/**
diff --git a/app/code/Magento/Bundle/Pricing/Adjustment/DefaultSelectionPriceListProvider.php b/app/code/Magento/Bundle/Pricing/Adjustment/DefaultSelectionPriceListProvider.php
index 56c403ad9960c..297c4659cb877 100644
--- a/app/code/Magento/Bundle/Pricing/Adjustment/DefaultSelectionPriceListProvider.php
+++ b/app/code/Magento/Bundle/Pricing/Adjustment/DefaultSelectionPriceListProvider.php
@@ -61,8 +61,8 @@ public function getPriceList(Product $bundleProduct, $searchMin, $useRegularPric
if (!$useRegularPrice) {
$selectionsCollection->addAttributeToSelect('special_price');
- $selectionsCollection->addAttributeToSelect('special_price_from');
- $selectionsCollection->addAttributeToSelect('special_price_to');
+ $selectionsCollection->addAttributeToSelect('special_from_date');
+ $selectionsCollection->addAttributeToSelect('special_to_date');
$selectionsCollection->addAttributeToSelect('tax_class_id');
}
diff --git a/app/code/Magento/Bundle/Pricing/Price/BundleOptionPrice.php b/app/code/Magento/Bundle/Pricing/Price/BundleOptionPrice.php
index 995572636e759..241902f6bba61 100644
--- a/app/code/Magento/Bundle/Pricing/Price/BundleOptionPrice.php
+++ b/app/code/Magento/Bundle/Pricing/Price/BundleOptionPrice.php
@@ -10,7 +10,7 @@
use Magento\Framework\Pricing\Price\AbstractPrice;
/**
- * Bundle option price model
+ * Bundle option price model with final price
*/
class BundleOptionPrice extends AbstractPrice implements BundleOptionPriceInterface
{
@@ -26,6 +26,7 @@ class BundleOptionPrice extends AbstractPrice implements BundleOptionPriceInterf
/**
* @var BundleSelectionFactory
+ * @deprecated
*/
protected $selectionFactory;
@@ -34,23 +35,32 @@ class BundleOptionPrice extends AbstractPrice implements BundleOptionPriceInterf
*/
protected $maximalPrice;
+ /**
+ * @var \Magento\Bundle\Pricing\Price\BundleOptions
+ */
+ private $bundleOptions;
+
/**
* @param Product $saleableItem
* @param float $quantity
* @param BundleCalculatorInterface $calculator
* @param \Magento\Framework\Pricing\PriceCurrencyInterface $priceCurrency
* @param BundleSelectionFactory $bundleSelectionFactory
+ * @param BundleOptions|null $bundleOptions
*/
public function __construct(
Product $saleableItem,
$quantity,
BundleCalculatorInterface $calculator,
\Magento\Framework\Pricing\PriceCurrencyInterface $priceCurrency,
- BundleSelectionFactory $bundleSelectionFactory
+ BundleSelectionFactory $bundleSelectionFactory,
+ BundleOptions $bundleOptions = null
) {
$this->selectionFactory = $bundleSelectionFactory;
parent::__construct($saleableItem, $quantity, $calculator, $priceCurrency);
$this->product->setQty($this->quantity);
+ $this->bundleOptions = $bundleOptions ?: \Magento\Framework\App\ObjectManager::getInstance()
+ ->get(\Magento\Bundle\Pricing\Price\BundleOptions::class);
}
/**
@@ -59,7 +69,7 @@ public function __construct(
public function getValue()
{
if (null === $this->value) {
- $this->value = $this->calculateOptions();
+ $this->value = $this->bundleOptions->calculateOptions($this->product);
}
return $this->value;
}
@@ -68,11 +78,12 @@ public function getValue()
* Getter for maximal price of options
*
* @return bool|float
+ * @deprecated
*/
public function getMaxValue()
{
if (null === $this->maximalPrice) {
- $this->maximalPrice = $this->calculateOptions(false);
+ $this->maximalPrice = $this->bundleOptions->calculateOptions($this->product, false);
}
return $this->maximalPrice;
}
@@ -84,21 +95,7 @@ public function getMaxValue()
*/
public function getOptions()
{
- $bundleProduct = $this->product;
- /** @var \Magento\Bundle\Model\Product\Type $typeInstance */
- $typeInstance = $bundleProduct->getTypeInstance();
- $typeInstance->setStoreFilter($bundleProduct->getStoreId(), $bundleProduct);
-
- /** @var \Magento\Bundle\Model\ResourceModel\Option\Collection $optionCollection */
- $optionCollection = $typeInstance->getOptionsCollection($bundleProduct);
-
- $selectionCollection = $typeInstance->getSelectionsCollection(
- $typeInstance->getOptionsIds($bundleProduct),
- $bundleProduct
- );
-
- $priceOptions = $optionCollection->appendSelections($selectionCollection, true, false);
- return $priceOptions;
+ return $this->bundleOptions->getOptions($this->product);
}
/**
@@ -109,22 +106,11 @@ public function getOptions()
*/
public function getOptionSelectionAmount($selection)
{
- $cacheKey = implode(
- '_',
- [
- $this->product->getId(),
- $selection->getOptionId(),
- $selection->getSelectionId()
- ]
+ return $this->bundleOptions->getOptionSelectionAmount(
+ $this->product,
+ $selection,
+ false
);
-
- if (!isset($this->optionSelecionAmountCache[$cacheKey])) {
- $selectionPrice = $this->selectionFactory
- ->create($this->product, $selection, $selection->getSelectionQty());
- $this->optionSelecionAmountCache[$cacheKey] = $selectionPrice->getAmount();
- }
-
- return $this->optionSelecionAmountCache[$cacheKey];
}
/**
@@ -135,18 +121,7 @@ public function getOptionSelectionAmount($selection)
*/
protected function calculateOptions($searchMin = true)
{
- $priceList = [];
- /* @var $option \Magento\Bundle\Model\Option */
- foreach ($this->getOptions() as $option) {
- if ($searchMin && !$option->getRequired()) {
- continue;
- }
- $selectionPriceList = $this->calculator->createSelectionPriceList($option, $this->product);
- $selectionPriceList = $this->calculator->processOptions($option, $selectionPriceList, $searchMin);
- $priceList = array_merge($priceList, $selectionPriceList);
- }
- $amount = $this->calculator->calculateBundleAmount(0., $this->product, $priceList);
- return $amount->getValue();
+ return $this->bundleOptions->calculateOptions($this->product, $searchMin);
}
/**
diff --git a/app/code/Magento/Bundle/Pricing/Price/BundleOptionRegularPrice.php b/app/code/Magento/Bundle/Pricing/Price/BundleOptionRegularPrice.php
new file mode 100644
index 0000000000000..d611619cf2dc5
--- /dev/null
+++ b/app/code/Magento/Bundle/Pricing/Price/BundleOptionRegularPrice.php
@@ -0,0 +1,98 @@
+product->setQty($this->quantity);
+ $this->bundleOptions = $bundleOptions;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getValue()
+ {
+ if (null === $this->value) {
+ $this->value = $this->bundleOptions->calculateOptions($this->product);
+ }
+ return $this->value;
+ }
+
+ /**
+ * Get Options with attached Selections collection
+ *
+ * @return \Magento\Bundle\Model\ResourceModel\Option\Collection
+ */
+ public function getOptions()
+ {
+ return $this->bundleOptions->getOptions($this->product);
+ }
+
+ /**
+ * Get selection amount
+ *
+ * @param \Magento\Bundle\Model\Selection $selection
+ * @return \Magento\Framework\Pricing\Amount\AmountInterface
+ */
+ public function getOptionSelectionAmount($selection)
+ {
+ return $this->bundleOptions->getOptionSelectionAmount(
+ $this->product,
+ $selection,
+ true
+ );
+ }
+
+ /**
+ * Get minimal amount of bundle price with options
+ *
+ * @return \Magento\Framework\Pricing\Amount\AmountInterface
+ */
+ public function getAmount()
+ {
+ return $this->calculator->getOptionsAmount($this->product);
+ }
+}
diff --git a/app/code/Magento/Bundle/Pricing/Price/BundleOptions.php b/app/code/Magento/Bundle/Pricing/Price/BundleOptions.php
new file mode 100644
index 0000000000000..3e7a41d993e7f
--- /dev/null
+++ b/app/code/Magento/Bundle/Pricing/Price/BundleOptions.php
@@ -0,0 +1,129 @@
+calculator = $calculator;
+ $this->selectionFactory = $bundleSelectionFactory;
+ }
+
+ /**
+ * Get Options with attached Selections collection
+ *
+ * @param \Magento\Framework\Pricing\SaleableInterface $bundleProduct
+ * @return \Magento\Bundle\Model\ResourceModel\Option\Collection
+ */
+ public function getOptions(\Magento\Framework\Pricing\SaleableInterface $bundleProduct)
+ {
+ /** @var \Magento\Bundle\Model\Product\Type $typeInstance */
+ $typeInstance = $bundleProduct->getTypeInstance();
+ $typeInstance->setStoreFilter($bundleProduct->getStoreId(), $bundleProduct);
+
+ /** @var \Magento\Bundle\Model\ResourceModel\Option\Collection $optionCollection */
+ $optionCollection = $typeInstance->getOptionsCollection($bundleProduct);
+
+ /** @var \Magento\Bundle\Model\ResourceModel\Selection\Collection $selectionCollection */
+ $selectionCollection = $typeInstance->getSelectionsCollection(
+ $typeInstance->getOptionsIds($bundleProduct),
+ $bundleProduct
+ );
+
+ $priceOptions = $optionCollection->appendSelections($selectionCollection, true, false);
+ return $priceOptions;
+ }
+
+ /**
+ * Calculate maximal or minimal options value
+ *
+ * @param \Magento\Framework\Pricing\SaleableInterface $bundleProduct
+ * @param bool $searchMin
+ * @return float
+ */
+ public function calculateOptions(
+ \Magento\Framework\Pricing\SaleableInterface $bundleProduct,
+ bool $searchMin = true
+ ) {
+ $priceList = [];
+ /* @var $option \Magento\Bundle\Model\Option */
+ foreach ($this->getOptions($bundleProduct) as $option) {
+ if ($searchMin && !$option->getRequired()) {
+ continue;
+ }
+ /** @var \Magento\Bundle\Pricing\Price\BundleSelectionPrice $selectionPriceList */
+ $selectionPriceList = $this->calculator->createSelectionPriceList($option, $bundleProduct);
+ $selectionPriceList = $this->calculator->processOptions($option, $selectionPriceList, $searchMin);
+ $priceList = array_merge($priceList, $selectionPriceList);
+ }
+ $amount = $this->calculator->calculateBundleAmount(0., $bundleProduct, $priceList);
+ return $amount->getValue();
+ }
+
+ /**
+ * Get selection amount
+ *
+ * @param \Magento\Catalog\Model\Product $bundleProduct
+ * @param \Magento\Bundle\Model\Selection $selection
+ * @param bool $useRegularPrice
+ * @return \Magento\Framework\Pricing\Amount\AmountInterface
+ */
+ public function getOptionSelectionAmount(
+ \Magento\Catalog\Model\Product $bundleProduct,
+ $selection,
+ bool $useRegularPrice = false
+ ) {
+ $cacheKey = implode(
+ '_',
+ [
+ $bundleProduct->getId(),
+ $selection->getOptionId(),
+ $selection->getSelectionId(),
+ $useRegularPrice ? 1 : 0
+ ]
+ );
+
+ if (!isset($this->optionSelectionAmountCache[$cacheKey])) {
+ $selectionPrice = $this->selectionFactory
+ ->create(
+ $bundleProduct,
+ $selection,
+ $selection->getSelectionQty(),
+ ['useRegularPrice' => $useRegularPrice]
+ );
+ $this->optionSelectionAmountCache[$cacheKey] = $selectionPrice->getAmount();
+ }
+
+ return $this->optionSelectionAmountCache[$cacheKey];
+ }
+}
diff --git a/app/code/Magento/Bundle/Pricing/Price/BundleRegularPrice.php b/app/code/Magento/Bundle/Pricing/Price/BundleRegularPrice.php
index 034b735764011..184f8b1e85eaa 100644
--- a/app/code/Magento/Bundle/Pricing/Price/BundleRegularPrice.php
+++ b/app/code/Magento/Bundle/Pricing/Price/BundleRegularPrice.php
@@ -52,7 +52,7 @@ public function getAmount()
if ($this->product->getPriceType() == Price::PRICE_TYPE_FIXED) {
/** @var \Magento\Catalog\Pricing\Price\CustomOptionPrice $customOptionPrice */
$customOptionPrice = $this->priceInfo->getPrice(CustomOptionPrice::PRICE_CODE);
- $price += $customOptionPrice->getCustomOptionRange(true);
+ $price += $customOptionPrice->getCustomOptionRange(true, $this->getPriceCode());
}
$this->amount[$this->getValue()] = $this->calculator->getMinRegularAmount($price, $this->product);
}
@@ -71,7 +71,7 @@ public function getMaximalPrice()
if ($this->product->getPriceType() == Price::PRICE_TYPE_FIXED) {
/** @var \Magento\Catalog\Pricing\Price\CustomOptionPrice $customOptionPrice */
$customOptionPrice = $this->priceInfo->getPrice(CustomOptionPrice::PRICE_CODE);
- $price += $customOptionPrice->getCustomOptionRange(false);
+ $price += $customOptionPrice->getCustomOptionRange(false, $this->getPriceCode());
}
$this->maximalPrice = $this->calculator->getMaxRegularAmount($price, $this->product);
}
diff --git a/app/code/Magento/Bundle/Pricing/Price/BundleSelectionFactory.php b/app/code/Magento/Bundle/Pricing/Price/BundleSelectionFactory.php
index 927b8fbff8d85..a28d721cc9a4e 100644
--- a/app/code/Magento/Bundle/Pricing/Price/BundleSelectionFactory.php
+++ b/app/code/Magento/Bundle/Pricing/Price/BundleSelectionFactory.php
@@ -54,7 +54,7 @@ public function create(
) {
$arguments['bundleProduct'] = $bundleProduct;
$arguments['saleableItem'] = $selection;
- $arguments['quantity'] = $quantity ? floatval($quantity) : 1.;
+ $arguments['quantity'] = $quantity ? (float)$quantity : 1.;
return $this->objectManager->create(self::SELECTION_CLASS_DEFAULT, $arguments);
}
diff --git a/app/code/Magento/Bundle/Pricing/Price/BundleSelectionPrice.php b/app/code/Magento/Bundle/Pricing/Price/BundleSelectionPrice.php
index 71c1b5c5e98cb..b98a9d05240b1 100644
--- a/app/code/Magento/Bundle/Pricing/Price/BundleSelectionPrice.php
+++ b/app/code/Magento/Bundle/Pricing/Price/BundleSelectionPrice.php
@@ -103,7 +103,10 @@ public function getValue()
return $this->value;
}
$product = $this->selection;
- $bundleSelectionKey = 'bundle-selection-value-' . $product->getSelectionId();
+ $bundleSelectionKey = 'bundle-selection-'
+ . ($this->useRegularPrice ? 'regular-' : '')
+ . 'value-'
+ . $product->getSelectionId();
if ($product->hasData($bundleSelectionKey)) {
return $product->getData($bundleSelectionKey);
}
@@ -128,7 +131,8 @@ public function getValue()
'catalog_product_get_final_price',
['product' => $product, 'qty' => $this->bundleProduct->getQty()]
);
- $value = $product->getData('final_price') * ($selectionPriceValue / 100);
+ $price = $this->useRegularPrice ? $product->getData('price') : $product->getData('final_price');
+ $value = $price * ($selectionPriceValue / 100);
} else {
// calculate price for selection type fixed
$value = $this->priceCurrency->convert($selectionPriceValue);
@@ -150,7 +154,10 @@ public function getValue()
public function getAmount()
{
$product = $this->selection;
- $bundleSelectionKey = 'bundle-selection-amount-' . $product->getSelectionId();
+ $bundleSelectionKey = 'bundle-selection'
+ . ($this->useRegularPrice ? 'regular-' : '')
+ . '-amount-'
+ . $product->getSelectionId();
if ($product->hasData($bundleSelectionKey)) {
return $product->getData($bundleSelectionKey);
}
@@ -177,8 +184,7 @@ public function getProduct()
{
if ($this->bundleProduct->getPriceType() == Price::PRICE_TYPE_DYNAMIC) {
return parent::getProduct();
- } else {
- return $this->bundleProduct;
}
+ return $this->bundleProduct;
}
}
diff --git a/app/code/Magento/Bundle/Pricing/Price/ConfiguredPrice.php b/app/code/Magento/Bundle/Pricing/Price/ConfiguredPrice.php
index 274ea95474120..8effab864868a 100644
--- a/app/code/Magento/Bundle/Pricing/Price/ConfiguredPrice.php
+++ b/app/code/Magento/Bundle/Pricing/Price/ConfiguredPrice.php
@@ -15,7 +15,6 @@
/**
* Configured price model
* @api
- * @since 100.0.2
*/
class ConfiguredPrice extends CatalogPrice\FinalPrice implements ConfiguredPriceInterface
{
@@ -41,6 +40,11 @@ class ConfiguredPrice extends CatalogPrice\FinalPrice implements ConfiguredPrice
*/
private $serializer;
+ /**
+ * @var \Magento\Catalog\Pricing\Price\ConfiguredPriceSelection
+ */
+ private $configuredPriceSelection;
+
/**
* @param Product $saleableItem
* @param float $quantity
@@ -48,6 +52,7 @@ class ConfiguredPrice extends CatalogPrice\FinalPrice implements ConfiguredPrice
* @param \Magento\Framework\Pricing\PriceCurrencyInterface $priceCurrency
* @param ItemInterface $item
* @param \Magento\Framework\Serialize\Serializer\Json|null $serializer
+ * @param \Magento\Catalog\Pricing\Price\ConfiguredPriceSelection|null $configuredPriceSelection
*/
public function __construct(
Product $saleableItem,
@@ -55,11 +60,15 @@ public function __construct(
BundleCalculatorInterface $calculator,
\Magento\Framework\Pricing\PriceCurrencyInterface $priceCurrency,
ItemInterface $item = null,
- \Magento\Framework\Serialize\Serializer\Json $serializer = null
+ \Magento\Framework\Serialize\Serializer\Json $serializer = null,
+ \Magento\Catalog\Pricing\Price\ConfiguredPriceSelection $configuredPriceSelection = null
) {
$this->item = $item;
$this->serializer = $serializer ?: \Magento\Framework\App\ObjectManager::getInstance()
->get(\Magento\Framework\Serialize\Serializer\Json::class);
+ $this->configuredPriceSelection = $configuredPriceSelection
+ ?: \Magento\Framework\App\ObjectManager::getInstance()
+ ->get(\Magento\Catalog\Pricing\Price\ConfiguredPriceSelection::class);
parent::__construct($saleableItem, $quantity, $calculator, $priceCurrency);
}
@@ -84,13 +93,14 @@ public function getOptions()
$bundleOptions = [];
/** @var \Magento\Bundle\Model\Product\Type $typeInstance */
$typeInstance = $bundleProduct->getTypeInstance();
-
- // get bundle options
- $optionsQuoteItemOption = $this->item->getOptionByCode('bundle_option_ids');
- $bundleOptionsIds = $optionsQuoteItemOption
- ? $this->serializer->unserialize($optionsQuoteItemOption->getValue())
- : [];
-
+ $bundleOptionsIds = [];
+ if ($this->item) {
+ // get bundle options
+ $optionsQuoteItemOption = $this->item->getOptionByCode('bundle_option_ids');
+ if ($optionsQuoteItemOption && $optionsQuoteItemOption->getValue()) {
+ $bundleOptionsIds = $this->serializer->unserialize($optionsQuoteItemOption->getValue());
+ }
+ }
if ($bundleOptionsIds) {
/** @var \Magento\Bundle\Model\ResourceModel\Option\Collection $optionsCollection */
$optionsCollection = $typeInstance->getOptionsByIds($bundleOptionsIds, $bundleProduct);
@@ -113,13 +123,7 @@ public function getOptions()
*/
public function getConfiguredAmount($baseValue = 0.)
{
- $selectionPriceList = [];
- foreach ($this->getOptions() as $option) {
- $selectionPriceList = array_merge(
- $selectionPriceList,
- $this->calculator->createSelectionPriceList($option, $this->product)
- );
- }
+ $selectionPriceList = $this->configuredPriceSelection->getSelectionPriceList($this);
return $this->calculator->calculateBundleAmount(
$baseValue,
$this->product,
@@ -140,9 +144,8 @@ public function getValue()
$this->priceInfo
->getPrice(BundleDiscountPrice::PRICE_CODE)
->calculateDiscount($configuredOptionsAmount);
- } else {
- return parent::getValue();
}
+ return parent::getValue();
}
/**
diff --git a/app/code/Magento/Bundle/Pricing/Price/ConfiguredRegularPrice.php b/app/code/Magento/Bundle/Pricing/Price/ConfiguredRegularPrice.php
new file mode 100644
index 0000000000000..07da42f791e4e
--- /dev/null
+++ b/app/code/Magento/Bundle/Pricing/Price/ConfiguredRegularPrice.php
@@ -0,0 +1,30 @@
+calculator->createSelectionPriceList($option, $this->product, true);
+ }
+}
diff --git a/app/code/Magento/Bundle/Test/Mftf/ActionGroup/BundleProductsOnAdminActionGroup.xml b/app/code/Magento/Bundle/Test/Mftf/ActionGroup/BundleProductsOnAdminActionGroup.xml
new file mode 100644
index 0000000000000..84e56e82410ff
--- /dev/null
+++ b/app/code/Magento/Bundle/Test/Mftf/ActionGroup/BundleProductsOnAdminActionGroup.xml
@@ -0,0 +1,50 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Bundle/Test/Mftf/Data/BundleLinkData.xml b/app/code/Magento/Bundle/Test/Mftf/Data/BundleLinkData.xml
new file mode 100644
index 0000000000000..7123a573bc2e1
--- /dev/null
+++ b/app/code/Magento/Bundle/Test/Mftf/Data/BundleLinkData.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+ 1
+ 0
+ 1.11
+ 1
+ 1
+
+
diff --git a/app/code/Magento/Bundle/Test/Mftf/Data/BundleOptionData.xml b/app/code/Magento/Bundle/Test/Mftf/Data/BundleOptionData.xml
new file mode 100644
index 0000000000000..7af276bcede7c
--- /dev/null
+++ b/app/code/Magento/Bundle/Test/Mftf/Data/BundleOptionData.xml
@@ -0,0 +1,39 @@
+
+
+
+
+
+ bundle-option-dropdown
+ true
+ select
+ 0
+
+
+
+ bundle-option-radio
+ true
+ radio
+ 1
+
+
+
+ bundle-option-checkbox
+ true
+ checkbox
+ 3
+
+
+
+ bundle-option-multipleselect
+ true
+ multi
+ 4
+
+
+
diff --git a/app/code/Magento/Bundle/Test/Mftf/Data/BundleProductData.xml b/app/code/Magento/Bundle/Test/Mftf/Data/BundleProductData.xml
new file mode 100644
index 0000000000000..2977f423d7e67
--- /dev/null
+++ b/app/code/Magento/Bundle/Test/Mftf/Data/BundleProductData.xml
@@ -0,0 +1,29 @@
+
+
+
+
+
+ bundle
+ 4
+ BundleOption
+ checkbox
+ 10
+ BundleProduct
+ BundleProduct
+ 1
+ bundleproduct
+ 4
+ TestOption
+ 10
+ 20
+ Drop-down
+ EavStockItem
+ CustomAttributeCategoryIds
+
+
diff --git a/app/code/Magento/Bundle/Test/Mftf/Data/CustomAttributeData.xml b/app/code/Magento/Bundle/Test/Mftf/Data/CustomAttributeData.xml
new file mode 100644
index 0000000000000..380b5b8959025
--- /dev/null
+++ b/app/code/Magento/Bundle/Test/Mftf/Data/CustomAttributeData.xml
@@ -0,0 +1,26 @@
+
+
+
+
+ price_type
+ 0
+
+
+ price_type
+ 1
+
+
+ price_view
+ 1
+
+
+ price_view
+ 0
+
+
diff --git a/app/code/Magento/Bundle/Test/Mftf/Data/ProductData.xml b/app/code/Magento/Bundle/Test/Mftf/Data/ProductData.xml
new file mode 100644
index 0000000000000..c55b0166b04ba
--- /dev/null
+++ b/app/code/Magento/Bundle/Test/Mftf/Data/ProductData.xml
@@ -0,0 +1,40 @@
+
+
+
+
+
+ Api Bundle Product
+ api-bundle-product
+ bundle
+ 4
+ 4
+ 1
+ api-bundle-product
+ EavStockItem
+ ApiProductDescription
+ ApiProductShortDescription
+ CustomAttributeDynamicPrice
+ CustomAttributePriceView
+
+
+ Api Bundle Product
+ api-bundle-product
+ bundle
+ 4
+ 4
+ 1
+ api-bundle-product
+ CustomAttributeCategoryIds
+ EavStockItem
+ ApiProductDescription
+ ApiProductShortDescription
+ CustomAttributeDynamicPrice
+ CustomAttributePriceViewRange
+
+
diff --git a/app/code/Magento/Bundle/Test/Mftf/LICENSE.txt b/app/code/Magento/Bundle/Test/Mftf/LICENSE.txt
new file mode 100644
index 0000000000000..49525fd99da9c
--- /dev/null
+++ b/app/code/Magento/Bundle/Test/Mftf/LICENSE.txt
@@ -0,0 +1,48 @@
+
+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/Bundle/Test/Mftf/LICENSE_AFL.txt b/app/code/Magento/Bundle/Test/Mftf/LICENSE_AFL.txt
new file mode 100644
index 0000000000000..f39d641b18a19
--- /dev/null
+++ b/app/code/Magento/Bundle/Test/Mftf/LICENSE_AFL.txt
@@ -0,0 +1,48 @@
+
+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/Bundle/Test/Mftf/Metadata/bundle_link-meta.xml b/app/code/Magento/Bundle/Test/Mftf/Metadata/bundle_link-meta.xml
new file mode 100644
index 0000000000000..ca39253aa54a0
--- /dev/null
+++ b/app/code/Magento/Bundle/Test/Mftf/Metadata/bundle_link-meta.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
+ application/json
+
+ string
+ integer
+ integer
+ integer
+ boolean
+ number
+ integer
+ integer
+
+
+
diff --git a/app/code/Magento/Bundle/Test/Mftf/Metadata/bundle_option-meta.xml b/app/code/Magento/Bundle/Test/Mftf/Metadata/bundle_option-meta.xml
new file mode 100644
index 0000000000000..c912ea5eac41a
--- /dev/null
+++ b/app/code/Magento/Bundle/Test/Mftf/Metadata/bundle_option-meta.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+ application/json
+
+ string
+ boolean
+ string
+ integer
+ string
+
+
+
diff --git a/app/code/Magento/Bundle/Test/Mftf/Page/AdminProductCreatePage.xml b/app/code/Magento/Bundle/Test/Mftf/Page/AdminProductCreatePage.xml
new file mode 100644
index 0000000000000..f0048e2fc95d4
--- /dev/null
+++ b/app/code/Magento/Bundle/Test/Mftf/Page/AdminProductCreatePage.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Bundle/Test/Mftf/Page/StorefrontProductPage.xml b/app/code/Magento/Bundle/Test/Mftf/Page/StorefrontProductPage.xml
new file mode 100644
index 0000000000000..a495b6be183ba
--- /dev/null
+++ b/app/code/Magento/Bundle/Test/Mftf/Page/StorefrontProductPage.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Bundle/Test/Mftf/README.md b/app/code/Magento/Bundle/Test/Mftf/README.md
new file mode 100644
index 0000000000000..8e8da0c15fa56
--- /dev/null
+++ b/app/code/Magento/Bundle/Test/Mftf/README.md
@@ -0,0 +1,3 @@
+# Bundle Functional Tests
+
+The Functional Test Module for **Magento Bundle** module.
diff --git a/app/code/Magento/Bundle/Test/Mftf/Section/AdminProductFormBundleSection.xml b/app/code/Magento/Bundle/Test/Mftf/Section/AdminProductFormBundleSection.xml
new file mode 100644
index 0000000000000..a843b942174d5
--- /dev/null
+++ b/app/code/Magento/Bundle/Test/Mftf/Section/AdminProductFormBundleSection.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
diff --git a/app/code/Magento/Bundle/Test/Mftf/Section/AdminProductFormSection.xml b/app/code/Magento/Bundle/Test/Mftf/Section/AdminProductFormSection.xml
new file mode 100644
index 0000000000000..06ed27f4e970c
--- /dev/null
+++ b/app/code/Magento/Bundle/Test/Mftf/Section/AdminProductFormSection.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Bundle/Test/Mftf/Section/StorefrontBundledSection.xml b/app/code/Magento/Bundle/Test/Mftf/Section/StorefrontBundledSection.xml
new file mode 100644
index 0000000000000..568cd5e2bba99
--- /dev/null
+++ b/app/code/Magento/Bundle/Test/Mftf/Section/StorefrontBundledSection.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
diff --git a/app/code/Magento/Bundle/Test/Mftf/Section/StorefrontCategoryProductSection.xml b/app/code/Magento/Bundle/Test/Mftf/Section/StorefrontCategoryProductSection.xml
new file mode 100644
index 0000000000000..c76f822a0913f
--- /dev/null
+++ b/app/code/Magento/Bundle/Test/Mftf/Section/StorefrontCategoryProductSection.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
diff --git a/app/code/Magento/Bundle/Test/Mftf/Section/StorefrontProductInfoMainSection.xml b/app/code/Magento/Bundle/Test/Mftf/Section/StorefrontProductInfoMainSection.xml
new file mode 100644
index 0000000000000..41c00b5eda184
--- /dev/null
+++ b/app/code/Magento/Bundle/Test/Mftf/Section/StorefrontProductInfoMainSection.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/AdminCreateBundleProductTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/AdminCreateBundleProductTest.xml
new file mode 100644
index 0000000000000..6f26c31a67427
--- /dev/null
+++ b/app/code/Magento/Bundle/Test/Mftf/Test/AdminCreateBundleProductTest.xml
@@ -0,0 +1,65 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontEditBundleProductTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontEditBundleProductTest.xml
new file mode 100644
index 0000000000000..e65a4dc99902b
--- /dev/null
+++ b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontEditBundleProductTest.xml
@@ -0,0 +1,125 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontVerifyDynamicBundleProductPricesForCombinationOfOptionsTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontVerifyDynamicBundleProductPricesForCombinationOfOptionsTest.xml
new file mode 100644
index 0000000000000..8ced50e26a448
--- /dev/null
+++ b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontVerifyDynamicBundleProductPricesForCombinationOfOptionsTest.xml
@@ -0,0 +1,242 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 4.99
+
+
+
+ 2.89
+
+
+
+ 7.33
+
+
+
+ 18.25
+
+
+
+ 10.00
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 2
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 5
+
+
+
+
+
+
+
+
+
+
+
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Bundle/Test/Unit/Block/Adminhtml/Sales/Order/Items/RendererTest.php b/app/code/Magento/Bundle/Test/Unit/Block/Adminhtml/Sales/Order/Items/RendererTest.php
index 414b460a1b81d..473fbbd035b00 100644
--- a/app/code/Magento/Bundle/Test/Unit/Block/Adminhtml/Sales/Order/Items/RendererTest.php
+++ b/app/code/Magento/Bundle/Test/Unit/Block/Adminhtml/Sales/Order/Items/RendererTest.php
@@ -46,6 +46,9 @@ public function testGetChildrenEmptyItems($class, $method, $returnClass)
$this->assertSame(null, $this->model->getChildren($item));
}
+ /**
+ * @return array
+ */
public function getChildrenEmptyItemsDataProvider()
{
return [
@@ -97,6 +100,9 @@ public function testGetChildren($parentItem)
$this->assertSame([2 => $this->orderItem], $this->model->getChildren($item));
}
+ /**
+ * @return array
+ */
public function getChildrenDataProvider()
{
return [
@@ -116,6 +122,9 @@ public function testIsShipmentSeparatelyWithoutItem($productOptions, $result)
$this->assertSame($result, $this->model->isShipmentSeparately());
}
+ /**
+ * @return array
+ */
public function isShipmentSeparatelyWithoutItemDataProvider()
{
return [
@@ -145,6 +154,9 @@ public function testIsShipmentSeparatelyWithItem($productOptions, $result, $pare
$this->assertSame($result, $this->model->isShipmentSeparately($this->orderItem));
}
+ /**
+ * @return array
+ */
public function isShipmentSeparatelyWithItemDataProvider()
{
return [
@@ -166,6 +178,9 @@ public function testIsChildCalculatedWithoutItem($productOptions, $result)
$this->assertSame($result, $this->model->isChildCalculated());
}
+ /**
+ * @return array
+ */
public function isChildCalculatedWithoutItemDataProvider()
{
return [
@@ -195,6 +210,9 @@ public function testIsChildCalculatedWithItem($productOptions, $result, $parentI
$this->assertSame($result, $this->model->isChildCalculated($this->orderItem));
}
+ /**
+ * @return array
+ */
public function isChildCalculatedWithItemDataProvider()
{
return [
@@ -257,6 +275,9 @@ public function testCanShowPriceInfo($parentItem, $productOptions, $result)
$this->assertSame($result, $this->model->canShowPriceInfo($this->orderItem));
}
+ /**
+ * @return array
+ */
public function canShowPriceInfoDataProvider()
{
return [
diff --git a/app/code/Magento/Bundle/Test/Unit/Block/Adminhtml/Sales/Order/View/Items/RendererTest.php b/app/code/Magento/Bundle/Test/Unit/Block/Adminhtml/Sales/Order/View/Items/RendererTest.php
index 95dcb48f84be1..5d8cabdd8c1b9 100644
--- a/app/code/Magento/Bundle/Test/Unit/Block/Adminhtml/Sales/Order/View/Items/RendererTest.php
+++ b/app/code/Magento/Bundle/Test/Unit/Block/Adminhtml/Sales/Order/View/Items/RendererTest.php
@@ -41,6 +41,9 @@ public function testIsShipmentSeparatelyWithoutItem($productOptions, $result)
$this->assertSame($result, $this->model->isShipmentSeparately());
}
+ /**
+ * @return array
+ */
public function isShipmentSeparatelyWithoutItemDataProvider()
{
return [
@@ -70,6 +73,9 @@ public function testIsShipmentSeparatelyWithItem($productOptions, $result, $pare
$this->assertSame($result, $this->model->isShipmentSeparately($this->orderItem));
}
+ /**
+ * @return array
+ */
public function isShipmentSeparatelyWithItemDataProvider()
{
return [
@@ -91,6 +97,9 @@ public function testIsChildCalculatedWithoutItem($productOptions, $result)
$this->assertSame($result, $this->model->isChildCalculated());
}
+ /**
+ * @return array
+ */
public function isChildCalculatedWithoutItemDataProvider()
{
return [
@@ -120,6 +129,9 @@ public function testIsChildCalculatedWithItem($productOptions, $result, $parentI
$this->assertSame($result, $this->model->isChildCalculated($this->orderItem));
}
+ /**
+ * @return array
+ */
public function isChildCalculatedWithItemDataProvider()
{
return [
@@ -151,6 +163,9 @@ public function testGetSelectionAttributesWithBundle()
$this->assertEquals($unserializedResult, $this->model->getSelectionAttributes($this->orderItem));
}
+ /**
+ * @return array
+ */
public function getSelectionAttributesDataProvider()
{
return [
@@ -184,6 +199,9 @@ public function testCanShowPriceInfo($parentItem, $productOptions, $result)
$this->assertSame($result, $this->model->canShowPriceInfo($this->orderItem));
}
+ /**
+ * @return array
+ */
public function canShowPriceInfoDataProvider()
{
return [
diff --git a/app/code/Magento/Bundle/Test/Unit/Block/Catalog/Product/View/Type/BundleTest.php b/app/code/Magento/Bundle/Test/Unit/Block/Catalog/Product/View/Type/BundleTest.php
index 97e8098b8181e..bda1c32d4d66e 100644
--- a/app/code/Magento/Bundle/Test/Unit/Block/Catalog/Product/View/Type/BundleTest.php
+++ b/app/code/Magento/Bundle/Test/Unit/Block/Catalog/Product/View/Type/BundleTest.php
@@ -244,12 +244,14 @@ public function testGetJsonConfigFixedPriceBundle()
),
]
);
+ $bundleOptionPriceMock = $this->getAmountPriceMock(
+ $baseAmount,
+ $regularPriceMock,
+ [['item' => $selections[0], 'value' => $basePriceValue, 'base_amount' => 321321]]
+ );
$prices = [
- 'bundle_option' => $this->getAmountPriceMock(
- $baseAmount,
- $regularPriceMock,
- [['item' => $selections[0], 'value' => $basePriceValue, 'base_amount' => 321321]]
- ),
+ 'bundle_option' => $bundleOptionPriceMock,
+ 'bundle_option_regular_price' => $bundleOptionPriceMock,
\Magento\Catalog\Pricing\Price\FinalPrice::PRICE_CODE => $finalPriceMock,
\Magento\Catalog\Pricing\Price\RegularPrice::PRICE_CODE => $regularPriceMock,
];
@@ -278,6 +280,7 @@ public function testGetJsonConfigFixedPriceBundle()
$this->assertEquals(110, $jsonConfig['prices']['oldPrice']['amount']);
$this->assertEquals(100, $jsonConfig['prices']['basePrice']['amount']);
$this->assertEquals(100, $jsonConfig['prices']['finalPrice']['amount']);
+ $this->assertEquals([1], $jsonConfig['positions']);
}
/**
@@ -328,6 +331,11 @@ private function updateBundleBlock($options, $priceInfo, $priceType)
->will($this->returnArgument(0));
}
+ /**
+ * @param $price
+ *
+ * @return \PHPUnit_Framework_MockObject_MockObject
+ */
private function getPriceInfoMock($price)
{
$priceInfoMock = $this->getMockBuilder(\Magento\Framework\Pricing\PriceInfo\Base::class)
@@ -352,6 +360,11 @@ private function getPriceInfoMock($price)
return $priceInfoMock;
}
+ /**
+ * @param $prices
+ *
+ * @return \PHPUnit_Framework_MockObject_MockObject
+ */
private function getPriceMock($prices)
{
$methods = [];
diff --git a/app/code/Magento/Bundle/Test/Unit/Block/Sales/Order/Items/RendererTest.php b/app/code/Magento/Bundle/Test/Unit/Block/Sales/Order/Items/RendererTest.php
index d79afdddfb7ae..2f5dcef391063 100644
--- a/app/code/Magento/Bundle/Test/Unit/Block/Sales/Order/Items/RendererTest.php
+++ b/app/code/Magento/Bundle/Test/Unit/Block/Sales/Order/Items/RendererTest.php
@@ -47,6 +47,9 @@ public function testGetChildrenEmptyItems($class, $method, $returnClass)
$this->assertSame(null, $this->model->getChildren($item));
}
+ /**
+ * @return array
+ */
public function getChildrenEmptyItemsDataProvider()
{
return [
@@ -96,6 +99,9 @@ public function testGetChildren($parentItem)
$this->assertSame([2 => $this->orderItem], $this->model->getChildren($item));
}
+ /**
+ * @return array
+ */
public function getChildrenDataProvider()
{
return [
@@ -115,6 +121,9 @@ public function testIsShipmentSeparatelyWithoutItem($productOptions, $result)
$this->assertSame($result, $this->model->isShipmentSeparately());
}
+ /**
+ * @return array
+ */
public function isShipmentSeparatelyWithoutItemDataProvider()
{
return [
@@ -144,6 +153,9 @@ public function testIsShipmentSeparatelyWithItem($productOptions, $result, $pare
$this->assertSame($result, $this->model->isShipmentSeparately($this->orderItem));
}
+ /**
+ * @return array
+ */
public function isShipmentSeparatelyWithItemDataProvider()
{
return [
@@ -165,6 +177,9 @@ public function testIsChildCalculatedWithoutItem($productOptions, $result)
$this->assertSame($result, $this->model->isChildCalculated());
}
+ /**
+ * @return array
+ */
public function isChildCalculatedWithoutItemDataProvider()
{
return [
@@ -194,6 +209,9 @@ public function testIsChildCalculatedWithItem($productOptions, $result, $parentI
$this->assertSame($result, $this->model->isChildCalculated($this->orderItem));
}
+ /**
+ * @return array
+ */
public function isChildCalculatedWithItemDataProvider()
{
return [
@@ -238,6 +256,9 @@ public function testCanShowPriceInfo($parentItem, $productOptions, $result)
$this->assertSame($result, $this->model->canShowPriceInfo($this->orderItem));
}
+ /**
+ * @return array
+ */
public function canShowPriceInfoDataProvider()
{
return [
diff --git a/app/code/Magento/Bundle/Test/Unit/Model/Product/CopyConstructor/BundleTest.php b/app/code/Magento/Bundle/Test/Unit/Model/Product/CopyConstructor/BundleTest.php
index 831098cc44c38..4df60d07d98ef 100644
--- a/app/code/Magento/Bundle/Test/Unit/Model/Product/CopyConstructor/BundleTest.php
+++ b/app/code/Magento/Bundle/Test/Unit/Model/Product/CopyConstructor/BundleTest.php
@@ -6,6 +6,7 @@
namespace Magento\Bundle\Test\Unit\Model\Product\CopyConstructor;
use Magento\Bundle\Api\Data\BundleOptionInterface;
+use Magento\Bundle\Model\Link;
use Magento\Bundle\Model\Product\CopyConstructor\Bundle;
use Magento\Catalog\Api\Data\ProductExtensionInterface;
use Magento\Catalog\Model\Product;
@@ -45,6 +46,7 @@ public function testBuildNegative()
*/
public function testBuildPositive()
{
+ /** @var Product|\PHPUnit_Framework_MockObject_MockObject $product */
$product = $this->getMockBuilder(Product::class)
->disableOriginalConstructor()
->getMock();
@@ -60,18 +62,42 @@ public function testBuildPositive()
->method('getExtensionAttributes')
->willReturn($extensionAttributesProduct);
+ $productLink = $this->getMockBuilder(Link::class)
+ ->setMethods(['setSelectionId'])
+ ->disableOriginalConstructor()
+ ->getMock();
+ $productLink->expects($this->exactly(2))
+ ->method('setSelectionId')
+ ->with($this->identicalTo(null));
+ $firstOption = $this->getMockBuilder(BundleOptionInterface::class)
+ ->setMethods(['getProductLinks'])
+ ->disableOriginalConstructor()
+ ->getMockForAbstractClass();
+ $firstOption->expects($this->once())
+ ->method('getProductLinks')
+ ->willReturn([$productLink]);
+ $firstOption->expects($this->once())
+ ->method('setOptionId')
+ ->with($this->identicalTo(null));
+ $secondOption = $this->getMockBuilder(BundleOptionInterface::class)
+ ->setMethods(['getProductLinks'])
+ ->disableOriginalConstructor()
+ ->getMockForAbstractClass();
+ $secondOption->expects($this->once())
+ ->method('getProductLinks')
+ ->willReturn([$productLink]);
+ $secondOption->expects($this->once())
+ ->method('setOptionId')
+ ->with($this->identicalTo(null));
$bundleOptions = [
- $this->getMockBuilder(BundleOptionInterface::class)
- ->disableOriginalConstructor()
- ->getMockForAbstractClass(),
- $this->getMockBuilder(BundleOptionInterface::class)
- ->disableOriginalConstructor()
- ->getMockForAbstractClass()
+ $firstOption,
+ $secondOption
];
$extensionAttributesProduct->expects($this->once())
->method('getBundleProductOptions')
->willReturn($bundleOptions);
+ /** @var Product|\PHPUnit_Framework_MockObject_MockObject $duplicate */
$duplicate = $this->getMockBuilder(Product::class)
->disableOriginalConstructor()
->getMock();
diff --git a/app/code/Magento/Bundle/Test/Unit/Model/ResourceModel/Selection/CollectionTest.php b/app/code/Magento/Bundle/Test/Unit/Model/ResourceModel/Selection/CollectionTest.php
deleted file mode 100644
index cbe34639e8267..0000000000000
--- a/app/code/Magento/Bundle/Test/Unit/Model/ResourceModel/Selection/CollectionTest.php
+++ /dev/null
@@ -1,127 +0,0 @@
-storeManager = $this->getMockBuilder(StoreManagerInterface::class)
- ->disableOriginalConstructor()
- ->getMock();
- $this->store = $this->getMockBuilder(StoreInterface::class)
- ->disableOriginalConstructor()
- ->getMock();
- $this->universalFactory = $this->getMockBuilder(UniversalFactory::class)
- ->disableOriginalConstructor()
- ->getMock();
- $this->entity = $this->getMockBuilder(AbstractEntity::class)
- ->disableOriginalConstructor()
- ->getMock();
- $this->adapter = $this->getMockBuilder(AdapterInterface::class)
- ->disableOriginalConstructor()
- ->getMock();
- $this->select = $this->getMockBuilder(Select::class)
- ->disableOriginalConstructor()
- ->getMock();
- $factory = $this->getMockBuilder(ProductLimitationFactory::class)
- ->disableOriginalConstructor()
- ->setMethods(['create'])
- ->getMock();
-
- $this->storeManager->expects($this->any())
- ->method('getStore')
- ->willReturn($this->store);
- $this->store->expects($this->any())
- ->method('getId')
- ->willReturn(1);
- $this->universalFactory->expects($this->any())
- ->method('create')
- ->willReturn($this->entity);
- $this->entity->expects($this->any())
- ->method('getConnection')
- ->willReturn($this->adapter);
- $this->entity->expects($this->any())
- ->method('getDefaultAttributes')
- ->willReturn([]);
- $this->adapter->expects($this->any())
- ->method('select')
- ->willReturn($this->select);
-
- $this->model = $objectManager->getObject(
- \Magento\Bundle\Model\ResourceModel\Selection\Collection::class,
- [
- 'storeManager' => $this->storeManager,
- 'universalFactory' => $this->universalFactory,
- 'productLimitationFactory' => $factory
- ]
- );
- }
-
- public function testAddQuantityFilter()
- {
- $tableName = 'cataloginventory_stock_status';
- $this->entity->expects($this->once())
- ->method('getTable')
- ->willReturn($tableName);
- $this->select->expects($this->once())
- ->method('joinInner')
- ->with(
- ['stock' => $tableName],
- 'selection.product_id = stock.product_id',
- []
- )->willReturnSelf();
- $this->assertEquals($this->model, $this->model->addQuantityFilter());
- }
-}
diff --git a/app/code/Magento/Bundle/Test/Unit/Model/Sales/Order/Pdf/Items/AbstractItemsTest.php b/app/code/Magento/Bundle/Test/Unit/Model/Sales/Order/Pdf/Items/AbstractItemsTest.php
index ecce34363819e..3e9aeaed5c5b4 100644
--- a/app/code/Magento/Bundle/Test/Unit/Model/Sales/Order/Pdf/Items/AbstractItemsTest.php
+++ b/app/code/Magento/Bundle/Test/Unit/Model/Sales/Order/Pdf/Items/AbstractItemsTest.php
@@ -49,6 +49,9 @@ public function testGetChildrenEmptyItems($class, $method, $returnClass)
$this->assertSame(null, $this->model->getChildren($item));
}
+ /**
+ * @return array
+ */
public function getChildrenEmptyItemsDataProvider()
{
return [
@@ -97,6 +100,9 @@ public function testGetChildren($parentItem)
$this->assertSame([2 => $this->orderItem], $this->model->getChildren($item));
}
+ /**
+ * @return array
+ */
public function getChildrenDataProvider()
{
return [
@@ -116,6 +122,9 @@ public function testIsShipmentSeparatelyWithoutItem($productOptions, $result)
$this->assertSame($result, $this->model->isShipmentSeparately());
}
+ /**
+ * @return array
+ */
public function isShipmentSeparatelyWithoutItemDataProvider()
{
return [
@@ -146,6 +155,9 @@ public function testIsShipmentSeparatelyWithItem($productOptions, $result, $pare
$this->assertSame($result, $this->model->isShipmentSeparately($this->orderItem));
}
+ /**
+ * @return array
+ */
public function isShipmentSeparatelyWithItemDataProvider()
{
return [
@@ -167,6 +179,9 @@ public function testIsChildCalculatedWithoutItem($productOptions, $result)
$this->assertSame($result, $this->model->isChildCalculated());
}
+ /**
+ * @return array
+ */
public function isChildCalculatedWithoutItemDataProvider()
{
return [
@@ -197,6 +212,9 @@ public function testIsChildCalculatedWithItem($productOptions, $result, $parentI
$this->assertSame($result, $this->model->isChildCalculated($this->orderItem));
}
+ /**
+ * @return array
+ */
public function isChildCalculatedWithItemDataProvider()
{
return [
@@ -217,6 +235,9 @@ public function testGetBundleOptions($productOptions, $result)
$this->assertSame($result, $this->model->getBundleOptions());
}
+ /**
+ * @return array
+ */
public function getBundleOptionsDataProvider()
{
return [
@@ -277,6 +298,9 @@ public function testCanShowPriceInfo($parentItem, $productOptions, $result)
$this->assertSame($result, $this->model->canShowPriceInfo($this->orderItem));
}
+ /**
+ * @return array
+ */
public function canShowPriceInfoDataProvider()
{
return [
diff --git a/app/code/Magento/Bundle/Test/Unit/Pricing/Adjustment/CalculatorTest.php b/app/code/Magento/Bundle/Test/Unit/Pricing/Adjustment/CalculatorTest.php
index f7f6b30daa300..8d29908b75280 100644
--- a/app/code/Magento/Bundle/Test/Unit/Pricing/Adjustment/CalculatorTest.php
+++ b/app/code/Magento/Bundle/Test/Unit/Pricing/Adjustment/CalculatorTest.php
@@ -585,6 +585,9 @@ public function testGetOptionsAmount($searchMin, $useRegularPrice)
$this->assertEquals($expectedResult, $result, 'Incorrect result');
}
+ /**
+ * @return array
+ */
public function getOptionsAmountDataProvider()
{
return [
diff --git a/app/code/Magento/Bundle/Test/Unit/Pricing/Price/BundleOptionPriceTest.php b/app/code/Magento/Bundle/Test/Unit/Pricing/Price/BundleOptionPriceTest.php
index b6485d0e441e9..8be7de774bab8 100644
--- a/app/code/Magento/Bundle/Test/Unit/Pricing/Price/BundleOptionPriceTest.php
+++ b/app/code/Magento/Bundle/Test/Unit/Pricing/Price/BundleOptionPriceTest.php
@@ -8,95 +8,42 @@
use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper;
-/**
- * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
- */
class BundleOptionPriceTest extends \PHPUnit\Framework\TestCase
{
/**
* @var \Magento\Bundle\Pricing\Price\BundleOptionPrice
*/
- protected $bundleOptionPrice;
-
- /**
- * @var \PHPUnit_Framework_MockObject_MockObject
- */
- protected $baseCalculator;
+ private $bundleOptionPrice;
/**
* @var ObjectManagerHelper
*/
- protected $objectManagerHelper;
+ private $objectManagerHelper;
/**
* @var \Magento\Framework\Pricing\SaleableInterface|\PHPUnit_Framework_MockObject_MockObject
*/
- protected $saleableItemMock;
+ private $saleableItemMock;
/**
* @var \Magento\Bundle\Pricing\Adjustment\BundleCalculatorInterface|\PHPUnit_Framework_MockObject_MockObject
*/
- protected $bundleCalculatorMock;
+ private $bundleCalculatorMock;
/**
- * @var \Magento\Bundle\Pricing\Price\BundleSelectionFactory|\PHPUnit_Framework_MockObject_MockObject
+ * @var \Magento\Bundle\Pricing\Price\BundleOptions|\PHPUnit_Framework_MockObject_MockObject
*/
- protected $selectionFactoryMock;
+ private $bundleOptionsMock;
/**
- * @var \PHPUnit_Framework_MockObject_MockObject
+ * @inheritdoc
*/
- protected $amountFactory;
-
- /**
- * @var \Magento\Framework\Pricing\PriceInfo\Base|\PHPUnit_Framework_MockObject_MockObject
- */
- protected $priceInfoMock;
-
protected function setUp()
{
- $this->priceInfoMock = $this->createMock(\Magento\Framework\Pricing\PriceInfo\Base::class);
+ $this->bundleOptionsMock = $this->createMock(\Magento\Bundle\Pricing\Price\BundleOptions::class);
$this->saleableItemMock = $this->createMock(\Magento\Catalog\Model\Product::class);
- $priceCurrency = $this->getMockBuilder(\Magento\Framework\Pricing\PriceCurrencyInterface::class)->getMock();
- $this->saleableItemMock->expects($this->once())
- ->method('getPriceInfo')
- ->will($this->returnValue($this->priceInfoMock));
+ $this->bundleCalculatorMock = $this->createMock(\Magento\Bundle\Pricing\Adjustment\Calculator::class);
- $store = $this->getMockBuilder(\Magento\Store\Model\Store::class)
- ->disableOriginalConstructor()
- ->getMock();
- $priceCurrency->expects($this->any())->method('round')->will($this->returnArgument(0));
-
- $this->saleableItemMock->expects($this->once())
- ->method('setQty')
- ->will($this->returnSelf());
-
- $this->saleableItemMock->expects($this->any())
- ->method('getStore')
- ->will($this->returnValue($store));
-
- $this->selectionFactoryMock = $this->getMockBuilder(\Magento\Bundle\Pricing\Price\BundleSelectionFactory::class)
- ->disableOriginalConstructor()
- ->getMock();
- $this->amountFactory = $this->createMock(\Magento\Framework\Pricing\Amount\AmountFactory::class);
- $factoryCallback = $this->returnCallback(
- function ($fullAmount, $adjustments) {
- return $this->createAmountMock(['amount' => $fullAmount, 'adjustmentAmounts' => $adjustments]);
- }
- );
- $this->amountFactory->expects($this->any())->method('create')->will($factoryCallback);
- $this->baseCalculator = $this->createMock(\Magento\Framework\Pricing\Adjustment\Calculator::class);
-
- $taxData = $this->getMockBuilder(\Magento\Tax\Helper\Data::class)
- ->disableOriginalConstructor()
- ->getMock();
-
- $this->bundleCalculatorMock = $this->getMockBuilder(\Magento\Bundle\Pricing\Adjustment\Calculator::class)
- ->setConstructorArgs(
- [$this->baseCalculator, $this->amountFactory, $this->selectionFactoryMock, $taxData, $priceCurrency]
- )
- ->setMethods(['getOptionsAmount'])
- ->getMock();
$this->objectManagerHelper = new ObjectManagerHelper($this);
$this->bundleOptionPrice = $this->objectManagerHelper->getObject(
\Magento\Bundle\Pricing\Price\BundleOptionPrice::class,
@@ -104,104 +51,47 @@ function ($fullAmount, $adjustments) {
'saleableItem' => $this->saleableItemMock,
'quantity' => 1.,
'calculator' => $this->bundleCalculatorMock,
- 'bundleSelectionFactory' => $this->selectionFactoryMock
+ 'bundleOptions' => $this->bundleOptionsMock
]
);
}
/**
- * @dataProvider getOptionsDataProvider
- */
- public function testGetOptions($selectionCollection)
- {
- $this->prepareOptionMocks($selectionCollection);
- $this->assertSame($selectionCollection, $this->bundleOptionPrice->getOptions());
- $this->assertSame($selectionCollection, $this->bundleOptionPrice->getOptions());
- }
-
- /**
- * @param array $selectionCollection
+ * Test method \Magento\Bundle\Pricing\Price\BundleOptionPrice::getOptions
+ *
* @return void
*/
- protected function prepareOptionMocks($selectionCollection)
+ public function testGetOptions()
{
- $this->saleableItemMock->expects($this->atLeastOnce())
- ->method('getStoreId')
- ->will($this->returnValue(1));
-
- $priceTypeMock = $this->createMock(\Magento\Bundle\Model\Product\Type::class);
- $priceTypeMock->expects($this->atLeastOnce())
- ->method('setStoreFilter')
- ->with($this->equalTo(1), $this->equalTo($this->saleableItemMock))
- ->will($this->returnSelf());
-
- $optionIds = ['41', '55'];
- $priceTypeMock->expects($this->atLeastOnce())
- ->method('getOptionsIds')
- ->with($this->equalTo($this->saleableItemMock))
- ->will($this->returnValue($optionIds));
-
- $priceTypeMock->expects($this->atLeastOnce())
- ->method('getSelectionsCollection')
- ->with($this->equalTo($optionIds), $this->equalTo($this->saleableItemMock))
- ->will($this->returnValue($selectionCollection));
-
$collection = $this->createMock(\Magento\Bundle\Model\ResourceModel\Option\Collection::class);
- $collection->expects($this->atLeastOnce())
- ->method('appendSelections')
- ->with($this->equalTo($selectionCollection), $this->equalTo(true), $this->equalTo(false))
- ->will($this->returnValue($selectionCollection));
-
- $priceTypeMock->expects($this->atLeastOnce())
- ->method('getOptionsCollection')
- ->with($this->equalTo($this->saleableItemMock))
+ $this->bundleOptionsMock->expects($this->any())
+ ->method('getOptions')
->will($this->returnValue($collection));
-
- $this->saleableItemMock->expects($this->atLeastOnce())
- ->method('getTypeInstance')
- ->will($this->returnValue($priceTypeMock));
- }
-
- public function getOptionsDataProvider()
- {
- return [
- ['1', '2']
- ];
+ $this->assertEquals($collection, $this->bundleOptionPrice->getOptions());
}
/**
- * @param float $selectionQty
- * @param float|bool $selectionAmount
- * @dataProvider selectionAmountDataProvider
+ * Test method \Magento\Bundle\Pricing\Price\BundleOptionPrice::getOptionSelectionAmount
+ *
+ * @return void
*/
- public function testGetOptionSelectionAmount($selectionQty, $selectionAmount)
+ public function testGetOptionSelectionAmount()
{
- $selection = $this->createPartialMock(\Magento\Catalog\Model\Product::class, ['getSelectionQty', '__wakeup']);
- $selection->expects($this->once())
- ->method('getSelectionQty')
- ->will($this->returnValue($selectionQty));
- $priceMock = $this->createMock(\Magento\Bundle\Pricing\Price\BundleSelectionPrice::class);
- $priceMock->expects($this->once())
- ->method('getAmount')
- ->will($this->returnValue($selectionAmount));
- $this->selectionFactoryMock->expects($this->once())
- ->method('create')
- ->with($this->equalTo($this->saleableItemMock), $this->equalTo($selection), $this->equalTo($selectionQty))
- ->will($this->returnValue($priceMock));
- $this->assertSame($selectionAmount, $this->bundleOptionPrice->getOptionSelectionAmount($selection));
+ $selectionAmount = $this->createMock(\Magento\Framework\Pricing\Amount\AmountInterface::class);
+ $product = $this->createMock(\Magento\Catalog\Model\Product::class);
+ $selection = $this->createMock(\Magento\Bundle\Model\Selection::class);
+ $this->bundleOptionsMock->expects($this->any())
+ ->method('getOptionSelectionAmount')
+ ->will($this->returnValue($selectionAmount))
+ ->with($product, $selection, false);
+ $this->assertEquals($selectionAmount, $this->bundleOptionPrice->getOptionSelectionAmount($selection));
}
/**
- * @return array
+ * Test method \Magento\Bundle\Pricing\Price\BundleOptionPrice::getAmount
+ *
+ * @return void
*/
- public function selectionAmountDataProvider()
- {
- return [
- [1., 50.5],
- [2.2, false]
- ];
- }
-
public function testGetAmount()
{
$amountMock = $this->createMock(\Magento\Framework\Pricing\Amount\AmountInterface::class);
@@ -213,204 +103,14 @@ public function testGetAmount()
}
/**
- * Create amount mock
- *
- * @param array $amountData
- * @return \Magento\Framework\Pricing\Amount\Base|\PHPUnit_Framework_MockObject_MockObject
- */
- protected function createAmountMock($amountData)
- {
- /** @var \PHPUnit_Framework_MockObject_MockObject|\Magento\Framework\Pricing\Amount\Base $amount */
- $amount = $this->createMock(\Magento\Framework\Pricing\Amount\Base::class);
- $amount->expects($this->any())->method('getAdjustmentAmounts')->will(
- $this->returnValue(isset($amountData['adjustmentAmounts']) ? $amountData['adjustmentAmounts'] : [])
- );
- $amount->expects($this->any())->method('getValue')->will($this->returnValue($amountData['amount']));
- return $amount;
- }
-
- /**
- * Create option mock
+ * Test method \Magento\Bundle\Pricing\Price\BundleOptionPrice::getValue
*
- * @param array $optionData
- * @return \Magento\Bundle\Model\Option|\PHPUnit_Framework_MockObject_MockObject
- */
- protected function createOptionMock($optionData)
- {
- /** @var \PHPUnit_Framework_MockObject_MockObject|\Magento\Bundle\Model\Option $option */
- $option = $this->createPartialMock(\Magento\Bundle\Model\Option::class, ['isMultiSelection', '__wakeup']);
- $option->expects($this->any())->method('isMultiSelection')
- ->will($this->returnValue($optionData['isMultiSelection']));
- $selections = [];
- foreach ($optionData['selections'] as $selectionData) {
- $selections[] = $this->createSelectionMock($selectionData);
- }
- foreach ($optionData['data'] as $key => $value) {
- $option->setData($key, $value);
- }
- $option->setData('selections', $selections);
- return $option;
- }
-
- /**
- * Create selection product mock
- *
- * @param array $selectionData
- * @return \Magento\Catalog\Model\Product|\PHPUnit_Framework_MockObject_MockObject
- */
- protected function createSelectionMock($selectionData)
- {
- $selection = $this->getMockBuilder(\Magento\Catalog\Model\Product::class)
- ->setMethods(['isSalable', 'getAmount', 'getQuantity', 'getProduct', '__wakeup'])
- ->disableOriginalConstructor()
- ->getMock();
-
- // All items are saleable
- $selection->expects($this->any())->method('isSalable')->will($this->returnValue(true));
- foreach ($selectionData['data'] as $key => $value) {
- $selection->setData($key, $value);
- }
- $amountMock = $this->createAmountMock($selectionData['amount']);
- $selection->expects($this->any())->method('getAmount')->will($this->returnValue($amountMock));
- $selection->expects($this->any())->method('getQuantity')->will($this->returnValue(1));
-
- $innerProduct = $this->getMockBuilder(\Magento\Catalog\Model\Product::class)
- ->setMethods(['getSelectionCanChangeQty', '__wakeup'])
- ->disableOriginalConstructor()
- ->getMock();
- $innerProduct->expects($this->any())->method('getSelectionCanChangeQty')->will($this->returnValue(true));
- $selection->expects($this->any())->method('getProduct')->will($this->returnValue($innerProduct));
-
- return $selection;
- }
-
- /**
- * @dataProvider getTestDataForCalculation
- */
- public function testCalculation($optionList, $expected)
- {
- $storeId = 1;
- $this->saleableItemMock->expects($this->any())->method('getStoreId')->will($this->returnValue($storeId));
- $this->selectionFactoryMock->expects($this->any())->method('create')->will($this->returnArgument(1));
-
- $this->baseCalculator->expects($this->atLeastOnce())->method('getAmount')
- ->will($this->returnValue($this->createAmountMock(['amount' => 0.])));
-
- $options = [];
- foreach ($optionList as $optionData) {
- $options[] = $this->createOptionMock($optionData);
- }
- /** @var \PHPUnit_Framework_MockObject_MockObject $optionsCollection */
- $optionsCollection = $this->createMock(\Magento\Bundle\Model\ResourceModel\Option\Collection::class);
- $optionsCollection->expects($this->atLeastOnce())->method('appendSelections')->will($this->returnSelf());
- $optionsCollection->expects($this->atLeastOnce())->method('getIterator')
- ->will($this->returnValue(new \ArrayIterator($options)));
-
- /** @var \PHPUnit_Framework_MockObject_MockObject|\Magento\Catalog\Model\Product\Type\AbstractType $typeMock */
- $typeMock = $this->createMock(\Magento\Bundle\Model\Product\Type::class);
- $typeMock->expects($this->any())->method('setStoreFilter')->with($storeId, $this->saleableItemMock);
- $typeMock->expects($this->any())->method('getOptionsCollection')->with($this->saleableItemMock)
- ->will($this->returnValue($optionsCollection));
- $this->saleableItemMock->expects($this->any())->method('getTypeInstance')->will($this->returnValue($typeMock));
-
- $this->assertEquals($expected['min'], $this->bundleOptionPrice->getValue());
- $this->assertEquals($expected['max'], $this->bundleOptionPrice->getMaxValue());
- }
-
- /**
- * @return array
+ * @return void
*/
- public function getTestDataForCalculation()
+ public function testGetValue()
{
- return [
- 'first case' => [
- 'optionList' => [
- // first option with single choice of product
- [
- 'isMultiSelection' => false,
- 'data' => [
- 'title' => 'test option 1',
- 'default_title' => 'test option 1',
- 'type' => 'select',
- 'option_id' => '1',
- 'position' => '0',
- 'required' => '1',
- ],
- 'selections' => [
- [
- 'data' => ['price' => 70.],
- 'amount' => ['amount' => 70],
- ],
- [
- 'data' => ['price' => 80.],
- 'amount' => ['amount' => 80]
- ],
- [
- 'data' => ['price' => 50.],
- 'amount' => ['amount' => 50]
- ],
- ]
- ],
- // second not required option
- [
- 'isMultiSelection' => false,
- 'data' => [
- 'title' => 'test option 2',
- 'default_title' => 'test option 2',
- 'type' => 'select',
- 'option_id' => '2',
- 'position' => '1',
- 'required' => '0',
- ],
- 'selections' => [
- [
- 'data' => ['value' => 20.],
- 'amount' => ['amount' => 20],
- ],
- ]
- ],
- // third with multi-selection
- [
- 'isMultiSelection' => true,
- 'data' => [
- 'title' => 'test option 3',
- 'default_title' => 'test option 3',
- 'type' => 'select',
- 'option_id' => '3',
- 'position' => '2',
- 'required' => '1',
- ],
- 'selections' => [
- [
- 'data' => ['price' => 40.],
- 'amount' => ['amount' => 40],
- ],
- [
- 'data' => ['price' => 20.],
- 'amount' => ['amount' => 20]
- ],
- [
- 'data' => ['price' => 60.],
- 'amount' => ['amount' => 60]
- ],
- ]
- ],
- // fourth without selections
- [
- 'isMultiSelection' => true,
- 'data' => [
- 'title' => 'test option 3',
- 'default_title' => 'test option 3',
- 'type' => 'select',
- 'option_id' => '4',
- 'position' => '3',
- 'required' => '1',
- ],
- 'selections' => []
- ],
- ],
- 'expected' => ['min' => 70, 'max' => 220],
- ]
- ];
+ $value = 1;
+ $this->bundleOptionsMock->expects($this->any())->method('calculateOptions')->will($this->returnValue($value));
+ $this->assertEquals($value, $this->bundleOptionPrice->getValue());
}
}
diff --git a/app/code/Magento/Bundle/Test/Unit/Pricing/Price/BundleOptionRegularPriceTest.php b/app/code/Magento/Bundle/Test/Unit/Pricing/Price/BundleOptionRegularPriceTest.php
new file mode 100644
index 0000000000000..c03955a1855a5
--- /dev/null
+++ b/app/code/Magento/Bundle/Test/Unit/Pricing/Price/BundleOptionRegularPriceTest.php
@@ -0,0 +1,116 @@
+bundleOptionsMock = $this->createMock(\Magento\Bundle\Pricing\Price\BundleOptions::class);
+ $this->saleableItemMock = $this->createMock(\Magento\Catalog\Model\Product::class);
+ $this->bundleCalculatorMock = $this->createMock(\Magento\Bundle\Pricing\Adjustment\Calculator::class);
+
+ $this->objectManagerHelper = new ObjectManagerHelper($this);
+ $this->bundleOptionRegularPrice = $this->objectManagerHelper->getObject(
+ \Magento\Bundle\Pricing\Price\BundleOptionRegularPrice::class,
+ [
+ 'saleableItem' => $this->saleableItemMock,
+ 'quantity' => 1.,
+ 'calculator' => $this->bundleCalculatorMock,
+ 'bundleOptions' => $this->bundleOptionsMock
+ ]
+ );
+ }
+
+ /**
+ * Test method \Magento\Bundle\Pricing\Price\BundleOptionRegularPrice::getOptions
+ *
+ * @return void
+ */
+ public function testGetOptions()
+ {
+ $collection = $this->createMock(\Magento\Bundle\Model\ResourceModel\Option\Collection::class);
+ $this->bundleOptionsMock->expects($this->any())
+ ->method('getOptions')
+ ->will($this->returnValue($collection));
+ $this->assertEquals($collection, $this->bundleOptionRegularPrice->getOptions());
+ }
+
+ /**
+ * Test method \Magento\Bundle\Pricing\Price\BundleOptionRegularPrice::getOptionSelectionAmount
+ *
+ * @return void
+ */
+ public function testGetOptionSelectionAmount()
+ {
+ $selectionAmount = $this->createMock(\Magento\Framework\Pricing\Amount\AmountInterface::class);
+ $product = $this->createMock(\Magento\Catalog\Model\Product::class);
+ $selection = $this->createMock(\Magento\Bundle\Model\Selection::class);
+ $this->bundleOptionsMock->expects($this->any())
+ ->method('getOptionSelectionAmount')
+ ->will($this->returnValue($selectionAmount))
+ ->with($product, $selection, true);
+ $this->assertEquals($selectionAmount, $this->bundleOptionRegularPrice->getOptionSelectionAmount($selection));
+ }
+
+ /**
+ * Test method \Magento\Bundle\Pricing\Price\BundleOptionRegularPrice::getAmount
+ *
+ * @return void
+ */
+ public function testGetAmount()
+ {
+ $amountMock = $this->createMock(\Magento\Framework\Pricing\Amount\AmountInterface::class);
+ $this->bundleCalculatorMock->expects($this->once())
+ ->method('getOptionsAmount')
+ ->with($this->equalTo($this->saleableItemMock))
+ ->will($this->returnValue($amountMock));
+ $this->assertSame($amountMock, $this->bundleOptionRegularPrice->getAmount());
+ }
+
+ /**
+ * Test method \Magento\Bundle\Pricing\Price\BundleOptionRegularPrice::getValue
+ *
+ * @return void
+ */
+ public function testGetValue()
+ {
+ $value = 1;
+ $this->bundleOptionsMock->expects($this->any())->method('calculateOptions')->will($this->returnValue($value));
+ $this->assertEquals($value, $this->bundleOptionRegularPrice->getValue());
+ }
+}
diff --git a/app/code/Magento/Bundle/Test/Unit/Pricing/Price/BundleOptionsTest.php b/app/code/Magento/Bundle/Test/Unit/Pricing/Price/BundleOptionsTest.php
new file mode 100644
index 0000000000000..2d8a73164008b
--- /dev/null
+++ b/app/code/Magento/Bundle/Test/Unit/Pricing/Price/BundleOptionsTest.php
@@ -0,0 +1,396 @@
+priceInfoMock = $this->createMock(\Magento\Framework\Pricing\PriceInfo\Base::class);
+ $this->saleableItemMock = $this->createMock(\Magento\Catalog\Model\Product::class);
+ $priceCurrency = $this->getMockBuilder(\Magento\Framework\Pricing\PriceCurrencyInterface::class)->getMock();
+ $priceCurrency->expects($this->any())->method('round')->will($this->returnArgument(0));
+ $this->selectionFactoryMock = $this->getMockBuilder(\Magento\Bundle\Pricing\Price\BundleSelectionFactory::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->amountFactory = $this->createMock(\Magento\Framework\Pricing\Amount\AmountFactory::class);
+ $factoryCallback = $this->returnCallback(
+ function ($fullAmount, $adjustments) {
+ return $this->createAmountMock(['amount' => $fullAmount, 'adjustmentAmounts' => $adjustments]);
+ }
+ );
+ $this->amountFactory->expects($this->any())->method('create')->will($factoryCallback);
+ $this->baseCalculator = $this->createMock(\Magento\Framework\Pricing\Adjustment\Calculator::class);
+
+ $taxData = $this->getMockBuilder(\Magento\Tax\Helper\Data::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->bundleCalculatorMock = $this->getMockBuilder(\Magento\Bundle\Pricing\Adjustment\Calculator::class)
+ ->setConstructorArgs(
+ [$this->baseCalculator, $this->amountFactory, $this->selectionFactoryMock, $taxData, $priceCurrency]
+ )
+ ->setMethods(['getOptionsAmount'])
+ ->getMock();
+ $this->objectManagerHelper = new ObjectManagerHelper($this);
+ $this->bundleOptions = $this->objectManagerHelper->getObject(
+ \Magento\Bundle\Pricing\Price\BundleOptions::class,
+ [
+ 'calculator' => $this->bundleCalculatorMock,
+ 'bundleSelectionFactory' => $this->selectionFactoryMock
+ ]
+ );
+ }
+
+ /**
+ * @dataProvider getOptionsDataProvider
+ */
+ public function testGetOptions(string $selectionCollection)
+ {
+ $this->prepareOptionMocks($selectionCollection);
+ $this->bundleOptions->getOptions($this->saleableItemMock);
+ $this->assertSame($selectionCollection, $this->bundleOptions->getOptions($this->saleableItemMock));
+ $this->assertSame($selectionCollection, $this->bundleOptions->getOptions($this->saleableItemMock));
+ }
+
+ /**
+ * @param array $selectionCollection
+ * @return void
+ */
+ private function prepareOptionMocks($selectionCollection)
+ {
+ $this->saleableItemMock->expects($this->atLeastOnce())
+ ->method('getStoreId')
+ ->will($this->returnValue(1));
+
+ $priceTypeMock = $this->createMock(\Magento\Bundle\Model\Product\Type::class);
+ $priceTypeMock->expects($this->atLeastOnce())
+ ->method('setStoreFilter')
+ ->with($this->equalTo(1), $this->equalTo($this->saleableItemMock))
+ ->will($this->returnSelf());
+
+ $optionIds = ['41', '55'];
+ $priceTypeMock->expects($this->atLeastOnce())
+ ->method('getOptionsIds')
+ ->with($this->equalTo($this->saleableItemMock))
+ ->will($this->returnValue($optionIds));
+
+ $priceTypeMock->expects($this->atLeastOnce())
+ ->method('getSelectionsCollection')
+ ->with($this->equalTo($optionIds), $this->equalTo($this->saleableItemMock))
+ ->will($this->returnValue($selectionCollection));
+
+ $collection = $this->createMock(\Magento\Bundle\Model\ResourceModel\Option\Collection::class);
+ $collection->expects($this->atLeastOnce())
+ ->method('appendSelections')
+ ->with($this->equalTo($selectionCollection), $this->equalTo(true), $this->equalTo(false))
+ ->will($this->returnValue($selectionCollection));
+
+ $priceTypeMock->expects($this->atLeastOnce())
+ ->method('getOptionsCollection')
+ ->with($this->equalTo($this->saleableItemMock))
+ ->will($this->returnValue($collection));
+
+ $this->saleableItemMock->expects($this->atLeastOnce())
+ ->method('getTypeInstance')
+ ->will($this->returnValue($priceTypeMock));
+ }
+
+ /**
+ * @return array
+ */
+ public function getOptionsDataProvider()
+ {
+ return [
+ ['1', '2']
+ ];
+ }
+
+ /**
+ * @param float $selectionQty
+ * @param float|bool $selectionAmount
+ * @param bool $useRegularPrice
+ * @dataProvider selectionAmountDataProvider
+ */
+ public function testGetOptionSelectionAmount($selectionQty, $selectionAmount, $useRegularPrice)
+ {
+ $selection = $this->createPartialMock(\Magento\Catalog\Model\Product::class, ['getSelectionQty', '__wakeup']);
+ $selection->expects($this->once())
+ ->method('getSelectionQty')
+ ->will($this->returnValue($selectionQty));
+ $priceMock = $this->createMock(\Magento\Bundle\Pricing\Price\BundleSelectionPrice::class);
+ $priceMock->expects($this->once())
+ ->method('getAmount')
+ ->will($this->returnValue($selectionAmount));
+ $this->selectionFactoryMock->expects($this->once())
+ ->method('create')
+ ->with($this->equalTo($this->saleableItemMock), $this->equalTo($selection), $this->equalTo($selectionQty))
+ ->will($this->returnValue($priceMock));
+ $this->assertSame(
+ $selectionAmount,
+ $this->bundleOptions->getOptionSelectionAmount($this->saleableItemMock, $selection, $useRegularPrice)
+ );
+ }
+
+ /**
+ * @return array
+ */
+ public function selectionAmountDataProvider(): array
+ {
+ return [
+ [1., 50.5, false],
+ [2.2, false, true]
+ ];
+ }
+
+ /**
+ * Create amount mock
+ *
+ * @param array $amountData
+ * @return \Magento\Framework\Pricing\Amount\Base|\PHPUnit_Framework_MockObject_MockObject
+ */
+ private function createAmountMock($amountData)
+ {
+ /** @var \PHPUnit_Framework_MockObject_MockObject|\Magento\Framework\Pricing\Amount\Base $amount */
+ $amount = $this->createMock(\Magento\Framework\Pricing\Amount\Base::class);
+ $amount->expects($this->any())->method('getAdjustmentAmounts')->will(
+ $this->returnValue($amountData['adjustmentAmounts'] ?? [])
+ );
+ $amount->expects($this->any())->method('getValue')->will($this->returnValue($amountData['amount']));
+ return $amount;
+ }
+
+ /**
+ * Create option mock
+ *
+ * @param array $optionData
+ * @return \Magento\Bundle\Model\Option|\PHPUnit_Framework_MockObject_MockObject
+ */
+ private function createOptionMock($optionData)
+ {
+ /** @var \PHPUnit_Framework_MockObject_MockObject|\Magento\Bundle\Model\Option $option */
+ $option = $this->createPartialMock(\Magento\Bundle\Model\Option::class, ['isMultiSelection', '__wakeup']);
+ $option->expects($this->any())->method('isMultiSelection')
+ ->will($this->returnValue($optionData['isMultiSelection']));
+ $selections = [];
+ foreach ($optionData['selections'] as $selectionData) {
+ $selections[] = $this->createSelectionMock($selectionData);
+ }
+ foreach ($optionData['data'] as $key => $value) {
+ $option->setData($key, $value);
+ }
+ $option->setData('selections', $selections);
+ return $option;
+ }
+
+ /**
+ * Create selection product mock
+ *
+ * @param array $selectionData
+ * @return \Magento\Catalog\Model\Product|\PHPUnit_Framework_MockObject_MockObject
+ */
+ private function createSelectionMock($selectionData)
+ {
+ $selection = $this->getMockBuilder(\Magento\Catalog\Model\Product::class)
+ ->setMethods(['isSalable', 'getAmount', 'getQuantity', 'getProduct', '__wakeup'])
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ // All items are saleable
+ $selection->expects($this->any())->method('isSalable')->will($this->returnValue(true));
+ foreach ($selectionData['data'] as $key => $value) {
+ $selection->setData($key, $value);
+ }
+ $amountMock = $this->createAmountMock($selectionData['amount']);
+ $selection->expects($this->any())->method('getAmount')->will($this->returnValue($amountMock));
+ $selection->expects($this->any())->method('getQuantity')->will($this->returnValue(1));
+
+ $innerProduct = $this->getMockBuilder(\Magento\Catalog\Model\Product::class)
+ ->setMethods(['getSelectionCanChangeQty', '__wakeup'])
+ ->disableOriginalConstructor()
+ ->getMock();
+ $innerProduct->expects($this->any())->method('getSelectionCanChangeQty')->will($this->returnValue(true));
+ $selection->expects($this->any())->method('getProduct')->will($this->returnValue($innerProduct));
+
+ return $selection;
+ }
+
+ /**
+ * @dataProvider getTestDataForCalculation
+ */
+ public function testCalculation(array $optionList, array $expected)
+ {
+ $storeId = 1;
+ $this->saleableItemMock->expects($this->any())->method('getStoreId')->will($this->returnValue($storeId));
+ $this->selectionFactoryMock->expects($this->any())->method('create')->will($this->returnArgument(1));
+
+ $this->baseCalculator->expects($this->atLeastOnce())->method('getAmount')
+ ->will($this->returnValue($this->createAmountMock(['amount' => 0.])));
+
+ $options = [];
+ foreach ($optionList as $optionData) {
+ $options[] = $this->createOptionMock($optionData);
+ }
+ /** @var \PHPUnit_Framework_MockObject_MockObject $optionsCollection */
+ $optionsCollection = $this->createMock(\Magento\Bundle\Model\ResourceModel\Option\Collection::class);
+ $optionsCollection->expects($this->atLeastOnce())->method('appendSelections')->will($this->returnSelf());
+ $optionsCollection->expects($this->atLeastOnce())->method('getIterator')
+ ->will($this->returnValue(new \ArrayIterator($options)));
+
+ /** @var \PHPUnit_Framework_MockObject_MockObject|\Magento\Catalog\Model\Product\Type\AbstractType $typeMock */
+ $typeMock = $this->createMock(\Magento\Bundle\Model\Product\Type::class);
+ $typeMock->expects($this->any())->method('setStoreFilter')->with($storeId, $this->saleableItemMock);
+ $typeMock->expects($this->any())->method('getOptionsCollection')->with($this->saleableItemMock)
+ ->will($this->returnValue($optionsCollection));
+ $this->saleableItemMock->expects($this->any())->method('getTypeInstance')->will($this->returnValue($typeMock));
+
+ $this->assertEquals($expected['min'], $this->bundleOptions->calculateOptions($this->saleableItemMock));
+ $this->assertEquals($expected['max'], $this->bundleOptions->calculateOptions($this->saleableItemMock, false));
+ }
+
+ /**
+ * @return array
+ */
+ public function getTestDataForCalculation()
+ {
+ return [
+ 'first case' => [
+ 'optionList' => [
+ // first option with single choice of product
+ [
+ 'isMultiSelection' => false,
+ 'data' => [
+ 'title' => 'test option 1',
+ 'default_title' => 'test option 1',
+ 'type' => 'select',
+ 'option_id' => '1',
+ 'position' => '0',
+ 'required' => '1',
+ ],
+ 'selections' => [
+ [
+ 'data' => ['price' => 70.],
+ 'amount' => ['amount' => 70],
+ ],
+ [
+ 'data' => ['price' => 80.],
+ 'amount' => ['amount' => 80]
+ ],
+ [
+ 'data' => ['price' => 50.],
+ 'amount' => ['amount' => 50]
+ ],
+ ]
+ ],
+ // second not required option
+ [
+ 'isMultiSelection' => false,
+ 'data' => [
+ 'title' => 'test option 2',
+ 'default_title' => 'test option 2',
+ 'type' => 'select',
+ 'option_id' => '2',
+ 'position' => '1',
+ 'required' => '0',
+ ],
+ 'selections' => [
+ [
+ 'data' => ['value' => 20.],
+ 'amount' => ['amount' => 20],
+ ],
+ ]
+ ],
+ // third with multi-selection
+ [
+ 'isMultiSelection' => true,
+ 'data' => [
+ 'title' => 'test option 3',
+ 'default_title' => 'test option 3',
+ 'type' => 'select',
+ 'option_id' => '3',
+ 'position' => '2',
+ 'required' => '1',
+ ],
+ 'selections' => [
+ [
+ 'data' => ['price' => 40.],
+ 'amount' => ['amount' => 40],
+ ],
+ [
+ 'data' => ['price' => 20.],
+ 'amount' => ['amount' => 20]
+ ],
+ [
+ 'data' => ['price' => 60.],
+ 'amount' => ['amount' => 60]
+ ],
+ ]
+ ],
+ // fourth without selections
+ [
+ 'isMultiSelection' => true,
+ 'data' => [
+ 'title' => 'test option 3',
+ 'default_title' => 'test option 3',
+ 'type' => 'select',
+ 'option_id' => '4',
+ 'position' => '3',
+ 'required' => '1',
+ ],
+ 'selections' => []
+ ],
+ ],
+ 'expected' => ['min' => 70, 'max' => 220],
+ ]
+ ];
+ }
+}
diff --git a/app/code/Magento/Bundle/Test/Unit/Pricing/Price/BundleSelectionPriceTest.php b/app/code/Magento/Bundle/Test/Unit/Pricing/Price/BundleSelectionPriceTest.php
index 64140ef920cbe..cbcfb1b85fc97 100644
--- a/app/code/Magento/Bundle/Test/Unit/Pricing/Price/BundleSelectionPriceTest.php
+++ b/app/code/Magento/Bundle/Test/Unit/Pricing/Price/BundleSelectionPriceTest.php
@@ -103,6 +103,9 @@ protected function setUp()
$this->setupSelectionPrice();
}
+ /**
+ * @param bool $useRegularPrice
+ */
protected function setupSelectionPrice($useRegularPrice = false)
{
$this->selectionPrice = new \Magento\Bundle\Pricing\Price\BundleSelectionPrice(
@@ -201,6 +204,7 @@ public function testGetValueTypeFixedWithSelectionPriceType($useRegularPrice)
[
['qty', null, 1],
['final_price', null, 100],
+ ['price', null, 100],
]
)
);
@@ -338,6 +342,9 @@ public function testFixedPriceWithMultipleQty($useRegularPrice)
$this->assertEquals($expectedPrice, $selectionPrice->getValue());
}
+ /**
+ * @return array
+ */
public function useRegularPriceDataProvider()
{
return [
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 7b4d42568f686..1c3cf33cbf73b 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
@@ -76,6 +76,9 @@ protected function setUp()
->getMock();
}
+ /**
+ * @return object
+ */
protected function getModel()
{
return $this->objectManager->getObject(BundleDataProvider::class, [
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 c0624be8e7a97..b265f6cb4c2b9 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
@@ -314,6 +314,7 @@ protected function getBundleOptions()
'template' => 'ui/dynamic-rows/templates/collapsible',
'additionalClasses' => 'admin__field-wide',
'dataScope' => 'data.bundle_options',
+ 'isDefaultFieldScope' => 'is_default',
'bundleSelectionsName' => 'product_bundle_container.bundle_selections'
],
],
@@ -378,6 +379,9 @@ protected function getBundleOptions()
'selection_qty' => '',
],
'links' => ['insertData' => '${ $.provider }:${ $.dataProvider }'],
+ 'imports' => [
+ 'inputType' => '${$.provider}:${$.dataScope}.type'
+ ],
'source' => 'product'
],
],
@@ -594,10 +598,13 @@ protected function getBundleSelections()
'config' => [
'componentType' => Container::NAME,
'isTemplate' => true,
- 'component' => 'Magento_Bundle/js/components/bundle-record',
+ 'component' => 'Magento_Ui/js/dynamic-rows/record',
'is_collection' => true,
'imports' => [
- 'onTypeChanged' => '${ $.provider }:${ $.bundleOptionsDataScope }.type'
+ 'inputType' => '${$.parentName}:inputType'
+ ],
+ 'exports' => [
+ 'isDefaultValue' => '${$.parentName}:isDefaultValue.${$.index}'
]
],
],
@@ -691,11 +698,15 @@ protected function getBundleSelections()
'componentType' => Form\Field::NAME,
'formElement' => Form\Element\Checkbox::NAME,
'dataType' => Form\Element\DataType\Price::NAME,
+ 'component' => 'Magento_Bundle/js/components/bundle-user-defined-checkbox',
'label' => __('User Defined'),
'dataScope' => 'selection_can_change_qty',
'value' => '1',
'valueMap' => ['true' => '1', 'false' => '0'],
'sortOrder' => 110,
+ 'imports' => [
+ 'inputType' => '${$.parentName}:inputType'
+ ]
],
],
],
diff --git a/app/code/Magento/Bundle/composer.json b/app/code/Magento/Bundle/composer.json
index a839c110402d5..fe883e783d6ff 100644
--- a/app/code/Magento/Bundle/composer.json
+++ b/app/code/Magento/Bundle/composer.json
@@ -2,30 +2,31 @@
"name": "magento/module-bundle",
"description": "N/A",
"require": {
- "php": "7.0.2|7.0.4|~7.0.6|~7.1.0",
+ "php": "~7.0.13|~7.1.0",
"magento/module-store": "100.2.*",
- "magento/module-catalog": "101.1.*",
+ "magento/module-catalog": "102.0.*",
"magento/module-tax": "100.2.*",
"magento/module-backend": "100.2.*",
- "magento/module-sales": "100.2.*",
+ "magento/module-sales": "101.0.*",
"magento/module-checkout": "100.2.*",
"magento/module-catalog-inventory": "100.2.*",
- "magento/module-customer": "100.2.*",
- "magento/module-catalog-rule": "100.2.*",
- "magento/module-eav": "100.2.*",
- "magento/module-config": "100.2.*",
+ "magento/module-customer": "101.0.*",
+ "magento/module-catalog-rule": "101.0.*",
+ "magento/module-eav": "101.0.*",
+ "magento/module-config": "101.0.*",
"magento/module-gift-message": "100.2.*",
- "magento/framework": "100.2.*",
- "magento/module-quote": "100.2.*",
+ "magento/framework": "101.0.*",
+ "magento/module-quote": "101.0.*",
"magento/module-media-storage": "100.2.*",
- "magento/module-ui": "100.2.*"
+ "magento/module-ui": "101.0.*"
},
"suggest": {
"magento/module-webapi": "100.2.*",
- "magento/module-bundle-sample-data": "Sample Data version:100.2.*"
+ "magento/module-bundle-sample-data": "Sample Data version:100.2.*",
+ "magento/module-sales-rule": "101.0.*"
},
"type": "magento2-module",
- "version": "100.2.0-dev",
+ "version": "100.2.5",
"license": [
"OSL-3.0",
"AFL-3.0"
diff --git a/app/code/Magento/Bundle/etc/di.xml b/app/code/Magento/Bundle/etc/di.xml
index 287a6c8bfdbc0..733b089dccd4b 100644
--- a/app/code/Magento/Bundle/etc/di.xml
+++ b/app/code/Magento/Bundle/etc/di.xml
@@ -50,7 +50,9 @@
- Magento\Catalog\Pricing\Price\CustomOptionPrice
- Magento\Catalog\Pricing\Price\BasePrice
- Magento\Bundle\Pricing\Price\ConfiguredPrice
+ - Magento\Bundle\Pricing\Price\ConfiguredRegularPrice
- Magento\Bundle\Pricing\Price\BundleOptionPrice
+ - Magento\Bundle\Pricing\Price\BundleOptionRegularPrice
- Magento\CatalogRule\Pricing\Price\CatalogRulePrice
@@ -75,6 +77,11 @@
Magento\Bundle\Pricing\Adjustment\BundleCalculatorInterface
+
+
+ Magento\Bundle\Pricing\Adjustment\BundleCalculatorInterface
+
+
@@ -200,4 +207,11 @@
+
+
+
+ - false
+
+
+
diff --git a/app/code/Magento/Bundle/view/adminhtml/layout/adminhtml_order_shipment_new.xml b/app/code/Magento/Bundle/view/adminhtml/layout/adminhtml_order_shipment_new.xml
index e1c4a0a0b308f..ce30a509bf03c 100644
--- a/app/code/Magento/Bundle/view/adminhtml/layout/adminhtml_order_shipment_new.xml
+++ b/app/code/Magento/Bundle/view/adminhtml/layout/adminhtml_order_shipment_new.xml
@@ -8,7 +8,7 @@
-
+
diff --git a/app/code/Magento/Bundle/view/adminhtml/layout/catalog_product_new.xml b/app/code/Magento/Bundle/view/adminhtml/layout/catalog_product_new.xml
index 34f47b745b8e9..8fcf88037a8fd 100644
--- a/app/code/Magento/Bundle/view/adminhtml/layout/catalog_product_new.xml
+++ b/app/code/Magento/Bundle/view/adminhtml/layout/catalog_product_new.xml
@@ -8,7 +8,7 @@
-
+
diff --git a/app/code/Magento/Bundle/view/adminhtml/layout/sales_order_creditmemo_new.xml b/app/code/Magento/Bundle/view/adminhtml/layout/sales_order_creditmemo_new.xml
index cacf7dd0c47de..95ae3bf4709b7 100644
--- a/app/code/Magento/Bundle/view/adminhtml/layout/sales_order_creditmemo_new.xml
+++ b/app/code/Magento/Bundle/view/adminhtml/layout/sales_order_creditmemo_new.xml
@@ -8,7 +8,7 @@
-
+
diff --git a/app/code/Magento/Bundle/view/adminhtml/layout/sales_order_creditmemo_updateqty.xml b/app/code/Magento/Bundle/view/adminhtml/layout/sales_order_creditmemo_updateqty.xml
index cacf7dd0c47de..95ae3bf4709b7 100644
--- a/app/code/Magento/Bundle/view/adminhtml/layout/sales_order_creditmemo_updateqty.xml
+++ b/app/code/Magento/Bundle/view/adminhtml/layout/sales_order_creditmemo_updateqty.xml
@@ -8,7 +8,7 @@
-
+
diff --git a/app/code/Magento/Bundle/view/adminhtml/layout/sales_order_creditmemo_view.xml b/app/code/Magento/Bundle/view/adminhtml/layout/sales_order_creditmemo_view.xml
index 8fc9eed8656a8..f9951c66abd53 100644
--- a/app/code/Magento/Bundle/view/adminhtml/layout/sales_order_creditmemo_view.xml
+++ b/app/code/Magento/Bundle/view/adminhtml/layout/sales_order_creditmemo_view.xml
@@ -8,7 +8,7 @@
-
+
diff --git a/app/code/Magento/Bundle/view/adminhtml/layout/sales_order_invoice_new.xml b/app/code/Magento/Bundle/view/adminhtml/layout/sales_order_invoice_new.xml
index fcde059ba0972..a64f46bb72ed1 100644
--- a/app/code/Magento/Bundle/view/adminhtml/layout/sales_order_invoice_new.xml
+++ b/app/code/Magento/Bundle/view/adminhtml/layout/sales_order_invoice_new.xml
@@ -8,7 +8,7 @@
-
+
diff --git a/app/code/Magento/Bundle/view/adminhtml/layout/sales_order_invoice_updateqty.xml b/app/code/Magento/Bundle/view/adminhtml/layout/sales_order_invoice_updateqty.xml
index fcde059ba0972..a64f46bb72ed1 100644
--- a/app/code/Magento/Bundle/view/adminhtml/layout/sales_order_invoice_updateqty.xml
+++ b/app/code/Magento/Bundle/view/adminhtml/layout/sales_order_invoice_updateqty.xml
@@ -8,7 +8,7 @@
-
+
diff --git a/app/code/Magento/Bundle/view/adminhtml/layout/sales_order_invoice_view.xml b/app/code/Magento/Bundle/view/adminhtml/layout/sales_order_invoice_view.xml
index 4d649c1b3c38c..0eaa57ab47952 100644
--- a/app/code/Magento/Bundle/view/adminhtml/layout/sales_order_invoice_view.xml
+++ b/app/code/Magento/Bundle/view/adminhtml/layout/sales_order_invoice_view.xml
@@ -8,7 +8,7 @@
-
+
diff --git a/app/code/Magento/Bundle/view/adminhtml/layout/sales_order_view.xml b/app/code/Magento/Bundle/view/adminhtml/layout/sales_order_view.xml
index e212c08215d41..1b1419870b98d 100644
--- a/app/code/Magento/Bundle/view/adminhtml/layout/sales_order_view.xml
+++ b/app/code/Magento/Bundle/view/adminhtml/layout/sales_order_view.xml
@@ -8,7 +8,7 @@
-
+
diff --git a/app/code/Magento/Bundle/view/adminhtml/web/js/components/bundle-checkbox.js b/app/code/Magento/Bundle/view/adminhtml/web/js/components/bundle-checkbox.js
index b608cff85b067..09331d37bb3b6 100644
--- a/app/code/Magento/Bundle/view/adminhtml/web/js/components/bundle-checkbox.js
+++ b/app/code/Magento/Bundle/view/adminhtml/web/js/components/bundle-checkbox.js
@@ -14,7 +14,10 @@ define([
clearing: false,
parentContainer: '',
parentSelections: '',
- changer: ''
+ changer: '',
+ exports: {
+ value: '${$.parentName}:isDefaultValue'
+ }
},
/**
@@ -58,10 +61,6 @@ define([
this.prefer = typeMap[type];
this.elementTmpl(this.templates[typeMap[type]]);
-
- if (this.prefer === 'radio' && this.checked()) {
- this.clearValues();
- }
},
/**
diff --git a/app/code/Magento/Bundle/view/adminhtml/web/js/components/bundle-dynamic-rows-grid.js b/app/code/Magento/Bundle/view/adminhtml/web/js/components/bundle-dynamic-rows-grid.js
index 428361f459544..a6fc84765cc65 100644
--- a/app/code/Magento/Bundle/view/adminhtml/web/js/components/bundle-dynamic-rows-grid.js
+++ b/app/code/Magento/Bundle/view/adminhtml/web/js/components/bundle-dynamic-rows-grid.js
@@ -14,7 +14,57 @@ define([
label: '',
columnsHeader: false,
columnsHeaderAfterRender: true,
- addButton: false
+ addButton: false,
+ isDefaultFieldScope: 'is_default',
+ defaultRecords: {
+ use: [],
+ moreThanOne: false,
+ state: {}
+ },
+ listens: {
+ inputType: 'onInputTypeChange',
+ isDefaultValue: 'onIsDefaultValue'
+ }
+ },
+
+ /**
+ * Handler for type select.
+ *
+ * @param {String} inputType - changed.
+ */
+ onInputTypeChange: function (inputType) {
+ if (this.defaultRecords.moreThanOne && (inputType === 'radio' || inputType === 'select')) {
+ _.each(this.defaultRecords.use, function (index, counter) {
+ this.source.set(
+ this.dataScope + '.bundle_selections.' + index + '.' + this.isDefaultFieldScope,
+ counter ? '0' : '1'
+ );
+ }.bind(this));
+ }
+ },
+
+ /**
+ * Handler for is_default field.
+ *
+ * @param {Object} data - changed data.
+ */
+ onIsDefaultValue: function (data) {
+ var cb,
+ use = 0;
+
+ this.defaultRecords.use = [];
+
+ cb = function (elem, key) {
+
+ if (~~elem) {
+ this.defaultRecords.use.push(key);
+ use++;
+ }
+
+ this.defaultRecords.moreThanOne = use > 1;
+ }.bind(this);
+
+ _.each(data, cb);
},
/**
@@ -29,7 +79,6 @@ define([
recordIndex;
this.parsePagesData(data);
- this.templates.record.bundleOptionsDataScope = this.dataScope;
if (newData.length) {
if (this.insertData().length) {
diff --git a/app/code/Magento/Bundle/view/adminhtml/web/js/components/bundle-user-defined-checkbox.js b/app/code/Magento/Bundle/view/adminhtml/web/js/components/bundle-user-defined-checkbox.js
new file mode 100644
index 0000000000000..a7ceded02d0c3
--- /dev/null
+++ b/app/code/Magento/Bundle/view/adminhtml/web/js/components/bundle-user-defined-checkbox.js
@@ -0,0 +1,30 @@
+/**
+ * Copyright © Magento, Inc. All rights reserved.
+ * See COPYING.txt for license details.
+ */
+
+define([
+ 'Magento_Ui/js/form/element/single-checkbox'
+], function (Checkbox) {
+ 'use strict';
+
+ return Checkbox.extend({
+ defaults: {
+ listens: {
+ inputType: 'onInputTypeChange'
+ }
+ },
+
+ /**
+ * Handler for "inputType" property
+ *
+ * @param {String} data
+ */
+ onInputTypeChange: function (data) {
+ data === 'checkbox' || data === 'multi' ?
+ this.clear()
+ .visible(false) :
+ this.visible(true);
+ }
+ });
+});
diff --git a/app/code/Magento/Bundle/view/frontend/layout/checkout_cart_item_renderers.xml b/app/code/Magento/Bundle/view/frontend/layout/checkout_cart_item_renderers.xml
index 5137782656269..aa74a80e64750 100644
--- a/app/code/Magento/Bundle/view/frontend/layout/checkout_cart_item_renderers.xml
+++ b/app/code/Magento/Bundle/view/frontend/layout/checkout_cart_item_renderers.xml
@@ -8,7 +8,7 @@
-
+
diff --git a/app/code/Magento/Bundle/view/frontend/layout/checkout_onepage_review_item_renderers.xml b/app/code/Magento/Bundle/view/frontend/layout/checkout_onepage_review_item_renderers.xml
index dce5945e3221d..686e12da7de21 100644
--- a/app/code/Magento/Bundle/view/frontend/layout/checkout_onepage_review_item_renderers.xml
+++ b/app/code/Magento/Bundle/view/frontend/layout/checkout_onepage_review_item_renderers.xml
@@ -8,7 +8,7 @@
-
+
diff --git a/app/code/Magento/Bundle/view/frontend/layout/sales_email_order_creditmemo_renderers.xml b/app/code/Magento/Bundle/view/frontend/layout/sales_email_order_creditmemo_renderers.xml
index 97f07511d2fda..f92a04de2b57c 100644
--- a/app/code/Magento/Bundle/view/frontend/layout/sales_email_order_creditmemo_renderers.xml
+++ b/app/code/Magento/Bundle/view/frontend/layout/sales_email_order_creditmemo_renderers.xml
@@ -8,7 +8,7 @@
-
+
diff --git a/app/code/Magento/Bundle/view/frontend/layout/sales_email_order_invoice_renderers.xml b/app/code/Magento/Bundle/view/frontend/layout/sales_email_order_invoice_renderers.xml
index 1041965f2c77b..051365f99021f 100644
--- a/app/code/Magento/Bundle/view/frontend/layout/sales_email_order_invoice_renderers.xml
+++ b/app/code/Magento/Bundle/view/frontend/layout/sales_email_order_invoice_renderers.xml
@@ -8,7 +8,7 @@
-
+
diff --git a/app/code/Magento/Bundle/view/frontend/layout/sales_email_order_renderers.xml b/app/code/Magento/Bundle/view/frontend/layout/sales_email_order_renderers.xml
index f46300afd518c..b48e196c52866 100644
--- a/app/code/Magento/Bundle/view/frontend/layout/sales_email_order_renderers.xml
+++ b/app/code/Magento/Bundle/view/frontend/layout/sales_email_order_renderers.xml
@@ -8,7 +8,7 @@
-
+
diff --git a/app/code/Magento/Bundle/view/frontend/layout/sales_email_order_shipment_renderers.xml b/app/code/Magento/Bundle/view/frontend/layout/sales_email_order_shipment_renderers.xml
index d6e8812b02165..c2c80e0ba6763 100644
--- a/app/code/Magento/Bundle/view/frontend/layout/sales_email_order_shipment_renderers.xml
+++ b/app/code/Magento/Bundle/view/frontend/layout/sales_email_order_shipment_renderers.xml
@@ -8,7 +8,7 @@
-
+
diff --git a/app/code/Magento/Bundle/view/frontend/layout/sales_order_creditmemo_renderers.xml b/app/code/Magento/Bundle/view/frontend/layout/sales_order_creditmemo_renderers.xml
index da9b7fa494ed4..deb7334b726b3 100644
--- a/app/code/Magento/Bundle/view/frontend/layout/sales_order_creditmemo_renderers.xml
+++ b/app/code/Magento/Bundle/view/frontend/layout/sales_order_creditmemo_renderers.xml
@@ -8,7 +8,7 @@
-
+
diff --git a/app/code/Magento/Bundle/view/frontend/layout/sales_order_invoice_renderers.xml b/app/code/Magento/Bundle/view/frontend/layout/sales_order_invoice_renderers.xml
index a1c1d23ad4788..c4865e22ad9be 100644
--- a/app/code/Magento/Bundle/view/frontend/layout/sales_order_invoice_renderers.xml
+++ b/app/code/Magento/Bundle/view/frontend/layout/sales_order_invoice_renderers.xml
@@ -8,7 +8,7 @@
-
+
diff --git a/app/code/Magento/Bundle/view/frontend/layout/sales_order_item_renderers.xml b/app/code/Magento/Bundle/view/frontend/layout/sales_order_item_renderers.xml
index c02075bee5e59..86cfb1c0f7ade 100644
--- a/app/code/Magento/Bundle/view/frontend/layout/sales_order_item_renderers.xml
+++ b/app/code/Magento/Bundle/view/frontend/layout/sales_order_item_renderers.xml
@@ -8,7 +8,7 @@
-
+
diff --git a/app/code/Magento/Bundle/view/frontend/layout/sales_order_print_creditmemo_renderers.xml b/app/code/Magento/Bundle/view/frontend/layout/sales_order_print_creditmemo_renderers.xml
index 2f16633a2bcca..bd1310acbcdee 100644
--- a/app/code/Magento/Bundle/view/frontend/layout/sales_order_print_creditmemo_renderers.xml
+++ b/app/code/Magento/Bundle/view/frontend/layout/sales_order_print_creditmemo_renderers.xml
@@ -8,7 +8,7 @@
-
+
diff --git a/app/code/Magento/Bundle/view/frontend/layout/sales_order_print_invoice_renderers.xml b/app/code/Magento/Bundle/view/frontend/layout/sales_order_print_invoice_renderers.xml
index f143da9b57dfe..5bea1dd1c672e 100644
--- a/app/code/Magento/Bundle/view/frontend/layout/sales_order_print_invoice_renderers.xml
+++ b/app/code/Magento/Bundle/view/frontend/layout/sales_order_print_invoice_renderers.xml
@@ -8,7 +8,7 @@
-
+
diff --git a/app/code/Magento/Bundle/view/frontend/layout/sales_order_print_renderers.xml b/app/code/Magento/Bundle/view/frontend/layout/sales_order_print_renderers.xml
index 384a0665a80f7..27c7aabfeb195 100644
--- a/app/code/Magento/Bundle/view/frontend/layout/sales_order_print_renderers.xml
+++ b/app/code/Magento/Bundle/view/frontend/layout/sales_order_print_renderers.xml
@@ -8,7 +8,7 @@
-
+
diff --git a/app/code/Magento/Bundle/view/frontend/layout/sales_order_print_shipment_renderers.xml b/app/code/Magento/Bundle/view/frontend/layout/sales_order_print_shipment_renderers.xml
index bfa85a0a9c9d8..481d72ae10bdb 100644
--- a/app/code/Magento/Bundle/view/frontend/layout/sales_order_print_shipment_renderers.xml
+++ b/app/code/Magento/Bundle/view/frontend/layout/sales_order_print_shipment_renderers.xml
@@ -8,7 +8,7 @@
-
+
diff --git a/app/code/Magento/Bundle/view/frontend/layout/sales_order_shipment_renderers.xml b/app/code/Magento/Bundle/view/frontend/layout/sales_order_shipment_renderers.xml
index e1b6ba0e86130..99124dfeeef93 100644
--- a/app/code/Magento/Bundle/view/frontend/layout/sales_order_shipment_renderers.xml
+++ b/app/code/Magento/Bundle/view/frontend/layout/sales_order_shipment_renderers.xml
@@ -8,7 +8,7 @@
-
+
diff --git a/app/code/Magento/Bundle/view/frontend/templates/sales/order/creditmemo/items/renderer.phtml b/app/code/Magento/Bundle/view/frontend/templates/sales/order/creditmemo/items/renderer.phtml
index b4c2c00e5ac43..b9d075966c5d1 100644
--- a/app/code/Magento/Bundle/view/frontend/templates/sales/order/creditmemo/items/renderer.phtml
+++ b/app/code/Magento/Bundle/view/frontend/templates/sales/order/creditmemo/items/renderer.phtml
@@ -12,7 +12,6 @@
getChildren($parentItem) ?>
getItem()->getOrderItem()->getOrder() ?>
-
diff --git a/app/code/Magento/Bundle/view/frontend/templates/sales/order/invoice/items/renderer.phtml b/app/code/Magento/Bundle/view/frontend/templates/sales/order/invoice/items/renderer.phtml
index f3866d7b1b682..e18d75ce77b9c 100644
--- a/app/code/Magento/Bundle/view/frontend/templates/sales/order/invoice/items/renderer.phtml
+++ b/app/code/Magento/Bundle/view/frontend/templates/sales/order/invoice/items/renderer.phtml
@@ -12,7 +12,6 @@
getItem()->getOrderItem()->getOrder() ?>
getChildren($parentItem) ?>
-
diff --git a/app/code/Magento/Bundle/view/frontend/templates/sales/order/items/renderer.phtml b/app/code/Magento/Bundle/view/frontend/templates/sales/order/items/renderer.phtml
index 84cbd54d3bdcc..063d66edb9e70 100644
--- a/app/code/Magento/Bundle/view/frontend/templates/sales/order/items/renderer.phtml
+++ b/app/code/Magento/Bundle/view/frontend/templates/sales/order/items/renderer.phtml
@@ -10,7 +10,6 @@
?>
getItem() ?>
getChildrenItems()); ?>
-
diff --git a/app/code/Magento/Bundle/view/frontend/templates/sales/order/shipment/items/renderer.phtml b/app/code/Magento/Bundle/view/frontend/templates/sales/order/shipment/items/renderer.phtml
index bd99afc59a8c0..0cd39156b2513 100644
--- a/app/code/Magento/Bundle/view/frontend/templates/sales/order/shipment/items/renderer.phtml
+++ b/app/code/Magento/Bundle/view/frontend/templates/sales/order/shipment/items/renderer.phtml
@@ -12,7 +12,6 @@
getItem() ?>
getOrderItem()], $parentItem->getOrderItem()->getChildrenItems()) ?>
getChildren($parentItem) ?>
-
diff --git a/app/code/Magento/Bundle/view/frontend/web/js/product-summary.js b/app/code/Magento/Bundle/view/frontend/web/js/product-summary.js
index d8d4cb1e99b7f..1e7fe6b6673d6 100644
--- a/app/code/Magento/Bundle/view/frontend/web/js/product-summary.js
+++ b/app/code/Magento/Bundle/view/frontend/web/js/product-summary.js
@@ -56,8 +56,9 @@ define([
// Clear Summary box
this.element.html('');
-
- $.each(this.cache.currentElement.selected, $.proxy(this._renderOption, this));
+ this.cache.currentElement.positions.forEach(function (optionId) {
+ this._renderOption(optionId, this.cache.currentElement.selected[optionId]);
+ }, this);
this.element
.parents(this.options.bundleSummaryContainer)
.toggleClass('empty', !this.cache.currentElementCount); // Zero elements equal '.empty' container
diff --git a/app/code/Magento/BundleImportExport/Model/Export/RowCustomizer.php b/app/code/Magento/BundleImportExport/Model/Export/RowCustomizer.php
index 9b8518a41ffff..15e4dafc7cb1b 100644
--- a/app/code/Magento/BundleImportExport/Model/Export/RowCustomizer.php
+++ b/app/code/Magento/BundleImportExport/Model/Export/RowCustomizer.php
@@ -9,9 +9,10 @@
use Magento\CatalogImportExport\Model\Export\RowCustomizerInterface;
use Magento\CatalogImportExport\Model\Import\Product as ImportProductModel;
use Magento\Bundle\Model\ResourceModel\Selection\Collection as SelectionCollection;
-use Magento\ImportExport\Controller\Adminhtml\Import;
use Magento\ImportExport\Model\Import as ImportModel;
use \Magento\Catalog\Model\Product\Type\AbstractType;
+use \Magento\Framework\App\ObjectManager;
+use \Magento\Store\Model\StoreManagerInterface;
/**
* Class RowCustomizer
@@ -105,6 +106,35 @@ class RowCustomizer implements RowCustomizerInterface
AbstractType::SHIPMENT_SEPARATELY => 'separately',
];
+ /**
+ * @var \Magento\Bundle\Model\ResourceModel\Option\Collection[]
+ */
+ private $optionCollections = [];
+
+ /**
+ * @var array
+ */
+ private $storeIdToCode = [];
+
+ /**
+ * @var string
+ */
+ private $optionCollectionCacheKey = '_cache_instance_options_collection';
+
+ /**
+ * @var StoreManagerInterface
+ */
+ private $storeManager;
+
+ /**
+ * @param StoreManagerInterface $storeManager
+ * @throws \RuntimeException
+ */
+ public function __construct(StoreManagerInterface $storeManager)
+ {
+ $this->storeManager = $storeManager;
+ }
+
/**
* Retrieve list of bundle specific columns
* @return array
@@ -207,15 +237,13 @@ protected function populateBundleData($collection)
*/
protected function getFormattedBundleOptionValues($product)
{
- /** @var \Magento\Bundle\Model\ResourceModel\Option\Collection $optionsCollection */
- $optionsCollection = $product->getTypeInstance()
- ->getOptionsCollection($product)
- ->setOrder('position', Collection::SORT_ORDER_ASC);
-
+ $optionCollections = $this->getProductOptionCollections($product);
$bundleData = '';
- foreach ($optionsCollection as $option) {
+ $optionTitles = $this->getBundleOptionTitles($product);
+ foreach ($optionCollections->getItems() as $option) {
+ $optionValues = $this->getFormattedOptionValues($option, $optionTitles);
$bundleData .= $this->getFormattedBundleSelections(
- $this->getFormattedOptionValues($option),
+ $optionValues,
$product->getTypeInstance()
->getSelectionsCollection([$option->getId()], $product)
->setOrder('position', Collection::SORT_ORDER_ASC)
@@ -266,16 +294,23 @@ function ($value, $key) {
* Retrieve option value of bundle product
*
* @param \Magento\Bundle\Model\Option $option
+ * @param string[] $optionTitles
* @return string
*/
- protected function getFormattedOptionValues($option)
+ protected function getFormattedOptionValues($option, $optionTitles = [])
{
- return 'name' . ImportProductModel::PAIR_NAME_VALUE_SEPARATOR
- . $option->getTitle() . ImportModel::DEFAULT_GLOBAL_MULTI_VALUE_SEPARATOR
- . 'type' . ImportProductModel::PAIR_NAME_VALUE_SEPARATOR
- . $option->getType() . ImportModel::DEFAULT_GLOBAL_MULTI_VALUE_SEPARATOR
- . 'required' . ImportProductModel::PAIR_NAME_VALUE_SEPARATOR
- . $option->getRequired();
+ $names = implode(ImportModel::DEFAULT_GLOBAL_MULTI_VALUE_SEPARATOR, array_map(
+ function ($title, $storeName) {
+ return $storeName . ImportProductModel::PAIR_NAME_VALUE_SEPARATOR . $title;
+ },
+ $optionTitles[$option->getOptionId()],
+ array_keys($optionTitles[$option->getOptionId()])
+ ));
+ return $names . ImportModel::DEFAULT_GLOBAL_MULTI_VALUE_SEPARATOR
+ . 'type' . ImportProductModel::PAIR_NAME_VALUE_SEPARATOR
+ . $option->getType() . ImportModel::DEFAULT_GLOBAL_MULTI_VALUE_SEPARATOR
+ . 'required' . ImportProductModel::PAIR_NAME_VALUE_SEPARATOR
+ . $option->getRequired();
}
/**
@@ -286,7 +321,7 @@ protected function getFormattedOptionValues($option)
*/
protected function getTypeValue($type)
{
- return isset($this->typeMapping[$type]) ? $this->typeMapping[$type] : self::VALUE_DYNAMIC;
+ return $this->typeMapping[$type] ?? self::VALUE_DYNAMIC;
}
/**
@@ -297,7 +332,7 @@ protected function getTypeValue($type)
*/
protected function getPriceViewValue($type)
{
- return isset($this->priceViewMapping[$type]) ? $this->priceViewMapping[$type] : self::VALUE_PRICE_RANGE;
+ return $this->priceViewMapping[$type] ?? self::VALUE_PRICE_RANGE;
}
/**
@@ -308,7 +343,7 @@ protected function getPriceViewValue($type)
*/
protected function getPriceTypeValue($type)
{
- return isset($this->priceTypeMapping[$type]) ? $this->priceTypeMapping[$type] : null;
+ return $this->priceTypeMapping[$type] ?? null;
}
/**
@@ -319,7 +354,7 @@ protected function getPriceTypeValue($type)
*/
private function getShipmentTypeValue($type)
{
- return isset($this->shipmentTypeMapping[$type]) ? $this->shipmentTypeMapping[$type] : null;
+ return $this->shipmentTypeMapping[$type] ?? null;
}
/**
@@ -380,4 +415,82 @@ private function parseAdditionalAttributes($additionalAttributes)
}
return $preparedAttributes;
}
+
+ /**
+ * Get product options titles.
+ *
+ * Values for all store views (default) should be specified with 'name' key.
+ * If user want to specify value or change existing for non default store views it should be specified with
+ * 'name_' prefix and needed store view suffix.
+ *
+ * For example:
+ * - 'name=All store views name' for all store views
+ * - 'name_specific_store=Specific store name' for store view with 'specific_store' store code
+ *
+ * @param \Magento\Catalog\Model\Product $product
+ * @return array
+ */
+ private function getBundleOptionTitles(\Magento\Catalog\Model\Product $product): array
+ {
+ $optionCollections = $this->getProductOptionCollections($product);
+ $optionsTitles = [];
+ /** @var \Magento\Bundle\Model\Option $option */
+ foreach ($optionCollections->getItems() as $option) {
+ $optionsTitles[$option->getId()]['name'] = $option->getTitle();
+ }
+ $storeIds = $product->getStoreIds();
+ if (count($storeIds) > 1) {
+ foreach ($storeIds as $storeId) {
+ $optionCollections = $this->getProductOptionCollections($product, $storeId);
+ /** @var \Magento\Bundle\Model\Option $option */
+ foreach ($optionCollections->getItems() as $option) {
+ $optionTitle = $option->getTitle();
+ if ($optionsTitles[$option->getId()]['name'] != $optionTitle) {
+ $optionsTitles[$option->getId()]['name_' . $this->getStoreCodeById($storeId)] = $optionTitle;
+ }
+ }
+ }
+ }
+ return $optionsTitles;
+ }
+
+ /**
+ * Get product options collection by provided product model.
+ *
+ * Set given store id to the product if it was defined (default store id will be set if was not).
+ *
+ * @param \Magento\Catalog\Model\Product $product $product
+ * @param int $storeId
+ * @return \Magento\Bundle\Model\ResourceModel\Option\Collection
+ */
+ private function getProductOptionCollections(
+ \Magento\Catalog\Model\Product $product,
+ $storeId = \Magento\Store\Model\Store::DEFAULT_STORE_ID
+ ): \Magento\Bundle\Model\ResourceModel\Option\Collection {
+ $productSku = $product->getSku();
+ if (!isset($this->optionCollections[$productSku][$storeId])) {
+ $product->unsetData($this->optionCollectionCacheKey);
+ $product->setStoreId($storeId);
+ $this->optionCollections[$productSku][$storeId] = $product->getTypeInstance()
+ ->getOptionsCollection($product)
+ ->setOrder('position', Collection::SORT_ORDER_ASC);
+ }
+ return $this->optionCollections[$productSku][$storeId];
+ }
+
+ /**
+ * Retrieve store code by it's ID.
+ *
+ * Collect store id in $storeIdToCode[] private variable if it was not initialized earlier.
+ *
+ * @param int $storeId
+ * @return string
+ */
+ private function getStoreCodeById($storeId): string
+ {
+ if (!isset($this->storeIdToCode[$storeId])) {
+ $this->storeIdToCode[$storeId] = $this->storeManager->getStore($storeId)->getCode();
+ }
+ return $this->storeIdToCode[$storeId];
+ }
}
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 96b7c7b1430b0..0acaf1aecd2ad 100644
--- a/app/code/Magento/BundleImportExport/Model/Import/Product/Type/Bundle.php
+++ b/app/code/Magento/BundleImportExport/Model/Import/Product/Type/Bundle.php
@@ -8,15 +8,18 @@
*/
namespace Magento\BundleImportExport\Model\Import\Product\Type;
-use \Magento\Framework\App\ObjectManager;
-use \Magento\Bundle\Model\Product\Price as BundlePrice;
-use \Magento\Catalog\Model\Product\Type\AbstractType;
+use Magento\Framework\App\ObjectManager;
+use Magento\Bundle\Model\Product\Price as BundlePrice;
+use Magento\Catalog\Model\Product\Type\AbstractType;
use Magento\CatalogImportExport\Model\Import\Product;
+use Magento\Store\Model\StoreManagerInterface;
+use Magento\Bundle\Model\ResourceModel\Bundle as BundleResourceModel;
/**
* Class Bundle
* @package Magento\BundleImportExport\Model\Import\Product\Type
* @SuppressWarnings(PHPMD.ExcessiveClassComplexity)
+ * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
*/
class Bundle extends \Magento\CatalogImportExport\Model\Import\Product\Type\AbstractType
{
@@ -136,6 +139,21 @@ class Bundle extends \Magento\CatalogImportExport\Model\Import\Product\Type\Abst
*/
private $relationsDataSaver;
+ /**
+ * @var StoreManagerInterface
+ */
+ private $storeManager;
+
+ /**
+ * @var array
+ */
+ private $storeCodeToId = [];
+
+ /**
+ * @var BundleResourceModel
+ */
+ private $bundleResourceModel;
+
/**
* @param \Magento\Eav\Model\ResourceModel\Entity\Attribute\Set\CollectionFactory $attrSetColFac
* @param \Magento\Catalog\Model\ResourceModel\Product\Attribute\CollectionFactory $prodAttrColFac
@@ -143,6 +161,9 @@ class Bundle extends \Magento\CatalogImportExport\Model\Import\Product\Type\Abst
* @param array $params
* @param \Magento\Framework\EntityManager\MetadataPool|null $metadataPool
* @param Bundle\RelationsDataSaver|null $relationsDataSaver
+ * @param StoreManagerInterface $storeManager
+ * @param BundleResourceModel $bundleResourceModel
+ * @throws \Magento\Framework\Exception\LocalizedException
*/
public function __construct(
\Magento\Eav\Model\ResourceModel\Entity\Attribute\Set\CollectionFactory $attrSetColFac,
@@ -150,12 +171,17 @@ public function __construct(
\Magento\Framework\App\ResourceConnection $resource,
array $params,
\Magento\Framework\EntityManager\MetadataPool $metadataPool = null,
- Bundle\RelationsDataSaver $relationsDataSaver = null
+ Bundle\RelationsDataSaver $relationsDataSaver = null,
+ StoreManagerInterface $storeManager = null,
+ BundleResourceModel $bundleResourceModel = null
) {
parent::__construct($attrSetColFac, $prodAttrColFac, $resource, $params, $metadataPool);
-
$this->relationsDataSaver = $relationsDataSaver
?: ObjectManager::getInstance()->get(Bundle\RelationsDataSaver::class);
+ $this->storeManager = $storeManager
+ ?: ObjectManager::getInstance()->get(StoreManagerInterface::class);
+ $this->bundleResourceModel = $bundleResourceModel
+ ?: ObjectManager::getInstance()->get(BundleResourceModel::class);
}
/**
@@ -261,20 +287,28 @@ protected function populateOptionTemplate($option, $entityId, $index = null)
* @param array $option
* @param int $optionId
* @param int $storeId
- *
- * @return array|bool
+ * @return array
*/
protected function populateOptionValueTemplate($option, $optionId, $storeId = 0)
{
- if (!isset($option['name']) || !isset($option['parent_id']) || !$optionId) {
- return false;
+ $optionValues = [];
+ if (isset($option['name']) && isset($option['parent_id']) && $optionId) {
+ $pattern = '/^name[_]?(.*)/';
+ $keys = array_keys($option);
+ $optionNames = preg_grep($pattern, $keys);
+ foreach ($optionNames as $optionName) {
+ preg_match($pattern, $optionName, $storeCodes);
+ $storeCode = array_pop($storeCodes);
+ $storeId = $storeCode ? $this->getStoreIdByCode($storeCode) : $storeId;
+ $optionValues[] = [
+ 'option_id' => $optionId,
+ 'parent_product_id' => $option['parent_id'],
+ 'store_id' => $storeId,
+ 'title' => $option[$optionName],
+ ];
+ }
}
- return [
- 'option_id' => $optionId,
- 'parent_product_id' => $option['parent_id'],
- 'store_id' => $storeId,
- 'title' => $option['name'],
- ];
+ return $optionValues;
}
/**
@@ -284,7 +318,7 @@ protected function populateOptionValueTemplate($option, $optionId, $storeId = 0)
* @param int $optionId
* @param int $parentId
* @param int $index
- * @return array
+ * @return array|bool
* @SuppressWarnings(PHPMD.NPathComplexity)
* @SuppressWarnings(PHPMD.CyclomaticComplexity)
*/
@@ -364,13 +398,15 @@ public function saveData()
if ($this->_type != $productData['type_id']) {
continue;
}
- $this->parseSelections($rowData, $productData[$this->getProductEntityLinkField()]);
+ $productId = $productData[$this->getProductEntityLinkField()];
+ $this->parseSelections($rowData, $productId);
}
if (!empty($this->_cachedOptions)) {
$this->retrieveProducsByCachedSkus();
$this->populateExistingOptions();
$this->insertOptions();
$this->insertSelections();
+ $this->insertProductRelations();
$this->clear();
}
}
@@ -564,21 +600,24 @@ protected function insertOptions()
*/
protected function populateInsertOptionValues($optionIds)
{
- $insertValues = [];
+ $optionValues = [];
foreach ($this->_cachedOptions as $entityId => $options) {
foreach ($options as $key => $option) {
foreach ($optionIds as $optionId => $assoc) {
if ($assoc['position'] == $this->_cachedOptions[$entityId][$key]['index']
&& $assoc['parent_id'] == $entityId) {
$option['parent_id'] = $entityId;
- $insertValues[] = $this->populateOptionValueTemplate($option, $optionId);
+ $optionValues = array_merge(
+ $optionValues,
+ $this->populateOptionValueTemplate($option, $optionId)
+ );
$this->_cachedOptions[$entityId][$key]['option_id'] = $optionId;
break;
}
}
}
}
- return $insertValues;
+ return $optionValues;
}
/**
@@ -615,6 +654,29 @@ protected function insertSelections()
return $this;
}
+ /**
+ * Insert product relations.
+ *
+ * @return void
+ */
+ private function insertProductRelations()
+ {
+ foreach ($this->_cachedOptions as $productId => $options) {
+ $childIds = [];
+ foreach ($options as $option) {
+ foreach ($option['selections'] as $selection) {
+ if (isset($this->_cachedSkuToProducts[$selection['sku']])) {
+ $childIds[] = $this->_cachedSkuToProducts[$selection['sku']];
+ }
+ }
+ }
+ if (!empty($childIds)) {
+ $childIds = array_unique($childIds);
+ $this->bundleResourceModel->saveProductRelations($productId, $childIds);
+ }
+ }
+ }
+
/**
* Initialize attributes parameters for all attributes' sets.
*
@@ -695,4 +757,21 @@ protected function clear()
$this->_cachedSkuToProducts = [];
return $this;
}
+
+ /**
+ * Get store id by store code.
+ *
+ * @param string $storeCode
+ * @return int
+ */
+ private function getStoreIdByCode(string $storeCode): int
+ {
+ if (!isset($this->storeIdToCode[$storeCode])) {
+ /** @var $store \Magento\Store\Model\Store */
+ foreach ($this->storeManager->getStores() as $store) {
+ $this->storeCodeToId[$store->getCode()] = $store->getId();
+ }
+ }
+ return $this->storeCodeToId[$storeCode];
+ }
}
diff --git a/app/code/Magento/BundleImportExport/Test/Mftf/LICENSE.txt b/app/code/Magento/BundleImportExport/Test/Mftf/LICENSE.txt
new file mode 100644
index 0000000000000..49525fd99da9c
--- /dev/null
+++ b/app/code/Magento/BundleImportExport/Test/Mftf/LICENSE.txt
@@ -0,0 +1,48 @@
+
+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/BundleImportExport/Test/Mftf/LICENSE_AFL.txt b/app/code/Magento/BundleImportExport/Test/Mftf/LICENSE_AFL.txt
new file mode 100644
index 0000000000000..f39d641b18a19
--- /dev/null
+++ b/app/code/Magento/BundleImportExport/Test/Mftf/LICENSE_AFL.txt
@@ -0,0 +1,48 @@
+
+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/BundleImportExport/Test/Mftf/README.md b/app/code/Magento/BundleImportExport/Test/Mftf/README.md
new file mode 100644
index 0000000000000..e4c6855132d05
--- /dev/null
+++ b/app/code/Magento/BundleImportExport/Test/Mftf/README.md
@@ -0,0 +1,3 @@
+# Bundle Import Export Functional Tests
+
+The Functional Test Module for **Magento Bundle Import Export** module.
diff --git a/app/code/Magento/BundleImportExport/Test/Unit/Model/Export/Product/RowCustomizerTest.php b/app/code/Magento/BundleImportExport/Test/Unit/Model/Export/Product/RowCustomizerTest.php
index e76e9e1ba565f..027d30e0da39d 100644
--- a/app/code/Magento/BundleImportExport/Test/Unit/Model/Export/Product/RowCustomizerTest.php
+++ b/app/code/Magento/BundleImportExport/Test/Unit/Model/Export/Product/RowCustomizerTest.php
@@ -52,14 +52,24 @@ class RowCustomizerTest extends \PHPUnit\Framework\TestCase
*/
protected $selection;
+ /** @var \Magento\Framework\App\ScopeResolverInterface|\PHPUnit_Framework_MockObject_MockObject */
+ private $scopeResolver;
+
/**
* Set up
*/
protected function setUp()
{
$this->objectManagerHelper = new ObjectManagerHelper($this);
+ $this->scopeResolver = $this->getMockBuilder(\Magento\Framework\App\ScopeResolverInterface::class)
+ ->disableOriginalConstructor()
+ ->setMethods(['getScope'])
+ ->getMockForAbstractClass();
$this->rowCustomizerMock = $this->objectManagerHelper->getObject(
- \Magento\BundleImportExport\Model\Export\RowCustomizer::class
+ \Magento\BundleImportExport\Model\Export\RowCustomizer::class,
+ [
+ 'scopeResolver' => $this->scopeResolver
+ ]
);
$this->productResourceCollection = $this->createPartialMock(
\Magento\Catalog\Model\ResourceModel\Product\Collection::class,
@@ -72,6 +82,8 @@ protected function setUp()
'getPriceType',
'getShipmentType',
'getSkuType',
+ 'getSku',
+ 'getStoreIds',
'getPriceView',
'getWeightType',
'getTypeInstance',
@@ -79,6 +91,7 @@ protected function setUp()
'getSelectionsCollection'
]
);
+ $this->product->expects($this->any())->method('getStoreIds')->willReturn([1]);
$this->product->expects($this->any())->method('getEntityId')->willReturn(1);
$this->product->expects($this->any())->method('getPriceType')->willReturn(1);
$this->product->expects($this->any())->method('getShipmentType')->willReturn(1);
@@ -88,19 +101,20 @@ protected function setUp()
$this->product->expects($this->any())->method('getTypeInstance')->willReturnSelf();
$this->optionsCollection = $this->createPartialMock(
\Magento\Bundle\Model\ResourceModel\Option\Collection::class,
- ['setOrder', 'getIterator']
+ ['setOrder', 'getItems']
);
$this->product->expects($this->any())->method('getOptionsCollection')->willReturn($this->optionsCollection);
$this->optionsCollection->expects($this->any())->method('setOrder')->willReturnSelf();
$this->option = $this->createPartialMock(
\Magento\Bundle\Model\Option::class,
- ['getId', 'getTitle', 'getType', 'getRequired']
+ ['getId', 'getOptionId', 'getTitle', 'getType', 'getRequired']
);
$this->option->expects($this->any())->method('getId')->willReturn(1);
+ $this->option->expects($this->any())->method('getOptionId')->willReturn(1);
$this->option->expects($this->any())->method('getTitle')->willReturn('title');
$this->option->expects($this->any())->method('getType')->willReturn(1);
$this->option->expects($this->any())->method('getRequired')->willReturn(1);
- $this->optionsCollection->expects($this->any())->method('getIterator')->will(
+ $this->optionsCollection->expects($this->any())->method('getItems')->will(
$this->returnValue(new \ArrayIterator([$this->option]))
);
$this->selection = $this->createPartialMock(
@@ -122,6 +136,7 @@ protected function setUp()
$this->product->expects($this->any())->method('getSelectionsCollection')->willReturn(
$this->selectionsCollection
);
+ $this->product->expects($this->any())->method('getSku')->willReturn(1);
$this->productResourceCollection->expects($this->any())->method('addAttributeToFilter')->willReturnSelf();
$this->productResourceCollection->expects($this->any())->method('getIterator')->will(
$this->returnValue(new \ArrayIterator([$this->product]))
@@ -133,6 +148,9 @@ protected function setUp()
*/
public function testPrepareData()
{
+ $scope = $this->getMockBuilder(\Magento\Framework\App\ScopeInterface::class)->getMockForAbstractClass();
+ $this->scopeResolver->expects($this->any())->method('getScope')
+ ->willReturn($scope);
$result = $this->rowCustomizerMock->prepareData($this->productResourceCollection, [1]);
$this->assertNotNull($result);
}
@@ -160,6 +178,9 @@ public function testAddHeaderColumns()
*/
public function testAddData()
{
+ $scope = $this->getMockBuilder(\Magento\Framework\App\ScopeInterface::class)->getMockForAbstractClass();
+ $this->scopeResolver->expects($this->any())->method('getScope')
+ ->willReturn($scope);
$preparedData = $this->rowCustomizerMock->prepareData($this->productResourceCollection, [1]);
$attributes = 'attribute=1,sku_type=1,attribute2="Text",price_type=1,price_view=1,weight_type=1,'
. 'values=values,shipment_type=1,attribute3=One,Two,Three';
diff --git a/app/code/Magento/BundleImportExport/Test/Unit/Model/Import/Product/Type/BundleTest.php b/app/code/Magento/BundleImportExport/Test/Unit/Model/Import/Product/Type/BundleTest.php
index 8e1243b5eb3af..773fa6a5349a5 100644
--- a/app/code/Magento/BundleImportExport/Test/Unit/Model/Import/Product/Type/BundleTest.php
+++ b/app/code/Magento/BundleImportExport/Test/Unit/Model/Import/Product/Type/BundleTest.php
@@ -58,6 +58,9 @@ class BundleTest extends \Magento\ImportExport\Test\Unit\Model\Import\AbstractIm
*/
protected $setCollection;
+ /** @var \Magento\Framework\App\ScopeResolverInterface|\PHPUnit_Framework_MockObject_MockObject */
+ private $scopeResolver;
+
/**
*
* @return void
@@ -170,14 +173,18 @@ protected function setUp()
0 => $this->entityModel,
1 => 'bundle'
];
-
+ $this->scopeResolver = $this->getMockBuilder(\Magento\Framework\App\ScopeResolverInterface::class)
+ ->disableOriginalConstructor()
+ ->setMethods(['getScope'])
+ ->getMockForAbstractClass();
$this->bundle = $this->objectManagerHelper->getObject(
\Magento\BundleImportExport\Model\Import\Product\Type\Bundle::class,
[
'attrSetColFac' => $this->attrSetColFac,
'prodAttrColFac' => $this->prodAttrColFac,
'resource' => $this->resource,
- 'params' => $this->params
+ 'params' => $this->params,
+ 'scopeResolver' => $this->scopeResolver
]
);
@@ -214,7 +221,8 @@ public function testSaveData($skus, $bunch, $allowImport)
$this->entityModel->expects($this->any())->method('isRowAllowedToImport')->will($this->returnValue(
$allowImport
));
-
+ $scope = $this->getMockBuilder(\Magento\Framework\App\ScopeInterface::class)->getMockForAbstractClass();
+ $this->scopeResolver->expects($this->any())->method('getScope')->willReturn($scope);
$this->connection->expects($this->any())->method('fetchAssoc')->with($this->select)->will($this->returnValue([
'1' => [
'option_id' => '1',
diff --git a/app/code/Magento/BundleImportExport/composer.json b/app/code/Magento/BundleImportExport/composer.json
index 8c45c565785b7..b21da5c7ae5b9 100644
--- a/app/code/Magento/BundleImportExport/composer.json
+++ b/app/code/Magento/BundleImportExport/composer.json
@@ -2,16 +2,17 @@
"name": "magento/module-bundle-import-export",
"description": "N/A",
"require": {
- "php": "7.0.2|7.0.4|~7.0.6|~7.1.0",
- "magento/module-catalog": "101.1.*",
+ "php": "~7.0.13|~7.1.0",
+ "magento/module-catalog": "102.0.*",
"magento/module-import-export": "100.2.*",
"magento/module-catalog-import-export": "100.2.*",
"magento/module-bundle": "100.2.*",
- "magento/module-eav": "100.2.*",
- "magento/framework": "100.2.*"
+ "magento/module-store": "100.2.*",
+ "magento/module-eav": "101.0.*",
+ "magento/framework": "101.0.*"
},
"type": "magento2-module",
- "version": "100.2.0-dev",
+ "version": "100.2.3",
"license": [
"OSL-3.0",
"AFL-3.0"
diff --git a/app/code/Magento/CacheInvalidate/Test/Mftf/LICENSE.txt b/app/code/Magento/CacheInvalidate/Test/Mftf/LICENSE.txt
new file mode 100644
index 0000000000000..49525fd99da9c
--- /dev/null
+++ b/app/code/Magento/CacheInvalidate/Test/Mftf/LICENSE.txt
@@ -0,0 +1,48 @@
+
+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/CacheInvalidate/Test/Mftf/LICENSE_AFL.txt b/app/code/Magento/CacheInvalidate/Test/Mftf/LICENSE_AFL.txt
new file mode 100644
index 0000000000000..f39d641b18a19
--- /dev/null
+++ b/app/code/Magento/CacheInvalidate/Test/Mftf/LICENSE_AFL.txt
@@ -0,0 +1,48 @@
+
+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/CacheInvalidate/Test/Mftf/README.md b/app/code/Magento/CacheInvalidate/Test/Mftf/README.md
new file mode 100644
index 0000000000000..403a6f15d089d
--- /dev/null
+++ b/app/code/Magento/CacheInvalidate/Test/Mftf/README.md
@@ -0,0 +1,3 @@
+# Cache Invalidate Functional Tests
+
+The Functional Test Module for **Magento Cache Invalidate** module.
diff --git a/app/code/Magento/CacheInvalidate/Test/Unit/Model/PurgeCacheTest.php b/app/code/Magento/CacheInvalidate/Test/Unit/Model/PurgeCacheTest.php
index 013ec3a467104..c66e27ea41025 100644
--- a/app/code/Magento/CacheInvalidate/Test/Unit/Model/PurgeCacheTest.php
+++ b/app/code/Magento/CacheInvalidate/Test/Unit/Model/PurgeCacheTest.php
@@ -84,6 +84,9 @@ public function testSendPurgeRequest($hosts)
$this->assertTrue($this->model->sendPurgeRequest('tags'));
}
+ /**
+ * @return array
+ */
public function sendPurgeRequestDataProvider()
{
return [
diff --git a/app/code/Magento/CacheInvalidate/composer.json b/app/code/Magento/CacheInvalidate/composer.json
index feb0466a3744b..825c9937c16d1 100644
--- a/app/code/Magento/CacheInvalidate/composer.json
+++ b/app/code/Magento/CacheInvalidate/composer.json
@@ -2,12 +2,12 @@
"name": "magento/module-cache-invalidate",
"description": "N/A",
"require": {
- "php": "7.0.2|7.0.4|~7.0.6|~7.1.0",
+ "php": "~7.0.13|~7.1.0",
"magento/module-page-cache": "100.2.*",
- "magento/framework": "100.2.*"
+ "magento/framework": "101.0.*"
},
"type": "magento2-module",
- "version": "100.2.0-dev",
+ "version": "100.2.2",
"license": [
"OSL-3.0",
"AFL-3.0"
diff --git a/app/code/Magento/Captcha/Block/Captcha/DefaultCaptcha.php b/app/code/Magento/Captcha/Block/Captcha/DefaultCaptcha.php
index c0a091ca2d0d9..027c9a9085b47 100644
--- a/app/code/Magento/Captcha/Block/Captcha/DefaultCaptcha.php
+++ b/app/code/Magento/Captcha/Block/Captcha/DefaultCaptcha.php
@@ -13,7 +13,7 @@ class DefaultCaptcha extends \Magento\Framework\View\Element\Template
/**
* @var string
*/
- protected $_template = 'default.phtml';
+ protected $_template = 'Magento_Captcha::default.phtml';
/**
* @var string
diff --git a/app/code/Magento/Captcha/Model/Customer/Plugin/AjaxLogin.php b/app/code/Magento/Captcha/Model/Customer/Plugin/AjaxLogin.php
index e496c36f4de75..91f3a785df36b 100644
--- a/app/code/Magento/Captcha/Model/Customer/Plugin/AjaxLogin.php
+++ b/app/code/Magento/Captcha/Model/Customer/Plugin/AjaxLogin.php
@@ -3,6 +3,7 @@
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
+
namespace Magento\Captcha\Model\Customer\Plugin;
use Magento\Captcha\Helper\Data as CaptchaHelper;
@@ -81,27 +82,37 @@ public function aroundExecute(
if ($content) {
$loginParams = $this->serializer->unserialize($content);
}
- $username = isset($loginParams['username']) ? $loginParams['username'] : null;
- $captchaString = isset($loginParams[$captchaInputName]) ? $loginParams[$captchaInputName] : null;
- $loginFormId = isset($loginParams[$captchaFormIdField]) ? $loginParams[$captchaFormIdField] : null;
+ $username = $loginParams['username'] ?? null;
+ $captchaString = $loginParams[$captchaInputName] ?? null;
+ $loginFormId = $loginParams[$captchaFormIdField] ?? null;
- foreach ($this->formIds as $formId) {
- $captchaModel = $this->helper->getCaptcha($formId);
- if ($captchaModel->isRequired($username) && !in_array($loginFormId, $this->formIds)) {
- $resultJson = $this->resultJsonFactory->create();
- return $resultJson->setData(['errors' => true, 'message' => __('Provided form does not exist')]);
- }
+ if (!in_array($loginFormId, $this->formIds) && $this->helper->getCaptcha($loginFormId)->isRequired($username)) {
+ return $this->returnJsonError(__('Provided form does not exist'));
+ }
- if ($formId == $loginFormId) {
- $captchaModel->logAttempt($username);
- if (!$captchaModel->isCorrect($captchaString)) {
- $this->sessionManager->setUsername($username);
- /** @var \Magento\Framework\Controller\Result\Json $resultJson */
- $resultJson = $this->resultJsonFactory->create();
- return $resultJson->setData(['errors' => true, 'message' => __('Incorrect CAPTCHA')]);
+ foreach ($this->formIds as $formId) {
+ if ($formId === $loginFormId) {
+ $captchaModel = $this->helper->getCaptcha($formId);
+ if ($captchaModel->isRequired($username)) {
+ $captchaModel->logAttempt($username);
+ if (!$captchaModel->isCorrect($captchaString)) {
+ $this->sessionManager->setUsername($username);
+ return $this->returnJsonError(__('Incorrect CAPTCHA'));
+ }
}
}
}
return $proceed();
}
+
+ /**
+ *
+ * @param \Magento\Framework\Phrase $phrase
+ * @return \Magento\Framework\Controller\Result\Json
+ */
+ private function returnJsonError(\Magento\Framework\Phrase $phrase): \Magento\Framework\Controller\Result\Json
+ {
+ $resultJson = $this->resultJsonFactory->create();
+ return $resultJson->setData(['errors' => true, 'message' => $phrase]);
+ }
}
diff --git a/app/code/Magento/Captcha/Model/DefaultModel.php b/app/code/Magento/Captcha/Model/DefaultModel.php
index e5c72ba1ae82e..cf6690df5c85d 100644
--- a/app/code/Magento/Captcha/Model/DefaultModel.php
+++ b/app/code/Magento/Captcha/Model/DefaultModel.php
@@ -5,6 +5,8 @@
*/
namespace Magento\Captcha\Model;
+use Magento\Captcha\Helper\Data;
+
/**
* Implementation of \Zend\Captcha\Image
*
@@ -29,7 +31,7 @@ class DefaultModel extends \Zend\Captcha\Image implements \Magento\Captcha\Model
const DEFAULT_WORD_LENGTH_TO = 5;
/**
- * @var \Magento\Captcha\Helper\Data
+ * @var Data
* @since 100.2.0
*/
protected $captchaData;
@@ -125,8 +127,8 @@ public function getBlockName()
*/
public function isRequired($login = null)
{
- if ($this->isUserAuth()
- && !$this->isShownToLoggedInUser()
+ if (($this->isUserAuth()
+ && !$this->isShownToLoggedInUser())
|| !$this->isEnabled()
|| !in_array(
$this->formId,
@@ -431,12 +433,14 @@ public function getWordLen()
*/
private function isShowAlways()
{
- if ((string)$this->captchaData->getConfig('mode') == \Magento\Captcha\Helper\Data::MODE_ALWAYS) {
+ $captchaMode = (string)$this->captchaData->getConfig('mode');
+
+ if ($captchaMode === Data::MODE_ALWAYS) {
return true;
}
- if ((string)$this->captchaData->getConfig('mode') == \Magento\Captcha\Helper\Data::MODE_AFTER_FAIL
- && $this->getAllowedAttemptsForSameLogin() == 0
+ if ($captchaMode === Data::MODE_AFTER_FAIL
+ && $this->getAllowedAttemptsForSameLogin() === 0
) {
return true;
}
diff --git a/app/code/Magento/Captcha/Observer/CaptchaStringResolver.php b/app/code/Magento/Captcha/Observer/CaptchaStringResolver.php
index 9b97225e60de9..39579616fa928 100644
--- a/app/code/Magento/Captcha/Observer/CaptchaStringResolver.php
+++ b/app/code/Magento/Captcha/Observer/CaptchaStringResolver.php
@@ -18,6 +18,6 @@ public function resolve(\Magento\Framework\App\RequestInterface $request, $formI
{
$captchaParams = $request->getPost(\Magento\Captcha\Helper\Data::INPUT_NAME_FIELD_VALUE);
- return isset($captchaParams[$formId]) ? $captchaParams[$formId] : '';
+ return $captchaParams[$formId] ?? '';
}
}
diff --git a/app/code/Magento/Captcha/Observer/CheckGuestCheckoutObserver.php b/app/code/Magento/Captcha/Observer/CheckGuestCheckoutObserver.php
deleted file mode 100644
index 40c215ec218a1..0000000000000
--- a/app/code/Magento/Captcha/Observer/CheckGuestCheckoutObserver.php
+++ /dev/null
@@ -1,83 +0,0 @@
-_helper = $helper;
- $this->_actionFlag = $actionFlag;
- $this->captchaStringResolver = $captchaStringResolver;
- $this->_typeOnepage = $typeOnepage;
- $this->jsonHelper = $jsonHelper;
- }
-
- /**
- * Check Captcha On Checkout as Guest Page
- *
- * @param \Magento\Framework\Event\Observer $observer
- * @return $this
- */
- public function execute(\Magento\Framework\Event\Observer $observer)
- {
- $formId = 'guest_checkout';
- $captchaModel = $this->_helper->getCaptcha($formId);
- $checkoutMethod = $this->_typeOnepage->getQuote()->getCheckoutMethod();
- if ($checkoutMethod == \Magento\Checkout\Model\Type\Onepage::METHOD_GUEST) {
- if ($captchaModel->isRequired()) {
- $controller = $observer->getControllerAction();
- if (!$captchaModel->isCorrect($this->captchaStringResolver->resolve($controller->getRequest(), $formId))
- ) {
- $this->_actionFlag->set('', \Magento\Framework\App\Action\Action::FLAG_NO_DISPATCH, true);
- $result = ['error' => 1, 'message' => __('Incorrect CAPTCHA')];
- $controller->getResponse()->representJson($this->jsonHelper->jsonEncode($result));
- }
- }
- }
-
- return $this;
- }
-}
diff --git a/app/code/Magento/Captcha/Observer/CheckRegisterCheckoutObserver.php b/app/code/Magento/Captcha/Observer/CheckRegisterCheckoutObserver.php
deleted file mode 100644
index 3bf2ac38debee..0000000000000
--- a/app/code/Magento/Captcha/Observer/CheckRegisterCheckoutObserver.php
+++ /dev/null
@@ -1,83 +0,0 @@
-_helper = $helper;
- $this->_actionFlag = $actionFlag;
- $this->captchaStringResolver = $captchaStringResolver;
- $this->_typeOnepage = $typeOnepage;
- $this->jsonHelper = $jsonHelper;
- }
-
- /**
- * Check Captcha On Checkout Register Page
- *
- * @param \Magento\Framework\Event\Observer $observer
- * @return $this
- */
- public function execute(\Magento\Framework\Event\Observer $observer)
- {
- $formId = 'register_during_checkout';
- $captchaModel = $this->_helper->getCaptcha($formId);
- $checkoutMethod = $this->_typeOnepage->getQuote()->getCheckoutMethod();
- if ($checkoutMethod == \Magento\Checkout\Model\Type\Onepage::METHOD_REGISTER) {
- if ($captchaModel->isRequired()) {
- $controller = $observer->getControllerAction();
- if (!$captchaModel->isCorrect($this->captchaStringResolver->resolve($controller->getRequest(), $formId))
- ) {
- $this->_actionFlag->set('', \Magento\Framework\App\Action\Action::FLAG_NO_DISPATCH, true);
- $result = ['error' => 1, 'message' => __('Incorrect CAPTCHA')];
- $controller->getResponse()->representJson($this->jsonHelper->jsonEncode($result));
- }
- }
- }
-
- return $this;
- }
-}
diff --git a/app/code/Magento/Captcha/Observer/CheckUserEditObserver.php b/app/code/Magento/Captcha/Observer/CheckUserEditObserver.php
index 9d3cd8d367093..a3921b6cb17aa 100644
--- a/app/code/Magento/Captcha/Observer/CheckUserEditObserver.php
+++ b/app/code/Magento/Captcha/Observer/CheckUserEditObserver.php
@@ -111,19 +111,21 @@ public function execute(\Magento\Framework\Event\Observer $observer)
)
)) {
$customerId = $this->customerSession->getCustomerId();
- $this->authentication->processAuthenticationFailure($customerId);
- if ($this->authentication->isLocked($customerId)) {
- $this->customerSession->logout();
- $this->customerSession->start();
- $message = __(
- 'The account is locked. Please wait and try again or contact %1.',
- $this->scopeConfig->getValue('contact/email/recipient_email')
- );
- $this->messageManager->addError($message);
+ if ($customerId) {
+ $this->authentication->processAuthenticationFailure($customerId);
+ if ($this->authentication->isLocked($customerId)) {
+ $this->customerSession->logout();
+ $this->customerSession->start();
+ $message = __(
+ 'The account is locked. Please wait and try again or contact %1.',
+ $this->scopeConfig->getValue('contact/email/recipient_email')
+ );
+ $this->messageManager->addError($message);
+ }
+ $this->messageManager->addError(__('Incorrect CAPTCHA'));
+ $this->actionFlag->set('', \Magento\Framework\App\Action\Action::FLAG_NO_DISPATCH, true);
+ $this->redirect->redirect($controller->getResponse(), '*/*/edit');
}
- $this->messageManager->addError(__('Incorrect CAPTCHA'));
- $this->actionFlag->set('', \Magento\Framework\App\Action\Action::FLAG_NO_DISPATCH, true);
- $this->redirect->redirect($controller->getResponse(), '*/*/edit');
}
}
diff --git a/app/code/Magento/Captcha/Observer/CheckUserForgotPasswordBackendObserver.php b/app/code/Magento/Captcha/Observer/CheckUserForgotPasswordBackendObserver.php
index 402fc028c5ad0..2de93dcf6b59b 100644
--- a/app/code/Magento/Captcha/Observer/CheckUserForgotPasswordBackendObserver.php
+++ b/app/code/Magento/Captcha/Observer/CheckUserForgotPasswordBackendObserver.php
@@ -69,18 +69,17 @@ public function execute(\Magento\Framework\Event\Observer $observer)
$controller = $observer->getControllerAction();
$email = (string)$observer->getControllerAction()->getRequest()->getParam('email');
$params = $observer->getControllerAction()->getRequest()->getParams();
- if (!empty($email) && !empty($params)) {
- if ($captchaModel->isRequired()) {
- if (!$captchaModel->isCorrect($this->captchaStringResolver->resolve($controller->getRequest(), $formId))
- ) {
- $this->_session->setEmail((string)$controller->getRequest()->getPost('email'));
- $this->_actionFlag->set('', \Magento\Framework\App\Action\Action::FLAG_NO_DISPATCH, true);
- $this->messageManager->addError(__('Incorrect CAPTCHA'));
- $controller->getResponse()->setRedirect(
- $controller->getUrl('*/*/forgotpassword', ['_nosecret' => true])
- );
- }
- }
+ if (!empty($email)
+ && !empty($params)
+ && $captchaModel->isRequired()
+ && !$captchaModel->isCorrect($this->captchaStringResolver->resolve($controller->getRequest(), $formId))
+ ) {
+ $this->_session->setEmail((string)$controller->getRequest()->getPost('email'));
+ $this->_actionFlag->set('', \Magento\Framework\App\Action\Action::FLAG_NO_DISPATCH, true);
+ $this->messageManager->addError(__('Incorrect CAPTCHA'));
+ $controller->getResponse()->setRedirect(
+ $controller->getUrl('*/*/forgotpassword', ['_nosecret' => true])
+ );
}
return $this;
diff --git a/app/code/Magento/Captcha/Observer/CheckUserLoginBackendObserver.php b/app/code/Magento/Captcha/Observer/CheckUserLoginBackendObserver.php
index 8cc907d7bd12b..924514cd48c5d 100644
--- a/app/code/Magento/Captcha/Observer/CheckUserLoginBackendObserver.php
+++ b/app/code/Magento/Captcha/Observer/CheckUserLoginBackendObserver.php
@@ -52,11 +52,11 @@ public function execute(\Magento\Framework\Event\Observer $observer)
$formId = 'backend_login';
$captchaModel = $this->_helper->getCaptcha($formId);
$login = $observer->getEvent()->getUsername();
- if ($captchaModel->isRequired($login)) {
- if (!$captchaModel->isCorrect($this->captchaStringResolver->resolve($this->_request, $formId))) {
- $captchaModel->logAttempt($login);
- throw new PluginAuthenticationException(__('Incorrect CAPTCHA.'));
- }
+ if ($captchaModel->isRequired($login)
+ && !$captchaModel->isCorrect($this->captchaStringResolver->resolve($this->_request, $formId))
+ ) {
+ $captchaModel->logAttempt($login);
+ throw new PluginAuthenticationException(__('Incorrect CAPTCHA.'));
}
$captchaModel->logAttempt($login);
diff --git a/app/code/Magento/Captcha/Test/Mftf/ActionGroup/CaptchaFormsDisplayingActionGroup.xml b/app/code/Magento/Captcha/Test/Mftf/ActionGroup/CaptchaFormsDisplayingActionGroup.xml
new file mode 100644
index 0000000000000..9640da9bd2f67
--- /dev/null
+++ b/app/code/Magento/Captcha/Test/Mftf/ActionGroup/CaptchaFormsDisplayingActionGroup.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Captcha/Test/Mftf/Data/CaptchaFormsDisplayingData.xml b/app/code/Magento/Captcha/Test/Mftf/Data/CaptchaFormsDisplayingData.xml
new file mode 100644
index 0000000000000..45b9e59a5e767
--- /dev/null
+++ b/app/code/Magento/Captcha/Test/Mftf/Data/CaptchaFormsDisplayingData.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+ Create user
+ Login
+ Forgot password
+ Contact Us
+ Change password
+ Register during Checkout
+ Check Out as Guest
+
+
diff --git a/app/code/Magento/Captcha/Test/Mftf/LICENSE.txt b/app/code/Magento/Captcha/Test/Mftf/LICENSE.txt
new file mode 100644
index 0000000000000..49525fd99da9c
--- /dev/null
+++ b/app/code/Magento/Captcha/Test/Mftf/LICENSE.txt
@@ -0,0 +1,48 @@
+
+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/Captcha/Test/Mftf/LICENSE_AFL.txt b/app/code/Magento/Captcha/Test/Mftf/LICENSE_AFL.txt
new file mode 100644
index 0000000000000..f39d641b18a19
--- /dev/null
+++ b/app/code/Magento/Captcha/Test/Mftf/LICENSE_AFL.txt
@@ -0,0 +1,48 @@
+
+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/Captcha/Test/Mftf/README.md b/app/code/Magento/Captcha/Test/Mftf/README.md
new file mode 100644
index 0000000000000..48be768712f2f
--- /dev/null
+++ b/app/code/Magento/Captcha/Test/Mftf/README.md
@@ -0,0 +1,3 @@
+# Captcha Functional Tests
+
+The Functional Test Module for **Magento Captcha** module.
diff --git a/app/code/Magento/Captcha/Test/Mftf/Section/AdminCaptchaFormsDisplayingSection.xml b/app/code/Magento/Captcha/Test/Mftf/Section/AdminCaptchaFormsDisplayingSection.xml
new file mode 100644
index 0000000000000..6def5f9ccdac9
--- /dev/null
+++ b/app/code/Magento/Captcha/Test/Mftf/Section/AdminCaptchaFormsDisplayingSection.xml
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Captcha/Test/Mftf/Test/AdminCaptchaFormsDisplayingTest.xml b/app/code/Magento/Captcha/Test/Mftf/Test/AdminCaptchaFormsDisplayingTest.xml
new file mode 100644
index 0000000000000..6e330390b372b
--- /dev/null
+++ b/app/code/Magento/Captcha/Test/Mftf/Test/AdminCaptchaFormsDisplayingTest.xml
@@ -0,0 +1,68 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{CaptchaData.checkoutAsGuest}}
+ $formItems
+
+
+ {{CaptchaData.register}}
+ $formItems
+
+
+
+
+ {{CaptchaData.createUser}}
+ $createUser
+
+
+
+ {{CaptchaData.login}}
+ login
+
+
+
+ {{CaptchaData.passwd}}
+ $forgotpassword
+
+
+
+ {{CaptchaData.contactUs}}
+ $contactUs
+
+
+
+ {{CaptchaData.changePasswd}}
+ $userEdit
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Captcha/Test/Unit/Model/Customer/Plugin/AjaxLoginTest.php b/app/code/Magento/Captcha/Test/Unit/Model/Customer/Plugin/AjaxLoginTest.php
index be9574fff2cfa..bda0d9705d3df 100644
--- a/app/code/Magento/Captcha/Test/Unit/Model/Customer/Plugin/AjaxLoginTest.php
+++ b/app/code/Magento/Captcha/Test/Unit/Model/Customer/Plugin/AjaxLoginTest.php
@@ -3,22 +3,23 @@
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
+
namespace Magento\Captcha\Test\Unit\Model\Customer\Plugin;
class AjaxLoginTest extends \PHPUnit\Framework\TestCase
{
/**
- * @var \PHPUnit_Framework_MockObject_MockObject
+ * @var \PHPUnit_Framework_MockObject_MockObject | \Magento\Checkout\Model\Session
*/
protected $sessionManagerMock;
/**
- * @var \PHPUnit_Framework_MockObject_MockObject
+ * @var \PHPUnit_Framework_MockObject_MockObject | \Magento\Captcha\Helper\Data
*/
protected $captchaHelperMock;
/**
- * @var \PHPUnit_Framework_MockObject_MockObject
+ * @var \PHPUnit_Framework_MockObject_MockObject | \Magento\Framework\Controller\Result\JsonFactory
*/
protected $jsonFactoryMock;
@@ -38,12 +39,12 @@ class AjaxLoginTest extends \PHPUnit\Framework\TestCase
protected $requestMock;
/**
- * @var \PHPUnit_Framework_MockObject_MockObject
+ * @var \PHPUnit_Framework_MockObject_MockObject | \Magento\Customer\Controller\Ajax\Login
*/
protected $loginControllerMock;
/**
- * @var \PHPUnit_Framework_MockObject_MockObject
+ * @var \PHPUnit_Framework_MockObject_MockObject | \Magento\Framework\Serialize\Serializer\Json
*/
protected $serializerMock;
@@ -72,8 +73,12 @@ protected function setUp()
$this->loginControllerMock->expects($this->any())->method('getRequest')
->will($this->returnValue($this->requestMock));
- $this->captchaHelperMock->expects($this->once())->method('getCaptcha')
- ->with('user_login')->will($this->returnValue($this->captchaMock));
+
+ $this->captchaHelperMock
+ ->expects($this->exactly(1))
+ ->method('getCaptcha')
+ ->will($this->returnValue($this->captchaMock));
+
$this->formIds = ['user_login'];
$this->serializerMock = $this->createMock(\Magento\Framework\Serialize\Serializer\Json::class);
@@ -103,11 +108,18 @@ public function testAroundExecute()
$this->captchaMock->expects($this->once())->method('logAttempt')->with($username);
$this->captchaMock->expects($this->once())->method('isCorrect')->with($captchaString)
->will($this->returnValue(true));
- $this->serializerMock->expects(($this->once()))->method('unserialize')->will($this->returnValue($requestData));
+ $this->serializerMock->expects($this->once())->method('unserialize')->will($this->returnValue($requestData));
$closure = function () {
return 'result';
};
+
+ $this->captchaHelperMock
+ ->expects($this->exactly(1))
+ ->method('getCaptcha')
+ ->with('user_login')
+ ->will($this->returnValue($this->captchaMock));
+
$this->assertEquals('result', $this->model->aroundExecute($this->loginControllerMock, $closure));
}
@@ -128,18 +140,21 @@ public function testAroundExecuteIncorrectCaptcha()
$this->captchaMock->expects($this->once())->method('logAttempt')->with($username);
$this->captchaMock->expects($this->once())->method('isCorrect')
->with($captchaString)->will($this->returnValue(false));
- $this->serializerMock->expects(($this->once()))->method('unserialize')->will($this->returnValue($requestData));
+ $this->serializerMock->expects($this->once())->method('unserialize')->will($this->returnValue($requestData));
$this->sessionManagerMock->expects($this->once())->method('setUsername')->with($username);
$this->jsonFactoryMock->expects($this->once())->method('create')
->will($this->returnValue($this->resultJsonMock));
- $this->resultJsonMock->expects($this->once())->method('setData')
- ->with(['errors' => true, 'message' => __('Incorrect CAPTCHA')])->will($this->returnValue('response'));
+ $this->resultJsonMock
+ ->expects($this->once())
+ ->method('setData')
+ ->with(['errors' => true, 'message' => __('Incorrect CAPTCHA')])
+ ->will($this->returnSelf());
$closure = function () {
};
- $this->assertEquals('response', $this->model->aroundExecute($this->loginControllerMock, $closure));
+ $this->assertEquals($this->resultJsonMock, $this->model->aroundExecute($this->loginControllerMock, $closure));
}
/**
@@ -151,7 +166,7 @@ public function testAroundExecuteCaptchaIsNotRequired($username, $requestContent
{
$this->requestMock->expects($this->once())->method('getContent')
->will($this->returnValue(json_encode($requestContent)));
- $this->serializerMock->expects(($this->once()))->method('unserialize')
+ $this->serializerMock->expects($this->once())->method('unserialize')
->will($this->returnValue($requestContent));
$this->captchaMock->expects($this->once())->method('isRequired')->with($username)
@@ -168,16 +183,39 @@ public function testAroundExecuteCaptchaIsNotRequired($username, $requestContent
/**
* @return array
*/
- public function aroundExecuteCaptchaIsNotRequired()
+ public function aroundExecuteCaptchaIsNotRequired(): array
{
return [
[
'username' => 'name',
'requestData' => ['username' => 'name', 'captcha_string' => 'string'],
],
+ [
+ 'username' => 'name',
+ 'requestData' =>
+ [
+ 'username' => 'name',
+ 'captcha_string' => 'string',
+ 'captcha_form_id' => $this->formIds[0]
+ ],
+ ],
[
'username' => null,
- 'requestData' => ['captcha_string' => 'string'],
+ 'requestData' =>
+ [
+ 'username' => null,
+ 'captcha_string' => 'string',
+ 'captcha_form_id' => $this->formIds[0]
+ ],
+ ],
+ [
+ 'username' => 'name',
+ 'requestData' =>
+ [
+ 'username' => 'name',
+ 'captcha_string' => 'string',
+ 'captcha_form_id' => null
+ ],
],
];
}
diff --git a/app/code/Magento/Captcha/Test/Unit/Model/DefaultTest.php b/app/code/Magento/Captcha/Test/Unit/Model/DefaultTest.php
index 429c13e802a87..63e7308eea34c 100644
--- a/app/code/Magento/Captcha/Test/Unit/Model/DefaultTest.php
+++ b/app/code/Magento/Captcha/Test/Unit/Model/DefaultTest.php
@@ -24,7 +24,7 @@ class DefaultTest extends \PHPUnit\Framework\TestCase
'enable' => '1',
'font' => 'linlibertine',
'mode' => 'after_fail',
- 'forms' => 'user_forgotpassword,user_create,guest_checkout,register_during_checkout',
+ 'forms' => 'user_forgotpassword,user_create',
'failed_attempts_login' => '3',
'failed_attempts_ip' => '1000',
'timeout' => '7',
@@ -35,8 +35,6 @@ class DefaultTest extends \PHPUnit\Framework\TestCase
'always_for' => [
'user_create',
'user_forgotpassword',
- 'guest_checkout',
- 'register_during_checkout',
'contact_us',
],
];
@@ -354,13 +352,15 @@ public function testIsShownToLoggedInUser($expectedResult, $formId)
$this->assertEquals($expectedResult, $captcha->isShownToLoggedInUser());
}
+ /**
+ * @return array
+ */
public function isShownToLoggedInUserDataProvider()
{
return [
[true, 'contact_us'],
[false, 'user_create'],
[false, 'user_forgotpassword'],
- [false, 'guest_checkout']
];
}
}
diff --git a/app/code/Magento/Captcha/Test/Unit/Observer/CaptchaStringResolverTest.php b/app/code/Magento/Captcha/Test/Unit/Observer/CaptchaStringResolverTest.php
new file mode 100644
index 0000000000000..2bd8ac6f16986
--- /dev/null
+++ b/app/code/Magento/Captcha/Test/Unit/Observer/CaptchaStringResolverTest.php
@@ -0,0 +1,69 @@
+objectManagerHelper = new ObjectManager($this);
+ $this->requestMock = $this->createMock(HttpRequest::class);
+ $this->captchaStringResolver = $this->objectManagerHelper->getObject(CaptchaStringResolver::class);
+ }
+
+ public function testResolveWithFormIdSet()
+ {
+ $formId = 'contact_us';
+ $captchaValue = 'some-value';
+
+ $this->requestMock->expects($this->once())
+ ->method('getPost')
+ ->with(CaptchaDataHelper::INPUT_NAME_FIELD_VALUE)
+ ->willReturn([$formId => $captchaValue]);
+
+ self::assertEquals(
+ $this->captchaStringResolver->resolve($this->requestMock, $formId),
+ $captchaValue
+ );
+ }
+
+ public function testResolveWithNoFormIdInRequest()
+ {
+ $formId = 'contact_us';
+
+ $this->requestMock->expects($this->once())
+ ->method('getPost')
+ ->with(CaptchaDataHelper::INPUT_NAME_FIELD_VALUE)
+ ->willReturn([]);
+
+ self::assertEquals(
+ $this->captchaStringResolver->resolve($this->requestMock, $formId),
+ ''
+ );
+ }
+}
diff --git a/app/code/Magento/Captcha/Test/Unit/Observer/CheckUserEditObserverTest.php b/app/code/Magento/Captcha/Test/Unit/Observer/CheckUserEditObserverTest.php
index 26fd8fd928c56..ee5001d146793 100644
--- a/app/code/Magento/Captcha/Test/Unit/Observer/CheckUserEditObserverTest.php
+++ b/app/code/Magento/Captcha/Test/Unit/Observer/CheckUserEditObserverTest.php
@@ -160,4 +160,53 @@ public function testExecute()
$this->observer->execute(new \Magento\Framework\Event\Observer(['controller_action' => $controller]));
}
+
+ /**
+ * @return void
+ */
+ public function testExecuteWithCustomerIdNull()
+ {
+ $customerId = null;
+ $captchaValue = 'some-value';
+
+ $captcha = $this->createMock(\Magento\Captcha\Model\DefaultModel::class);
+ $captcha->expects($this->once())
+ ->method('isRequired')
+ ->willReturn(true);
+ $captcha->expects($this->once())
+ ->method('isCorrect')
+ ->with($captchaValue)
+ ->willReturn(false);
+
+ $this->helperMock->expects($this->once())
+ ->method('getCaptcha')
+ ->with(\Magento\Captcha\Observer\CheckUserEditObserver::FORM_ID)
+ ->willReturn($captcha);
+
+ $request = $this->createMock(\Magento\Framework\App\Request\Http::class);
+ $request->expects($this->any())
+ ->method('getPost')
+ ->with(\Magento\Captcha\Helper\Data::INPUT_NAME_FIELD_VALUE, null)
+ ->willReturn([\Magento\Captcha\Observer\CheckUserEditObserver::FORM_ID => $captchaValue]);
+
+ $controller = $this->createMock(\Magento\Framework\App\Action\Action::class);
+ $controller->expects($this->any())->method('getRequest')->will($this->returnValue($request));
+
+ $this->captchaStringResolverMock->expects($this->once())
+ ->method('resolve')
+ ->with($request, \Magento\Captcha\Observer\CheckUserEditObserver::FORM_ID)
+ ->willReturn($captchaValue);
+
+ $customerDataMock = $this->createMock(\Magento\Customer\Model\Data\Customer::class);
+
+ $this->customerSessionMock->expects($this->once())
+ ->method('getCustomerId')
+ ->willReturn($customerId);
+
+ $this->customerSessionMock->expects($this->atLeastOnce())
+ ->method('getCustomer')
+ ->willReturn($customerDataMock);
+
+ $this->observer->execute(new \Magento\Framework\Event\Observer(['controller_action' => $controller]));
+ }
}
diff --git a/app/code/Magento/Captcha/composer.json b/app/code/Magento/Captcha/composer.json
index 08f49a584fbe7..a8a63e19e67df 100644
--- a/app/code/Magento/Captcha/composer.json
+++ b/app/code/Magento/Captcha/composer.json
@@ -2,18 +2,18 @@
"name": "magento/module-captcha",
"description": "N/A",
"require": {
- "php": "7.0.2|7.0.4|~7.0.6|~7.1.0",
+ "php": "~7.0.13|~7.1.0",
"magento/module-store": "100.2.*",
- "magento/module-customer": "100.2.*",
+ "magento/module-customer": "101.0.*",
"magento/module-checkout": "100.2.*",
"magento/module-backend": "100.2.*",
- "magento/framework": "100.2.*",
+ "magento/framework": "101.0.*",
"zendframework/zend-db": "^2.8.2",
"zendframework/zend-captcha": "^2.7.1",
"zendframework/zend-session": "^2.7.3"
},
"type": "magento2-module",
- "version": "100.2.0-dev",
+ "version": "100.2.3",
"license": [
"OSL-3.0",
"AFL-3.0"
diff --git a/app/code/Magento/Captcha/etc/config.xml b/app/code/Magento/Captcha/etc/config.xml
index 71c474de90ff4..dd748dd05ccda 100644
--- a/app/code/Magento/Captcha/etc/config.xml
+++ b/app/code/Magento/Captcha/etc/config.xml
@@ -53,8 +53,6 @@
1
1
- 1
- 1
1
@@ -77,12 +75,6 @@
Forgot password
-
- Check Out as Guest
-
-
- Register during Checkout
-
Contact Us
diff --git a/app/code/Magento/Captcha/etc/di.xml b/app/code/Magento/Captcha/etc/di.xml
index 955896eb12744..3a929f5e6cc00 100644
--- a/app/code/Magento/Captcha/etc/di.xml
+++ b/app/code/Magento/Captcha/etc/di.xml
@@ -33,7 +33,6 @@
- user_login
- - guest_checkout
diff --git a/app/code/Magento/Captcha/etc/events.xml b/app/code/Magento/Captcha/etc/events.xml
index e3ddd19de2d12..970c0d077260c 100644
--- a/app/code/Magento/Captcha/etc/events.xml
+++ b/app/code/Magento/Captcha/etc/events.xml
@@ -18,10 +18,6 @@
-
-
-
-
diff --git a/app/code/Magento/Captcha/etc/frontend/di.xml b/app/code/Magento/Captcha/etc/frontend/di.xml
index 225e62c8e8203..0c4ab0cda0735 100644
--- a/app/code/Magento/Captcha/etc/frontend/di.xml
+++ b/app/code/Magento/Captcha/etc/frontend/di.xml
@@ -17,7 +17,6 @@
- user_login
- - guest_checkout
diff --git a/app/code/Magento/Captcha/i18n/en_US.csv b/app/code/Magento/Captcha/i18n/en_US.csv
index 2de4ab5345c88..480107df8adfe 100644
--- a/app/code/Magento/Captcha/i18n/en_US.csv
+++ b/app/code/Magento/Captcha/i18n/en_US.csv
@@ -4,10 +4,10 @@ Always,Always
"Incorrect CAPTCHA","Incorrect CAPTCHA"
"Incorrect CAPTCHA.","Incorrect CAPTCHA."
"The account is locked. Please wait and try again or contact %1.","The account is locked. Please wait and try again or contact %1."
-"Please enter the letters from the image","Please enter the letters from the image"
+"Please enter the letters and numbers from the image","Please enter the letters and numbers from the image"
"Attention : Captcha is case sensitive.","Attention : Captcha is case sensitive."
"Reload captcha","Reload captcha"
-"Please type the letters below","Please type the letters below"
+"Please type the letters and numbers below","Please type the letters and numbers below"
"Attention: Captcha is case sensitive.","Attention: Captcha is case sensitive."
CAPTCHA,CAPTCHA
"Enable CAPTCHA in Admin","Enable CAPTCHA in Admin"
@@ -20,11 +20,7 @@ Forms,Forms
"Number of Symbols","Number of Symbols"
"Please specify 8 symbols at the most. Range allowed (e.g. 3-5)","Please specify 8 symbols at the most. Range allowed (e.g. 3-5)"
"Symbols Used in CAPTCHA","Symbols Used in CAPTCHA"
-"
- Please use only letters (a-z or A-Z) or numbers (0-9) in this field. No spaces or other characters are allowed. Similar looking characters (e.g. ""i"", ""l"", ""1"") decrease chance of correct recognition by customer.
- ","
- Please use only letters (a-z or A-Z) or numbers (0-9) in this field. No spaces or other characters are allowed. Similar looking characters (e.g. ""i"", ""l"", ""1"") decrease chance of correct recognition by customer.
- "
+"Please use only letters (a-z or A-Z) or numbers (0-9) in this field. No spaces or other characters are allowed. Similar looking characters (e.g. ""i"", ""l"", ""1"") decrease chance of correct recognition by customer.","Please use only letters (a-z or A-Z) or numbers (0-9) in this field. No spaces or other characters are allowed. Similar looking characters (e.g. ""i"", ""l"", ""1"") decrease chance of correct recognition by customer."
"Case Sensitive","Case Sensitive"
"Enable CAPTCHA on Storefront","Enable CAPTCHA on Storefront"
"CAPTCHA for ""Create user"" and ""Forgot password"" forms is always enabled if chosen.","CAPTCHA for ""Create user"" and ""Forgot password"" forms is always enabled if chosen."
diff --git a/app/code/Magento/Captcha/view/adminhtml/templates/default.phtml b/app/code/Magento/Captcha/view/adminhtml/templates/default.phtml
index b8dcd6c654c8e..1be4bd19cd4ba 100644
--- a/app/code/Magento/Captcha/view/adminhtml/templates/default.phtml
+++ b/app/code/Magento/Captcha/view/adminhtml/templates/default.phtml
@@ -13,7 +13,7 @@ $captcha = $block->getCaptchaModel();
?>
- = $block->escapeHtml(__('Please enter the letters from the image')) ?>
+ = $block->escapeHtml(__('Please enter the letters and numbers from the image')) ?>
- Magento_Captcha/js/view/checkout/loginCaptcha
- additional-login-form-fields
-
- guest_checkout
-
- checkoutConfig
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Magento_Captcha/js/view/checkout/loginCaptcha
- - additional-login-form-fields
- - guest_checkout
+ - user_login
- checkoutConfig
diff --git a/app/code/Magento/Captcha/view/frontend/requirejs-config.js b/app/code/Magento/Captcha/view/frontend/requirejs-config.js
index 3b322711f8b1f..42c80632d3e92 100644
--- a/app/code/Magento/Captcha/view/frontend/requirejs-config.js
+++ b/app/code/Magento/Captcha/view/frontend/requirejs-config.js
@@ -6,7 +6,8 @@
var config = {
map: {
'*': {
- captcha: 'Magento_Captcha/captcha'
+ captcha: 'Magento_Captcha/js/captcha',
+ 'Magento_Captcha/captcha': 'Magento_Captcha/js/captcha'
}
}
};
diff --git a/app/code/Magento/Captcha/view/frontend/templates/default.phtml b/app/code/Magento/Captcha/view/frontend/templates/default.phtml
index 9851b1cd8bd7d..6c9a5fe85f596 100644
--- a/app/code/Magento/Captcha/view/frontend/templates/default.phtml
+++ b/app/code/Magento/Captcha/view/frontend/templates/default.phtml
@@ -12,9 +12,9 @@
$captcha = $block->getCaptchaModel();
?>
-
= $block->escapeHtml(__('Please type the letters below')) ?>
+
= $block->escapeHtml(__('Please type the letters and numbers below')) ?>
-
+
getCaptchaModel();
"imageLoader": "= $block->escapeUrl($block->getViewFileUrl('images/loader-2.gif')) ?>",
"type": "= $block->escapeHtmlAttr($block->getFormId()) ?>"}}'>
-
+
= $block->escapeHtml(__('Reload captcha')) ?>
diff --git a/app/code/Magento/Captcha/view/frontend/web/captcha.js b/app/code/Magento/Captcha/view/frontend/web/js/captcha.js
similarity index 100%
rename from app/code/Magento/Captcha/view/frontend/web/captcha.js
rename to app/code/Magento/Captcha/view/frontend/web/js/captcha.js
diff --git a/app/code/Magento/Captcha/view/frontend/web/onepage.js b/app/code/Magento/Captcha/view/frontend/web/onepage.js
deleted file mode 100644
index 7f5f11d20572b..0000000000000
--- a/app/code/Magento/Captcha/view/frontend/web/onepage.js
+++ /dev/null
@@ -1,22 +0,0 @@
-/**
- * Copyright © Magento, Inc. All rights reserved.
- * See COPYING.txt for license details.
- */
-
-/**
- * @deprecated since version 2.2.0
- */
-define(['jquery'], function ($) {
- 'use strict';
-
- $(document).on('login', function () {
- var type;
-
- $('[data-captcha="guest_checkout"], [data-captcha="register_during_checkout"]').hide();
- $('[role="guest_checkout"], [role="register_during_checkout"]').hide();
- type = $('#login\\:guest').is(':checked') ? 'guest_checkout' : 'register_during_checkout';
- $('[role="' + type + '"], [data-captcha="' + type + '"]').show();
- }).on('billingSave', function () {
- $('.captcha-reload:visible').trigger('click');
- });
-});
diff --git a/app/code/Magento/Captcha/view/frontend/web/template/checkout/captcha.html b/app/code/Magento/Captcha/view/frontend/web/template/checkout/captcha.html
index 6767a121d849d..575b3ca6f732e 100644
--- a/app/code/Magento/Captcha/view/frontend/web/template/checkout/captcha.html
+++ b/app/code/Magento/Captcha/view/frontend/web/template/checkout/captcha.html
@@ -6,15 +6,16 @@
-->
-
+
-
+
_productFactory = $productFactory;
$this->_coreRegistry = $coreRegistry;
+ $this->visibility = $visibility ?: ObjectManager::getInstance()->get(Visibility::class);
+ $this->status = $status ?: ObjectManager::getInstance()->get(Status::class);
parent::__construct($context, $backendHelper, $data);
}
@@ -102,6 +121,10 @@ protected function _prepareCollection()
'name'
)->addAttributeToSelect(
'sku'
+ )->addAttributeToSelect(
+ 'visibility'
+ )->addAttributeToSelect(
+ 'status'
)->addAttributeToSelect(
'price'
)->joinField(
@@ -159,6 +182,28 @@ protected function _prepareColumns()
);
$this->addColumn('name', ['header' => __('Name'), 'index' => 'name']);
$this->addColumn('sku', ['header' => __('SKU'), 'index' => 'sku']);
+ $this->addColumn(
+ 'visibility',
+ [
+ 'header' => __('Visibility'),
+ 'index' => 'visibility',
+ 'type' => 'options',
+ 'options' => $this->visibility->getOptionArray(),
+ 'header_css_class' => 'col-visibility',
+ 'column_css_class' => 'col-visibility'
+ ]
+ );
+
+ $this->addColumn(
+ 'status',
+ [
+ 'header' => __('Status'),
+ 'index' => 'status',
+ 'type' => 'options',
+ 'options' => $this->status->getOptionArray()
+ ]
+ );
+
$this->addColumn(
'price',
[
diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Category/Tree.php b/app/code/Magento/Catalog/Block/Adminhtml/Category/Tree.php
index 6f8a45c6ac7ed..ed615b41644e2 100644
--- a/app/code/Magento/Catalog/Block/Adminhtml/Category/Tree.php
+++ b/app/code/Magento/Catalog/Block/Adminhtml/Category/Tree.php
@@ -29,7 +29,7 @@ class Tree extends \Magento\Catalog\Block\Adminhtml\Category\AbstractCategory
/**
* @var string
*/
- protected $_template = 'catalog/category/tree.phtml';
+ protected $_template = 'Magento_Catalog::catalog/category/tree.phtml';
/**
* @var \Magento\Backend\Model\Auth\Session
@@ -228,7 +228,7 @@ public function getStoreSwitcherHtml()
public function getLoadTreeUrl($expanded = null)
{
$params = ['_current' => true, 'id' => null, 'store' => null];
- if (is_null($expanded) && $this->_backendSession->getIsTreeWasExpanded() || $expanded == true) {
+ if ($expanded === null && $this->_backendSession->getIsTreeWasExpanded() || $expanded == true) {
$params['expand_all'] = true;
}
return $this->getUrl('*/*/categoriesJson', $params);
@@ -325,7 +325,7 @@ public function getBreadcrumbsJavascript($path, $javascriptVarName)
*
* @param Node|array $node
* @param int $level
- * @return string
+ * @return array
* @SuppressWarnings(PHPMD.CyclomaticComplexity)
* @SuppressWarnings(PHPMD.NPathComplexity)
*/
diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Category/Widget/Chooser.php b/app/code/Magento/Catalog/Block/Adminhtml/Category/Widget/Chooser.php
index 5e98313f95f0f..9c83d4aea61c7 100644
--- a/app/code/Magento/Catalog/Block/Adminhtml/Category/Widget/Chooser.php
+++ b/app/code/Magento/Catalog/Block/Adminhtml/Category/Widget/Chooser.php
@@ -24,7 +24,7 @@ class Chooser extends \Magento\Catalog\Block\Adminhtml\Category\Tree
*
* @var string
*/
- protected $_template = 'catalog/category/widget/tree.phtml';
+ protected $_template = 'Magento_Catalog::catalog/category/widget/tree.phtml';
/**
* @return void
@@ -144,7 +144,7 @@ function (node, e) {
*
* @param \Magento\Framework\Data\Tree\Node|array $node
* @param int $level
- * @return string
+ * @return array
*/
protected function _getNodeJson($node, $level = 0)
{
diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Form/Renderer/Config/YearRange.php b/app/code/Magento/Catalog/Block/Adminhtml/Form/Renderer/Config/YearRange.php
index 0026e52e039ef..cd6c5021f0cc9 100644
--- a/app/code/Magento/Catalog/Block/Adminhtml/Form/Renderer/Config/YearRange.php
+++ b/app/code/Magento/Catalog/Block/Adminhtml/Form/Renderer/Config/YearRange.php
@@ -32,10 +32,9 @@ protected function _getElementHtml(AbstractElement $element)
$from = $element->setValue(isset($values[0]) ? $values[0] : null)->getElementHtml();
$to = $element->setValue(isset($values[1]) ? $values[1] : null)->getElementHtml();
- return __(
- '
from '
- ) . $from . __(
- '
to '
- ) . $to;
+ return '
' . __('from') . ' '
+ . $from .
+ '
' . __('to') . ' '
+ . $to;
}
}
diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Form/Renderer/Fieldset/Element.php b/app/code/Magento/Catalog/Block/Adminhtml/Form/Renderer/Fieldset/Element.php
index 10214fc1d16fd..ad6df27b89334 100644
--- a/app/code/Magento/Catalog/Block/Adminhtml/Form/Renderer/Fieldset/Element.php
+++ b/app/code/Magento/Catalog/Block/Adminhtml/Form/Renderer/Fieldset/Element.php
@@ -21,7 +21,7 @@ class Element extends \Magento\Backend\Block\Widget\Form\Renderer\Fieldset\Eleme
/**
* Retrieve data object related with form
*
- * @return \Magento\Catalog\Model\Product || \Magento\Catalog\Model\Category
+ * @return \Magento\Catalog\Model\Product|\Magento\Catalog\Model\Category
*/
public function getDataObject()
{
diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Grid.php b/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Grid.php
index ab5026b1e69b9..66e04ef03f771 100644
--- a/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Grid.php
+++ b/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Grid.php
@@ -101,8 +101,7 @@ protected function _prepareColumns()
'type' => 'options',
'options' => ['1' => __('Yes'), '0' => __('No')],
'align' => 'center'
- ],
- 'is_user_defined'
+ ]
);
$this->_eventManager->dispatch('product_attribute_grid_build', ['grid' => $this]);
diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Set/Main.php b/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Set/Main.php
index c6e48c02805a6..1b188de40710f 100644
--- a/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Set/Main.php
+++ b/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Set/Main.php
@@ -22,7 +22,7 @@ class Main extends \Magento\Backend\Block\Template
/**
* @var string
*/
- protected $_template = 'catalog/product/attribute/set/main.phtml';
+ protected $_template = 'Magento_Catalog::catalog/product/attribute/set/main.phtml';
/**
* Core registry
@@ -233,7 +233,7 @@ public function getGroupTreeJson()
/* @var $node \Magento\Eav\Model\Entity\Attribute\Group */
foreach ($groups as $node) {
$item = [];
- $item['text'] = $node->getAttributeGroupName();
+ $item['text'] = $this->escapeHtml($node->getAttributeGroupName());
$item['id'] = $node->getAttributeGroupId();
$item['cls'] = 'folder';
$item['allowDrop'] = true;
@@ -280,7 +280,7 @@ public function getAttributeTreeJson()
foreach ($attributes as $child) {
$attr = [
- 'text' => $child->getAttributeCode(),
+ 'text' => $this->escapeHtml($child->getAttributeCode()),
'id' => $child->getAttributeId(),
'cls' => 'leaf',
'allowDrop' => false,
diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Set/Main/Formgroup.php b/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Set/Main/Formgroup.php
index ee92fd7c19b80..6f6ad4f909815 100644
--- a/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Set/Main/Formgroup.php
+++ b/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Set/Main/Formgroup.php
@@ -81,12 +81,10 @@ protected function _prepareForm()
*/
protected function _getSetId()
{
- return intval(
- $this->getRequest()->getParam('id')
- ) > 0 ? intval(
- $this->getRequest()->getParam('id')
- ) : $this->_typeFactory->create()->load(
- $this->_coreRegistry->registry('entityType')
- )->getDefaultAttributeSetId();
+ return (int)$this->getRequest()->getParam('id') > 0
+ ? (int)$this->getRequest()->getParam('id')
+ : $this->_typeFactory->create()->load(
+ $this->_coreRegistry->registry('entityType')
+ )->getDefaultAttributeSetId();
}
}
diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Set/Main/Tree/Attribute.php b/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Set/Main/Tree/Attribute.php
index f5e3f94418687..cb0a739b56e4e 100644
--- a/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Set/Main/Tree/Attribute.php
+++ b/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Set/Main/Tree/Attribute.php
@@ -14,5 +14,5 @@ class Attribute extends \Magento\Backend\Block\Template
/**
* @var string
*/
- protected $_template = 'catalog/product/attribute/set/main/tree/attribute.phtml';
+ protected $_template = 'Magento_Catalog::catalog/product/attribute/set/main/tree/attribute.phtml';
}
diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Set/Main/Tree/Group.php b/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Set/Main/Tree/Group.php
index cf8de44c3d9df..93c2dcc76263c 100644
--- a/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Set/Main/Tree/Group.php
+++ b/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Set/Main/Tree/Group.php
@@ -14,5 +14,5 @@ class Group extends \Magento\Backend\Block\Template
/**
* @var string
*/
- protected $_template = 'catalog/product/attribute/set/main/tree/group.phtml';
+ protected $_template = 'Magento_Catalog::catalog/product/attribute/set/main/tree/group.phtml';
}
diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Set/Toolbar/Add.php b/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Set/Toolbar/Add.php
index 329afa968307c..f69e58985bfc5 100644
--- a/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Set/Toolbar/Add.php
+++ b/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Set/Toolbar/Add.php
@@ -18,7 +18,7 @@ class Add extends \Magento\Backend\Block\Template
/**
* @var string
*/
- protected $_template = 'catalog/product/attribute/set/toolbar/add.phtml';
+ protected $_template = 'Magento_Catalog::catalog/product/attribute/set/toolbar/add.phtml';
/**
* @return AbstractBlock
diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Set/Toolbar/Main.php b/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Set/Toolbar/Main.php
index f707c5c340b68..e29ab26065dc3 100644
--- a/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Set/Toolbar/Main.php
+++ b/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Set/Toolbar/Main.php
@@ -20,7 +20,7 @@ class Main extends \Magento\Backend\Block\Template
/**
* @var string
*/
- protected $_template = 'catalog/product/attribute/set/toolbar/main.phtml';
+ protected $_template = 'Magento_Catalog::catalog/product/attribute/set/toolbar/main.phtml';
/**
* @return $this
diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Product/Composite/Configure.php b/app/code/Magento/Catalog/Block/Adminhtml/Product/Composite/Configure.php
index 9270c1ac38ba3..98280d8d31237 100644
--- a/app/code/Magento/Catalog/Block/Adminhtml/Product/Composite/Configure.php
+++ b/app/code/Magento/Catalog/Block/Adminhtml/Product/Composite/Configure.php
@@ -22,7 +22,7 @@ class Configure extends \Magento\Backend\Block\Widget
/**
* @var string
*/
- protected $_template = 'catalog/product/composite/configure.phtml';
+ protected $_template = 'Magento_Catalog::catalog/product/composite/configure.phtml';
/**
* Core registry
diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit.php b/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit.php
index c18106fe567d0..285caa974fd17 100644
--- a/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit.php
+++ b/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit.php
@@ -17,7 +17,7 @@ class Edit extends \Magento\Backend\Block\Widget
/**
* @var string
*/
- protected $_template = 'catalog/product/edit.phtml';
+ protected $_template = 'Magento_Catalog::catalog/product/edit.phtml';
/**
* Core registry
diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Action/Attribute/Tab/Inventory.php b/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Action/Attribute/Tab/Inventory.php
index 750bf6f8a0216..4aebd521fe60d 100644
--- a/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Action/Attribute/Tab/Inventory.php
+++ b/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Action/Attribute/Tab/Inventory.php
@@ -74,7 +74,7 @@ public function getFieldSuffix()
public function getStoreId()
{
$storeId = $this->getRequest()->getParam('store');
- return intval($storeId);
+ return (int)$storeId;
}
/**
diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Alerts.php b/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Alerts.php
index 4a3d7c4e14e30..d620cd5fee2b9 100644
--- a/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Alerts.php
+++ b/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Alerts.php
@@ -18,7 +18,7 @@ class Alerts extends \Magento\Backend\Block\Widget\Tab
/**
* @var string
*/
- protected $_template = 'catalog/product/tab/alert.phtml';
+ protected $_template = 'Magento_Catalog::catalog/product/tab/alert.phtml';
/**
* @return $this
diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Attributes/Search.php b/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Attributes/Search.php
index d5f66231f1d82..e1b97f996c769 100644
--- a/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Attributes/Search.php
+++ b/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Attributes/Search.php
@@ -81,7 +81,7 @@ public function getSelectorOptions()
*
* @param string $labelPart
* @param int $templateId
- * @return \Magento\Catalog\Model\ResourceModel\Product\Attribute\Collection
+ * @return array
*/
public function getSuggestedAttributes($labelPart, $templateId = null)
{
diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Inventory.php b/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Inventory.php
index e52c1d3aa4985..20e12889cae0d 100644
--- a/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Inventory.php
+++ b/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Inventory.php
@@ -15,7 +15,7 @@ class Inventory extends \Magento\Backend\Block\Widget
/**
* @var string
*/
- protected $_template = 'catalog/product/tab/inventory.phtml';
+ protected $_template = 'Magento_Catalog::catalog/product/tab/inventory.phtml';
/**
* @var \Magento\Framework\Module\Manager
diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Options.php b/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Options.php
index a7a919e53f56e..2c79f5a6fa718 100644
--- a/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Options.php
+++ b/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Options.php
@@ -16,7 +16,7 @@ class Options extends Widget
/**
* @var string
*/
- protected $_template = 'catalog/product/edit/options.phtml';
+ protected $_template = 'Magento_Catalog::catalog/product/edit/options.phtml';
/**
* @return Widget
diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Options/Option.php b/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Options/Option.php
index 64856a5c69dc7..651d9a00095a3 100644
--- a/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Options/Option.php
+++ b/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Options/Option.php
@@ -38,7 +38,7 @@ class Option extends Widget
/**
* @var string
*/
- protected $_template = 'catalog/product/edit/options/option.phtml';
+ protected $_template = 'Magento_Catalog::catalog/product/edit/options/option.phtml';
/**
* Core registry
@@ -313,9 +313,9 @@ public function getOptionValues()
$value['checkboxScopeTitle'] = $this->getCheckboxScopeHtml(
$option->getOptionId(),
'title',
- is_null($option->getStoreTitle())
+ $option->getStoreTitle() === null
);
- $value['scopeTitleDisabled'] = is_null($option->getStoreTitle()) ? 'disabled' : null;
+ $value['scopeTitleDisabled'] = $option->getStoreTitle() === null ? 'disabled' : null;
}
if ($option->getGroupByType() == ProductCustomOptionInterface::OPTION_GROUP_SELECT) {
@@ -341,22 +341,22 @@ public function getOptionValues()
$value['optionValues'][$i]['checkboxScopeTitle'] = $this->getCheckboxScopeHtml(
$_value->getOptionId(),
'title',
- is_null($_value->getStoreTitle()),
+ $_value->getStoreTitle() === null,
$_value->getOptionTypeId()
);
- $value['optionValues'][$i]['scopeTitleDisabled'] = is_null(
- $_value->getStoreTitle()
+ $value['optionValues'][$i]['scopeTitleDisabled'] = (
+ $_value->getStoreTitle() === null
) ? 'disabled' : null;
if ($scope == \Magento\Store\Model\Store::PRICE_SCOPE_WEBSITE) {
$value['optionValues'][$i]['checkboxScopePrice'] = $this->getCheckboxScopeHtml(
$_value->getOptionId(),
'price',
- is_null($_value->getstorePrice()),
+ $_value->getstorePrice() === null,
$_value->getOptionTypeId(),
['$(this).up(1).previous()']
);
- $value['optionValues'][$i]['scopePriceDisabled'] = is_null(
- $_value->getStorePrice()
+ $value['optionValues'][$i]['scopePriceDisabled'] = (
+ $_value->getStorePrice() === null
) ? 'disabled' : null;
}
}
@@ -379,9 +379,9 @@ public function getOptionValues()
$value['checkboxScopePrice'] = $this->getCheckboxScopeHtml(
$option->getOptionId(),
'price',
- is_null($option->getStorePrice())
+ $option->getStorePrice() === null
);
- $value['scopePriceDisabled'] = is_null($option->getStorePrice()) ? 'disabled' : null;
+ $value['scopePriceDisabled'] = $option->getStorePrice() === null ? 'disabled' : null;
}
}
$values[] = new \Magento\Framework\DataObject($value);
diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Options/Type/Date.php b/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Options/Type/Date.php
index babfc1b072bd2..a0bbc4ad033de 100644
--- a/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Options/Type/Date.php
+++ b/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Options/Type/Date.php
@@ -16,5 +16,5 @@ class Date extends \Magento\Catalog\Block\Adminhtml\Product\Edit\Tab\Options\Typ
/**
* @var string
*/
- protected $_template = 'catalog/product/edit/options/type/date.phtml';
+ protected $_template = 'Magento_Catalog::catalog/product/edit/options/type/date.phtml';
}
diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Options/Type/File.php b/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Options/Type/File.php
index 322aa02f97731..d3d5f08fa9eae 100644
--- a/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Options/Type/File.php
+++ b/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Options/Type/File.php
@@ -16,5 +16,5 @@ class File extends \Magento\Catalog\Block\Adminhtml\Product\Edit\Tab\Options\Typ
/**
* @var string
*/
- protected $_template = 'catalog/product/edit/options/type/file.phtml';
+ protected $_template = 'Magento_Catalog::catalog/product/edit/options/type/file.phtml';
}
diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Options/Type/Select.php b/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Options/Type/Select.php
index 24de84958ef4a..f6ab5134ae6bd 100644
--- a/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Options/Type/Select.php
+++ b/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Options/Type/Select.php
@@ -16,7 +16,7 @@ class Select extends \Magento\Catalog\Block\Adminhtml\Product\Edit\Tab\Options\T
/**
* @var string
*/
- protected $_template = 'catalog/product/edit/options/type/select.phtml';
+ protected $_template = 'Magento_Catalog::catalog/product/edit/options/type/select.phtml';
/**
* Class constructor
diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Options/Type/Text.php b/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Options/Type/Text.php
index 7241128fac3b4..e6f78dc3ed169 100644
--- a/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Options/Type/Text.php
+++ b/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Options/Type/Text.php
@@ -16,5 +16,5 @@ class Text extends \Magento\Catalog\Block\Adminhtml\Product\Edit\Tab\Options\Typ
/**
* @var string
*/
- protected $_template = 'catalog/product/edit/options/type/text.phtml';
+ protected $_template = 'Magento_Catalog::catalog/product/edit/options/type/text.phtml';
}
diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Price/Tier.php b/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Price/Tier.php
index a80ddd8c122a1..7cb1c2c9e4263 100644
--- a/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Price/Tier.php
+++ b/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Price/Tier.php
@@ -13,7 +13,7 @@ class Tier extends Group\AbstractGroup
/**
* @var string
*/
- protected $_template = 'catalog/product/edit/price/tier.phtml';
+ protected $_template = 'Magento_Catalog::catalog/product/edit/price/tier.phtml';
/**
* Retrieve list of initial customer groups
diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Websites.php b/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Websites.php
index 6a3347b44512f..6189a97dbe761 100644
--- a/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Websites.php
+++ b/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Websites.php
@@ -21,7 +21,7 @@ class Websites extends \Magento\Backend\Block\Store\Switcher
/**
* @var string
*/
- protected $_template = 'catalog/product/edit/websites.phtml';
+ protected $_template = 'Magento_Catalog::catalog/product/edit/websites.phtml';
/**
* Core registry
diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Product/Helper/Form/Gallery.php b/app/code/Magento/Catalog/Block/Adminhtml/Product/Helper/Form/Gallery.php
index 49a75990ec0d2..9c533705fcd52 100644
--- a/app/code/Magento/Catalog/Block/Adminhtml/Product/Helper/Form/Gallery.php
+++ b/app/code/Magento/Catalog/Block/Adminhtml/Product/Helper/Form/Gallery.php
@@ -13,6 +13,8 @@
*/
namespace Magento\Catalog\Block\Adminhtml\Product\Helper\Form;
+use Magento\Framework\App\ObjectManager;
+use Magento\Framework\App\Request\DataPersistorInterface;
use Magento\Framework\Registry;
use Magento\Catalog\Model\Product;
use Magento\Eav\Model\Entity\Attribute;
@@ -68,6 +70,11 @@ class Gallery extends \Magento\Framework\View\Element\AbstractBlock
*/
protected $registry;
+ /**
+ * @var DataPersistorInterface
+ */
+ private $dataPersistor;
+
/**
* @param \Magento\Framework\View\Element\Context $context
* @param \Magento\Store\Model\StoreManagerInterface $storeManager
@@ -80,11 +87,13 @@ public function __construct(
\Magento\Store\Model\StoreManagerInterface $storeManager,
Registry $registry,
\Magento\Framework\Data\Form $form,
- $data = []
+ $data = [],
+ DataPersistorInterface $dataPersistor = null
) {
$this->storeManager = $storeManager;
$this->registry = $registry;
$this->form = $form;
+ $this->dataPersistor = $dataPersistor ?: ObjectManager::getInstance()->get(DataPersistorInterface::class);
parent::__construct($context, $data);
}
@@ -104,7 +113,24 @@ public function getElementHtml()
*/
public function getImages()
{
- return $this->registry->registry('current_product')->getData('media_gallery') ?: null;
+ $images = $this->getDataObject()->getData('media_gallery') ?: null;
+ if ($images === null) {
+ $images = ((array)$this->dataPersistor->get('catalog_product'))['product']['media_gallery'] ?? null;
+ }
+
+ return $images;
+ }
+
+ /**
+ * Get value for given type.
+ *
+ * @param string $type
+ * @return string|null
+ */
+ public function getImageValue(string $type)
+ {
+ $product = (array)$this->dataPersistor->get('catalog_product');
+ return $product['product'][$type] ?? null;
}
/**
diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Product/Helper/Form/Gallery/Content.php b/app/code/Magento/Catalog/Block/Adminhtml/Product/Helper/Form/Gallery/Content.php
index 5188bf365e5e9..ac9e75493bdc3 100644
--- a/app/code/Magento/Catalog/Block/Adminhtml/Product/Helper/Form/Gallery/Content.php
+++ b/app/code/Magento/Catalog/Block/Adminhtml/Product/Helper/Form/Gallery/Content.php
@@ -23,7 +23,7 @@ class Content extends \Magento\Backend\Block\Widget
/**
* @var string
*/
- protected $_template = 'catalog/product/helper/gallery.phtml';
+ protected $_template = 'Magento_Catalog::catalog/product/helper/gallery.phtml';
/**
* @var \Magento\Catalog\Model\Product\Media\Config
@@ -193,9 +193,11 @@ public function getImageTypes()
$imageTypes = [];
foreach ($this->getMediaAttributes() as $attribute) {
/* @var $attribute \Magento\Eav\Model\Entity\Attribute */
+ $value = $this->getElement()->getDataObject()->getData($attribute->getAttributeCode())
+ ?: $this->getElement()->getImageValue($attribute->getAttributeCode());
$imageTypes[$attribute->getAttributeCode()] = [
'code' => $attribute->getAttributeCode(),
- 'value' => $this->getElement()->getDataObject()->getData($attribute->getAttributeCode()),
+ 'value' => $value,
'label' => $attribute->getFrontend()->getLabel(),
'scope' => __($this->getElement()->getScopeLabel($attribute)),
'name' => $this->getElement()->getAttributeFieldName($attribute),
diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Product/Widget/Chooser/Container.php b/app/code/Magento/Catalog/Block/Adminhtml/Product/Widget/Chooser/Container.php
index 19c1574d6e9a5..b8967f1f30e55 100644
--- a/app/code/Magento/Catalog/Block/Adminhtml/Product/Widget/Chooser/Container.php
+++ b/app/code/Magento/Catalog/Block/Adminhtml/Product/Widget/Chooser/Container.php
@@ -18,5 +18,5 @@ class Container extends Template
/**
* @var string
*/
- protected $_template = 'catalog/product/widget/chooser/container.phtml';
+ protected $_template = 'Magento_Catalog::catalog/product/widget/chooser/container.phtml';
}
diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Rss/Grid/Link.php b/app/code/Magento/Catalog/Block/Adminhtml/Rss/Grid/Link.php
index dbeff93683bc0..a9ec80c5f0232 100644
--- a/app/code/Magento/Catalog/Block/Adminhtml/Rss/Grid/Link.php
+++ b/app/code/Magento/Catalog/Block/Adminhtml/Rss/Grid/Link.php
@@ -13,7 +13,7 @@ class Link extends \Magento\Framework\View\Element\Template
/**
* @var string
*/
- protected $_template = 'rss/grid/link.phtml';
+ protected $_template = 'Magento_Catalog::rss/grid/link.phtml';
/**
* @var \Magento\Framework\App\Rss\UrlBuilderInterface
@@ -69,7 +69,7 @@ public function isRssAllowed()
}
/**
- * @return string
+ * @return array
*/
protected function getLinkParams()
{
diff --git a/app/code/Magento/Catalog/Block/Category/Plugin/PriceBoxTags.php b/app/code/Magento/Catalog/Block/Category/Plugin/PriceBoxTags.php
index 37d087049e4aa..99399110505b7 100644
--- a/app/code/Magento/Catalog/Block/Category/Plugin/PriceBoxTags.php
+++ b/app/code/Magento/Catalog/Block/Category/Plugin/PriceBoxTags.php
@@ -71,7 +71,7 @@ public function afterGetCacheKey(PriceBox $subject, $result)
'-',
[
$result,
- $this->priceCurrency->getCurrencySymbol(),
+ $this->priceCurrency->getCurrency()->getCode(),
$this->dateTime->scopeDate($this->scopeResolver->getScope()->getId())->format('Ymd'),
$this->scopeResolver->getScope()->getId(),
$this->customerSession->getCustomerGroupId(),
@@ -102,8 +102,8 @@ private function getTaxRateIds(PriceBox $subject)
if (!empty($billingAddress) || !empty($shippingAddress)) {
$rateRequest = $this->getTaxCalculation()->getRateRequest(
- $billingAddress,
$shippingAddress,
+ $billingAddress,
$customerTaxClassId,
$this->scopeResolver->getScope()->getId(),
$this->customerSession->getCustomerId()
diff --git a/app/code/Magento/Catalog/Block/Category/Rss/Link.php b/app/code/Magento/Catalog/Block/Category/Rss/Link.php
index 0599d5f4b989c..e40b81200574c 100644
--- a/app/code/Magento/Catalog/Block/Category/Rss/Link.php
+++ b/app/code/Magento/Catalog/Block/Category/Rss/Link.php
@@ -62,7 +62,7 @@ public function getLabel()
}
/**
- * @return string
+ * @return array
*/
protected function getLinkParams()
{
diff --git a/app/code/Magento/Catalog/Block/Product/AbstractProduct.php b/app/code/Magento/Catalog/Block/Product/AbstractProduct.php
index d4af775ad20da..58800802cc60d 100644
--- a/app/code/Magento/Catalog/Block/Product/AbstractProduct.php
+++ b/app/code/Magento/Catalog/Block/Product/AbstractProduct.php
@@ -195,7 +195,7 @@ public function getAddToCompareUrl()
* Gets minimal sales quantity
*
* @param \Magento\Catalog\Model\Product $product
- * @return int|null
+ * @return float|null
*/
public function getMinimalQty($product)
{
diff --git a/app/code/Magento/Catalog/Block/Product/Compare/ListCompare.php b/app/code/Magento/Catalog/Block/Product/Compare/ListCompare.php
index 05699f0cce5ab..f6a3ef8712192 100644
--- a/app/code/Magento/Catalog/Block/Product/Compare/ListCompare.php
+++ b/app/code/Magento/Catalog/Block/Product/Compare/ListCompare.php
@@ -214,6 +214,23 @@ public function getProductAttributeValue($product, $attribute)
return (string)$value == '' ? __('No') : $value;
}
+ /**
+ * Check if any of the products has a value set for the attribute.
+ *
+ * @param \Magento\Catalog\Model\ResourceModel\Eav\Attribute $attribute
+ * @return bool
+ */
+ public function hasAttributeValueForProducts(\Magento\Catalog\Model\ResourceModel\Eav\Attribute $attribute) : bool
+ {
+ foreach ($this->getItems() as $item) {
+ if ($item->hasData($attribute->getAttributeCode())) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
/**
* Retrieve Print URL
*
diff --git a/app/code/Magento/Catalog/Block/Product/Image.php b/app/code/Magento/Catalog/Block/Product/Image.php
index a1fcdf43f6eb0..3ce97bd53f8d7 100644
--- a/app/code/Magento/Catalog/Block/Product/Image.php
+++ b/app/code/Magento/Catalog/Block/Product/Image.php
@@ -20,16 +20,19 @@
class Image extends \Magento\Framework\View\Element\Template
{
/**
+ * @deprecated Property isn't used
* @var \Magento\Catalog\Helper\Image
*/
protected $imageHelper;
/**
+ * @deprecated Property isn't used
* @var \Magento\Catalog\Model\Product
*/
protected $product;
/**
+ * @deprecated Property isn't used
* @var array
*/
protected $attributes = [];
diff --git a/app/code/Magento/Catalog/Block/Product/ImageBuilder.php b/app/code/Magento/Catalog/Block/Product/ImageBuilder.php
index d47218db37142..f1149f15c41d3 100644
--- a/app/code/Magento/Catalog/Block/Product/ImageBuilder.php
+++ b/app/code/Magento/Catalog/Block/Product/ImageBuilder.php
@@ -6,6 +6,8 @@
namespace Magento\Catalog\Block\Product;
use Magento\Catalog\Helper\ImageFactory as HelperFactory;
+use Magento\Catalog\Model\Product;
+use Magento\Catalog\Model\Product\Image\NotLoadInfoImageException;
class ImageBuilder
{
@@ -20,7 +22,7 @@ class ImageBuilder
protected $helperFactory;
/**
- * @var \Magento\Catalog\Model\Product
+ * @var Product
*/
protected $product;
@@ -49,10 +51,10 @@ public function __construct(
/**
* Set product
*
- * @param \Magento\Catalog\Model\Product $product
+ * @param Product $product
* @return $this
*/
- public function setProduct(\Magento\Catalog\Model\Product $product)
+ public function setProduct(Product $product)
{
$this->product = $product;
return $this;
@@ -78,9 +80,7 @@ public function setImageId($imageId)
*/
public function setAttributes(array $attributes)
{
- if ($attributes) {
- $this->attributes = $attributes;
- }
+ $this->attributes = $attributes;
return $this;
}
@@ -129,7 +129,11 @@ public function create()
? 'Magento_Catalog::product/image.phtml'
: 'Magento_Catalog::product/image_with_borders.phtml';
- $imagesize = $helper->getResizedImageInfo();
+ try {
+ $imagesize = $helper->getResizedImageInfo();
+ } catch (NotLoadInfoImageException $exception) {
+ $imagesize = [$helper->getWidth(), $helper->getHeight()];
+ }
$data = [
'data' => [
@@ -140,8 +144,9 @@ public function create()
'label' => $helper->getLabel(),
'ratio' => $this->getRatio($helper),
'custom_attributes' => $this->getCustomAttributes(),
- 'resized_image_width' => !empty($imagesize[0]) ? $imagesize[0] : $helper->getWidth(),
- 'resized_image_height' => !empty($imagesize[1]) ? $imagesize[1] : $helper->getHeight(),
+ 'resized_image_width' => $imagesize[0],
+ 'resized_image_height' => $imagesize[1],
+ 'product_id' => $this->product->getId()
],
];
diff --git a/app/code/Magento/Catalog/Block/Product/ListProduct.php b/app/code/Magento/Catalog/Block/Product/ListProduct.php
index 7289aa85ea016..0b9de2183786a 100644
--- a/app/code/Magento/Catalog/Block/Product/ListProduct.php
+++ b/app/code/Magento/Catalog/Block/Product/ListProduct.php
@@ -9,11 +9,20 @@
use Magento\Catalog\Api\CategoryRepositoryInterface;
use Magento\Catalog\Block\Product\ProductList\Toolbar;
use Magento\Catalog\Model\Category;
+use Magento\Catalog\Model\Config;
+use Magento\Catalog\Model\Layer;
+use Magento\Catalog\Model\Layer\Resolver;
use Magento\Catalog\Model\Product;
use Magento\Catalog\Model\ResourceModel\Product\Collection;
+use Magento\Catalog\Pricing\Price\FinalPrice;
use Magento\Eav\Model\Entity\Collection\AbstractCollection;
+use Magento\Framework\App\ActionInterface;
+use Magento\Framework\App\Config\Element;
+use Magento\Framework\Data\Helper\PostHelper;
use Magento\Framework\DataObject\IdentityInterface;
use Magento\Framework\Exception\NoSuchEntityException;
+use Magento\Framework\Pricing\Render;
+use Magento\Framework\Url\Helper\Data;
/**
* Product list
@@ -40,17 +49,17 @@ class ListProduct extends AbstractProduct implements IdentityInterface
/**
* Catalog layer
*
- * @var \Magento\Catalog\Model\Layer
+ * @var Layer
*/
protected $_catalogLayer;
/**
- * @var \Magento\Framework\Data\Helper\PostHelper
+ * @var PostHelper
*/
protected $_postDataHelper;
/**
- * @var \Magento\Framework\Url\Helper\Data
+ * @var Data
*/
protected $urlHelper;
@@ -61,18 +70,18 @@ class ListProduct extends AbstractProduct implements IdentityInterface
/**
* @param Context $context
- * @param \Magento\Framework\Data\Helper\PostHelper $postDataHelper
- * @param \Magento\Catalog\Model\Layer\Resolver $layerResolver
+ * @param PostHelper $postDataHelper
+ * @param Resolver $layerResolver
* @param CategoryRepositoryInterface $categoryRepository
- * @param \Magento\Framework\Url\Helper\Data $urlHelper
+ * @param Data $urlHelper
* @param array $data
*/
public function __construct(
- \Magento\Catalog\Block\Product\Context $context,
- \Magento\Framework\Data\Helper\PostHelper $postDataHelper,
- \Magento\Catalog\Model\Layer\Resolver $layerResolver,
+ Context $context,
+ PostHelper $postDataHelper,
+ Resolver $layerResolver,
CategoryRepositoryInterface $categoryRepository,
- \Magento\Framework\Url\Helper\Data $urlHelper,
+ Data $urlHelper,
array $data = []
) {
$this->_catalogLayer = $layerResolver->get();
@@ -113,7 +122,7 @@ protected function _getProductCollection()
/**
* Get catalog layer model
*
- * @return \Magento\Catalog\Model\Layer
+ * @return Layer
*/
public function getLayer()
{
@@ -137,7 +146,35 @@ public function getLoadedProductCollection()
*/
public function getMode()
{
- return $this->getChildBlock('toolbar')->getCurrentMode();
+ if ($this->getChildBlock('toolbar')) {
+ return $this->getChildBlock('toolbar')->getCurrentMode();
+ }
+
+ return $this->getDefaultListingMode();
+ }
+
+ /**
+ * Get listing mode for products if toolbar is removed from layout.
+ * Use the general configuration for product list mode from config path catalog/frontend/list_mode as default value
+ * or mode data from block declaration from layout.
+ *
+ * @return string
+ */
+ private function getDefaultListingMode()
+ {
+ // default Toolbar when the toolbar layout is not used
+ $defaultToolbar = $this->getToolbarBlock();
+ $availableModes = $defaultToolbar->getModes();
+
+ // layout config mode
+ $mode = $this->getData('mode');
+
+ if (!$mode || !isset($availableModes[$mode])) {
+ // default config mode
+ $mode = $defaultToolbar->getCurrentMode();
+ }
+
+ return $mode;
}
/**
@@ -148,28 +185,60 @@ public function getMode()
protected function _beforeToHtml()
{
$collection = $this->_getProductCollection();
- $this->configureToolbar($this->getToolbarBlock(), $collection);
+
+ $this->addToolbarBlock($collection);
+
$collection->load();
return parent::_beforeToHtml();
}
/**
- * Retrieve Toolbar block
+ * Add toolbar block from product listing layout
+ *
+ * @param Collection $collection
+ */
+ private function addToolbarBlock(Collection $collection)
+ {
+ $toolbarLayout = $this->getToolbarFromLayout();
+
+ if ($toolbarLayout) {
+ $this->configureToolbar($toolbarLayout, $collection);
+ }
+ }
+
+ /**
+ * Retrieve Toolbar block from layout or a default Toolbar
*
* @return Toolbar
*/
public function getToolbarBlock()
+ {
+ $block = $this->getToolbarFromLayout();
+
+ if (!$block) {
+ $block = $this->getLayout()->createBlock($this->_defaultToolbarBlock, uniqid(microtime()));
+ }
+
+ return $block;
+ }
+
+ /**
+ * Get toolbar block from layout
+ *
+ * @return bool|Toolbar
+ */
+ private function getToolbarFromLayout()
{
$blockName = $this->getToolbarBlockName();
+
+ $toolbarLayout = false;
+
if ($blockName) {
- $block = $this->getLayout()->getBlock($blockName);
- if ($block) {
- return $block;
- }
+ $toolbarLayout = $this->getLayout()->getBlock($blockName);
}
- $block = $this->getLayout()->createBlock($this->_defaultToolbarBlock, uniqid(microtime()));
- return $block;
+
+ return $toolbarLayout;
}
/**
@@ -203,7 +272,7 @@ public function setCollection($collection)
}
/**
- * @param array|string|integer|\Magento\Framework\App\Config\Element $code
+ * @param array|string|integer| Element $code
* @return $this
*/
public function addAttribute($code)
@@ -223,7 +292,7 @@ public function getPriceBlockTemplate()
/**
* Retrieve Catalog Config object
*
- * @return \Magento\Catalog\Model\Config
+ * @return Config
*/
protected function _getConfig()
{
@@ -233,8 +302,8 @@ protected function _getConfig()
/**
* Prepare Sort By fields from Category Data
*
- * @param \Magento\Catalog\Model\Category $category
- * @return \Magento\Catalog\Block\Product\ListProduct
+ * @param Category $category
+ * @return $this
*/
public function prepareSortableFieldsByCategory($category)
{
@@ -265,51 +334,59 @@ public function prepareSortableFieldsByCategory($category)
public function getIdentities()
{
$identities = [];
- foreach ($this->_getProductCollection() as $item) {
- $identities = array_merge($identities, $item->getIdentities());
- }
+
$category = $this->getLayer()->getCurrentCategory();
if ($category) {
$identities[] = Product::CACHE_PRODUCT_CATEGORY_TAG . '_' . $category->getId();
}
+
+ //Check if category page shows only static block (No products)
+ if ($category->getData('display_mode') == Category::DM_PAGE) {
+ return $identities;
+ }
+
+ foreach ($this->_getProductCollection() as $item) {
+ $identities = array_merge($identities, $item->getIdentities());
+ }
+
return $identities;
}
/**
* Get post parameters
*
- * @param \Magento\Catalog\Model\Product $product
- * @return string
+ * @param Product $product
+ * @return array
*/
- public function getAddToCartPostParams(\Magento\Catalog\Model\Product $product)
+ public function getAddToCartPostParams(Product $product)
{
$url = $this->getAddToCartUrl($product);
return [
'action' => $url,
'data' => [
'product' => $product->getEntityId(),
- \Magento\Framework\App\ActionInterface::PARAM_NAME_URL_ENCODED => $this->urlHelper->getEncodedUrl($url),
+ ActionInterface::PARAM_NAME_URL_ENCODED => $this->urlHelper->getEncodedUrl($url),
]
];
}
/**
- * @param \Magento\Catalog\Model\Product $product
+ * @param Product $product
* @return string
*/
- public function getProductPrice(\Magento\Catalog\Model\Product $product)
+ public function getProductPrice(Product $product)
{
$priceRender = $this->getPriceRender();
$price = '';
if ($priceRender) {
$price = $priceRender->render(
- \Magento\Catalog\Pricing\Price\FinalPrice::PRICE_CODE,
+ FinalPrice::PRICE_CODE,
$product,
[
'include_container' => true,
'display_minimal_price' => true,
- 'zone' => \Magento\Framework\Pricing\Render::ZONE_ITEM_LIST,
+ 'zone' => Render::ZONE_ITEM_LIST,
'list_category_page' => true
]
);
@@ -322,7 +399,7 @@ public function getProductPrice(\Magento\Catalog\Model\Product $product)
* Specifies that price rendering should be done for the list of products
* i.e. rendering happens in the scope of product list, but not single product
*
- * @return \Magento\Framework\Pricing\Render
+ * @return Render
*/
protected function getPriceRender()
{
@@ -348,7 +425,7 @@ protected function getPriceRender()
private function initializeProductCollection()
{
$layer = $this->getLayer();
- /* @var $layer \Magento\Catalog\Model\Layer */
+ /* @var $layer Layer */
if ($this->getShowRootCategory()) {
$this->setCategoryId($this->_storeManager->getStore()->getRootCategoryId());
}
@@ -386,9 +463,8 @@ private function initializeProductCollection()
if ($origCategory) {
$layer->setCurrentCategory($origCategory);
}
-
- $toolbar = $this->getToolbarBlock();
- $this->configureToolbar($toolbar, $collection);
+
+ $this->addToolbarBlock($collection);
$this->_eventManager->dispatch(
'catalog_block_product_list_collection',
diff --git a/app/code/Magento/Catalog/Block/Product/ProductList/Related.php b/app/code/Magento/Catalog/Block/Product/ProductList/Related.php
index 95d9b1ae61208..3f9dac98033aa 100644
--- a/app/code/Magento/Catalog/Block/Product/ProductList/Related.php
+++ b/app/code/Magento/Catalog/Block/Product/ProductList/Related.php
@@ -121,7 +121,7 @@ public function getItems()
* getIdentities() depends on _itemCollection populated, but it can be empty if the block is hidden
* @see https://github.com/magento/magento2/issues/5897
*/
- if (is_null($this->_itemCollection)) {
+ if ($this->_itemCollection === null) {
$this->_prepareData();
}
return $this->_itemCollection;
diff --git a/app/code/Magento/Catalog/Block/Product/ProductList/Toolbar.php b/app/code/Magento/Catalog/Block/Product/ProductList/Toolbar.php
index f461b52515253..674b20a49c837 100644
--- a/app/code/Magento/Catalog/Block/Product/ProductList/Toolbar.php
+++ b/app/code/Magento/Catalog/Block/Product/ProductList/Toolbar.php
@@ -83,7 +83,7 @@ class Toolbar extends \Magento\Framework\View\Element\Template
/**
* @var string
*/
- protected $_template = 'product/list/toolbar.phtml';
+ protected $_template = 'Magento_Catalog::product/list/toolbar.phtml';
/**
* Catalog config
@@ -192,7 +192,14 @@ public function setCollection($collection)
$this->_collection->setPageSize($limit);
}
if ($this->getCurrentOrder()) {
- $this->_collection->setOrder($this->getCurrentOrder(), $this->getCurrentDirection());
+ if ($this->getCurrentOrder() == 'position') {
+ $this->_collection->addAttributeToSort(
+ $this->getCurrentOrder(),
+ $this->getCurrentDirection()
+ );
+ } else {
+ $this->_collection->setOrder($this->getCurrentOrder(), $this->getCurrentDirection());
+ }
}
return $this;
}
@@ -682,7 +689,7 @@ public function getWidgetOptionsJson(array $customOptions = [])
'limit' => ToolbarModel::LIMIT_PARAM_NAME,
'modeDefault' => $defaultMode,
'directionDefault' => $this->_direction ?: ProductList::DEFAULT_SORT_DIRECTION,
- 'orderDefault' => $this->_productListHelper->getDefaultSortField(),
+ 'orderDefault' => $this->getOrderField(),
'limitDefault' => $this->_productListHelper->getDefaultLimitPerPageValue($defaultMode),
'url' => $this->getPagerUrl(),
];
diff --git a/app/code/Magento/Catalog/Block/Product/ProductList/Upsell.php b/app/code/Magento/Catalog/Block/Product/ProductList/Upsell.php
index f97d1a788dafb..726f083c07cd4 100644
--- a/app/code/Magento/Catalog/Block/Product/ProductList/Upsell.php
+++ b/app/code/Magento/Catalog/Block/Product/ProductList/Upsell.php
@@ -140,7 +140,7 @@ public function getItemCollection()
* getIdentities() depends on _itemCollection populated, but it can be empty if the block is hidden
* @see https://github.com/magento/magento2/issues/5897
*/
- if (is_null($this->_itemCollection)) {
+ if ($this->_itemCollection === null) {
$this->_prepareData();
}
return $this->_itemCollection;
@@ -151,7 +151,7 @@ public function getItemCollection()
*/
public function getItems()
{
- if (is_null($this->_items)) {
+ if ($this->_items === null) {
$this->_items = $this->getItemCollection()->getItems();
}
return $this->_items;
@@ -171,8 +171,8 @@ public function getRowCount()
*/
public function setColumnCount($columns)
{
- if (intval($columns) > 0) {
- $this->_columnCount = intval($columns);
+ if ((int)$columns > 0) {
+ $this->_columnCount = (int)$columns;
}
return $this;
}
@@ -214,8 +214,8 @@ public function getIterableItem()
*/
public function setItemLimit($type, $limit)
{
- if (intval($limit) > 0) {
- $this->_itemLimits[$type] = intval($limit);
+ if ((int)$limit > 0) {
+ $this->_itemLimits[$type] = (int)$limit;
}
return $this;
}
diff --git a/app/code/Magento/Catalog/Block/Product/View.php b/app/code/Magento/Catalog/Block/Product/View.php
index b3b5dea9602f6..8055d17a64a84 100644
--- a/app/code/Magento/Catalog/Block/Product/View.php
+++ b/app/code/Magento/Catalog/Block/Product/View.php
@@ -120,51 +120,6 @@ public function getWishlistOptions()
return ['productType' => $this->getProduct()->getTypeId()];
}
- /**
- * Add meta information from product to head block
- *
- * @return \Magento\Catalog\Block\Product\View
- */
- protected function _prepareLayout()
- {
- $this->getLayout()->createBlock(\Magento\Catalog\Block\Breadcrumbs::class);
- $product = $this->getProduct();
- if (!$product) {
- return parent::_prepareLayout();
- }
-
- $title = $product->getMetaTitle();
- if ($title) {
- $this->pageConfig->getTitle()->set($title);
- }
- $keyword = $product->getMetaKeyword();
- $currentCategory = $this->_coreRegistry->registry('current_category');
- if ($keyword) {
- $this->pageConfig->setKeywords($keyword);
- } elseif ($currentCategory) {
- $this->pageConfig->setKeywords($product->getName());
- }
- $description = $product->getMetaDescription();
- if ($description) {
- $this->pageConfig->setDescription($description);
- } else {
- $this->pageConfig->setDescription($this->string->substr($product->getDescription(), 0, 255));
- }
- if ($this->_productHelper->canUseCanonicalTag()) {
- $this->pageConfig->addRemotePageAsset(
- $product->getUrlModel()->getUrl($product, ['_ignore_category' => true]),
- 'canonical',
- ['attributes' => ['rel' => 'canonical']]
- );
- }
-
- $pageMainTitle = $this->getLayout()->getBlock('page.main.title');
- if ($pageMainTitle) {
- $pageMainTitle->setPageTitle($product->getName());
- }
- return parent::_prepareLayout();
- }
-
/**
* Retrieve current product model
*
diff --git a/app/code/Magento/Catalog/Block/Product/View/Additional.php b/app/code/Magento/Catalog/Block/Product/View/Additional.php
index 37c82c2bc84c8..66527985e41fd 100644
--- a/app/code/Magento/Catalog/Block/Product/View/Additional.php
+++ b/app/code/Magento/Catalog/Block/Product/View/Additional.php
@@ -25,7 +25,7 @@ class Additional extends \Magento\Framework\View\Element\Template
/**
* @var string
*/
- protected $_template = 'product/view/additional.phtml';
+ protected $_template = 'Magento_Catalog::product/view/additional.phtml';
/**
* @return array
diff --git a/app/code/Magento/Catalog/Block/Product/View/Attributes.php b/app/code/Magento/Catalog/Block/Product/View/Attributes.php
index fbdda684343b5..b353e477a056c 100644
--- a/app/code/Magento/Catalog/Block/Product/View/Attributes.php
+++ b/app/code/Magento/Catalog/Block/Product/View/Attributes.php
@@ -83,15 +83,13 @@ public function getAdditionalData(array $excludeAttr = [])
if ($attribute->getIsVisibleOnFront() && !in_array($attribute->getAttributeCode(), $excludeAttr)) {
$value = $attribute->getFrontend()->getValue($product);
- if (!$product->hasData($attribute->getAttributeCode())) {
- $value = __('N/A');
- } elseif ((string)$value == '') {
- $value = __('No');
+ if ($value instanceof Phrase) {
+ $value = (string)$value;
} elseif ($attribute->getFrontendInput() == 'price' && is_string($value)) {
$value = $this->priceCurrency->convertAndFormat($value);
}
- if ($value instanceof Phrase || (is_string($value) && strlen($value))) {
+ if (is_string($value) && strlen($value)) {
$data[$attribute->getAttributeCode()] = [
'label' => __($attribute->getStoreLabel()),
'value' => $value,
diff --git a/app/code/Magento/Catalog/Block/Product/View/Gallery.php b/app/code/Magento/Catalog/Block/Product/View/Gallery.php
index 44dd3b9f97cbf..17828b9d375d3 100644
--- a/app/code/Magento/Catalog/Block/Product/View/Gallery.php
+++ b/app/code/Magento/Catalog/Block/Product/View/Gallery.php
@@ -116,7 +116,7 @@ public function getGalleryImagesJson()
'thumb' => $image->getData('small_image_url'),
'img' => $image->getData('medium_image_url'),
'full' => $image->getData('large_image_url'),
- 'caption' => $image->getLabel(),
+ 'caption' => ($image->getLabel() ?: $this->getProduct()->getName()),
'position' => $image->getPosition(),
'isMain' => $this->isMainImage($image),
'type' => str_replace('external-', '', $image->getMediaType()),
@@ -175,7 +175,7 @@ public function getImageAttribute($imageId, $attributeName, $default = null)
{
$attributes =
$this->getConfigView()->getMediaAttributes('Magento_Catalog', Image::MEDIA_TYPE_CONFIG_NODE, $imageId);
- return isset($attributes[$attributeName]) ? $attributes[$attributeName] : $default;
+ return $attributes[$attributeName] ?? $default;
}
/**
diff --git a/app/code/Magento/Catalog/Block/Product/View/Options/AbstractOptions.php b/app/code/Magento/Catalog/Block/Product/View/Options/AbstractOptions.php
index 66bf5eafb156e..181211a0fc4a2 100644
--- a/app/code/Magento/Catalog/Block/Product/View/Options/AbstractOptions.php
+++ b/app/code/Magento/Catalog/Block/Product/View/Options/AbstractOptions.php
@@ -105,9 +105,11 @@ public function getOption()
}
/**
+ * Retrieve formatted price
+ *
* @return string
*/
- public function getFormatedPrice()
+ public function getFormattedPrice()
{
if ($option = $this->getOption()) {
return $this->_formatPrice(
@@ -120,6 +122,17 @@ public function getFormatedPrice()
return '';
}
+ /**
+ * @return string
+ *
+ * @deprecated
+ * @see getFormattedPrice()
+ */
+ public function getFormatedPrice()
+ {
+ return $this->getFormattedPrice();
+ }
+
/**
* Return formated price
*
@@ -178,7 +191,7 @@ public function getPrice($price, $includingTax = null)
* Returns price converted to current currency rate
*
* @param float $price
- * @return float
+ * @return float|string
*/
public function getCurrencyPrice($price)
{
diff --git a/app/code/Magento/Catalog/Block/Product/View/Options/Type/Select.php b/app/code/Magento/Catalog/Block/Product/View/Options/Type/Select.php
index d546ef483132b..7df9b972e1501 100644
--- a/app/code/Magento/Catalog/Block/Product/View/Options/Type/Select.php
+++ b/app/code/Magento/Catalog/Block/Product/View/Options/Type/Select.php
@@ -44,9 +44,9 @@ public function getValuesHtml()
]
);
if ($_option->getType() == \Magento\Catalog\Api\Data\ProductCustomOptionInterface::OPTION_TYPE_DROP_DOWN) {
- $select->setName('options[' . $_option->getid() . ']')->addOption('', __('-- Please Select --'));
+ $select->setName('options[' . $_option->getId() . ']')->addOption('', __('-- Please Select --'));
} else {
- $select->setName('options[' . $_option->getid() . '][]');
+ $select->setName('options[' . $_option->getId() . '][]');
$select->setClass('multiselect admin__control-multiselect' . $require . ' product-custom-option');
}
foreach ($_option->getValues() as $_value) {
diff --git a/app/code/Magento/Catalog/Block/Product/View/Price.php b/app/code/Magento/Catalog/Block/Product/View/Price.php
index c38625247b533..37598dfb1a8da 100644
--- a/app/code/Magento/Catalog/Block/Product/View/Price.php
+++ b/app/code/Magento/Catalog/Block/Product/View/Price.php
@@ -9,6 +9,8 @@
*/
namespace Magento\Catalog\Block\Product\View;
+use Magento\Catalog\Model\Product;
+
class Price extends \Magento\Framework\View\Element\Template
{
/**
@@ -37,7 +39,8 @@ public function __construct(
*/
public function getPrice()
{
+ /** @var Product $product */
$product = $this->_coreRegistry->registry('product');
- return $product->getFormatedPrice();
+ return $product->getFormattedPrice();
}
}
diff --git a/app/code/Magento/Catalog/Block/Product/Widget/NewWidget.php b/app/code/Magento/Catalog/Block/Product/Widget/NewWidget.php
index 704271b58f483..e880e0aba35cc 100644
--- a/app/code/Magento/Catalog/Block/Product/Widget/NewWidget.php
+++ b/app/code/Magento/Catalog/Block/Product/Widget/NewWidget.php
@@ -139,7 +139,7 @@ public function getCacheKeyInfo()
[
$this->getDisplayType(),
$this->getProductsPerPage(),
- intval($this->getRequest()->getParam($this->getData('page_var_name'), 1)),
+ (int)$this->getRequest()->getParam($this->getData('page_var_name'), 1),
$this->serializer->serialize($this->getRequest()->getParams())
]
);
diff --git a/app/code/Magento/Catalog/Block/Rss/Product/Special.php b/app/code/Magento/Catalog/Block/Rss/Product/Special.php
index c61bee4417cbc..a9107f14cc5e4 100644
--- a/app/code/Magento/Catalog/Block/Rss/Product/Special.php
+++ b/app/code/Magento/Catalog/Block/Rss/Product/Special.php
@@ -107,7 +107,7 @@ protected function _construct()
}
/**
- * @return string
+ * @return array
*/
public function getRssData()
{
diff --git a/app/code/Magento/Catalog/Console/Command/ImagesResizeCommand.php b/app/code/Magento/Catalog/Console/Command/ImagesResizeCommand.php
index 49f82562f33db..e44f9a89609bf 100644
--- a/app/code/Magento/Catalog/Console/Command/ImagesResizeCommand.php
+++ b/app/code/Magento/Catalog/Console/Command/ImagesResizeCommand.php
@@ -5,44 +5,101 @@
*/
namespace Magento\Catalog\Console\Command;
-class ImagesResizeCommand extends \Symfony\Component\Console\Command\Command
+use Magento\Catalog\Api\ProductRepositoryInterface;
+use Magento\Catalog\Model\Product\Image\CacheFactory;
+use Magento\Catalog\Model\ResourceModel\Product\CollectionFactory;
+use Magento\Catalog\Model\ResourceModel\Product\Image as ProductImage;
+use Magento\Framework\App\Area;
+use Magento\Framework\App\ObjectManager;
+use Magento\Framework\App\State;
+use Magento\Catalog\Helper\Image as ImageHelper;
+use Magento\Framework\Console\Cli;
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+use Magento\Framework\View\ConfigInterface as ViewConfig;
+use Magento\Theme\Model\ResourceModel\Theme\Collection as ThemeCollection;
+use Magento\Catalog\Model\Product\Image;
+use Magento\Catalog\Model\Product\ImageFactory as ProductImageFactory;
+use Symfony\Component\Console\Helper\ProgressBar;
+
+/**
+ * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
+ */
+class ImagesResizeCommand extends Command
{
/**
- * @var \Magento\Framework\App\State
+ * @var State
*/
protected $appState;
/**
- * @var \Magento\Catalog\Model\ResourceModel\Product\CollectionFactory
+ * @deprecated
+ * @var CollectionFactory
*/
protected $productCollectionFactory;
/**
- * @var \Magento\Catalog\Api\ProductRepositoryInterface
+ * @deprecated
+ * @var ProductRepositoryInterface
*/
protected $productRepository;
/**
- * @var \Magento\Catalog\Model\Product\Image\CacheFactory
+ * @deprecated
+ * @var CacheFactory
*/
protected $imageCacheFactory;
/**
- * @param \Magento\Framework\App\State $appState
- * @param \Magento\Catalog\Model\ResourceModel\Product\CollectionFactory $productCollectionFactory
- * @param \Magento\Catalog\Api\ProductRepositoryInterface $productRepository
- * @param \Magento\Catalog\Model\Product\Image\CacheFactory $imageCacheFactory
+ * @var ProductImage
+ */
+ private $productImage;
+
+ /**
+ * @var ViewConfig
+ */
+ private $viewConfig;
+
+ /**
+ * @var ThemeCollection
+ */
+ private $themeCollection;
+
+ /**
+ * @var ProductImageFactory
+ */
+ private $productImageFactory;
+
+ /**
+ * @param State $appState
+ * @param CollectionFactory $productCollectionFactory
+ * @param ProductRepositoryInterface $productRepository
+ * @param CacheFactory $imageCacheFactory
+ * @param ProductImage $productImage
+ * @param ViewConfig $viewConfig
+ * @param ThemeCollection $themeCollection
+ * @param ProductImageFactory $productImageFactory
*/
public function __construct(
- \Magento\Framework\App\State $appState,
- \Magento\Catalog\Model\ResourceModel\Product\CollectionFactory $productCollectionFactory,
- \Magento\Catalog\Api\ProductRepositoryInterface $productRepository,
- \Magento\Catalog\Model\Product\Image\CacheFactory $imageCacheFactory
+ State $appState,
+ CollectionFactory $productCollectionFactory,
+ ProductRepositoryInterface $productRepository,
+ CacheFactory $imageCacheFactory,
+ ProductImage $productImage = null,
+ ViewConfig $viewConfig = null,
+ ThemeCollection $themeCollection = null,
+ ProductImageFactory $productImageFactory = null
) {
$this->appState = $appState;
$this->productCollectionFactory = $productCollectionFactory;
$this->productRepository = $productRepository;
$this->imageCacheFactory = $imageCacheFactory;
+ $this->productImage = $productImage ?: ObjectManager::getInstance()->get(ProductImage::class);
+ $this->viewConfig = $viewConfig ?: ObjectManager::getInstance()->get(ViewConfig::class);
+ $this->themeCollection = $themeCollection ?: ObjectManager::getInstance()->get(ThemeCollection::class);
+ $this->productImageFactory = $productImageFactory
+ ?: ObjectManager::getInstance()->get(ProductImageFactory::class);
parent::__construct();
}
@@ -58,43 +115,124 @@ protected function configure()
/**
* {@inheritdoc}
*/
- protected function execute(
- \Symfony\Component\Console\Input\InputInterface $input,
- \Symfony\Component\Console\Output\OutputInterface $output
- ) {
- $this->appState->setAreaCode(\Magento\Framework\App\Area::AREA_GLOBAL);
-
- /** @var \Magento\Catalog\Model\ResourceModel\Product\Collection $productCollection */
- $productCollection = $this->productCollectionFactory->create();
- $productIds = $productCollection->getAllIds();
- if (!count($productIds)) {
- $output->writeln("
No product images to resize ");
- // we must have an exit code higher than zero to indicate something was wrong
- return \Magento\Framework\Console\Cli::RETURN_SUCCESS;
- }
+ protected function execute(InputInterface $input, OutputInterface $output)
+ {
+ $this->appState->setAreaCode(Area::AREA_GLOBAL);
try {
- foreach ($productIds as $productId) {
- try {
- /** @var \Magento\Catalog\Model\Product $product */
- $product = $this->productRepository->getById($productId);
- } catch (\Magento\Framework\Exception\NoSuchEntityException $e) {
- continue;
- }
+ $count = $this->productImage->getCountAllProductImages();
+ if (!$count) {
+ $output->writeln("
No product images to resize ");
+ return Cli::RETURN_SUCCESS;
+ }
+
+ $productImages = $this->productImage->getAllProductImages();
- /** @var \Magento\Catalog\Model\Product\Image\Cache $imageCache */
- $imageCache = $this->imageCacheFactory->create();
- $imageCache->generate($product);
+ $themes = $this->themeCollection->loadRegisteredThemes();
+ $viewImages = $this->getViewImages($themes->getItems());
- $output->write(".");
+ $progress = new ProgressBar($output, $count);
+ $progress->setFormat(
+ "%current%/%max% [%bar%] %percent:3s%% %elapsed% %memory:6s% \t|
%message% "
+ );
+
+ if ($output->getVerbosity() !== OutputInterface::VERBOSITY_NORMAL) {
+ $progress->setOverwrite(false);
+ }
+
+ foreach ($productImages as $image) {
+ $originalImageName = $image['filepath'];
+
+ foreach ($viewImages as $viewImage) {
+ $image = $this->makeImage($originalImageName, $viewImage);
+ $image->resize();
+ $image->saveFile();
+ }
+ $progress->setMessage($originalImageName);
+ $progress->advance();
}
} catch (\Exception $e) {
$output->writeln("
{$e->getMessage()} ");
// we must have an exit code higher than zero to indicate something was wrong
- return \Magento\Framework\Console\Cli::RETURN_FAILURE;
+ return Cli::RETURN_FAILURE;
}
$output->write("\n");
- $output->writeln("
Product images resized successfully ");
+ $output->writeln("
Product images resized successfully. ");
+
+ return 0;
+ }
+
+ /**
+ * Make image
+ * @param string $originalImagePath
+ * @param array $imageParams
+ * @return Image
+ */
+ private function makeImage(string $originalImagePath, array $imageParams): Image
+ {
+ $image = $this->productImageFactory->create();
+
+ if (isset($imageParams['height'])) {
+ $image->setHeight($imageParams['height']);
+ }
+ if (isset($imageParams['width'])) {
+ $image->setWidth($imageParams['width']);
+ }
+ if (isset($imageParams['aspect_ratio'])) {
+ $image->setKeepAspectRatio($imageParams['aspect_ratio']);
+ }
+ if (isset($imageParams['frame'])) {
+ $image->setKeepFrame($imageParams['frame']);
+ }
+ if (isset($imageParams['transparency'])) {
+ $image->setKeepTransparency($imageParams['transparency']);
+ }
+ if (isset($imageParams['constrain'])) {
+ $image->setConstrainOnly($imageParams['constrain']);
+ }
+ if (isset($imageParams['background'])) {
+ $image->setBackgroundColor($imageParams['background']);
+ }
+
+ $image->setDestinationSubdir($imageParams['type']);
+ $image->setBaseFile($originalImagePath);
+
+ return $image;
+ }
+
+ /**
+ * Get view images data from themes
+ * @param array $themes
+ * @return array
+ */
+ private function getViewImages(array $themes): array
+ {
+ $viewImages = [];
+ foreach ($themes as $theme) {
+ $config = $this->viewConfig->getViewConfig([
+ 'area' => Area::AREA_FRONTEND,
+ 'themeModel' => $theme,
+ ]);
+ $images = $config->getMediaEntities('Magento_Catalog', ImageHelper::MEDIA_TYPE_CONFIG_NODE);
+ foreach ($images as $imageId => $imageData) {
+ $uniqIndex = $this->getUniqueImageIndex($imageData);
+ $imageData['id'] = $imageId;
+ $viewImages[$uniqIndex] = $imageData;
+ }
+ }
+ return $viewImages;
+ }
+
+ /**
+ * Get unique image index
+ * @param array $imageData
+ * @return string
+ */
+ private function getUniqueImageIndex(array $imageData): string
+ {
+ ksort($imageData);
+ unset($imageData['type']);
+ return md5(json_encode($imageData));
}
}
diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Category.php b/app/code/Magento/Catalog/Controller/Adminhtml/Category.php
index 13c4353e65204..6bcd2287ae03c 100644
--- a/app/code/Magento/Catalog/Controller/Adminhtml/Category.php
+++ b/app/code/Magento/Catalog/Controller/Adminhtml/Category.php
@@ -3,12 +3,27 @@
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
+declare(strict_types=1);
+
namespace Magento\Catalog\Controller\Adminhtml;
+use Magento\Store\Model\Store;
+use Magento\Backend\App\Action\Context;
+use Magento\Framework\Stdlib\DateTime\Filter\Date;
+use Magento\Catalog\Model\Category as CategoryModel;
+use Magento\Backend\App\Action;
+use Magento\Store\Model\StoreManagerInterface;
+use Magento\Framework\Registry;
+use Magento\Cms\Model\Wysiwyg\Config;
+use Magento\Backend\Model\View\Result\Page;
+use Magento\Framework\Controller\Result\Json;
+use Magento\Backend\Model\Auth\Session;
+use Magento\Framework\DataObject;
+
/**
* Catalog category controller
*/
-abstract class Category extends \Magento\Backend\App\Action
+abstract class Category extends Action
{
/**
* Authorization level of a basic admin session
@@ -18,18 +33,16 @@ abstract class Category extends \Magento\Backend\App\Action
const ADMIN_RESOURCE = 'Magento_Catalog::categories';
/**
- * @var \Magento\Framework\Stdlib\DateTime\Filter\Date
+ * @var Date
*/
protected $dateFilter;
/**
- * @param \Magento\Backend\App\Action\Context $context
- * @param \Magento\Framework\Stdlib\DateTime\Filter\Date|null $dateFilter
+ * @param Context $context
+ * @param Date|null $dateFilter
*/
- public function __construct(
- \Magento\Backend\App\Action\Context $context,
- \Magento\Framework\Stdlib\DateTime\Filter\Date $dateFilter = null
- ) {
+ public function __construct(Context $context, Date $dateFilter = null)
+ {
$this->dateFilter = $dateFilter;
parent::__construct($context);
}
@@ -39,20 +52,20 @@ public function __construct(
* Root category can be returned, if inappropriate store/category is specified
*
* @param bool $getRootInstead
- * @return \Magento\Catalog\Model\Category|false
+ * @return CategoryModel|false
*/
- protected function _initCategory($getRootInstead = false)
+ protected function _initCategory(bool $getRootInstead = false)
{
$categoryId = $this->resolveCategoryId();
- $storeId = (int)$this->getRequest()->getParam('store');
- $category = $this->_objectManager->create(\Magento\Catalog\Model\Category::class);
+ $storeId = $this->resolveStoreId();
+ $category = $this->_objectManager->create(CategoryModel::class);
$category->setStoreId($storeId);
if ($categoryId) {
$category->load($categoryId);
if ($storeId) {
$rootId = $this->_objectManager->get(
- \Magento\Store\Model\StoreManagerInterface::class
+ StoreManagerInterface::class
)->getStore(
$storeId
)->getRootCategoryId();
@@ -67,9 +80,9 @@ protected function _initCategory($getRootInstead = false)
}
}
- $this->_objectManager->get(\Magento\Framework\Registry::class)->register('category', $category);
- $this->_objectManager->get(\Magento\Framework\Registry::class)->register('current_category', $category);
- $this->_objectManager->get(\Magento\Cms\Model\Wysiwyg\Config::class)
+ $this->_objectManager->get(Registry::class)->register('category', $category);
+ $this->_objectManager->get(Registry::class)->register('current_category', $category);
+ $this->_objectManager->get(Config::class)
->setStoreId($this->getRequest()->getParam('store'));
return $category;
}
@@ -79,31 +92,46 @@ protected function _initCategory($getRootInstead = false)
*
* @return int
*/
- private function resolveCategoryId()
+ private function resolveCategoryId(): int
{
$categoryId = (int)$this->getRequest()->getParam('id', false);
return $categoryId ?: (int)$this->getRequest()->getParam('entity_id', false);
}
+ /**
+ * Resolve store id
+ *
+ * Tries to take store id from store HTTP parameter
+ * @see Store
+ *
+ * @return int
+ */
+ private function resolveStoreId(): int
+ {
+ $storeId = (int)$this->getRequest()->getParam('store', false);
+
+ return $storeId ?: (int)$this->getRequest()->getParam('store_id', Store::DEFAULT_STORE_ID);
+ }
+
/**
* Build response for ajax request
*
- * @param \Magento\Catalog\Model\Category $category
- * @param \Magento\Backend\Model\View\Result\Page $resultPage
+ * @param CategoryModel $category
+ * @param Page $resultPage
*
- * @return \Magento\Framework\Controller\Result\Json
+ * @return Json
*
* @deprecated 101.0.0
*/
- protected function ajaxRequestResponse($category, $resultPage)
+ protected function ajaxRequestResponse(CategoryModel $category, Page $resultPage): Json
{
// prepare breadcrumbs of selected category, if any
$breadcrumbsPath = $category->getPath();
if (empty($breadcrumbsPath)) {
// but if no category, and it is deleted - prepare breadcrumbs from path, saved in session
$breadcrumbsPath = $this->_objectManager->get(
- \Magento\Backend\Model\Auth\Session::class
+ Session::class
)->getDeletedPath(
true
);
@@ -119,7 +147,7 @@ protected function ajaxRequestResponse($category, $resultPage)
}
}
- $eventResponse = new \Magento\Framework\DataObject([
+ $eventResponse = new DataObject([
'content' => $resultPage->getLayout()->getUiComponent('category_form')->getFormHtml()
. $resultPage->getLayout()->getBlock('category.tree')
->getBreadcrumbsJavascript($breadcrumbsPath, 'editingCategoryBreadcrumbs'),
@@ -130,22 +158,23 @@ protected function ajaxRequestResponse($category, $resultPage)
'category_prepare_ajax_response',
['response' => $eventResponse, 'controller' => $this]
);
- /** @var \Magento\Framework\Controller\Result\Json $resultJson */
- $resultJson = $this->_objectManager->get(\Magento\Framework\Controller\Result\Json::class);
+ /** @var Json $resultJson */
+ $resultJson = $this->_objectManager->get(Json::class);
$resultJson->setHeader('Content-type', 'application/json', true);
$resultJson->setData($eventResponse->getData());
+
return $resultJson;
}
/**
* Datetime data preprocessing
*
- * @param \Magento\Catalog\Model\Category $category
+ * @param CategoryModel $category
* @param array $postData
*
* @return array
*/
- protected function dateTimePreprocessing($category, $postData)
+ protected function dateTimePreprocessing(CategoryModel $category, array $postData): array
{
$dateFieldFilters = [];
$attributes = $category->getAttributes();
diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Category/Delete.php b/app/code/Magento/Catalog/Controller/Adminhtml/Category/Delete.php
index 8f570e35989cb..0a54475b15f9c 100644
--- a/app/code/Magento/Catalog/Controller/Adminhtml/Category/Delete.php
+++ b/app/code/Magento/Catalog/Controller/Adminhtml/Category/Delete.php
@@ -44,12 +44,12 @@ public function execute()
$this->_eventManager->dispatch('catalog_controller_category_delete', ['category' => $category]);
$this->_auth->getAuthStorage()->setDeletedPath($category->getPath());
$this->categoryRepository->delete($category);
- $this->messageManager->addSuccess(__('You deleted the category.'));
+ $this->messageManager->addSuccessMessage(__('You deleted the category.'));
} catch (\Magento\Framework\Exception\LocalizedException $e) {
- $this->messageManager->addError($e->getMessage());
+ $this->messageManager->addErrorMessage($e->getMessage());
return $resultRedirect->setPath('catalog/*/edit', ['_current' => true]);
} catch (\Exception $e) {
- $this->messageManager->addError(__('Something went wrong while trying to delete the category.'));
+ $this->messageManager->addErrorMessage(__('Something went wrong while trying to delete the category.'));
return $resultRedirect->setPath('catalog/*/edit', ['_current' => true]);
}
}
diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Category/Image/Upload.php b/app/code/Magento/Catalog/Controller/Adminhtml/Category/Image/Upload.php
index 4cc0f2d89d179..e24b142411b83 100644
--- a/app/code/Magento/Catalog/Controller/Adminhtml/Category/Image/Upload.php
+++ b/app/code/Magento/Catalog/Controller/Adminhtml/Category/Image/Upload.php
@@ -54,14 +54,6 @@ public function execute()
try {
$result = $this->imageUploader->saveFileToTmpDir($imageId);
-
- $result['cookie'] = [
- 'name' => $this->_getSession()->getName(),
- 'value' => $this->_getSession()->getSessionId(),
- 'lifetime' => $this->_getSession()->getCookieLifetime(),
- 'path' => $this->_getSession()->getCookiePath(),
- 'domain' => $this->_getSession()->getCookieDomain(),
- ];
} catch (\Exception $e) {
$result = ['error' => $e->getMessage(), 'errorcode' => $e->getCode()];
}
diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Category/RefreshPath.php b/app/code/Magento/Catalog/Controller/Adminhtml/Category/RefreshPath.php
index 962cd52d39338..9384397b67f93 100644
--- a/app/code/Magento/Catalog/Controller/Adminhtml/Category/RefreshPath.php
+++ b/app/code/Magento/Catalog/Controller/Adminhtml/Category/RefreshPath.php
@@ -38,7 +38,11 @@ public function execute()
/** @var \Magento\Framework\Controller\Result\Json $resultJson */
$resultJson = $this->resultJsonFactory->create();
- return $resultJson->setData(['id' => $categoryId, 'path' => $category->getPath()]);
+ return $resultJson->setData([
+ 'id' => $categoryId,
+ 'path' => $category->getPath(),
+ 'parentId' => $category->getParentId(),
+ ]);
}
}
}
diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Category/Save.php b/app/code/Magento/Catalog/Controller/Adminhtml/Category/Save.php
index d1ec3be1a8895..af20f75d95226 100644
--- a/app/code/Magento/Catalog/Controller/Adminhtml/Category/Save.php
+++ b/app/code/Magento/Catalog/Controller/Adminhtml/Category/Save.php
@@ -126,8 +126,7 @@ public function execute()
return $resultRedirect->setPath('catalog/*/', ['_current' => true, 'id' => null]);
}
- $data['general'] = $this->getRequest()->getPostValue();
- $categoryPostData = $data['general'];
+ $categoryPostData = $this->getRequest()->getPostValue();
$isNewCategory = !isset($categoryPostData['entity_id']);
$categoryPostData = $this->stringToBoolConverting($categoryPostData);
@@ -139,6 +138,9 @@ public function execute()
$parentId = isset($categoryPostData['parent']) ? $categoryPostData['parent'] : null;
if ($categoryPostData) {
$category->addData($categoryPostData);
+ if ($parentId) {
+ $category->setParentId($parentId);
+ }
if ($isNewCategory) {
$parentCategory = $this->getParentCategory($parentId, $storeId);
$category->setPath($parentCategory->getPath());
@@ -277,7 +279,7 @@ public function imagePreprocessing($data)
continue;
}
- $data[$attributeCode] = false;
+ $data[$attributeCode] = '';
}
return $data;
diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Action/Attribute.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Action/Attribute.php
index 6958b6671d054..8a94899cf5041 100644
--- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Action/Attribute.php
+++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Action/Attribute.php
@@ -56,7 +56,7 @@ protected function _validateProducts()
}
if ($error) {
- $this->messageManager->addError($error);
+ $this->messageManager->addErrorMessage($error);
}
return !$error;
diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Action/Attribute/Save.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Action/Attribute/Save.php
index 82496446aef9f..0fbf9054ef1bd 100644
--- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Action/Attribute/Save.php
+++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Action/Attribute/Save.php
@@ -192,7 +192,7 @@ public function execute()
$this->_eventManager->dispatch('catalog_product_to_website_change', ['products' => $productIds]);
}
- $this->messageManager->addSuccess(
+ $this->messageManager->addSuccessMessage(
__('A total of %1 record(s) were updated.', count($this->attributeHelper->getProductIds()))
);
@@ -205,9 +205,9 @@ public function execute()
$this->_productPriceIndexerProcessor->reindexList($this->attributeHelper->getProductIds());
}
} catch (\Magento\Framework\Exception\LocalizedException $e) {
- $this->messageManager->addError($e->getMessage());
+ $this->messageManager->addErrorMessage($e->getMessage());
} catch (\Exception $e) {
- $this->messageManager->addException(
+ $this->messageManager->addExceptionMessage(
$e,
__('Something went wrong while updating the product(s) attributes.')
);
diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Action/Attribute/Validate.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Action/Attribute/Validate.php
index bb18436be6102..a873f08d082d7 100644
--- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Action/Attribute/Validate.php
+++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Action/Attribute/Validate.php
@@ -68,7 +68,7 @@ public function execute()
$response->setError(true);
$response->setMessage($e->getMessage());
} catch (\Exception $e) {
- $this->messageManager->addException(
+ $this->messageManager->addExceptionMessage(
$e,
__('Something went wrong while updating the product(s) attributes.')
);
diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Attribute/Delete.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Attribute/Delete.php
index cc5a658a9296d..bef6aee0e2afd 100644
--- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Attribute/Delete.php
+++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Attribute/Delete.php
@@ -21,23 +21,23 @@ public function execute()
// entity type check
$model->load($id);
if ($model->getEntityTypeId() != $this->_entityTypeId) {
- $this->messageManager->addError(__('We can\'t delete the attribute.'));
+ $this->messageManager->addErrorMessage(__('We can\'t delete the attribute.'));
return $resultRedirect->setPath('catalog/*/');
}
try {
$model->delete();
- $this->messageManager->addSuccess(__('You deleted the product attribute.'));
+ $this->messageManager->addSuccessMessage(__('You deleted the product attribute.'));
return $resultRedirect->setPath('catalog/*/');
} catch (\Exception $e) {
- $this->messageManager->addError($e->getMessage());
+ $this->messageManager->addErrorMessage($e->getMessage());
return $resultRedirect->setPath(
'catalog/*/edit',
['attribute_id' => $this->getRequest()->getParam('attribute_id')]
);
}
}
- $this->messageManager->addError(__('We can\'t find an attribute to delete.'));
+ $this->messageManager->addErrorMessage(__('We can\'t find an attribute to delete.'));
return $resultRedirect->setPath('catalog/*/');
}
}
diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Attribute/Edit.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Attribute/Edit.php
index 5a9f244a2bbe1..52c79e2a66c74 100644
--- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Attribute/Edit.php
+++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Attribute/Edit.php
@@ -25,14 +25,14 @@ public function execute()
$model->load($id);
if (!$model->getId()) {
- $this->messageManager->addError(__('This attribute no longer exists.'));
+ $this->messageManager->addErrorMessage(__('This attribute no longer exists.'));
$resultRedirect = $this->resultRedirectFactory->create();
return $resultRedirect->setPath('catalog/*/');
}
// entity type check
if ($model->getEntityTypeId() != $this->_entityTypeId) {
- $this->messageManager->addError(__('This attribute cannot be edited.'));
+ $this->messageManager->addErrorMessage(__('This attribute cannot be edited.'));
$resultRedirect = $this->resultRedirectFactory->create();
return $resultRedirect->setPath('catalog/*/');
}
diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Attribute/Save.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Attribute/Save.php
index 8eb13af6f96f2..91a98424c9ae1 100644
--- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Attribute/Save.php
+++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Attribute/Save.php
@@ -5,79 +5,95 @@
* See COPYING.txt for license details.
*/
-// @codingStandardsIgnoreFile
-
namespace Magento\Catalog\Controller\Adminhtml\Product\Attribute;
+use Magento\Backend\App\Action\Context;
+use Magento\Backend\Model\View\Result\Redirect;
+use Magento\Catalog\Controller\Adminhtml\Product\Attribute;
+use Magento\Catalog\Model\Product\AttributeSet\BuildFactory;
+use Magento\Catalog\Helper\Product;
+use Magento\Catalog\Api\Data\ProductAttributeInterface;
+use Magento\Catalog\Model\ResourceModel\Eav\AttributeFactory;
+use Magento\Eav\Model\Entity\Attribute\Set;
+use Magento\Eav\Model\Adminhtml\System\Config\Source\Inputtype\Validator;
+use Magento\Eav\Model\Adminhtml\System\Config\Source\Inputtype\ValidatorFactory;
+use Magento\Eav\Model\ResourceModel\Entity\Attribute\Group\CollectionFactory;
+use Magento\Framework\Cache\FrontendInterface;
use Magento\Framework\Controller\ResultFactory;
+use Magento\Framework\Controller\Result\Json;
use Magento\Framework\Exception\AlreadyExistsException;
+use Magento\Framework\Exception\LocalizedException;
+use Magento\Framework\Filter\FilterManager;
+use Magento\Framework\Registry;
+use Magento\Framework\View\LayoutFactory;
+use Magento\Framework\View\Result\PageFactory;
/**
* @SuppressWarnings(PHPMD.CouplingBetweenObjects)
*/
-class Save extends \Magento\Catalog\Controller\Adminhtml\Product\Attribute
+class Save extends Attribute
{
/**
- * @var \Magento\Catalog\Model\Product\AttributeSet\BuildFactory
+ * @var BuildFactory
*/
protected $buildFactory;
/**
- * @var \Magento\Framework\Filter\FilterManager
+ * @var FilterManager
*/
protected $filterManager;
/**
- * @var \Magento\Catalog\Helper\Product
+ * @var Product
*/
protected $productHelper;
/**
- * @var \Magento\Catalog\Model\ResourceModel\Eav\AttributeFactory
+ * @var AttributeFactory
*/
protected $attributeFactory;
/**
- * @var \Magento\Eav\Model\Adminhtml\System\Config\Source\Inputtype\ValidatorFactory
+ * @var ValidatorFactory
*/
protected $validatorFactory;
/**
- * @var \Magento\Eav\Model\ResourceModel\Entity\Attribute\Group\CollectionFactory
+ * @var CollectionFactory
*/
protected $groupCollectionFactory;
/**
- * @var \Magento\Framework\View\LayoutFactory
+ * @var LayoutFactory
*/
private $layoutFactory;
/**
- * @param \Magento\Backend\App\Action\Context $context
- * @param \Magento\Framework\Cache\FrontendInterface $attributeLabelCache
- * @param \Magento\Framework\Registry $coreRegistry
- * @param \Magento\Catalog\Model\Product\AttributeSet\BuildFactory $buildFactory
- * @param \Magento\Framework\View\Result\PageFactory $resultPageFactory
- * @param \Magento\Catalog\Model\ResourceModel\Eav\AttributeFactory $attributeFactory
- * @param \Magento\Eav\Model\Adminhtml\System\Config\Source\Inputtype\ValidatorFactory $validatorFactory
- * @param \Magento\Eav\Model\ResourceModel\Entity\Attribute\Group\CollectionFactory $groupCollectionFactory
- * @param \Magento\Framework\Filter\FilterManager $filterManager
- * @param \Magento\Catalog\Helper\Product $productHelper
- * @param \Magento\Framework\View\LayoutFactory $layoutFactory
+ * @param Context $context
+ * @param FrontendInterface $attributeLabelCache
+ * @param Registry $coreRegistry
+ * @param BuildFactory $buildFactory
+ * @param PageFactory $resultPageFactory
+ * @param AttributeFactory $attributeFactory
+ * @param ValidatorFactory $validatorFactory
+ * @param CollectionFactory $groupCollectionFactory
+ * @param FilterManager $filterManager
+ * @param Product $productHelper
+ * @param LayoutFactory $layoutFactory
* @SuppressWarnings(PHPMD.ExcessiveParameterList)
*/
public function __construct(
- \Magento\Backend\App\Action\Context $context,
- \Magento\Framework\Cache\FrontendInterface $attributeLabelCache,
- \Magento\Framework\Registry $coreRegistry,
- \Magento\Framework\View\Result\PageFactory $resultPageFactory,
- \Magento\Catalog\Model\Product\AttributeSet\BuildFactory $buildFactory,
- \Magento\Catalog\Model\ResourceModel\Eav\AttributeFactory $attributeFactory,
- \Magento\Eav\Model\Adminhtml\System\Config\Source\Inputtype\ValidatorFactory $validatorFactory,
- \Magento\Eav\Model\ResourceModel\Entity\Attribute\Group\CollectionFactory $groupCollectionFactory,
- \Magento\Framework\Filter\FilterManager $filterManager,
- \Magento\Catalog\Helper\Product $productHelper,
- \Magento\Framework\View\LayoutFactory $layoutFactory
+ Context $context,
+ FrontendInterface $attributeLabelCache,
+ Registry $coreRegistry,
+ PageFactory $resultPageFactory,
+ BuildFactory $buildFactory,
+ AttributeFactory $attributeFactory,
+ ValidatorFactory $validatorFactory,
+ CollectionFactory $groupCollectionFactory,
+ FilterManager $filterManager,
+ Product $productHelper,
+ LayoutFactory $layoutFactory
) {
parent::__construct($context, $attributeLabelCache, $coreRegistry, $resultPageFactory);
$this->buildFactory = $buildFactory;
@@ -90,7 +106,7 @@ public function __construct(
}
/**
- * @return \Magento\Backend\Model\View\Result\Redirect
+ * @return Redirect
* @SuppressWarnings(PHPMD.CyclomaticComplexity)
* @SuppressWarnings(PHPMD.NPathComplexity)
* @SuppressWarnings(PHPMD.ExcessiveMethodLength)
@@ -99,6 +115,7 @@ public function execute()
{
$data = $this->getRequest()->getPostValue();
if ($data) {
+ $this->preprocessOptionsData($data);
$setId = $this->getRequest()->getParam('set');
$attributeSet = null;
@@ -107,36 +124,51 @@ public function execute()
$name = trim($name);
try {
- /** @var $attributeSet \Magento\Eav\Model\Entity\Attribute\Set */
+ /** @var $attributeSet Set */
$attributeSet = $this->buildFactory->create()
->setEntityTypeId($this->_entityTypeId)
->setSkeletonId($setId)
->setName($name)
->getAttributeSet();
} catch (AlreadyExistsException $alreadyExists) {
- $this->messageManager->addError(__('An attribute set named \'%1\' already exists.', $name));
+ $this->messageManager->addErrorMessage(__('An attribute set named \'%1\' already exists.', $name));
$this->_session->setAttributeData($data);
+
return $this->returnResult('catalog/*/edit', ['_current' => true], ['error' => true]);
- } catch (\Magento\Framework\Exception\LocalizedException $e) {
- $this->messageManager->addError($e->getMessage());
+ } catch (LocalizedException $e) {
+ $this->messageManager->addErrorMessage($e->getMessage());
} catch (\Exception $e) {
- $this->messageManager->addException($e, __('Something went wrong while saving the attribute.'));
+ $this->messageManager->addExceptionMessage(
+ $e,
+ __('Something went wrong while saving the attribute.')
+ );
}
}
$attributeId = $this->getRequest()->getParam('attribute_id');
- $attributeCode = $this->getRequest()->getParam('attribute_code')
- ?: $this->generateCode($this->getRequest()->getParam('frontend_label')[0]);
+
+ /** @var $model ProductAttributeInterface */
+ $model = $this->attributeFactory->create();
+ if ($attributeId) {
+ $model->load($attributeId);
+ }
+ $attributeCode = $model && $model->getId()
+ ? $model->getAttributeCode()
+ : $this->getRequest()->getParam('attribute_code');
+ $attributeCode = $attributeCode ?: $this->generateCode($this->getRequest()->getParam('frontend_label')[0]);
if (strlen($attributeCode) > 0) {
- $validatorAttrCode = new \Zend_Validate_Regex(['pattern' => '/^[a-z\x{600}-\x{6FF}][a-z\x{600}-\x{6FF}_0-9]{0,30}$/u']);
+ $validatorAttrCode = new \Zend_Validate_Regex(
+ ['pattern' => '/^[a-z\x{600}-\x{6FF}][a-z\x{600}-\x{6FF}_0-9]{0,30}$/u']
+ );
if (!$validatorAttrCode->isValid($attributeCode)) {
- $this->messageManager->addError(
+ $this->messageManager->addErrorMessage(
__(
'Attribute code "%1" is invalid. Please use only letters (a-z), ' .
'numbers (0-9) or underscore(_) in this field, first character should be a letter.',
$attributeCode
)
);
+
return $this->returnResult(
'catalog/*/edit',
['attribute_id' => $attributeId, '_current' => true],
@@ -148,12 +180,13 @@ public function execute()
//validate frontend_input
if (isset($data['frontend_input'])) {
- /** @var $inputType \Magento\Eav\Model\Adminhtml\System\Config\Source\Inputtype\Validator */
+ /** @var $inputType Validator */
$inputType = $this->validatorFactory->create();
if (!$inputType->isValid($data['frontend_input'])) {
foreach ($inputType->getMessages() as $message) {
- $this->messageManager->addError($message);
+ $this->messageManager->addErrorMessage($message);
}
+
return $this->returnResult(
'catalog/*/edit',
['attribute_id' => $attributeId, '_current' => true],
@@ -162,25 +195,23 @@ public function execute()
}
}
- /* @var $model \Magento\Catalog\Model\ResourceModel\Eav\Attribute */
- $model = $this->attributeFactory->create();
-
if ($attributeId) {
- $model->load($attributeId);
if (!$model->getId()) {
- $this->messageManager->addError(__('This attribute no longer exists.'));
+ $this->messageManager->addErrorMessage(__('This attribute no longer exists.'));
+
return $this->returnResult('catalog/*/', [], ['error' => true]);
}
// entity type check
if ($model->getEntityTypeId() != $this->_entityTypeId) {
- $this->messageManager->addError(__('We can\'t update the attribute.'));
+ $this->messageManager->addErrorMessage(__('We can\'t update the attribute.'));
$this->_session->setAttributeData($data);
+
return $this->returnResult('catalog/*/', [], ['error' => true]);
}
$data['attribute_code'] = $model->getAttributeCode();
$data['is_user_defined'] = $model->getIsUserDefined();
- $data['frontend_input'] = $model->getFrontendInput();
+ $data['frontend_input'] = $data['frontend_input'] ?? $model->getFrontendInput();
} else {
/**
* @todo add to helper and specify all relations for properties
@@ -191,14 +222,14 @@ public function execute()
$data['backend_model'] = $this->productHelper->getAttributeBackendModelByInputType(
$data['frontend_input']
);
- }
-
- $data += ['is_filterable' => 0, 'is_filterable_in_search' => 0, 'apply_to' => []];
- if (is_null($model->getIsUserDefined()) || $model->getIsUserDefined() != 0) {
- $data['backend_type'] = $model->getBackendTypeByInput($data['frontend_input']);
+ if ($model->getIsUserDefined() === null) {
+ $data['backend_type'] = $model->getBackendTypeByInput($data['frontend_input']);
+ }
}
+ $data += ['is_filterable' => 0, 'is_filterable_in_search' => 0];
+
$defaultValueField = $model->getDefaultValueByInput($data['frontend_input']);
if ($defaultValueField) {
$data['default_value'] = $this->getRequest()->getParam($defaultValueField);
@@ -241,7 +272,7 @@ public function execute()
try {
$model->save();
- $this->messageManager->addSuccess(__('You saved the product attribute.'));
+ $this->messageManager->addSuccessMessage(__('You saved the product attribute.'));
$this->_attributeLabelCache->clean();
$this->_session->setAttributeData(false);
@@ -252,9 +283,10 @@ public function execute()
'_current' => true,
'product_tab' => $this->getRequest()->getParam('product_tab'),
];
- if (!is_null($attributeSet)) {
+ if ($attributeSet !== null) {
$requestParams['new_attribute_set_id'] = $attributeSet->getId();
}
+
return $this->returnResult('catalog/product/addAttribute', $requestParams, ['error' => false]);
} elseif ($this->getRequest()->getParam('back', false)) {
return $this->returnResult(
@@ -263,10 +295,12 @@ public function execute()
['error' => false]
);
}
+
return $this->returnResult('catalog/*/', [], ['error' => false]);
} catch (\Exception $e) {
- $this->messageManager->addError($e->getMessage());
+ $this->messageManager->addErrorMessage($e->getMessage());
$this->_session->setAttributeData($data);
+
return $this->returnResult(
'catalog/*/edit',
['attribute_id' => $attributeId, '_current' => true],
@@ -274,14 +308,38 @@ public function execute()
);
}
}
+
return $this->returnResult('catalog/*/', [], ['error' => true]);
}
+ /**
+ * Extract options data from serialized options field and append to data array.
+ *
+ * This logic is required to overcome max_input_vars php limit
+ * that may vary and/or be inaccessible to change on different instances.
+ *
+ * @param array $data
+ * @return void
+ */
+ private function preprocessOptionsData(&$data)
+ {
+ if (isset($data['serialized_options'])) {
+ $serializedOptions = json_decode($data['serialized_options'], JSON_OBJECT_AS_ARRAY);
+ foreach ($serializedOptions as $serializedOption) {
+ $option = [];
+ $serializedOptionWithParsedAmpersand = str_replace('&', '%26', $serializedOption);
+ parse_str($serializedOptionWithParsedAmpersand, $option);
+ $data = array_replace_recursive($data, $option);
+ }
+ }
+ unset($data['serialized_options']);
+ }
+
/**
* @param string $path
* @param array $params
* @param array $response
- * @return \Magento\Framework\Controller\Result\Json|\Magento\Backend\Model\View\Result\Redirect
+ * @return Json|Redirect
*/
private function returnResult($path = '', array $params = [], array $response = [])
{
@@ -291,8 +349,10 @@ private function returnResult($path = '', array $params = [], array $response =
$response['messages'] = [$layout->getMessagesBlock()->getGroupedHtml()];
$response['params'] = $params;
+
return $this->resultFactory->create(ResultFactory::TYPE_JSON)->setData($response);
}
+
return $this->resultFactory->create(ResultFactory::TYPE_REDIRECT)->setPath($path, $params);
}
diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Attribute/Validate.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Attribute/Validate.php
index 6d3ceaae7f0b6..7fe012a87d929 100644
--- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Attribute/Validate.php
+++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Attribute/Validate.php
@@ -91,7 +91,7 @@ public function execute()
$attributeSet->setEntityTypeId($this->_entityTypeId)->load($setName, 'attribute_set_name');
if ($attributeSet->getId()) {
$setName = $this->_objectManager->get(\Magento\Framework\Escaper::class)->escapeHtml($setName);
- $this->messageManager->addError(__('An attribute set named \'%1\' already exists.', $setName));
+ $this->messageManager->addErrorMessage(__('An attribute set named \'%1\' already exists.', $setName));
$layout = $this->layoutFactory->create();
$layout->initMessages();
@@ -110,6 +110,11 @@ public function execute()
$options
);
$valueOptions = (isset($options['value']) && is_array($options['value'])) ? $options['value'] : [];
+ foreach (array_keys($valueOptions) as $key) {
+ if (!empty($options['delete'][$key])) {
+ unset($valueOptions[$key]);
+ }
+ }
$this->checkEmptyOption($response, $valueOptions);
}
diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Builder.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Builder.php
index 4fa61b2b372c2..125406061aed7 100644
--- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Builder.php
+++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Builder.php
@@ -11,6 +11,9 @@
use Magento\Store\Model\StoreFactory;
use Psr\Log\LoggerInterface as Logger;
use Magento\Framework\Registry;
+use Magento\Catalog\Api\ProductRepositoryInterface;
+use Magento\Catalog\Model\Product;
+use Magento\Catalog\Model\Product\Type as ProductTypes;
class Builder
{
@@ -39,6 +42,11 @@ class Builder
*/
protected $storeFactory;
+ /**
+ * @var ProductRepositoryInterface
+ */
+ private $productRepository;
+
/**
* Constructor
*
@@ -47,13 +55,15 @@ class Builder
* @param Registry $registry
* @param WysiwygModel\Config $wysiwygConfig
* @param StoreFactory|null $storeFactory
+ * @param ProductRepositoryInterface|null $productRepository
*/
public function __construct(
ProductFactory $productFactory,
Logger $logger,
Registry $registry,
WysiwygModel\Config $wysiwygConfig,
- StoreFactory $storeFactory = null
+ StoreFactory $storeFactory = null,
+ ProductRepositoryInterface $productRepository = null
) {
$this->productFactory = $productFactory;
$this->logger = $logger;
@@ -61,6 +71,8 @@ public function __construct(
$this->wysiwygConfig = $wysiwygConfig;
$this->storeFactory = $storeFactory ?: \Magento\Framework\App\ObjectManager::getInstance()
->get(\Magento\Store\Model\StoreFactory::class);
+ $this->productRepository = $productRepository ?: \Magento\Framework\App\ObjectManager::getInstance()
+ ->get(ProductRepositoryInterface::class);
}
/**
@@ -68,40 +80,62 @@ public function __construct(
*
* @param RequestInterface $request
* @return \Magento\Catalog\Model\Product
+ * @throws \RuntimeException
*/
public function build(RequestInterface $request)
{
- $productId = (int)$request->getParam('id');
- /** @var $product \Magento\Catalog\Model\Product */
- $product = $this->productFactory->create();
- $product->setStoreId($request->getParam('store', 0));
- $store = $this->storeFactory->create();
- $store->load($request->getParam('store', 0));
-
+ $productId = (int) $request->getParam('id');
+ $storeId = $request->getParam('store', 0);
+ $attributeSetId = (int) $request->getParam('set');
$typeId = $request->getParam('type');
- if (!$productId && $typeId) {
- $product->setTypeId($typeId);
- }
- $product->setData('_edit_mode', true);
if ($productId) {
try {
- $product->load($productId);
+ $product = $this->productRepository->getById($productId, true, $storeId);
} catch (\Exception $e) {
- $product->setTypeId(\Magento\Catalog\Model\Product\Type::DEFAULT_TYPE);
+ $product = $this->createEmptyProduct(ProductTypes::DEFAULT_TYPE, $attributeSetId, $storeId);
$this->logger->critical($e);
}
+ } else {
+ $product = $this->createEmptyProduct($typeId, $attributeSetId, $storeId);
}
- $setId = (int)$request->getParam('set');
- if ($setId) {
- $product->setAttributeSetId($setId);
- }
+ $store = $this->storeFactory->create();
+ $store->load($storeId);
$this->registry->register('product', $product);
$this->registry->register('current_product', $product);
$this->registry->register('current_store', $store);
- $this->wysiwygConfig->setStoreId($request->getParam('store'));
+
+ $this->wysiwygConfig->setStoreId($storeId);
+
+ return $product;
+ }
+
+ /**
+ * @param int $typeId
+ * @param int $attributeSetId
+ * @param int $storeId
+ * @return \Magento\Catalog\Model\Product
+ */
+ private function createEmptyProduct($typeId, $attributeSetId, $storeId): Product
+ {
+ /** @var $product \Magento\Catalog\Model\Product */
+ $product = $this->productFactory->create();
+ $product->setData('_edit_mode', true);
+
+ if ($typeId !== null) {
+ $product->setTypeId($typeId);
+ }
+
+ if ($storeId !== null) {
+ $product->setStoreId($storeId);
+ }
+
+ if ($attributeSetId) {
+ $product->setAttributeSetId($attributeSetId);
+ }
+
return $product;
}
}
diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Duplicate.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Duplicate.php
index 7e8b03a66f603..63e52eead064c 100644
--- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Duplicate.php
+++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Duplicate.php
@@ -43,11 +43,11 @@ public function execute()
$product = $this->productBuilder->build($this->getRequest());
try {
$newProduct = $this->productCopier->copy($product);
- $this->messageManager->addSuccess(__('You duplicated the product.'));
+ $this->messageManager->addSuccessMessage(__('You duplicated the product.'));
$resultRedirect->setPath('catalog/*/edit', ['_current' => true, 'id' => $newProduct->getId()]);
} catch (\Exception $e) {
$this->_objectManager->get(\Psr\Log\LoggerInterface::class)->critical($e);
- $this->messageManager->addError($e->getMessage());
+ $this->messageManager->addErrorMessage($e->getMessage());
$resultRedirect->setPath('catalog/*/edit', ['_current' => true]);
}
return $resultRedirect;
diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Edit.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Edit.php
index 9f99ad0fe6aca..3a4d3af4b39b1 100644
--- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Edit.php
+++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Edit.php
@@ -52,12 +52,12 @@ public function execute()
if (($productId && !$product->getEntityId())) {
/** @var \Magento\Backend\Model\View\Result\Redirect $resultRedirect */
$resultRedirect = $this->resultRedirectFactory->create();
- $this->messageManager->addError(__('This product doesn\'t exist.'));
+ $this->messageManager->addErrorMessage(__('This product doesn\'t exist.'));
return $resultRedirect->setPath('catalog/*/');
} elseif ($productId === 0) {
/** @var \Magento\Backend\Model\View\Result\Redirect $resultRedirect */
$resultRedirect = $this->resultRedirectFactory->create();
- $this->messageManager->addError(__('Invalid product id. Should be numeric value greater than 0'));
+ $this->messageManager->addErrorMessage(__('Invalid product id. Should be numeric value greater than 0'));
return $resultRedirect->setPath('catalog/*/');
}
diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Group/Save.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Group/Save.php
index 4909e22775e55..8a5f375f2b706 100644
--- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Group/Save.php
+++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Group/Save.php
@@ -29,12 +29,12 @@ public function execute()
);
if ($model->itemExists()) {
- $this->messageManager->addError(__('A group with the same name already exists.'));
+ $this->messageManager->addErrorMessage(__('A group with the same name already exists.'));
} else {
try {
$model->save();
} catch (\Exception $e) {
- $this->messageManager->addError(__('Something went wrong while saving this group.'));
+ $this->messageManager->addErrorMessage(__('Something went wrong while saving this group.'));
}
}
}
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 beb6f2b13bcfe..7153f9fd0f3f9 100644
--- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Initialization/Helper.php
+++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Initialization/Helper.php
@@ -3,14 +3,18 @@
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
+
namespace Magento\Catalog\Controller\Adminhtml\Product\Initialization;
use Magento\Catalog\Api\Data\ProductCustomOptionInterfaceFactory as CustomOptionFactory;
use Magento\Catalog\Api\Data\ProductLinkInterfaceFactory as ProductLinkFactory;
+use Magento\Catalog\Api\ProductRepositoryInterface;
use Magento\Catalog\Api\ProductRepositoryInterface\Proxy as ProductRepository;
+use Magento\Catalog\Controller\Adminhtml\Product\Initialization\Helper\AttributeDefaultValueFilter;
use Magento\Catalog\Model\Product;
use Magento\Catalog\Model\Product\Initialization\Helper\ProductLinks;
use Magento\Catalog\Model\Product\Link\Resolver as LinkResolver;
+use Magento\Catalog\Model\Product\LinkTypeProvider;
use Magento\Framework\App\ObjectManager;
use Magento\Catalog\Controller\Adminhtml\Product\Initialization\Helper\AttributeFilter;
@@ -81,7 +85,7 @@ class Helper
private $dateTimeFilter;
/**
- * @var \Magento\Catalog\Model\Product\LinkTypeProvider
+ * @var LinkTypeProvider
*/
private $linkTypeProvider;
@@ -99,10 +103,10 @@ class Helper
* @param ProductLinks $productLinks
* @param \Magento\Backend\Helper\Js $jsHelper
* @param \Magento\Framework\Stdlib\DateTime\Filter\Date $dateFilter
- * @param \Magento\Catalog\Api\Data\ProductCustomOptionInterfaceFactory|null $customOptionFactory
- * @param \Magento\Catalog\Api\Data\ProductLinkInterfaceFactory|null $productLinkFactory
- * @param \Magento\Catalog\Api\ProductRepositoryInterface|null $productRepository
- * @param \Magento\Catalog\Model\Product\LinkTypeProvider|null $linkTypeProvider
+ * @param CustomOptionFactory|null $customOptionFactory
+ * @param ProductLinkFactory |null $productLinkFactory
+ * @param ProductRepositoryInterface|null $productRepository
+ * @param LinkTypeProvider|null $linkTypeProvider
* @param AttributeFilter|null $attributeFilter
* @SuppressWarnings(PHPMD.ExcessiveParameterList)
*/
@@ -113,10 +117,10 @@ public function __construct(
\Magento\Catalog\Model\Product\Initialization\Helper\ProductLinks $productLinks,
\Magento\Backend\Helper\Js $jsHelper,
\Magento\Framework\Stdlib\DateTime\Filter\Date $dateFilter,
- \Magento\Catalog\Api\Data\ProductCustomOptionInterfaceFactory $customOptionFactory = null,
- \Magento\Catalog\Api\Data\ProductLinkInterfaceFactory $productLinkFactory = null,
- \Magento\Catalog\Api\ProductRepositoryInterface $productRepository = null,
- \Magento\Catalog\Model\Product\LinkTypeProvider $linkTypeProvider = null,
+ CustomOptionFactory $customOptionFactory = null,
+ ProductLinkFactory $productLinkFactory = null,
+ ProductRepositoryInterface $productRepository = null,
+ LinkTypeProvider $linkTypeProvider = null,
AttributeFilter $attributeFilter = null
) {
$this->request = $request;
@@ -125,16 +129,13 @@ public function __construct(
$this->productLinks = $productLinks;
$this->jsHelper = $jsHelper;
$this->dateFilter = $dateFilter;
- $this->customOptionFactory = $customOptionFactory ?: \Magento\Framework\App\ObjectManager::getInstance()
- ->get(\Magento\Catalog\Api\Data\ProductCustomOptionInterfaceFactory::class);
- $this->productLinkFactory = $productLinkFactory ?: \Magento\Framework\App\ObjectManager::getInstance()
- ->get(\Magento\Catalog\Api\Data\ProductLinkInterfaceFactory::class);
- $this->productRepository = $productRepository ?: \Magento\Framework\App\ObjectManager::getInstance()
- ->get(\Magento\Catalog\Api\ProductRepositoryInterface::class);
- $this->linkTypeProvider = $linkTypeProvider ?: \Magento\Framework\App\ObjectManager::getInstance()
- ->get(\Magento\Catalog\Model\Product\LinkTypeProvider::class);
- $this->attributeFilter = $attributeFilter ?: \Magento\Framework\App\ObjectManager::getInstance()
- ->get(AttributeFilter::class);
+
+ $objectManager = \Magento\Framework\App\ObjectManager::getInstance();
+ $this->customOptionFactory = $customOptionFactory ?: $objectManager->get(CustomOptionFactory::class);
+ $this->productLinkFactory = $productLinkFactory ?: $objectManager->get(ProductLinkFactory::class);
+ $this->productRepository = $productRepository ?: $objectManager->get(ProductRepositoryInterface::class);
+ $this->linkTypeProvider = $linkTypeProvider ?: $objectManager->get(LinkTypeProvider::class);
+ $this->attributeFilter = $attributeFilter ?: $objectManager->get(AttributeFilter::class);
}
/**
@@ -150,8 +151,7 @@ public function __construct(
*/
public function initializeFromData(\Magento\Catalog\Model\Product $product, array $productData)
{
- unset($productData['custom_attributes']);
- unset($productData['extension_attributes']);
+ unset($productData['custom_attributes'], $productData['extension_attributes']);
if ($productData) {
$stockData = isset($productData['stock_data']) ? $productData['stock_data'] : [];
@@ -159,6 +159,7 @@ public function initializeFromData(\Magento\Catalog\Model\Product $product, arra
}
$productData = $this->normalize($productData);
+ $productData = $this->convertSpecialFromDateStringToObject($productData);
if (!empty($productData['is_downloadable'])) {
$productData['product_has_weight'] = 0;
@@ -199,28 +200,13 @@ public function initializeFromData(\Magento\Catalog\Model\Product $product, arra
$productData['tier_price'] = isset($productData['tier_price']) ? $productData['tier_price'] : [];
$useDefaults = (array)$this->request->getPost('use_default', []);
-
$productData = $this->attributeFilter->prepareProductAttributes($product, $productData, $useDefaults);
-
$product->addData($productData);
if ($wasLockedMedia) {
$product->lockAttribute('media');
}
- /**
- * Check "Use Default Value" checkboxes values
- */
- foreach ($useDefaults as $attributeCode => $useDefaultState) {
- if ($useDefaultState) {
- $product->setData($attributeCode, null);
- // UI component sends value even if field is disabled, so 'Use Config Settings' must be reset to false
- if ($product->hasData('use_config_' . $attributeCode)) {
- $product->setData('use_config_' . $attributeCode, false);
- }
- }
- }
-
$product = $this->setProductLinks($product);
$product = $this->fillProductOptions($product, $productOptions);
@@ -467,4 +453,19 @@ private function fillProductOptions(Product $product, array $productOptions)
return $product->setOptions($customOptions);
}
+
+ /**
+ * Convert string date presentation into object
+ *
+ * @param array $productData
+ * @return array
+ */
+ private function convertSpecialFromDateStringToObject($productData)
+ {
+ if (isset($productData['special_from_date']) && $productData['special_from_date'] != '') {
+ $productData['special_from_date'] = new \DateTime($productData['special_from_date']);
+ }
+
+ return $productData;
+ }
}
diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Initialization/Helper/AttributeFilter.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Initialization/Helper/AttributeFilter.php
index 237168282afae..abd8c2dd9f377 100644
--- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Initialization/Helper/AttributeFilter.php
+++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Initialization/Helper/AttributeFilter.php
@@ -25,17 +25,74 @@ class AttributeFilter
* @param array $useDefaults
* @return array
*/
- public function prepareProductAttributes(Product $product, array $productData, array $useDefaults)
+ public function prepareProductAttributes(Product $product, array $productData, array $useDefaults): array
{
- foreach ($productData as $attribute => $value) {
- $considerUseDefaultsAttribute = !isset($useDefaults[$attribute]) || $useDefaults[$attribute] === "1";
- if ($value === '' && $considerUseDefaultsAttribute) {
- /** @var $product Product */
- if ((bool)$product->getData($attribute) === (bool)$value) {
- unset($productData[$attribute]);
- }
+ $attributeList = $product->getAttributes();
+ foreach ($productData as $attributeCode => $attributeValue) {
+ if ($this->isAttributeShouldNotBeUpdated($product, $useDefaults, $attributeCode, $attributeValue)) {
+ unset($productData[$attributeCode]);
+ }
+
+ if (isset($useDefaults[$attributeCode]) && $useDefaults[$attributeCode] === '1') {
+ $productData = $this->prepareDefaultData($attributeList, $attributeCode, $productData);
+ $productData = $this->prepareConfigData($product, $attributeCode, $productData);
+ }
+ }
+
+ return $productData;
+ }
+
+ /**
+ * @param Product $product
+ * @param string $attributeCode
+ * @param array $productData
+ * @return array
+ */
+ private function prepareConfigData(Product $product, $attributeCode, array $productData): array
+ {
+ // UI component sends value even if field is disabled, so 'Use Config Settings' must be reset to false
+ if ($product->hasData('use_config_' . $attributeCode)) {
+ $productData['use_config_' . $attributeCode] = false;
+ }
+
+ return $productData;
+ }
+
+ /**
+ * @param array $attributeList
+ * @param string $attributeCode
+ * @param array $productData
+ * @return array
+ */
+ private function prepareDefaultData(array $attributeList, $attributeCode, array $productData): array
+ {
+ if (isset($attributeList[$attributeCode])) {
+ /** @var \Magento\Catalog\Model\ResourceModel\Eav\Attribute $attribute */
+ $attribute = $attributeList[$attributeCode];
+ $attributeType = $attribute->getBackendType();
+ // For non-numberic types set the attributeValue to 'false' to trigger their removal from the db
+ if ($attributeType === 'varchar' || $attributeType === 'text' || $attributeType === 'datetime') {
+ $attribute->setIsRequired(false);
+ $productData[$attributeCode] = false;
+ } else {
+ $productData[$attributeCode] = null;
}
}
+
return $productData;
}
+
+ /**
+ * @param Product $product
+ * @param $useDefaults
+ * @param $attribute
+ * @param $value
+ * @return bool
+ */
+ private function isAttributeShouldNotBeUpdated(Product $product, $useDefaults, $attribute, $value): bool
+ {
+ $considerUseDefaultsAttribute = !isset($useDefaults[$attribute]) || $useDefaults[$attribute] === '1';
+
+ return ($value === '' && $considerUseDefaultsAttribute && !$product->getData($attribute));
+ }
}
diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/MassDelete.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/MassDelete.php
index 2402fb213cda0..d04284b4b323c 100644
--- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/MassDelete.php
+++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/MassDelete.php
@@ -11,6 +11,7 @@
use Magento\Ui\Component\MassAction\Filter;
use Magento\Catalog\Model\ResourceModel\Product\CollectionFactory;
use Magento\Catalog\Api\ProductRepositoryInterface;
+use Magento\Framework\Exception\NotFoundException;
class MassDelete extends \Magento\Catalog\Controller\Adminhtml\Product
{
@@ -54,9 +55,15 @@ public function __construct(
/**
* @return \Magento\Backend\Model\View\Result\Redirect
+ * @throws NotFoundException
+ * @throws \Magento\Framework\Exception\LocalizedException
+ * @throws \Magento\Framework\Exception\StateException
*/
public function execute()
{
+ if (!$this->getRequest()->isPost()) {
+ throw new NotFoundException(__('Page not found'));
+ }
$collection = $this->filter->getCollection($this->collectionFactory->create());
$productDeleted = 0;
/** @var \Magento\Catalog\Model\Product $product */
@@ -64,9 +71,12 @@ public function execute()
$this->productRepository->delete($product);
$productDeleted++;
}
- $this->messageManager->addSuccess(
- __('A total of %1 record(s) have been deleted.', $productDeleted)
- );
+
+ if ($productDeleted) {
+ $this->messageManager->addSuccessMessage(
+ __('A total of %1 record(s) have been deleted.', $productDeleted)
+ );
+ }
return $this->resultFactory->create(ResultFactory::TYPE_REDIRECT)->setPath('catalog/*/index');
}
diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/MassStatus.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/MassStatus.php
index ec5eb9de3fe35..b6e7e31fb9efd 100644
--- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/MassStatus.php
+++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/MassStatus.php
@@ -94,10 +94,12 @@ public function execute()
$this->_validateMassStatus($productIds, $status);
$this->_objectManager->get(\Magento\Catalog\Model\Product\Action::class)
->updateAttributes($productIds, ['status' => $status], $storeId);
- $this->messageManager->addSuccess(__('A total of %1 record(s) have been updated.', count($productIds)));
+ $this->messageManager->addSuccessMessage(
+ __('A total of %1 record(s) have been updated.', count($productIds))
+ );
$this->_productPriceIndexerProcessor->reindexList($productIds);
} catch (\Magento\Framework\Exception\LocalizedException $e) {
- $this->messageManager->addError($e->getMessage());
+ $this->messageManager->addErrorMessage($e->getMessage());
} catch (\Exception $e) {
$this->_getSession()->addException($e, __('Something went wrong while updating the product(s) status.'));
}
diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Save.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Save.php
index d34f4bedd80e4..bf0d740fc98fb 100644
--- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Save.php
+++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Save.php
@@ -7,6 +7,7 @@
namespace Magento\Catalog\Controller\Adminhtml\Product;
use Magento\Backend\App\Action;
+use Magento\Catalog\Api\Data\ProductInterface;
use Magento\Catalog\Controller\Adminhtml\Product;
use Magento\Store\Model\StoreManagerInterface;
use Magento\Framework\App\Request\DataPersistorInterface;
@@ -101,12 +102,12 @@ public function execute()
$this->productBuilder->build($this->getRequest())
);
$this->productTypeManager->processProduct($product);
-
if (isset($data['product'][$product->getIdFieldName()])) {
throw new \Magento\Framework\Exception\LocalizedException(__('Unable to save product'));
}
$originalSku = $product->getSku();
+ $canSaveCustomOptions = $product->getCanSaveCustomOptions();
$product->save();
$this->handleImageRemoveError($data, $product->getId());
$this->getCategoryLinkManagement()->assignProductToCategories(
@@ -116,9 +117,9 @@ public function execute()
$productId = $product->getEntityId();
$productAttributeSetId = $product->getAttributeSetId();
$productTypeId = $product->getTypeId();
-
- $this->copyToStores($data, $productId);
-
+ $extendedData = $data;
+ $extendedData['can_save_custom_options'] = $canSaveCustomOptions;
+ $this->copyToStores($extendedData, $productId);
$this->messageManager->addSuccessMessage(__('You saved the product.'));
$this->getDataPersistor()->clear('catalog_product');
if ($product->getSku() != $originalSku) {
@@ -140,17 +141,20 @@ public function execute()
);
if ($redirectBack === 'duplicate') {
+ $product->unsetData('quantity_and_stock_status');
$newProduct = $this->productCopier->copy($product);
$this->messageManager->addSuccessMessage(__('You duplicated the product.'));
}
} catch (\Magento\Framework\Exception\LocalizedException $e) {
$this->_objectManager->get(\Psr\Log\LoggerInterface::class)->critical($e);
$this->messageManager->addExceptionMessage($e);
+ $data = isset($product) ? $this->persistMediaData($product, $data) : $data;
$this->getDataPersistor()->set('catalog_product', $data);
$redirectBack = $productId ? true : 'new';
} catch (\Exception $e) {
$this->_objectManager->get(\Psr\Log\LoggerInterface::class)->critical($e);
$this->messageManager->addErrorMessage($e->getMessage());
+ $data = isset($product) ? $this->persistMediaData($product, $data) : $data;
$this->getDataPersistor()->set('catalog_product', $data);
$redirectBack = $productId ? true : 'new';
}
@@ -213,6 +217,9 @@ private function handleImageRemoveError($postData, $productId)
/**
* Do copying data to stores
*
+ * If the 'copy_from' field is not specified in the input data,
+ * the store fallback mechanism will automatically take the admin store's default value.
+ *
* @param array $data
* @param int $productId
* @return void
@@ -224,15 +231,18 @@ protected function copyToStores($data, $productId)
if (isset($data['product']['website_ids'][$websiteId])
&& (bool)$data['product']['website_ids'][$websiteId]) {
foreach ($group as $store) {
- $copyFrom = (isset($store['copy_from'])) ? $store['copy_from'] : 0;
- $copyTo = (isset($store['copy_to'])) ? $store['copy_to'] : 0;
- if ($copyTo) {
- $this->_objectManager->create(\Magento\Catalog\Model\Product::class)
- ->setStoreId($copyFrom)
- ->load($productId)
- ->setStoreId($copyTo)
- ->setCopyFromView(true)
- ->save();
+ if (isset($store['copy_from'])) {
+ $copyFrom = $store['copy_from'];
+ $copyTo = (isset($store['copy_to'])) ? $store['copy_to'] : 0;
+ if ($copyTo) {
+ $this->_objectManager->create(\Magento\Catalog\Model\Product::class)
+ ->setStoreId($copyFrom)
+ ->load($productId)
+ ->setStoreId($copyTo)
+ ->setCanSaveCustomOptions($data['can_save_custom_options'])
+ ->setCopyFromView(true)
+ ->save();
+ }
}
}
}
@@ -279,4 +289,36 @@ protected function getDataPersistor()
return $this->dataPersistor;
}
+
+ /**
+ * Persist media gallery on error, in order to show already saved images on next run.
+ *
+ * @param ProductInterface $product
+ * @param array $data
+ * @return array
+ */
+ private function persistMediaData(ProductInterface $product, array $data)
+ {
+ $mediaGallery = $product->getData('media_gallery');
+ if (!empty($mediaGallery['images'])) {
+ foreach ($mediaGallery['images'] as $key => $image) {
+ if (!isset($image['new_file'])) {
+ //Remove duplicates.
+ unset($mediaGallery['images'][$key]);
+ }
+ }
+ $data['product']['media_gallery'] = $mediaGallery;
+ $fields = [
+ 'image',
+ 'small_image',
+ 'thumbnail',
+ 'swatch_image',
+ ];
+ foreach ($fields as $field) {
+ $data['product'][$field] = $product->getData($field);
+ }
+ }
+
+ return $data;
+ }
}
diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Set/Delete.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Set/Delete.php
index b49a4dabe223c..f2695311732f0 100644
--- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Set/Delete.php
+++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Set/Delete.php
@@ -36,10 +36,10 @@ public function execute()
$resultRedirect = $this->resultRedirectFactory->create();
try {
$this->attributeSetRepository->deleteById($setId);
- $this->messageManager->addSuccess(__('The attribute set has been removed.'));
+ $this->messageManager->addSuccessMessage(__('The attribute set has been removed.'));
$resultRedirect->setPath('catalog/*/');
} catch (\Exception $e) {
- $this->messageManager->addError(__('We can\'t delete this set right now.'));
+ $this->messageManager->addErrorMessage(__('We can\'t delete this set right now.'));
$resultRedirect->setUrl($this->_redirect->getRedirectUrl($this->getUrl('*')));
}
return $resultRedirect;
diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Set/Save.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Set/Save.php
index 00a836309e58e..c5dd9ce6d8e77 100644
--- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Set/Save.php
+++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Set/Save.php
@@ -6,6 +6,11 @@
*/
namespace Magento\Catalog\Controller\Adminhtml\Product\Set;
+use Magento\Framework\App\ObjectManager;
+
+/**
+ * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
+ */
class Save extends \Magento\Catalog\Controller\Adminhtml\Product\Set
{
/**
@@ -17,22 +22,49 @@ class Save extends \Magento\Catalog\Controller\Adminhtml\Product\Set
* @var \Magento\Framework\Controller\Result\JsonFactory
*/
protected $resultJsonFactory;
-
+
+ /*
+ * @var \Magento\Eav\Model\Entity\Attribute\SetFactory
+ */
+ private $attributeSetFactory;
+
+ /*
+ * @var \Magento\Framework\Filter\FilterManager
+ */
+ private $filterManager;
+
+ /*
+ * @var \Magento\Framework\Json\Helper\Data
+ */
+ private $jsonHelper;
+
/**
* @param \Magento\Backend\App\Action\Context $context
* @param \Magento\Framework\Registry $coreRegistry
* @param \Magento\Framework\View\LayoutFactory $layoutFactory
* @param \Magento\Framework\Controller\Result\JsonFactory $resultJsonFactory
+ * @param \Magento\Eav\Model\Entity\Attribute\SetFactory $attributeSetFactory
+ * @param \Magento\Framework\Filter\FilterManager $filterManager
+ * @param \Magento\Framework\Json\Helper\Data $jsonHelper
*/
public function __construct(
\Magento\Backend\App\Action\Context $context,
\Magento\Framework\Registry $coreRegistry,
\Magento\Framework\View\LayoutFactory $layoutFactory,
- \Magento\Framework\Controller\Result\JsonFactory $resultJsonFactory
+ \Magento\Framework\Controller\Result\JsonFactory $resultJsonFactory,
+ \Magento\Eav\Model\Entity\Attribute\SetFactory $attributeSetFactory = null,
+ \Magento\Framework\Filter\FilterManager $filterManager = null,
+ \Magento\Framework\Json\Helper\Data $jsonHelper = null
) {
parent::__construct($context, $coreRegistry);
$this->layoutFactory = $layoutFactory;
$this->resultJsonFactory = $resultJsonFactory;
+ $this->attributeSetFactory = $attributeSetFactory ?: ObjectManager::getInstance()
+ ->get(\Magento\Eav\Model\Entity\Attribute\SetFactory::class);
+ $this->filterManager = $filterManager ?: ObjectManager::getInstance()
+ ->get(\Magento\Framework\Filter\FilterManager::class);
+ $this->jsonHelper = $jsonHelper ?: ObjectManager::getInstance()
+ ->get(\Magento\Framework\Json\Helper\Data::class);
}
/**
@@ -65,16 +97,12 @@ public function execute()
$isNewSet = $this->getRequest()->getParam('gotoEdit', false) == '1';
/* @var $model \Magento\Eav\Model\Entity\Attribute\Set */
- $model = $this->_objectManager->create(\Magento\Eav\Model\Entity\Attribute\Set::class)
- ->setEntityTypeId($entityTypeId);
-
- /** @var $filterManager \Magento\Framework\Filter\FilterManager */
- $filterManager = $this->_objectManager->get(\Magento\Framework\Filter\FilterManager::class);
+ $model = $this->attributeSetFactory->create()->setEntityTypeId($entityTypeId);
try {
if ($isNewSet) {
//filter html tags
- $name = $filterManager->stripTags($this->getRequest()->getParam('attribute_set_name'));
+ $name = $this->filterManager->stripTags($this->getRequest()->getParam('attribute_set_name'));
$model->setAttributeSetName(trim($name));
} else {
if ($attributeSetId) {
@@ -85,11 +113,10 @@ public function execute()
__('This attribute set no longer exists.')
);
}
- $data = $this->_objectManager->get(\Magento\Framework\Json\Helper\Data::class)
- ->jsonDecode($this->getRequest()->getPost('data'));
+ $data = $this->jsonHelper->jsonDecode($this->getRequest()->getPost('data'));
//filter html tags
- $data['attribute_set_name'] = $filterManager->stripTags($data['attribute_set_name']);
+ $data['attribute_set_name'] = $this->filterManager->stripTags($data['attribute_set_name']);
$model->organizeData($data);
}
@@ -100,15 +127,15 @@ public function execute()
$model->initFromSkeleton($this->getRequest()->getParam('skeleton_set'));
}
$model->save();
- $this->messageManager->addSuccess(__('You saved the attribute set.'));
+ $this->messageManager->addSuccessMessage(__('You saved the attribute set.'));
} catch (\Magento\Framework\Exception\AlreadyExistsException $e) {
$this->messageManager->addErrorMessage($e->getMessage());
$hasError = true;
} catch (\Magento\Framework\Exception\LocalizedException $e) {
- $this->messageManager->addError($e->getMessage());
+ $this->messageManager->addErrorMessage($e->getMessage());
$hasError = true;
} catch (\Exception $e) {
- $this->messageManager->addException($e, __('Something went wrong while saving the attribute set.'));
+ $this->messageManager->addExceptionMessage($e, __('Something went wrong while saving the attribute set.'));
$hasError = true;
}
diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Validate.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Validate.php
index 63f46fd32e6f7..e131bfe38c546 100644
--- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Validate.php
+++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Validate.php
@@ -137,7 +137,7 @@ public function execute()
$response->setError(true);
$response->setMessages([$e->getMessage()]);
} catch (\Exception $e) {
- $this->messageManager->addError($e->getMessage());
+ $this->messageManager->addErrorMessage($e->getMessage());
$layout = $this->layoutFactory->create();
$layout->initMessages();
$response->setError(true);
diff --git a/app/code/Magento/Catalog/Controller/Category/View.php b/app/code/Magento/Catalog/Controller/Category/View.php
index a72f764a5f1f6..e31b1a17ecdc3 100644
--- a/app/code/Magento/Catalog/Controller/Category/View.php
+++ b/app/code/Magento/Catalog/Controller/Category/View.php
@@ -111,7 +111,7 @@ public function __construct(
/**
* Initialize requested category object
*
- * @return \Magento\Catalog\Model\Category
+ * @return \Magento\Catalog\Model\Category|bool
*/
protected function _initCategory()
{
@@ -152,7 +152,9 @@ protected function _initCategory()
*/
public function execute()
{
- if ($this->_request->getParam(\Magento\Framework\App\ActionInterface::PARAM_NAME_URL_ENCODED)) {
+ if (!$this->_request->getParam('___from_store')
+ && $this->_request->getParam(self::PARAM_NAME_URL_ENCODED)
+ ) {
return $this->resultRedirectFactory->create()->setUrl($this->_redirect->getRedirectUrl());
}
$category = $this->_initCategory();
diff --git a/app/code/Magento/Catalog/Controller/Product/Compare/Add.php b/app/code/Magento/Catalog/Controller/Product/Compare/Add.php
index 89eb6c9be929f..eb9cc83125541 100644
--- a/app/code/Magento/Catalog/Controller/Product/Compare/Add.php
+++ b/app/code/Magento/Catalog/Controller/Product/Compare/Add.php
@@ -36,7 +36,14 @@ public function execute()
$productName = $this->_objectManager->get(
\Magento\Framework\Escaper::class
)->escapeHtml($product->getName());
- $this->messageManager->addSuccess(__('You added product %1 to the comparison list.', $productName));
+ $this->messageManager->addComplexSuccessMessage(
+ 'addCompareSuccessMessage',
+ [
+ 'product_name' => $productName,
+ 'compare_list_url' => $this->_url->getUrl('catalog/product_compare')
+ ]
+ );
+
$this->_eventManager->dispatch('catalog_product_compare_add_product', ['product' => $product]);
}
diff --git a/app/code/Magento/Catalog/Controller/Product/Compare/Clear.php b/app/code/Magento/Catalog/Controller/Product/Compare/Clear.php
index 30470d13f002d..568fbf1d05677 100644
--- a/app/code/Magento/Catalog/Controller/Product/Compare/Clear.php
+++ b/app/code/Magento/Catalog/Controller/Product/Compare/Clear.php
@@ -30,12 +30,12 @@ public function execute()
try {
$items->clear();
- $this->messageManager->addSuccess(__('You cleared the comparison list.'));
+ $this->messageManager->addSuccessMessage(__('You cleared the comparison list.'));
$this->_objectManager->get(\Magento\Catalog\Helper\Product\Compare::class)->calculate();
} catch (\Magento\Framework\Exception\LocalizedException $e) {
- $this->messageManager->addError($e->getMessage());
+ $this->messageManager->addErrorMessage($e->getMessage());
} catch (\Exception $e) {
- $this->messageManager->addException($e, __('Something went wrong clearing the comparison list.'));
+ $this->messageManager->addExceptionMessage($e, __('Something went wrong clearing the comparison list.'));
}
/** @var \Magento\Framework\Controller\Result\Redirect $resultRedirect */
diff --git a/app/code/Magento/Catalog/Controller/Product/Compare/Remove.php b/app/code/Magento/Catalog/Controller/Product/Compare/Remove.php
index fadb94761a236..2acbe5ce4d582 100644
--- a/app/code/Magento/Catalog/Controller/Product/Compare/Remove.php
+++ b/app/code/Magento/Catalog/Controller/Product/Compare/Remove.php
@@ -44,7 +44,7 @@ public function execute()
$item->delete();
$productName = $this->_objectManager->get(\Magento\Framework\Escaper::class)
->escapeHtml($product->getName());
- $this->messageManager->addSuccess(
+ $this->messageManager->addSuccessMessage(
__('You removed product %1 from the comparison list.', $productName)
);
$this->_eventManager->dispatch(
diff --git a/app/code/Magento/Catalog/Controller/Product/View.php b/app/code/Magento/Catalog/Controller/Product/View.php
index 4c577eb897589..4ec35b3382786 100644
--- a/app/code/Magento/Catalog/Controller/Product/View.php
+++ b/app/code/Magento/Catalog/Controller/Product/View.php
@@ -76,15 +76,21 @@ public function execute()
$productId = (int) $this->getRequest()->getParam('id');
$specifyOptions = $this->getRequest()->getParam('options');
- if ($this->getRequest()->isPost() && $this->getRequest()->getParam(self::PARAM_NAME_URL_ENCODED)) {
+ if (!$this->_request->getParam('___from_store')
+ && $this->_request->isPost()
+ && $this->_request->getParam(self::PARAM_NAME_URL_ENCODED)
+ ) {
$product = $this->_initProduct();
+
if (!$product) {
return $this->noProductRedirect();
}
+
if ($specifyOptions) {
$notice = $product->getTypeInstance()->getSpecifyOptionMessage();
- $this->messageManager->addNotice($notice);
+ $this->messageManager->addNoticeMessage($notice);
}
+
if ($this->getRequest()->isAjax()) {
$this->getResponse()->representJson(
$this->_objectManager->get(\Magento\Framework\Json\Helper\Data::class)->jsonEncode([
diff --git a/app/code/Magento/Catalog/Cron/DeleteOutdatedPriceValues.php b/app/code/Magento/Catalog/Cron/DeleteOutdatedPriceValues.php
index de8b6f06afbea..e1ff39575f3ca 100644
--- a/app/code/Magento/Catalog/Cron/DeleteOutdatedPriceValues.php
+++ b/app/code/Magento/Catalog/Cron/DeleteOutdatedPriceValues.php
@@ -13,7 +13,7 @@
/**
* Cron operation is responsible for deleting all product prices on WEBSITE level
- * in case 'Catalog Price Scope' configuratoin parameter is set to GLOBAL.
+ * in case 'Catalog Price Scope' configuration parameter is set to GLOBAL.
*/
class DeleteOutdatedPriceValues
{
@@ -48,27 +48,46 @@ public function __construct(
}
/**
- * Delete all price values for non-admin stores if PRICE_SCOPE is global
+ * Delete all price values for non-admin stores if PRICE_SCOPE is set to global.
*
* @return void
*/
public function execute()
{
- $priceScope = $this->scopeConfig->getValue(Store::XML_PATH_PRICE_SCOPE);
- if ($priceScope == Store::PRICE_SCOPE_GLOBAL) {
- /** @var \Magento\Catalog\Model\ResourceModel\Eav\Attribute $priceAttribute */
- $priceAttribute = $this->attributeRepository
- ->get(ProductAttributeInterface::ENTITY_TYPE_CODE, ProductAttributeInterface::CODE_PRICE);
- $connection = $this->resource->getConnection();
- $conditions = [
- $connection->quoteInto('attribute_id = ?', $priceAttribute->getId()),
- $connection->quoteInto('store_id != ?', Store::DEFAULT_STORE_ID),
- ];
+ if ($this->isPriceScopeSetToGlobal() === false) {
+ return;
+ }
+
+ /** @var \Magento\Catalog\Model\ResourceModel\Eav\Attribute $priceAttribute */
+ $priceAttribute = $this->attributeRepository
+ ->get(ProductAttributeInterface::ENTITY_TYPE_CODE, ProductAttributeInterface::CODE_PRICE);
+ $connection = $this->resource->getConnection();
+ $conditions = [
+ $connection->quoteInto('attribute_id = ?', $priceAttribute->getId()),
+ $connection->quoteInto('store_id != ?', Store::DEFAULT_STORE_ID),
+ ];
- $connection->delete(
- $priceAttribute->getBackend()->getTable(),
- $conditions
- );
+ $connection->delete(
+ $priceAttribute->getBackend()->getTable(),
+ $conditions
+ );
+ }
+
+ /**
+ * Checks if price scope config option explicitly equal to global value.
+ *
+ * Such strict comparision is required to prevent price deleting when
+ * price scope config option is null for some reason.
+ *
+ * @return bool
+ */
+ private function isPriceScopeSetToGlobal()
+ {
+ $priceScope = $this->scopeConfig->getValue(Store::XML_PATH_PRICE_SCOPE);
+ if ($priceScope === null) {
+ return false;
}
+
+ return (int)$priceScope === Store::PRICE_SCOPE_GLOBAL;
}
}
diff --git a/app/code/Magento/Catalog/Cron/FrontendActionsFlush.php b/app/code/Magento/Catalog/Cron/FrontendActionsFlush.php
index 6e7699abb4776..99e9898eab3c0 100644
--- a/app/code/Magento/Catalog/Cron/FrontendActionsFlush.php
+++ b/app/code/Magento/Catalog/Cron/FrontendActionsFlush.php
@@ -57,8 +57,7 @@ private function getLifeTimeByNamespace($namespace)
];
}
- return isset($configuration['lifetime']) ?
- (int) $configuration['lifetime'] : FrontendStorageConfigurationInterface::DEFAULT_LIFETIME;
+ return (int)$configuration['lifetime'] ?? FrontendStorageConfigurationInterface::DEFAULT_LIFETIME;
}
/**
diff --git a/app/code/Magento/Catalog/CustomerData/CompareProducts.php b/app/code/Magento/Catalog/CustomerData/CompareProducts.php
index 0e688042c615a..afbeab8c9070e 100644
--- a/app/code/Magento/Catalog/CustomerData/CompareProducts.php
+++ b/app/code/Magento/Catalog/CustomerData/CompareProducts.php
@@ -19,6 +19,11 @@ class CompareProducts implements SectionSourceInterface
*/
protected $productUrl;
+ /**
+ * @var \Magento\Catalog\Helper\Output
+ */
+ private $outputHelper;
+
/**
* @param \Magento\Catalog\Helper\Product\Compare $helper
* @param \Magento\Catalog\Model\Product\Url $productUrl
@@ -54,6 +59,7 @@ public function getSectionData()
protected function getItems()
{
$items = [];
+ /** @var \Magento\Catalog\Model\Product $item */
foreach ($this->helper->getItemCollection() as $item) {
$items[] = [
'id' => $item->getId(),
diff --git a/app/code/Magento/Catalog/Helper/Image.php b/app/code/Magento/Catalog/Helper/Image.php
index 380fd0298c2d7..4e8e63c17199d 100644
--- a/app/code/Magento/Catalog/Helper/Image.php
+++ b/app/code/Magento/Catalog/Helper/Image.php
@@ -859,7 +859,7 @@ public function getFrame()
*/
protected function getAttribute($name)
{
- return isset($this->attributes[$name]) ? $this->attributes[$name] : null;
+ return $this->attributes[$name] ?? null;
}
/**
diff --git a/app/code/Magento/Catalog/Helper/Output.php b/app/code/Magento/Catalog/Helper/Output.php
index facd5351f269a..ecd3fe6a32a83 100644
--- a/app/code/Magento/Catalog/Helper/Output.php
+++ b/app/code/Magento/Catalog/Helper/Output.php
@@ -105,7 +105,7 @@ public function addHandler($method, $handler)
public function getHandlers($method)
{
$method = strtolower($method);
- return isset($this->_handlers[$method]) ? $this->_handlers[$method] : [];
+ return $this->_handlers[$method] ?? [];
}
/**
@@ -151,7 +151,7 @@ public function productAttribute($product, $attributeHtml, $attributeName)
$attributeHtml = nl2br($attributeHtml);
}
}
- if ($attribute->getIsHtmlAllowedOnFront() && $attribute->getIsWysiwygEnabled()) {
+ if ($attribute->getIsHtmlAllowedOnFront() || $attribute->getIsWysiwygEnabled()) {
if ($this->_catalogData->isUrlDirectivesParsingAllowed()) {
$attributeHtml = $this->_getTemplateProcessor()->filter($attributeHtml);
}
diff --git a/app/code/Magento/Catalog/Helper/Product/View.php b/app/code/Magento/Catalog/Helper/Product/View.php
index 46ac05168715b..90027ffb65a58 100644
--- a/app/code/Magento/Catalog/Helper/Product/View.php
+++ b/app/code/Magento/Catalog/Helper/Product/View.php
@@ -61,6 +61,11 @@ class View extends \Magento\Framework\App\Helper\AbstractHelper
*/
protected $categoryUrlPathGenerator;
+ /**
+ * @var \Magento\Framework\Stdlib\StringUtils
+ */
+ private $string;
+
/**
* Constructor
*
@@ -72,6 +77,7 @@ class View extends \Magento\Framework\App\Helper\AbstractHelper
* @param \Magento\Framework\Message\ManagerInterface $messageManager
* @param \Magento\CatalogUrlRewrite\Model\CategoryUrlPathGenerator $categoryUrlPathGenerator
* @param array $messageGroups
+ * @param \Magento\Framework\Stdlib\StringUtils|null $string
*/
public function __construct(
\Magento\Framework\App\Helper\Context $context,
@@ -81,7 +87,8 @@ public function __construct(
\Magento\Framework\Registry $coreRegistry,
\Magento\Framework\Message\ManagerInterface $messageManager,
\Magento\CatalogUrlRewrite\Model\CategoryUrlPathGenerator $categoryUrlPathGenerator,
- array $messageGroups = []
+ array $messageGroups = [],
+ \Magento\Framework\Stdlib\StringUtils $string = null
) {
$this->_catalogSession = $catalogSession;
$this->_catalogDesign = $catalogDesign;
@@ -90,9 +97,57 @@ public function __construct(
$this->messageGroups = $messageGroups;
$this->messageManager = $messageManager;
$this->categoryUrlPathGenerator = $categoryUrlPathGenerator;
+ $this->string = $string ?: \Magento\Framework\App\ObjectManager::getInstance()
+ ->get(\Magento\Framework\Stdlib\StringUtils::class);
parent::__construct($context);
}
+ /**
+ * Add meta information from product to layout
+ *
+ * @param \Magento\Framework\View\Result\Page $resultPage
+ * @param \Magento\Catalog\Model\Product $product
+ * @return \Magento\Framework\View\Result\Page
+ */
+ private function preparePageMetadata(ResultPage $resultPage, $product)
+ {
+ $pageConfig = $resultPage->getConfig();
+
+ $metaTitle = $product->getMetaTitle();
+ $pageConfig->setMetaTitle($metaTitle);
+ $pageConfig->getTitle()->set($metaTitle ?: $product->getName());
+
+ $keyword = $product->getMetaKeyword();
+ $currentCategory = $this->_coreRegistry->registry('current_category');
+ if ($keyword) {
+ $pageConfig->setKeywords($keyword);
+ } elseif ($currentCategory) {
+ $pageConfig->setKeywords($product->getName());
+ }
+
+ $description = $product->getMetaDescription();
+ if ($description) {
+ $pageConfig->setDescription($description);
+ } else {
+ $pageConfig->setDescription($this->string->substr(strip_tags($product->getDescription()), 0, 255));
+ }
+
+ if ($this->_catalogProduct->canUseCanonicalTag()) {
+ $pageConfig->addRemotePageAsset(
+ $product->getUrlModel()->getUrl($product, ['_ignore_category' => true]),
+ 'canonical',
+ ['attributes' => ['rel' => 'canonical']]
+ );
+ }
+
+ $pageMainTitle = $resultPage->getLayout()->getBlock('page.main.title');
+ if ($pageMainTitle) {
+ $pageMainTitle->setPageTitle($product->getName());
+ }
+
+ return $this;
+ }
+
/**
* Init layout for viewing product page
*
@@ -122,18 +177,18 @@ public function initProductLayout(ResultPage $resultPage, $product, $params = nu
// Load default page handles and page configurations
if ($params && $params->getBeforeHandles()) {
foreach ($params->getBeforeHandles() as $handle) {
- $resultPage->addPageLayoutHandles(['id' => $product->getId(), 'sku' => $urlSafeSku], $handle);
$resultPage->addPageLayoutHandles(['type' => $product->getTypeId()], $handle, false);
+ $resultPage->addPageLayoutHandles(['id' => $product->getId(), 'sku' => $urlSafeSku], $handle);
}
}
-
- $resultPage->addPageLayoutHandles(['id' => $product->getId(), 'sku' => $urlSafeSku]);
+
$resultPage->addPageLayoutHandles(['type' => $product->getTypeId()], null, false);
+ $resultPage->addPageLayoutHandles(['id' => $product->getId(), 'sku' => $urlSafeSku]);
if ($params && $params->getAfterHandles()) {
foreach ($params->getAfterHandles() as $handle) {
- $resultPage->addPageLayoutHandles(['id' => $product->getId(), 'sku' => $urlSafeSku], $handle);
$resultPage->addPageLayoutHandles(['type' => $product->getTypeId()], $handle, false);
+ $resultPage->addPageLayoutHandles(['id' => $product->getId(), 'sku' => $urlSafeSku], $handle);
}
}
@@ -227,6 +282,7 @@ public function prepareAndRender(ResultPage $resultPage, $productId, $controller
}
$this->initProductLayout($resultPage, $product, $params);
+ $this->preparePageMetadata($resultPage, $product);
return $this;
}
}
diff --git a/app/code/Magento/Catalog/Model/Api/SearchCriteria/CollectionProcessor/ConditionProcessor/ConditionBuilder/EavAttributeCondition.php b/app/code/Magento/Catalog/Model/Api/SearchCriteria/CollectionProcessor/ConditionProcessor/ConditionBuilder/EavAttributeCondition.php
new file mode 100644
index 0000000000000..ca8b53ea0db00
--- /dev/null
+++ b/app/code/Magento/Catalog/Model/Api/SearchCriteria/CollectionProcessor/ConditionProcessor/ConditionBuilder/EavAttributeCondition.php
@@ -0,0 +1,129 @@
+eavConfig = $eavConfig;
+ $this->resourceConnection = $resourceConnection;
+ }
+
+ /**
+ * @param Filter $filter
+ * @return string
+ * @throws \DomainException
+ * @throws \Magento\Framework\Exception\LocalizedException
+ */
+ public function build(Filter $filter): string
+ {
+ $attribute = $this->getAttributeByCode($filter->getField());
+ $tableAlias = 'ca_' . $attribute->getAttributeCode();
+
+ $conditionType = $this->mapConditionType($filter->getConditionType());
+ $conditionValue = $this->mapConditionValue($conditionType, $filter->getValue());
+
+ // NOTE: store scope was ignored intentionally to perform search across all stores
+ $attributeSelect = $this->resourceConnection->getConnection()
+ ->select()
+ ->from(
+ [$tableAlias => $attribute->getBackendTable()],
+ $tableAlias . '.' . $attribute->getEntityIdField()
+ )->where(
+ $this->resourceConnection->getConnection()->prepareSqlCondition(
+ $tableAlias . '.' . $attribute->getIdFieldName(),
+ ['eq' => $attribute->getAttributeId()]
+ )
+ )->where(
+ $this->resourceConnection->getConnection()->prepareSqlCondition(
+ $tableAlias . '.value',
+ [$conditionType => $conditionValue]
+ )
+ );
+
+ return $this->resourceConnection
+ ->getConnection()
+ ->prepareSqlCondition(
+ Collection::MAIN_TABLE_ALIAS . '.' . $attribute->getEntityIdField(),
+ [
+ 'in' => $attributeSelect
+ ]
+ );
+ }
+
+ /**
+ * @param string $field
+ * @return Attribute
+ * @throws \Magento\Framework\Exception\LocalizedException
+ */
+ private function getAttributeByCode(string $field): Attribute
+ {
+ return $this->eavConfig->getAttribute(\Magento\Catalog\Model\Product::ENTITY, $field);
+ }
+
+ /**
+ * Map equal and not equal conditions to in and not in
+ *
+ * @param string $conditionType
+ * @return mixed
+ */
+ private function mapConditionType(string $conditionType): string
+ {
+ $conditionsMap = [
+ 'eq' => 'in',
+ 'neq' => 'nin'
+ ];
+
+ return isset($conditionsMap[$conditionType]) ? $conditionsMap[$conditionType] : $conditionType;
+ }
+
+ /**
+ * Wraps value with '%' if condition type is 'like' or 'not like'
+ *
+ * @param string $conditionType
+ * @param string $conditionValue
+ * @return string
+ */
+ private function mapConditionValue(string $conditionType, string $conditionValue): string
+ {
+ $conditionsMap = ['like', 'nlike'];
+
+ if (in_array($conditionType, $conditionsMap)) {
+ $conditionValue = '%' . $conditionValue . '%';
+ }
+
+ return $conditionValue;
+ }
+}
diff --git a/app/code/Magento/Catalog/Model/Api/SearchCriteria/CollectionProcessor/ConditionProcessor/ConditionBuilder/Factory.php b/app/code/Magento/Catalog/Model/Api/SearchCriteria/CollectionProcessor/ConditionProcessor/ConditionBuilder/Factory.php
new file mode 100644
index 0000000000000..808878d1481a9
--- /dev/null
+++ b/app/code/Magento/Catalog/Model/Api/SearchCriteria/CollectionProcessor/ConditionProcessor/ConditionBuilder/Factory.php
@@ -0,0 +1,84 @@
+eavConfig = $eavConfig;
+ $this->productResource = $productResource;
+ $this->eavAttributeConditionBuilder = $eavAttributeConditionBuilder;
+ $this->nativeAttributeConditionBuilder = $nativeAttributeConditionBuilder;
+ }
+
+ /**
+ * @param Filter $filter
+ * @return CustomConditionInterface
+ * @throws \Magento\Framework\Exception\LocalizedException
+ */
+ public function createByFilter(Filter $filter): CustomConditionInterface
+ {
+ $attribute = $this->getAttributeByCode($filter->getField());
+
+ if ($attribute->getBackendTable() === $this->productResource->getEntityTable()) {
+ return $this->nativeAttributeConditionBuilder;
+ }
+
+ return $this->eavAttributeConditionBuilder;
+ }
+
+ /**
+ * @param string $field
+ * @return \Magento\Catalog\Model\ResourceModel\Eav\Attribute
+ * @throws \Magento\Framework\Exception\LocalizedException
+ */
+ private function getAttributeByCode(string $field): Attribute
+ {
+ return $this->eavConfig->getAttribute(\Magento\Catalog\Model\Product::ENTITY, $field);
+ }
+}
diff --git a/app/code/Magento/Catalog/Model/Api/SearchCriteria/CollectionProcessor/ConditionProcessor/ConditionBuilder/NativeAttributeCondition.php b/app/code/Magento/Catalog/Model/Api/SearchCriteria/CollectionProcessor/ConditionProcessor/ConditionBuilder/NativeAttributeCondition.php
new file mode 100644
index 0000000000000..3c2837fd2e9a1
--- /dev/null
+++ b/app/code/Magento/Catalog/Model/Api/SearchCriteria/CollectionProcessor/ConditionProcessor/ConditionBuilder/NativeAttributeCondition.php
@@ -0,0 +1,97 @@
+resourceConnection = $resourceConnection;
+ }
+
+ /**
+ * @param Filter $filter
+ * @return string
+ * @throws \DomainException
+ */
+ public function build(Filter $filter): string
+ {
+ $conditionType = $this->mapConditionType($filter->getConditionType(), $filter->getField());
+ $conditionValue = $this->mapConditionValue($conditionType, $filter->getValue());
+
+ return $this->resourceConnection
+ ->getConnection()
+ ->prepareSqlCondition(
+ Collection::MAIN_TABLE_ALIAS . '.' . $filter->getField(),
+ [
+ $conditionType => $conditionValue
+ ]
+ );
+ }
+
+ /**
+ * Map equal and not equal conditions to in and not in
+ *
+ * @param string $conditionType
+ * @param string $field
+ * @return mixed
+ */
+ private function mapConditionType(string $conditionType, string $field): string
+ {
+ if (strtolower($field) === ProductInterface::SKU) {
+ $conditionsMap = [
+ 'eq' => 'like',
+ 'neq' => 'nlike'
+ ];
+ } else {
+ $conditionsMap = [
+ 'eq' => 'in',
+ 'neq' => 'nin'
+ ];
+ }
+
+ return $conditionsMap[$conditionType] ?? $conditionType;
+ }
+
+ /**
+ * Wraps value with '%' if condition type is 'like' or 'not like'
+ *
+ * @param string $conditionType
+ * @param string $conditionValue
+ * @return string
+ */
+ private function mapConditionValue(string $conditionType, string $conditionValue): string
+ {
+ $conditionsMap = ['like', 'nlike'];
+
+ if (in_array($conditionType, $conditionsMap)) {
+ $conditionValue = '%' . $conditionValue . '%';
+ }
+
+ return $conditionValue;
+ }
+}
diff --git a/app/code/Magento/Catalog/Model/Api/SearchCriteria/CollectionProcessor/ConditionProcessor/DefaultCondition.php b/app/code/Magento/Catalog/Model/Api/SearchCriteria/CollectionProcessor/ConditionProcessor/DefaultCondition.php
new file mode 100644
index 0000000000000..5189da35ab52a
--- /dev/null
+++ b/app/code/Magento/Catalog/Model/Api/SearchCriteria/CollectionProcessor/ConditionProcessor/DefaultCondition.php
@@ -0,0 +1,43 @@
+conditionBuilderFactory = $conditionBuilderFactory;
+ }
+
+ /**
+ * @param Filter $filter
+ * @return string
+ */
+ public function build(Filter $filter): string
+ {
+ $filterBuilder = $this->conditionBuilderFactory->createByFilter($filter);
+
+ return $filterBuilder->build($filter);
+ }
+}
diff --git a/app/code/Magento/Catalog/Model/Api/SearchCriteria/CollectionProcessor/ConditionProcessor/ProductCategoryCondition.php b/app/code/Magento/Catalog/Model/Api/SearchCriteria/CollectionProcessor/ConditionProcessor/ProductCategoryCondition.php
new file mode 100644
index 0000000000000..09c3a3864bbc7
--- /dev/null
+++ b/app/code/Magento/Catalog/Model/Api/SearchCriteria/CollectionProcessor/ConditionProcessor/ProductCategoryCondition.php
@@ -0,0 +1,124 @@
+resourceConnection = $resourceConnection;
+ $this->categoryRepository = $categoryRepository;
+ }
+
+ /**
+ * @param Filter $filter
+ * @return string
+ */
+ public function build(Filter $filter): string
+ {
+ $categorySelect = $this->resourceConnection->getConnection()->select()
+ ->from(
+ ['cat' => $this->resourceConnection->getTableName('catalog_category_product')],
+ 'cat.product_id'
+ )->where(
+ $this->resourceConnection->getConnection()->prepareSqlCondition(
+ 'cat.category_id',
+ [$this->mapConditionType($filter->getConditionType()) => $this->getCategoryIds($filter)]
+ )
+ );
+
+ $selectCondition = [
+ 'in' => $categorySelect
+ ];
+
+ return $this->resourceConnection->getConnection()
+ ->prepareSqlCondition(Collection::MAIN_TABLE_ALIAS . '.entity_id', $selectCondition);
+ }
+
+ /**
+ * Extracts required category ids from Filter
+ * If category is anchor all children categories will be included too
+ * If category is root all children categories will be included too
+ *
+ * @param Filter $filter
+ * @return array
+ */
+ private function getCategoryIds(Filter $filter): array
+ {
+ $categoryIds = explode(',', $filter->getValue());
+ $childCategoryIds = [];
+
+ foreach ($categoryIds as $categoryId) {
+ try {
+ $category = $this->categoryRepository->get($categoryId);
+ } catch (CategoryDoesNotExistException $exception) {
+ continue;
+ }
+
+ if ($category->getIsAnchor()) {
+ $childCategoryIds[] = $category->getAllChildren(true);
+ }
+
+ // This is the simplest way to check if category is root
+ if ((int)$category->getLevel() === $this->rootCategoryLevel) {
+ $childCategoryIds[] = $category->getAllChildren(true);
+ }
+ }
+
+ return array_unique(array_merge($categoryIds, ...$childCategoryIds));
+ }
+
+ /**
+ * Map equal and not equal conditions to in and not in
+ *
+ * @param string $conditionType
+ * @return mixed
+ */
+ private function mapConditionType(string $conditionType): string
+ {
+ $conditionsMap = [
+ 'eq' => 'in',
+ 'neq' => 'nin',
+ 'like' => 'in',
+ 'nlike' => 'nin',
+ ];
+ return $conditionsMap[$conditionType] ?? $conditionType;
+ }
+}
diff --git a/app/code/Magento/Catalog/Model/Api/SearchCriteria/CollectionProcessor/FilterProcessor/ProductCategoryFilter.php b/app/code/Magento/Catalog/Model/Api/SearchCriteria/CollectionProcessor/FilterProcessor/ProductCategoryFilter.php
index e0fbc16421f55..1f0f2361df507 100644
--- a/app/code/Magento/Catalog/Model/Api/SearchCriteria/CollectionProcessor/FilterProcessor/ProductCategoryFilter.php
+++ b/app/code/Magento/Catalog/Model/Api/SearchCriteria/CollectionProcessor/FilterProcessor/ProductCategoryFilter.php
@@ -21,8 +21,14 @@ class ProductCategoryFilter implements CustomFilterInterface
*/
public function apply(Filter $filter, AbstractDb $collection)
{
- $conditionType = $filter->getConditionType() ? $filter->getConditionType() : 'eq';
- $categoryFilter = [$conditionType => [$filter->getValue()]];
+ $value = $filter->getValue();
+ $conditionType = $filter->getConditionType() ?: 'in';
+ if (($conditionType === 'in' || $conditionType === 'nin') && is_string($value)) {
+ $value = explode(',', $value);
+ } else {
+ $value = [$value];
+ }
+ $categoryFilter = [$conditionType => $value];
/** @var Collection $collection */
$collection->addCategoriesFilter($categoryFilter);
diff --git a/app/code/Magento/Catalog/Model/Category.php b/app/code/Magento/Catalog/Model/Category.php
index 9c472385f6db4..00b093b2918f1 100644
--- a/app/code/Magento/Catalog/Model/Category.php
+++ b/app/code/Magento/Catalog/Model/Category.php
@@ -28,6 +28,8 @@
* @method Category setUrlPath(string $urlPath)
* @method Category getSkipDeleteChildren()
* @method Category setSkipDeleteChildren(boolean $value)
+ * @method Category setChangedProductIds(array $categoryIds) Set products ids that inserted or deleted for category
+ * @method array getChangedProductIds() Get products ids that inserted or deleted for category
*
* @SuppressWarnings(PHPMD.LongVariable)
* @SuppressWarnings(PHPMD.ExcessivePublicCount)
@@ -69,6 +71,11 @@ class Category extends \Magento\Catalog\Model\AbstractModel implements
const CACHE_TAG = 'cat_c';
+ /**
+ * Category Store Id
+ */
+ const STORE_ID = 'store_id';
+
/**#@+
* Constants
*/
@@ -110,6 +117,11 @@ class Category extends \Magento\Catalog\Model\AbstractModel implements
*/
protected $_url;
+ /**
+ * @var ResourceModel\Category
+ */
+ protected $_resource;
+
/**
* URL rewrite model
*
@@ -330,6 +342,16 @@ protected function getCustomAttributesCodes()
return $this->customAttributesCodes;
}
+ /**
+ * @throws \Magento\Framework\Exception\LocalizedException
+ * @return \Magento\Catalog\Model\ResourceModel\Category
+ * @deprecated because resource models should be used directly
+ */
+ protected function _getResource()
+ {
+ return parent::_getResource();
+ }
+
/**
* Get flat resource model flag
*
@@ -571,12 +593,12 @@ public function getStoreIds()
*
* If store id is underfined for category return current active store id
*
- * @return integer
+ * @return int
*/
public function getStoreId()
{
- if ($this->hasData('store_id')) {
- return (int)$this->_getData('store_id');
+ if ($this->hasData(self::STORE_ID)) {
+ return (int)$this->_getData(self::STORE_ID);
}
return (int)$this->_storeManager->getStore()->getId();
}
@@ -592,7 +614,7 @@ public function setStoreId($storeId)
if (!is_numeric($storeId)) {
$storeId = $this->_storeManager->getStore($storeId)->getId();
}
- $this->setData('store_id', $storeId);
+ $this->setData(self::STORE_ID, $storeId);
$this->getResource()->setStoreId($storeId);
return $this;
}
@@ -704,7 +726,7 @@ public function getParentId()
return $parentId;
}
$parentIds = $this->getParentIds();
- return intval(array_pop($parentIds));
+ return (int)array_pop($parentIds);
}
/**
@@ -780,11 +802,14 @@ public function getAllChildren($asArray = false)
/**
* Retrieve children ids comma separated
*
+ * @param boolean $recursive
+ * @param boolean $isActive
+ * @param boolean $sortByPosition
* @return string
*/
- public function getChildren()
+ public function getChildren($recursive = false, $isActive = true, $sortByPosition = false)
{
- return implode(',', $this->getResource()->getChildren($this, false));
+ return implode(',', $this->getResource()->getChildren($this, $recursive, $isActive, $sortByPosition));
}
/**
@@ -938,8 +963,11 @@ public function getAnchorsAbove()
*/
public function getProductCount()
{
- $count = $this->_getResource()->getProductCount($this);
- $this->setData(self::KEY_PRODUCT_COUNT, $count);
+ if (!$this->hasData(self::KEY_PRODUCT_COUNT)) {
+ $count = $this->_getResource()->getProductCount($this);
+ $this->setData(self::KEY_PRODUCT_COUNT, $count);
+ }
+
return $this->getData(self::KEY_PRODUCT_COUNT);
}
@@ -1094,7 +1122,11 @@ public function reindex()
if ($this->flatState->isFlatEnabled()) {
$flatIndexer = $this->indexerRegistry->get(Indexer\Category\Flat\State::INDEXER_ID);
if (!$flatIndexer->isScheduled()) {
- $flatIndexer->reindexRow($this->getId());
+ $idsList = [$this->getId()];
+ if ($this->dataHasChangedFor('url_key')) {
+ $idsList = array_merge($idsList, explode(',', $this->getAllChildren()));
+ }
+ $flatIndexer->reindexList($idsList);
}
}
$productIndexer = $this->indexerRegistry->get(Indexer\Category\Product::INDEXER_ID);
@@ -1123,15 +1155,25 @@ public function afterDeleteCommit()
*/
public function getIdentities()
{
- $identities = [
- self::CACHE_TAG . '_' . $this->getId(),
- ];
- if (!$this->getId() || $this->hasDataChanges()
- || $this->isDeleted() || $this->dataHasChangedFor(self::KEY_INCLUDE_IN_MENU)
- ) {
- $identities[] = self::CACHE_TAG;
- $identities[] = Product::CACHE_PRODUCT_CATEGORY_TAG . '_' . $this->getId();
+ $identities = [];
+ if ($this->getId()) {
+ if ($this->getAffectedCategoryIds()) {
+ foreach (array_unique($this->getAffectedCategoryIds()) as $affectedCategoryId) {
+ $identities[] = self::CACHE_TAG . '_' . $affectedCategoryId;
+ }
+ } else {
+ $identities[] = self::CACHE_TAG . '_' . $this->getId();
+ }
+
+ if ($this->hasDataChanges() || $this->isDeleted() || $this->dataHasChangedFor(self::KEY_INCLUDE_IN_MENU)) {
+ $identities[] = Product::CACHE_PRODUCT_CATEGORY_TAG . '_' . $this->getId();
+ }
+
+ if ($this->isObjectNew()) {
+ $identities[] = self::CACHE_TAG;
+ }
}
+
return $identities;
}
diff --git a/app/code/Magento/Catalog/Model/Category/Attribute.php b/app/code/Magento/Catalog/Model/Category/Attribute.php
index 968db224c01f5..b1803a0db947e 100644
--- a/app/code/Magento/Catalog/Model/Category/Attribute.php
+++ b/app/code/Magento/Catalog/Model/Category/Attribute.php
@@ -29,14 +29,15 @@ class Attribute extends \Magento\Catalog\Model\Entity\Attribute implements
*/
public function getApplyTo()
{
- if ($this->getData(self::APPLY_TO)) {
- if (is_array($this->getData(self::APPLY_TO))) {
- return $this->getData(self::APPLY_TO);
+ $applyTo = $this->_getData(self::APPLY_TO);
+ if ($applyTo) {
+ if (is_array($applyTo)) {
+ return $applyTo;
}
- return explode(',', $this->getData(self::APPLY_TO));
- } else {
- return [];
+ return explode(',', $applyTo);
}
+
+ return [];
}
/**
@@ -59,7 +60,7 @@ public function setApplyTo($applyTo)
*/
public function getIsWysiwygEnabled()
{
- return $this->getData(self::IS_WYSIWYG_ENABLED);
+ return $this->_getData(self::IS_WYSIWYG_ENABLED);
}
/**
@@ -70,7 +71,7 @@ public function getIsWysiwygEnabled()
*/
public function setIsWysiwygEnabled($isWysiwygEnabled)
{
- return $this->getData(self::IS_WYSIWYG_ENABLED, $isWysiwygEnabled);
+ return $this->setData(self::IS_WYSIWYG_ENABLED, $isWysiwygEnabled);
}
/**
@@ -78,7 +79,7 @@ public function setIsWysiwygEnabled($isWysiwygEnabled)
*/
public function getIsHtmlAllowedOnFront()
{
- return $this->getData(self::IS_HTML_ALLOWED_ON_FRONT);
+ return $this->_getData(self::IS_HTML_ALLOWED_ON_FRONT);
}
/**
@@ -97,7 +98,7 @@ public function setIsHtmlAllowedOnFront($isHtmlAllowedOnFront)
*/
public function getUsedForSortBy()
{
- return $this->getData(self::USED_FOR_SORT_BY);
+ return $this->_getData(self::USED_FOR_SORT_BY);
}
/**
@@ -116,7 +117,7 @@ public function setUsedForSortBy($usedForSortBy)
*/
public function getIsFilterable()
{
- return $this->getData(self::IS_FILTERABLE);
+ return $this->_getData(self::IS_FILTERABLE);
}
/**
@@ -135,7 +136,7 @@ public function setIsFilterable($isFilterable)
*/
public function getIsFilterableInSearch()
{
- return $this->getData(self::IS_FILTERABLE_IN_SEARCH);
+ return $this->_getData(self::IS_FILTERABLE_IN_SEARCH);
}
/**
@@ -143,7 +144,7 @@ public function getIsFilterableInSearch()
*/
public function getIsUsedInGrid()
{
- return (bool)$this->getData(self::IS_USED_IN_GRID);
+ return (bool)$this->_getData(self::IS_USED_IN_GRID);
}
/**
@@ -151,7 +152,7 @@ public function getIsUsedInGrid()
*/
public function getIsVisibleInGrid()
{
- return (bool)$this->getData(self::IS_VISIBLE_IN_GRID);
+ return (bool)$this->_getData(self::IS_VISIBLE_IN_GRID);
}
/**
@@ -159,7 +160,7 @@ public function getIsVisibleInGrid()
*/
public function getIsFilterableInGrid()
{
- return (bool)$this->getData(self::IS_FILTERABLE_IN_GRID);
+ return (bool)$this->_getData(self::IS_FILTERABLE_IN_GRID);
}
/**
@@ -170,7 +171,7 @@ public function getIsFilterableInGrid()
*/
public function setIsFilterableInSearch($isFilterableInSearch)
{
- return $this->getData(self::IS_FILTERABLE_IN_SEARCH, $isFilterableInSearch);
+ return $this->setData(self::IS_FILTERABLE_IN_SEARCH, $isFilterableInSearch);
}
/**
@@ -178,7 +179,7 @@ public function setIsFilterableInSearch($isFilterableInSearch)
*/
public function getPosition()
{
- return $this->getData(self::POSITION);
+ return $this->_getData(self::POSITION);
}
/**
@@ -197,7 +198,7 @@ public function setPosition($position)
*/
public function getIsSearchable()
{
- return $this->getData(self::IS_SEARCHABLE);
+ return $this->_getData(self::IS_SEARCHABLE);
}
/**
@@ -216,7 +217,7 @@ public function setIsSearchable($isSearchable)
*/
public function getIsVisibleInAdvancedSearch()
{
- return $this->getData(self::IS_VISIBLE_IN_ADVANCED_SEARCH);
+ return $this->_getData(self::IS_VISIBLE_IN_ADVANCED_SEARCH);
}
/**
@@ -235,7 +236,7 @@ public function setIsVisibleInAdvancedSearch($isVisibleInAdvancedSearch)
*/
public function getIsComparable()
{
- return $this->getData(self::IS_COMPARABLE);
+ return $this->_getData(self::IS_COMPARABLE);
}
/**
@@ -254,7 +255,7 @@ public function setIsComparable($isComparable)
*/
public function getIsUsedForPromoRules()
{
- return $this->getData(self::IS_USED_FOR_PROMO_RULES);
+ return $this->_getData(self::IS_USED_FOR_PROMO_RULES);
}
/**
@@ -273,7 +274,7 @@ public function setIsUsedForPromoRules($isUsedForPromoRules)
*/
public function getIsVisibleOnFront()
{
- return $this->getData(self::IS_VISIBLE_ON_FRONT);
+ return $this->_getData(self::IS_VISIBLE_ON_FRONT);
}
/**
@@ -292,7 +293,7 @@ public function setIsVisibleOnFront($isVisibleOnFront)
*/
public function getUsedInProductListing()
{
- return $this->getData(self::USED_IN_PRODUCT_LISTING);
+ return $this->_getData(self::USED_IN_PRODUCT_LISTING);
}
/**
@@ -311,7 +312,7 @@ public function setUsedInProductListing($usedInProductListing)
*/
public function getIsVisible()
{
- return $this->getData(self::IS_VISIBLE);
+ return $this->_getData(self::IS_VISIBLE);
}
/**
@@ -332,7 +333,7 @@ public function setIsVisible($isVisible)
*/
public function getScope()
{
- $scope = $this->getData(self::KEY_IS_GLOBAL);
+ $scope = $this->_getData(self::KEY_IS_GLOBAL);
if ($scope == self::SCOPE_GLOBAL) {
return self::SCOPE_GLOBAL_TEXT;
} elseif ($scope == self::SCOPE_WEBSITE) {
diff --git a/app/code/Magento/Catalog/Model/Category/Attribute/Backend/Image.php b/app/code/Magento/Catalog/Model/Category/Attribute/Backend/Image.php
index 19587ce56f592..0c907fb9467a2 100644
--- a/app/code/Magento/Catalog/Model/Category/Attribute/Backend/Image.php
+++ b/app/code/Magento/Catalog/Model/Category/Attribute/Backend/Image.php
@@ -99,7 +99,7 @@ public function beforeSave($object)
$object->setData($this->additionalData . $attributeName, $value);
$object->setData($attributeName, $imageName);
} elseif (!is_string($value)) {
- $object->setData($attributeName, '');
+ $object->setData($attributeName, null);
}
return parent::beforeSave($object);
diff --git a/app/code/Magento/Catalog/Model/Category/DataProvider.php b/app/code/Magento/Catalog/Model/Category/DataProvider.php
index a1ccfc9f20993..5f02e50082e2c 100644
--- a/app/code/Magento/Catalog/Model/Category/DataProvider.php
+++ b/app/code/Magento/Catalog/Model/Category/DataProvider.php
@@ -494,8 +494,8 @@ private function convertValues($category, $categoryData)
$categoryData[$attributeCode][0]['name'] = $fileName;
$categoryData[$attributeCode][0]['url'] = $category->getImageUrl($attributeCode);
- $categoryData['image'][0]['size'] = isset($stat) ? $stat['size'] : 0;
- $categoryData['image'][0]['type'] = $mime;
+ $categoryData[$attributeCode][0]['size'] = isset($stat) ? $stat['size'] : 0;
+ $categoryData[$attributeCode][0]['type'] = $mime;
}
}
}
diff --git a/app/code/Magento/Catalog/Model/Category/Link/SaveHandler.php b/app/code/Magento/Catalog/Model/Category/Link/SaveHandler.php
index 29a1ef60a43de..5af421b5bc34c 100644
--- a/app/code/Magento/Catalog/Model/Category/Link/SaveHandler.php
+++ b/app/code/Magento/Catalog/Model/Category/Link/SaveHandler.php
@@ -60,7 +60,7 @@ public function execute($entity, $arguments = [])
if ($dtoCategoryLinks !== null) {
$hydrator = $this->hydratorPool->getHydrator(CategoryLinkInterface::class);
$dtoCategoryLinks = array_map(function ($categoryLink) use ($hydrator) {
- return $hydrator->extract($categoryLink) ;
+ return $hydrator->extract($categoryLink);
}, $dtoCategoryLinks);
$processLinks = $this->mergeCategoryLinks($dtoCategoryLinks, $modelCategoryLinks);
} else {
@@ -119,7 +119,9 @@ private function mergeCategoryLinks($newCategoryPositions, $oldCategoryPositions
if ($key === false) {
$result[] = $newCategoryPosition;
- } elseif ($oldCategoryPositions[$key]['position'] != $newCategoryPosition['position']) {
+ } elseif (isset($oldCategoryPositions[$key])
+ && $oldCategoryPositions[$key]['position'] != $newCategoryPosition['position']
+ ) {
$result[] = $newCategoryPositions[$key];
unset($oldCategoryPositions[$key]);
}
diff --git a/app/code/Magento/Catalog/Model/Category/Product/PositionResolver.php b/app/code/Magento/Catalog/Model/Category/Product/PositionResolver.php
new file mode 100644
index 0000000000000..97941f2d23b9f
--- /dev/null
+++ b/app/code/Magento/Catalog/Model/Category/Product/PositionResolver.php
@@ -0,0 +1,59 @@
+_init('catalog_product_entity', 'entity_id');
+ }
+
+ /**
+ * Get category product positions
+ *
+ * @param int $categoryId
+ * @return array
+ */
+ public function getPositions(int $categoryId)
+ {
+ $connection = $this->getConnection();
+
+ $select = $connection->select()->from(
+ ['cpe' => $this->getTable('catalog_product_entity')],
+ 'entity_id'
+ )->joinLeft(
+ ['ccp' => $this->getTable('catalog_category_product')],
+ 'ccp.product_id=cpe.entity_id'
+ )->where(
+ 'ccp.category_id = ?',
+ $categoryId
+ )->order(
+ 'ccp.position ' . \Magento\Framework\DB\Select::SQL_ASC
+ );
+
+ return array_flip($connection->fetchCol($select));
+ }
+}
diff --git a/app/code/Magento/Catalog/Model/CategoryRepository.php b/app/code/Magento/Catalog/Model/CategoryRepository.php
index 505c729ac1001..3739b91d2348e 100644
--- a/app/code/Magento/Catalog/Model/CategoryRepository.php
+++ b/app/code/Magento/Catalog/Model/CategoryRepository.php
@@ -129,7 +129,7 @@ public function save(\Magento\Catalog\Api\Data\CategoryInterface $category)
*/
public function get($categoryId, $storeId = null)
{
- $cacheKey = null !== $storeId ? $storeId : 'all';
+ $cacheKey = $storeId ?? 'all';
if (!isset($this->instances[$categoryId][$cacheKey])) {
/** @var Category $category */
$category = $this->categoryFactory->create();
diff --git a/app/code/Magento/Catalog/Model/Config.php b/app/code/Magento/Catalog/Model/Config.php
index 227821463b7f0..01ab0de024577 100644
--- a/app/code/Magento/Catalog/Model/Config.php
+++ b/app/code/Magento/Catalog/Model/Config.php
@@ -382,7 +382,7 @@ public function getProductTypeName($id)
$this->loadProductTypes();
- return isset($this->_productTypesById[$id]) ? $this->_productTypesById[$id] : false;
+ return $this->_productTypesById[$id] ?? false;
}
/**
@@ -407,7 +407,7 @@ public function getSourceOptionId($source, $value)
*/
public function getProductAttributes()
{
- if (is_null($this->_productAttributes)) {
+ if ($this->_productAttributes === null) {
$this->_productAttributes = array_keys($this->getAttributesUsedInProductListing());
}
return $this->_productAttributes;
@@ -430,7 +430,7 @@ protected function _getResource()
*/
public function getAttributesUsedInProductListing()
{
- if (is_null($this->_usedInProductListing)) {
+ if ($this->_usedInProductListing === null) {
$this->_usedInProductListing = [];
$entityType = \Magento\Catalog\Model\Product::ENTITY;
$attributesData = $this->_getResource()->setStoreId($this->getStoreId())->getAttributesUsedInListing();
@@ -453,7 +453,7 @@ public function getAttributesUsedInProductListing()
*/
public function getAttributesUsedForSortBy()
{
- if (is_null($this->_usedForSortBy)) {
+ if ($this->_usedForSortBy === null) {
$this->_usedForSortBy = [];
$entityType = \Magento\Catalog\Model\Product::ENTITY;
$attributesData = $this->_getResource()->getAttributesUsedForSortBy();
diff --git a/app/code/Magento/Catalog/Model/ImageExtractor.php b/app/code/Magento/Catalog/Model/ImageExtractor.php
index c12db4dba49d7..f13d682f505cd 100644
--- a/app/code/Magento/Catalog/Model/ImageExtractor.php
+++ b/app/code/Magento/Catalog/Model/ImageExtractor.php
@@ -5,10 +5,11 @@
*/
namespace Magento\Catalog\Model;
-use Magento\Catalog\Model\Product\Attribute\Backend\Media\ImageEntryConverter;
use Magento\Catalog\Helper\Image;
+use Magento\Catalog\Model\Product\Attribute\Backend\Media\ImageEntryConverter;
+use Magento\Framework\View\Xsd\Media\TypeDataExtractorInterface;
-class ImageExtractor implements \Magento\Framework\View\Xsd\Media\TypeDataExtractorInterface
+class ImageExtractor implements TypeDataExtractorInterface
{
/**
* Extract configuration data of images from the DOM structure
@@ -30,10 +31,17 @@ public function process(\DOMElement $mediaNode, $mediaParentTag)
if ($attribute->nodeType != XML_ELEMENT_NODE) {
continue;
}
- if ($attribute->tagName == 'background') {
- $nodeValue = $this->processImageBackground($attribute->nodeValue);
+ $attributeTagName = $attribute->tagName;
+ if ((bool)$attribute->getAttribute('xsi:nil') !== true) {
+ if ($attributeTagName === 'background') {
+ $nodeValue = $this->processImageBackground($attribute->nodeValue);
+ } elseif ($attributeTagName === 'width' || $attributeTagName === 'height') {
+ $nodeValue = (int)$attribute->nodeValue;
+ } else {
+ $nodeValue = $attribute->nodeValue;
+ }
} else {
- $nodeValue = $attribute->nodeValue;
+ $nodeValue = null;
}
$result[$mediaParentTag][$moduleNameImage][Image::MEDIA_TYPE_CONFIG_NODE][$imageId][$attribute->tagName]
= $nodeValue;
@@ -55,6 +63,7 @@ private function processImageBackground($backgroundString)
$backgroundArray = [];
if (preg_match($pattern, $backgroundString, $backgroundArray)) {
array_shift($backgroundArray);
+ $backgroundArray = array_map('intval', $backgroundArray);
}
return $backgroundArray;
}
diff --git a/app/code/Magento/Catalog/Model/ImageUploader.php b/app/code/Magento/Catalog/Model/ImageUploader.php
index 6aa76ca8c1e43..7599e46f76e30 100644
--- a/app/code/Magento/Catalog/Model/ImageUploader.php
+++ b/app/code/Magento/Catalog/Model/ImageUploader.php
@@ -3,6 +3,8 @@
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
+declare(strict_types=1);
+
namespace Magento\Catalog\Model;
/**
@@ -64,6 +66,18 @@ class ImageUploader
*/
protected $allowedExtensions;
+ /**
+ * List of allowed image mime types
+ *
+ * @var array
+ */
+ private $allowedMimeTypes = [
+ 'image/jpg',
+ 'image/jpeg',
+ 'image/gif',
+ 'image/png'
+ ];
+
/**
* ImageUploader constructor
*
@@ -218,6 +232,7 @@ public function moveFileFromTmp($imageName)
* @return string[]
*
* @throws \Magento\Framework\Exception\LocalizedException
+ * @throws \Exception
*/
public function saveFileToTmpDir($fileId)
{
@@ -227,7 +242,9 @@ public function saveFileToTmpDir($fileId)
$uploader = $this->uploaderFactory->create(['fileId' => $fileId]);
$uploader->setAllowedExtensions($this->getAllowedExtensions());
$uploader->setAllowRenameFiles(true);
-
+ if (!$uploader->checkMimeType($this->allowedMimeTypes)) {
+ throw new \Magento\Framework\Exception\LocalizedException(__('File validation failed.'));
+ }
$result = $uploader->save($this->mediaDirectory->getAbsolutePath($baseTmpPath));
unset($result['path']);
diff --git a/app/code/Magento/Catalog/Model/Indexer/Category/Flat/AbstractAction.php b/app/code/Magento/Catalog/Model/Indexer/Category/Flat/AbstractAction.php
index 24d81f0054c5a..a94ff06c62ffb 100644
--- a/app/code/Magento/Catalog/Model/Indexer/Category/Flat/AbstractAction.php
+++ b/app/code/Magento/Catalog/Model/Indexer/Category/Flat/AbstractAction.php
@@ -113,7 +113,7 @@ public function getColumns()
public function getMainStoreTable($storeId = \Magento\Store\Model\Store::DEFAULT_STORE_ID)
{
if (is_string($storeId)) {
- $storeId = intval($storeId);
+ $storeId = (int)$storeId;
}
$suffix = sprintf('store_%d', $storeId);
@@ -369,25 +369,55 @@ protected function getAttributeValues($entityIds, $storeId)
}
$values = [];
- foreach ($entityIds as $entityId) {
- $values[$entityId] = [];
+ $linkIds = $this->getLinkIds($entityIds);
+ foreach ($linkIds as $linkId) {
+ $values[$linkId] = [];
}
+
$attributes = $this->getAttributes();
$attributesType = ['varchar', 'int', 'decimal', 'text', 'datetime'];
+ $linkField = $this->getCategoryMetadata()->getLinkField();
foreach ($attributesType as $type) {
foreach ($this->getAttributeTypeValues($type, $entityIds, $storeId) as $row) {
- if (isset($row[$this->getCategoryMetadata()->getLinkField()]) && isset($row['attribute_id'])) {
+ if (isset($row[$linkField]) && isset($row['attribute_id'])) {
$attributeId = $row['attribute_id'];
if (isset($attributes[$attributeId])) {
$attributeCode = $attributes[$attributeId]['attribute_code'];
- $values[$row[$this->getCategoryMetadata()->getLinkField()]][$attributeCode] = $row['value'];
+ $values[$row[$linkField]][$attributeCode] = $row['value'];
}
}
}
}
+
return $values;
}
+ /**
+ * Translate entity ids into link ids
+ *
+ * Used for rows with no EAV attributes set.
+ *
+ * @param array $entityIds
+ * @return array
+ */
+ private function getLinkIds(array $entityIds)
+ {
+ $linkField = $this->getCategoryMetadata()->getLinkField();
+ if ($linkField === 'entity_id') {
+ return $entityIds;
+ }
+
+ $select = $this->connection->select()->from(
+ ['e' => $this->connection->getTableName($this->getTableName('catalog_category_entity'))],
+ [$linkField]
+ )->where(
+ 'e.entity_id IN (?)',
+ $entityIds
+ );
+
+ return $this->connection->fetchCol($select);
+ }
+
/**
* Return attribute values for given entities and store of specific attribute type
*
diff --git a/app/code/Magento/Catalog/Model/Indexer/Category/Flat/Action/Full.php b/app/code/Magento/Catalog/Model/Indexer/Category/Flat/Action/Full.php
index eeac2e80af97e..64a8f930d83ee 100644
--- a/app/code/Magento/Catalog/Model/Indexer/Category/Flat/Action/Full.php
+++ b/app/code/Magento/Catalog/Model/Indexer/Category/Flat/Action/Full.php
@@ -64,18 +64,22 @@ protected function populateFlatTables(array $stores)
}
/** @TODO Do something with chunks */
$categoriesIdsChunks = array_chunk($categoriesIds[$store->getRootCategoryId()], 500);
+
foreach ($categoriesIdsChunks as $categoriesIdsChunk) {
$attributesData = $this->getAttributeValues($categoriesIdsChunk, $store->getId());
+ $linkField = $this->categoryMetadata->getLinkField();
+
$data = [];
foreach ($categories[$store->getRootCategoryId()] as $category) {
- if (!isset($attributesData[$category[$this->categoryMetadata->getLinkField()]])) {
+ if (!isset($attributesData[$category[$linkField]])) {
continue;
}
$category['store_id'] = $store->getId();
$data[] = $this->prepareValuesToInsert(
- array_merge($category, $attributesData[$category[$this->categoryMetadata->getLinkField()]])
+ array_merge($category, $attributesData[$category[$linkField]])
);
}
+
$this->connection->insertMultiple(
$this->addTemporaryTableSuffix($this->getMainStoreTable($store->getId())),
$data
diff --git a/app/code/Magento/Catalog/Model/Indexer/Category/Flat/Action/Rows.php b/app/code/Magento/Catalog/Model/Indexer/Category/Flat/Action/Rows.php
index 2368c27e02d72..bc17d731f04c0 100644
--- a/app/code/Magento/Catalog/Model/Indexer/Category/Flat/Action/Rows.php
+++ b/app/code/Magento/Catalog/Model/Indexer/Category/Flat/Action/Rows.php
@@ -69,22 +69,24 @@ public function reindex(array $entityIds = [], $useTempTable = false)
$categoriesIdsChunk = $this->filterIdsByStore($categoriesIdsChunk, $store);
$attributesData = $this->getAttributeValues($categoriesIdsChunk, $store->getId());
+ $linkField = $this->categoryMetadata->getLinkField();
$data = [];
foreach ($categoriesIdsChunk as $categoryId) {
- if (!isset($attributesData[$categoryId])) {
- continue;
- }
-
try {
$category = $this->categoryRepository->get($categoryId);
} catch (NoSuchEntityException $e) {
continue;
}
+ $categoryData = $category->getData();
+ if (!isset($attributesData[$categoryData[$linkField]])) {
+ continue;
+ }
+
$data[] = $this->prepareValuesToInsert(
array_merge(
- $category->getData(),
- $attributesData[$categoryId],
+ $categoryData,
+ $attributesData[$categoryData[$linkField]],
['store_id' => $store->getId()]
)
);
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 c7ddb14a7649d..5375faa9d9cd1 100644
--- a/app/code/Magento/Catalog/Model/Indexer/Category/Product/AbstractAction.php
+++ b/app/code/Magento/Catalog/Model/Indexer/Category/Product/AbstractAction.php
@@ -8,9 +8,15 @@
namespace Magento\Catalog\Model\Indexer\Category\Product;
-use Magento\Framework\DB\Query\Generator as QueryGenerator;
+use Magento\Catalog\Api\Data\ProductInterface;
+use Magento\Catalog\Model\Product;
+use Magento\Framework\App\ObjectManager;
use Magento\Framework\App\ResourceConnection;
+use Magento\Framework\DB\Query\Generator as QueryGenerator;
+use Magento\Framework\DB\Select;
use Magento\Framework\EntityManager\MetadataPool;
+use Magento\Store\Model\Store;
+use Magento\Catalog\Model\Indexer\Category\Product\TableMaintainer;
/**
* Class AbstractAction
@@ -39,27 +45,28 @@ abstract class AbstractAction
/**
* Suffix for table to show it is temporary
+ * @deprecated
*/
const TEMPORARY_TABLE_SUFFIX = '_tmp';
/**
* Cached non anchor categories select by store id
*
- * @var \Magento\Framework\DB\Select[]
+ * @var Select[]
*/
protected $nonAnchorSelects = [];
/**
* Cached anchor categories select by store id
*
- * @var \Magento\Framework\DB\Select[]
+ * @var Select[]
*/
protected $anchorSelects = [];
/**
* Cached all product select by store id
*
- * @var \Magento\Framework\DB\Select[]
+ * @var Select[]
*/
protected $productsSelects = [];
@@ -103,6 +110,11 @@ abstract class AbstractAction
*/
protected $metadataPool;
+ /**
+ * @var TableMaintainer
+ */
+ protected $tableMaintainer;
+
/**
* @var string
* @since 101.0.0
@@ -119,19 +131,24 @@ abstract class AbstractAction
* @param \Magento\Store\Model\StoreManagerInterface $storeManager
* @param \Magento\Catalog\Model\Config $config
* @param QueryGenerator $queryGenerator
+ * @param MetadataPool|null $metadataPool
+ * @param TableMaintainer|null $tableMaintainer
*/
public function __construct(
\Magento\Framework\App\ResourceConnection $resource,
\Magento\Store\Model\StoreManagerInterface $storeManager,
\Magento\Catalog\Model\Config $config,
- QueryGenerator $queryGenerator = null
+ QueryGenerator $queryGenerator = null,
+ MetadataPool $metadataPool = null,
+ TableMaintainer $tableMaintainer = null
) {
$this->resource = $resource;
$this->connection = $resource->getConnection();
$this->storeManager = $storeManager;
$this->config = $config;
- $this->queryGenerator = $queryGenerator ?: \Magento\Framework\App\ObjectManager::getInstance()
- ->get(QueryGenerator::class);
+ $this->queryGenerator = $queryGenerator ?: ObjectManager::getInstance()->get(QueryGenerator::class);
+ $this->metadataPool = $metadataPool ?: ObjectManager::getInstance()->get(MetadataPool::class);
+ $this->tableMaintainer = $tableMaintainer ?: ObjectManager::getInstance()->get(TableMaintainer::class);
}
/**
@@ -175,6 +192,7 @@ protected function getTable($table)
* The name is switched between 'catalog_category_product_index' and 'catalog_category_product_index_replica'
*
* @return string
+ * @deprecated
*/
protected function getMainTable()
{
@@ -185,12 +203,26 @@ protected function getMainTable()
* Return temporary index table name
*
* @return string
+ * @deprecated
*/
protected function getMainTmpTable()
{
- return $this->useTempTable ? $this->getTable(
- self::MAIN_INDEX_TABLE . self::TEMPORARY_TABLE_SUFFIX
- ) : $this->getMainTable();
+ return $this->useTempTable
+ ? $this->getTable(self::MAIN_INDEX_TABLE . self::TEMPORARY_TABLE_SUFFIX)
+ : $this->getMainTable();
+ }
+
+ /**
+ * Return index table name
+ *
+ * @param int $storeId
+ * @return string
+ */
+ protected function getIndexTable($storeId)
+ {
+ return $this->useTempTable
+ ? $this->tableMaintainer->getMainReplicaTable($storeId)
+ : $this->tableMaintainer->getMainTable($storeId);
}
/**
@@ -218,24 +250,25 @@ protected function getPathFromCategoryId($categoryId)
/**
* Retrieve select for reindex products of non anchor categories
*
- * @param \Magento\Store\Model\Store $store
- * @return \Magento\Framework\DB\Select
+ * @param Store $store
+ * @return Select
+ * @throws \Exception when metadata not found for ProductInterface
*/
- protected function getNonAnchorCategoriesSelect(\Magento\Store\Model\Store $store)
+ protected function getNonAnchorCategoriesSelect(Store $store)
{
if (!isset($this->nonAnchorSelects[$store->getId()])) {
$statusAttributeId = $this->config->getAttribute(
- \Magento\Catalog\Model\Product::ENTITY,
+ Product::ENTITY,
'status'
)->getId();
$visibilityAttributeId = $this->config->getAttribute(
- \Magento\Catalog\Model\Product::ENTITY,
+ Product::ENTITY,
'visibility'
)->getId();
$rootPath = $this->getPathFromCategoryId($store->getRootCategoryId());
- $metadata = $this->getMetadataPool()->getMetadata(\Magento\Catalog\Api\Data\ProductInterface::class);
+ $metadata = $this->metadataPool->getMetadata(ProductInterface::class);
$linkField = $metadata->getLinkField();
$select = $this->connection->select()->from(
['cc' => $this->getTable('catalog_category_entity')],
@@ -304,12 +337,65 @@ protected function getNonAnchorCategoriesSelect(\Magento\Store\Model\Store $stor
]
);
+ $this->addFilteringByChildProductsToSelect($select, $store);
+
$this->nonAnchorSelects[$store->getId()] = $select;
}
return $this->nonAnchorSelects[$store->getId()];
}
+ /**
+ * Add filtering by child products to select
+ *
+ * It's used for correct handling of composite products.
+ * This method makes assumption that select already joins `catalog_product_entity` as `cpe`.
+ *
+ * @param Select $select
+ * @param Store $store
+ * @return void
+ * @throws \Exception when metadata not found for ProductInterface
+ */
+ private function addFilteringByChildProductsToSelect(Select $select, Store $store)
+ {
+ $metadata = $this->metadataPool->getMetadata(ProductInterface::class);
+ $linkField = $metadata->getLinkField();
+
+ $statusAttributeId = $this->config->getAttribute(Product::ENTITY, 'status')->getId();
+
+ $select->joinLeft(
+ ['relation' => $this->getTable('catalog_product_relation')],
+ 'cpe.' . $linkField . ' = relation.parent_id',
+ []
+ )->joinLeft(
+ ['relation_product_entity' => $this->getTable('catalog_product_entity')],
+ 'relation.child_id = relation_product_entity.entity_id',
+ []
+ )->joinLeft(
+ ['child_cpsd' => $this->getTable('catalog_product_entity_int')],
+ 'child_cpsd.' . $linkField . ' = '. 'relation_product_entity.' . $linkField
+ . ' AND child_cpsd.store_id = 0'
+ . ' AND child_cpsd.attribute_id = ' . $statusAttributeId,
+ []
+ )->joinLeft(
+ ['child_cpss' => $this->getTable('catalog_product_entity_int')],
+ 'child_cpss.' . $linkField . ' = '. 'relation_product_entity.' . $linkField . ''
+ . ' AND child_cpss.attribute_id = child_cpsd.attribute_id'
+ . ' AND child_cpss.store_id = ' . $store->getId(),
+ []
+ )->where(
+ 'relation.child_id IS NULL OR '
+ . $this->connection->getIfNullSql('child_cpss.value', 'child_cpsd.value') . ' = ?',
+ \Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED
+ )->group(
+ [
+ 'cc.entity_id',
+ 'ccp.product_id',
+ 'visibility'
+ ]
+ );
+ }
+
/**
* Check whether select ranging is needed
*
@@ -323,16 +409,13 @@ protected function isRangingNeeded()
/**
* Return selects cut by min and max
*
- * @param \Magento\Framework\DB\Select $select
+ * @param Select $select
* @param string $field
* @param int $range
- * @return \Magento\Framework\DB\Select[]
+ * @return Select[]
*/
- protected function prepareSelectsByRange(
- \Magento\Framework\DB\Select $select,
- $field,
- $range = self::RANGE_CATEGORY_STEP
- ) {
+ protected function prepareSelectsByRange(Select $select, $field, $range = self::RANGE_CATEGORY_STEP)
+ {
if ($this->isRangingNeeded()) {
$iterator = $this->queryGenerator->generate(
$field,
@@ -353,17 +436,17 @@ protected function prepareSelectsByRange(
/**
* Reindex products of non anchor categories
*
- * @param \Magento\Store\Model\Store $store
+ * @param Store $store
* @return void
*/
- protected function reindexNonAnchorCategories(\Magento\Store\Model\Store $store)
+ protected function reindexNonAnchorCategories(Store $store)
{
$selects = $this->prepareSelectsByRange($this->getNonAnchorCategoriesSelect($store), 'entity_id');
foreach ($selects as $select) {
$this->connection->query(
$this->connection->insertFromSelect(
$select,
- $this->getMainTmpTable(),
+ $this->getIndexTable($store->getId()),
['category_id', 'product_id', 'position', 'is_parent', 'store_id', 'visibility'],
\Magento\Framework\DB\Adapter\AdapterInterface::INSERT_ON_DUPLICATE
)
@@ -374,10 +457,10 @@ protected function reindexNonAnchorCategories(\Magento\Store\Model\Store $store)
/**
* Check if anchor select isset
*
- * @param \Magento\Store\Model\Store $store
+ * @param Store $store
* @return bool
*/
- protected function hasAnchorSelect(\Magento\Store\Model\Store $store)
+ protected function hasAnchorSelect(Store $store)
{
return isset($this->anchorSelects[$store->getId()]);
}
@@ -385,19 +468,20 @@ protected function hasAnchorSelect(\Magento\Store\Model\Store $store)
/**
* Create anchor select
*
- * @param \Magento\Store\Model\Store $store
- * @return \Magento\Framework\DB\Select
+ * @param Store $store
+ * @return Select
+ * @throws \Exception when metadata not found for ProductInterface or CategoryInterface
* @SuppressWarnings(PHPMD.ExcessiveMethodLength)
*/
- protected function createAnchorSelect(\Magento\Store\Model\Store $store)
+ protected function createAnchorSelect(Store $store)
{
$isAnchorAttributeId = $this->config->getAttribute(
\Magento\Catalog\Model\Category::ENTITY,
'is_anchor'
)->getId();
- $statusAttributeId = $this->config->getAttribute(\Magento\Catalog\Model\Product::ENTITY, 'status')->getId();
+ $statusAttributeId = $this->config->getAttribute(Product::ENTITY, 'status')->getId();
$visibilityAttributeId = $this->config->getAttribute(
- \Magento\Catalog\Model\Product::ENTITY,
+ Product::ENTITY,
'visibility'
)->getId();
$rootCatIds = explode('/', $this->getPathFromCategoryId($store->getRootCategoryId()));
@@ -405,12 +489,12 @@ protected function createAnchorSelect(\Magento\Store\Model\Store $store)
$temporaryTreeTable = $this->makeTempCategoryTreeIndex();
- $productMetadata = $this->getMetadataPool()->getMetadata(\Magento\Catalog\Api\Data\ProductInterface::class);
- $categoryMetadata = $this->getMetadataPool()->getMetadata(\Magento\Catalog\Api\Data\CategoryInterface::class);
+ $productMetadata = $this->metadataPool->getMetadata(ProductInterface::class);
+ $categoryMetadata = $this->metadataPool->getMetadata(\Magento\Catalog\Api\Data\CategoryInterface::class);
$productLinkField = $productMetadata->getLinkField();
$categoryLinkField = $categoryMetadata->getLinkField();
- return $this->connection->select()->from(
+ $select = $this->connection->select()->from(
['cc' => $this->getTable('catalog_category_entity')],
[]
)->joinInner(
@@ -492,6 +576,10 @@ protected function createAnchorSelect(\Magento\Store\Model\Store $store)
'visibility' => new \Zend_Db_Expr($this->connection->getIfNullSql('cpvs.value', 'cpvd.value')),
]
);
+
+ $this->addFilteringByChildProductsToSelect($select, $store);
+
+ return $select;
}
/**
@@ -506,7 +594,7 @@ protected function getTemporaryTreeIndexTableName()
if (empty($this->tempTreeIndexTableName)) {
$this->tempTreeIndexTableName = $this->connection->getTableName('temp_catalog_category_tree_index')
. '_'
- . substr(md5(time() . rand(0, 999999999)), 0, 8);
+ . substr(md5(time() . random_int(0, 999999999)), 0, 8);
}
return $this->tempTreeIndexTableName;
@@ -545,6 +633,12 @@ protected function makeTempCategoryTreeIndex()
['type' => \Magento\Framework\DB\Adapter\AdapterInterface::INDEX_TYPE_PRIMARY]
);
+ $temporaryTable->addIndex(
+ 'child_id',
+ ['child_id'],
+ ['type' => \Magento\Framework\DB\Adapter\AdapterInterface::INDEX_TYPE_INDEX]
+ );
+
// Drop the temporary table in case it already exists on this (persistent?) connection.
$this->connection->dropTemporaryTable($temporaryName);
$this->connection->createTemporaryTable($temporaryTable);
@@ -562,34 +656,52 @@ protected function makeTempCategoryTreeIndex()
*/
protected function fillTempCategoryTreeIndex($temporaryName)
{
- // This finds all children (cc2) that descend from a parent (cc) by path.
- // For example, cc.path may be '1/2', and cc2.path may be '1/2/3/4/5'.
- $temporarySelect = $this->connection->select()->from(
- ['cc' => $this->getTable('catalog_category_entity')],
- ['parent_id' => 'entity_id']
- )->joinInner(
- ['cc2' => $this->getTable('catalog_category_entity')],
- 'cc2.path LIKE ' . $this->connection->getConcatSql(
- [$this->connection->quoteIdentifier('cc.path'), $this->connection->quote('/%')]
- ),
- ['child_id' => 'entity_id']
- );
+ $offset = 0;
+ $limit = 500;
- $this->connection->query(
- $temporarySelect->insertFromSelect(
- $temporaryName,
- ['parent_id', 'child_id']
- )
- );
+ $categoryTable = $this->getTable('catalog_category_entity');
+
+ $categoriesSelect = $this->connection->select()
+ ->from(
+ ['c' => $categoryTable],
+ ['entity_id', 'path']
+ )->limit($limit, $offset);
+
+ $categories = $this->connection->fetchAll($categoriesSelect);
+
+ while ($categories) {
+ $values = [];
+
+ foreach ($categories as $category) {
+ foreach (explode('/', $category['path']) as $parentId) {
+ if ($parentId !== $category['entity_id']) {
+ $values[] = [$parentId, $category['entity_id']];
+ }
+ }
+ }
+
+ if (count($values) > 0) {
+ $this->connection->insertArray($temporaryName, ['parent_id', 'child_id'], $values);
+ }
+
+ $offset += $limit;
+ $categoriesSelect = $this->connection->select()
+ ->from(
+ ['c' => $categoryTable],
+ ['entity_id', 'path']
+ )->limit($limit, $offset);
+
+ $categories = $this->connection->fetchAll($categoriesSelect);
+ }
}
/**
* Retrieve select for reindex products of non anchor categories
*
- * @param \Magento\Store\Model\Store $store
- * @return \Magento\Framework\DB\Select
+ * @param Store $store
+ * @return Select
*/
- protected function getAnchorCategoriesSelect(\Magento\Store\Model\Store $store)
+ protected function getAnchorCategoriesSelect(Store $store)
{
if (!$this->hasAnchorSelect($store)) {
$this->anchorSelects[$store->getId()] = $this->createAnchorSelect($store);
@@ -600,10 +712,10 @@ protected function getAnchorCategoriesSelect(\Magento\Store\Model\Store $store)
/**
* Reindex products of anchor categories
*
- * @param \Magento\Store\Model\Store $store
+ * @param Store $store
* @return void
*/
- protected function reindexAnchorCategories(\Magento\Store\Model\Store $store)
+ protected function reindexAnchorCategories(Store $store)
{
$selects = $this->prepareSelectsByRange($this->getAnchorCategoriesSelect($store), 'entity_id');
@@ -611,7 +723,7 @@ protected function reindexAnchorCategories(\Magento\Store\Model\Store $store)
$this->connection->query(
$this->connection->insertFromSelect(
$select,
- $this->getMainTmpTable(),
+ $this->getIndexTable($store->getId()),
['category_id', 'product_id', 'position', 'is_parent', 'store_id', 'visibility'],
\Magento\Framework\DB\Adapter\AdapterInterface::INSERT_ON_DUPLICATE
)
@@ -622,22 +734,23 @@ protected function reindexAnchorCategories(\Magento\Store\Model\Store $store)
/**
* Get select for all products
*
- * @param \Magento\Store\Model\Store $store
- * @return \Magento\Framework\DB\Select
+ * @param Store $store
+ * @return Select
+ * @throws \Exception when metadata not found for ProductInterface
*/
- protected function getAllProducts(\Magento\Store\Model\Store $store)
+ protected function getAllProducts(Store $store)
{
if (!isset($this->productsSelects[$store->getId()])) {
$statusAttributeId = $this->config->getAttribute(
- \Magento\Catalog\Model\Product::ENTITY,
+ Product::ENTITY,
'status'
)->getId();
$visibilityAttributeId = $this->config->getAttribute(
- \Magento\Catalog\Model\Product::ENTITY,
+ Product::ENTITY,
'visibility'
)->getId();
- $metadata = $this->getMetadataPool()->getMetadata(\Magento\Catalog\Api\Data\ProductInterface::class);
+ $metadata = $this->metadataPool->getMetadata(ProductInterface::class);
$linkField = $metadata->getLinkField();
$select = $this->connection->select()->from(
@@ -726,10 +839,10 @@ protected function isIndexRootCategoryNeeded()
/**
* Reindex all products to root category
*
- * @param \Magento\Store\Model\Store $store
+ * @param Store $store
* @return void
*/
- protected function reindexRootCategory(\Magento\Store\Model\Store $store)
+ protected function reindexRootCategory(Store $store)
{
if ($this->isIndexRootCategoryNeeded()) {
$selects = $this->prepareSelectsByRange(
@@ -742,7 +855,7 @@ protected function reindexRootCategory(\Magento\Store\Model\Store $store)
$this->connection->query(
$this->connection->insertFromSelect(
$select,
- $this->getMainTmpTable(),
+ $this->getIndexTable($store->getId()),
['category_id', 'product_id', 'position', 'is_parent', 'store_id', 'visibility'],
\Magento\Framework\DB\Adapter\AdapterInterface::INSERT_ON_DUPLICATE
)
@@ -750,16 +863,4 @@ protected function reindexRootCategory(\Magento\Store\Model\Store $store)
}
}
}
-
- /**
- * @return \Magento\Framework\EntityManager\MetadataPool
- */
- private function getMetadataPool()
- {
- if (null === $this->metadataPool) {
- $this->metadataPool = \Magento\Framework\App\ObjectManager::getInstance()
- ->get(\Magento\Framework\EntityManager\MetadataPool::class);
- }
- return $this->metadataPool;
- }
}
diff --git a/app/code/Magento/Catalog/Model/Indexer/Category/Product/Action/Full.php b/app/code/Magento/Catalog/Model/Indexer/Category/Product/Action/Full.php
index ae0c3554c0d32..f8121b55dbf99 100644
--- a/app/code/Magento/Catalog/Model/Indexer/Category/Product/Action/Full.php
+++ b/app/code/Magento/Catalog/Model/Indexer/Category/Product/Action/Full.php
@@ -8,6 +8,7 @@
use Magento\Catalog\Model\ResourceModel\Indexer\ActiveTableSwitcher;
use Magento\Framework\DB\Query\Generator as QueryGenerator;
use Magento\Framework\App\ResourceConnection;
+use Magento\Indexer\Model\ProcessManager;
/**
* Class Full reindex action
@@ -44,6 +45,11 @@ class Full extends \Magento\Catalog\Model\Indexer\Category\Product\AbstractActio
*/
private $activeTableSwitcher;
+ /**
+ * @var ProcessManager
+ */
+ private $processManager;
+
/**
* @param ResourceConnection $resource
* @param \Magento\Store\Model\StoreManagerInterface $storeManager
@@ -52,9 +58,10 @@ class Full extends \Magento\Catalog\Model\Indexer\Category\Product\AbstractActio
* @param \Magento\Framework\Indexer\BatchSizeManagementInterface|null $batchSizeManagement
* @param \Magento\Framework\Indexer\BatchProviderInterface|null $batchProvider
* @param \Magento\Framework\EntityManager\MetadataPool|null $metadataPool
- * @param \Magento\Indexer\Model\Indexer\StateFactory|null $stateFactory
* @param int|null $batchRowsCount
* @param ActiveTableSwitcher|null $activeTableSwitcher
+ * @param ProcessManager $processManager
+ * @SuppressWarnings(PHPMD.ExcessiveParameterList)
*/
public function __construct(
\Magento\Framework\App\ResourceConnection $resource,
@@ -65,7 +72,8 @@ public function __construct(
\Magento\Framework\Indexer\BatchProviderInterface $batchProvider = null,
\Magento\Framework\EntityManager\MetadataPool $metadataPool = null,
$batchRowsCount = null,
- ActiveTableSwitcher $activeTableSwitcher = null
+ ActiveTableSwitcher $activeTableSwitcher = null,
+ ProcessManager $processManager = null
) {
parent::__construct(
$resource,
@@ -85,6 +93,39 @@ public function __construct(
);
$this->batchRowsCount = $batchRowsCount;
$this->activeTableSwitcher = $activeTableSwitcher ?: $objectManager->get(ActiveTableSwitcher::class);
+ $this->processManager = $processManager ?: $objectManager->get(ProcessManager::class);
+ }
+
+ /**
+ * @return void
+ */
+ private function createTables()
+ {
+ foreach ($this->storeManager->getStores() as $store) {
+ $this->tableMaintainer->createTablesForStore($store->getId());
+ }
+ }
+
+ /**
+ * @return void
+ */
+ private function clearReplicaTables()
+ {
+ foreach ($this->storeManager->getStores() as $store) {
+ $this->connection->truncateTable($this->tableMaintainer->getMainReplicaTable($store->getId()));
+ }
+ }
+
+ /**
+ * @return void
+ */
+ private function switchTables()
+ {
+ $tablesToSwitch = [];
+ foreach ($this->storeManager->getStores() as $store) {
+ $tablesToSwitch[] = $this->tableMaintainer->getMainTable($store->getId());
+ }
+ $this->activeTableSwitcher->switchTable($this->connection, $tablesToSwitch);
}
/**
@@ -94,57 +135,58 @@ public function __construct(
*/
public function execute()
{
+ $this->createTables();
+ $this->clearReplicaTables();
$this->reindex();
- $this->activeTableSwitcher->switchTable($this->connection, [$this->getMainTable()]);
+ $this->switchTables();
return $this;
}
/**
- * Return select for remove unnecessary data
+ * Run reindexation
*
- * @return \Magento\Framework\DB\Select
+ * @return void
*/
- protected function getSelectUnnecessaryData()
+ protected function reindex()
{
- return $this->connection->select()->from(
- $this->getMainTable(),
- []
- )->joinLeft(
- ['t' => $this->getMainTable()],
- $this->getMainTable() .
- '.category_id = t.category_id AND ' .
- $this->getMainTable() .
- '.store_id = t.store_id AND ' .
- $this->getMainTable() .
- '.product_id = t.product_id',
- []
- )->where(
- 't.category_id IS NULL'
- );
+ $userFunctions = [];
+
+ foreach ($this->storeManager->getStores() as $store) {
+ if ($this->getPathFromCategoryId($store->getRootCategoryId())) {
+ $userFunctions[$store->getId()] = function () use ($store) {
+ return $this->reindexStore($store);
+ };
+ }
+ }
+
+ $this->processManager->execute($userFunctions);
}
/**
- * Remove unnecessary data
+ * Execute indexation by store
*
- * @return void
+ * @param \Magento\Store\Model\Store $store
*/
- protected function removeUnnecessaryData()
+ private function reindexStore($store)
{
- $this->connection->query(
- $this->connection->deleteFromSelect($this->getSelectUnnecessaryData(), $this->getMainTable())
- );
+ $this->reindexRootCategory($store);
+ $this->reindexAnchorCategories($store);
+ $this->reindexNonAnchorCategories($store);
}
/**
- * Publish data from tmp to index
+ * Publish data from tmp to replica table
*
+ * @param \Magento\Store\Model\Store $store
* @return void
*/
- protected function publishData()
+ private function publishData($store)
{
- $select = $this->connection->select()->from($this->getMainTmpTable());
- $columns = array_keys($this->connection->describeTable($this->getMainTable()));
- $tableName = $this->activeTableSwitcher->getAdditionalTableName($this->getMainTable());
+ $select = $this->connection->select()->from($this->tableMaintainer->getMainTmpTable($store->getId()));
+ $columns = array_keys(
+ $this->connection->describeTable($this->tableMaintainer->getMainReplicaTable($store->getId()))
+ );
+ $tableName = $this->tableMaintainer->getMainReplicaTable($store->getId());
$this->connection->query(
$this->connection->insertFromSelect(
@@ -156,23 +198,13 @@ protected function publishData()
);
}
- /**
- * Clear all index data
- *
- * @return void
- */
- protected function clearTmpData()
- {
- $this->connection->delete($this->getMainTmpTable());
- }
-
/**
* {@inheritdoc}
*/
protected function reindexRootCategory(\Magento\Store\Model\Store $store)
{
if ($this->isIndexRootCategoryNeeded()) {
- $this->reindexCategoriesBySelect($this->getAllProducts($store), 'cp.entity_id IN (?)');
+ $this->reindexCategoriesBySelect($this->getAllProducts($store), 'cp.entity_id IN (?)', $store);
}
}
@@ -184,7 +216,7 @@ protected function reindexRootCategory(\Magento\Store\Model\Store $store)
*/
protected function reindexAnchorCategories(\Magento\Store\Model\Store $store)
{
- $this->reindexCategoriesBySelect($this->getAnchorCategoriesSelect($store), 'ccp.product_id IN (?)');
+ $this->reindexCategoriesBySelect($this->getAnchorCategoriesSelect($store), 'ccp.product_id IN (?)', $store);
}
/**
@@ -195,7 +227,7 @@ protected function reindexAnchorCategories(\Magento\Store\Model\Store $store)
*/
protected function reindexNonAnchorCategories(\Magento\Store\Model\Store $store)
{
- $this->reindexCategoriesBySelect($this->getNonAnchorCategoriesSelect($store), 'ccp.product_id IN (?)');
+ $this->reindexCategoriesBySelect($this->getNonAnchorCategoriesSelect($store), 'ccp.product_id IN (?)', $store);
}
/**
@@ -203,12 +235,17 @@ protected function reindexNonAnchorCategories(\Magento\Store\Model\Store $store)
*
* @param \Magento\Framework\DB\Select $basicSelect
* @param string $whereCondition
+ * @param \Magento\Store\Model\Store $store
* @return void
*/
- private function reindexCategoriesBySelect(\Magento\Framework\DB\Select $basicSelect, $whereCondition)
+ private function reindexCategoriesBySelect(\Magento\Framework\DB\Select $basicSelect, $whereCondition, $store)
{
+ $this->tableMaintainer->createMainTmpTable($store->getId());
+
$entityMetadata = $this->metadataPool->getMetadata(\Magento\Catalog\Api\Data\ProductInterface::class);
- $columns = array_keys($this->connection->describeTable($this->getMainTmpTable()));
+ $columns = array_keys(
+ $this->connection->describeTable($this->tableMaintainer->getMainTmpTable($store->getId()))
+ );
$this->batchSizeManagement->ensureBatchSize($this->connection, $this->batchRowsCount);
$batches = $this->batchProvider->getBatches(
$this->connection,
@@ -217,7 +254,7 @@ private function reindexCategoriesBySelect(\Magento\Framework\DB\Select $basicSe
$this->batchRowsCount
);
foreach ($batches as $batch) {
- $this->clearTmpData();
+ $this->connection->delete($this->tableMaintainer->getMainTmpTable($store->getId()));
$resultSelect = clone $basicSelect;
$select = $this->connection->select();
$select->distinct(true);
@@ -227,13 +264,12 @@ private function reindexCategoriesBySelect(\Magento\Framework\DB\Select $basicSe
$this->connection->query(
$this->connection->insertFromSelect(
$resultSelect,
- $this->getMainTmpTable(),
+ $this->tableMaintainer->getMainTmpTable($store->getId()),
$columns,
\Magento\Framework\DB\Adapter\AdapterInterface::INSERT_ON_DUPLICATE
)
);
- $this->publishData();
- $this->removeUnnecessaryData();
+ $this->publishData($store);
}
}
}
diff --git a/app/code/Magento/Catalog/Model/Indexer/Category/Product/Action/Rows.php b/app/code/Magento/Catalog/Model/Indexer/Category/Product/Action/Rows.php
index 248ec970d2250..3bd4910767587 100644
--- a/app/code/Magento/Catalog/Model/Indexer/Category/Product/Action/Rows.php
+++ b/app/code/Magento/Catalog/Model/Indexer/Category/Product/Action/Rows.php
@@ -36,17 +36,16 @@ public function execute(array $entityIds = [], $useTempTable = false)
/**
* Return array of all category root IDs + tree root ID
*
- * @return int[]
+ * @param \Magento\Store\Model\Store $store
+ * @return int
*/
- protected function getRootCategoryIds()
+ private function getRootCategoryId($store)
{
- $rootIds = [\Magento\Catalog\Model\Category::TREE_ROOT_ID];
- foreach ($this->storeManager->getStores() as $store) {
- if ($this->getPathFromCategoryId($store->getRootCategoryId())) {
- $rootIds[] = $store->getRootCategoryId();
- }
+ $rootId = \Magento\Catalog\Model\Category::TREE_ROOT_ID;
+ if ($this->getPathFromCategoryId($store->getRootCategoryId())) {
+ $rootId = $store->getRootCategoryId();
}
- return $rootIds;
+ return $rootId;
}
/**
@@ -54,10 +53,15 @@ protected function getRootCategoryIds()
*
* @return void
*/
- protected function removeEntries()
+ private function removeEntries()
{
- $removalCategoryIds = array_diff($this->limitationByCategories, $this->getRootCategoryIds());
- $this->connection->delete($this->getMainTable(), ['category_id IN (?)' => $removalCategoryIds]);
+ foreach ($this->storeManager->getStores() as $store) {
+ $removalCategoryIds = array_diff($this->limitationByCategories, [$this->getRootCategoryId($store)]);
+ $this->connection->delete(
+ $this->getIndexTable($store->getId()),
+ ['category_id IN (?)' => $removalCategoryIds]
+ );
+ }
}
/**
diff --git a/app/code/Magento/Catalog/Model/Indexer/Category/Product/Plugin/StoreGroup.php b/app/code/Magento/Catalog/Model/Indexer/Category/Product/Plugin/StoreGroup.php
index 2ee46b3a6096b..7770b90dda0a5 100644
--- a/app/code/Magento/Catalog/Model/Indexer/Category/Product/Plugin/StoreGroup.php
+++ b/app/code/Magento/Catalog/Model/Indexer/Category/Product/Plugin/StoreGroup.php
@@ -9,6 +9,8 @@
use Magento\Framework\Model\ResourceModel\Db\AbstractDb;
use Magento\Framework\Model\AbstractModel;
use Magento\Catalog\Model\Indexer\Category\Product;
+use Magento\Framework\App\ObjectManager;
+use Magento\Catalog\Model\Indexer\Category\Product\TableMaintainer;
class StoreGroup
{
@@ -22,12 +24,21 @@ class StoreGroup
*/
protected $indexerRegistry;
+ /**
+ * @var TableMaintainer
+ */
+ protected $tableMaintainer;
+
/**
* @param IndexerRegistry $indexerRegistry
+ * @param TableMaintainer $tableMaintainer
*/
- public function __construct(IndexerRegistry $indexerRegistry)
- {
+ public function __construct(
+ IndexerRegistry $indexerRegistry,
+ TableMaintainer $tableMaintainer
+ ) {
$this->indexerRegistry = $indexerRegistry;
+ $this->tableMaintainer = $tableMaintainer;
}
/**
@@ -73,4 +84,22 @@ protected function validate(AbstractModel $group)
return ($group->dataHasChangedFor('website_id') || $group->dataHasChangedFor('root_category_id'))
&& !$group->isObjectNew();
}
+
+ /**
+ * Delete catalog_category_product indexer tables for deleted store group
+ *
+ * @param AbstractDb $subject
+ * @param AbstractDb $objectResource
+ * @param AbstractModel $storeGroup
+ *
+ * @return AbstractDb
+ * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+ */
+ public function afterDelete(AbstractDb $subject, AbstractDb $objectResource, AbstractModel $storeGroup)
+ {
+ foreach ($storeGroup->getStores() as $store) {
+ $this->tableMaintainer->dropTablesForStore($store->getId());
+ }
+ return $objectResource;
+ }
}
diff --git a/app/code/Magento/Catalog/Model/Indexer/Category/Product/Plugin/StoreView.php b/app/code/Magento/Catalog/Model/Indexer/Category/Product/Plugin/StoreView.php
index f49b685ba6f7f..114d2a94f5b35 100644
--- a/app/code/Magento/Catalog/Model/Indexer/Category/Product/Plugin/StoreView.php
+++ b/app/code/Magento/Catalog/Model/Indexer/Category/Product/Plugin/StoreView.php
@@ -5,6 +5,9 @@
*/
namespace Magento\Catalog\Model\Indexer\Category\Product\Plugin;
+use Magento\Framework\Model\ResourceModel\Db\AbstractDb;
+use Magento\Framework\Model\AbstractModel;
+
class StoreView extends StoreGroup
{
/**
@@ -17,4 +20,38 @@ protected function validate(\Magento\Framework\Model\AbstractModel $store)
{
return $store->isObjectNew() || $store->dataHasChangedFor('group_id');
}
+
+ /**
+ * Invalidate catalog_category_product indexer
+ *
+ * @param AbstractDb $subject
+ * @param AbstractDb $objectResource
+ * @param AbstractModel $store
+ *
+ * @return AbstractDb
+ */
+ public function afterSave(AbstractDb $subject, AbstractDb $objectResource, AbstractModel $store = null)
+ {
+ if ($store->isObjectNew()) {
+ $this->tableMaintainer->createTablesForStore($store->getId());
+ }
+
+ return parent::afterSave($subject, $objectResource);
+ }
+
+ /**
+ * Delete catalog_category_product indexer table for deleted store
+ *
+ * @param AbstractDb $subject
+ * @param AbstractDb $objectResource
+ * @param AbstractModel $store
+ *
+ * @return AbstractDb
+ * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+ */
+ public function afterDelete(AbstractDb $subject, AbstractDb $objectResource, AbstractModel $store)
+ {
+ $this->tableMaintainer->dropTablesForStore($store->getId());
+ return $objectResource;
+ }
}
diff --git a/app/code/Magento/Catalog/Model/Indexer/Category/Product/Plugin/TableResolver.php b/app/code/Magento/Catalog/Model/Indexer/Category/Product/Plugin/TableResolver.php
new file mode 100644
index 0000000000000..84c0eb46429cd
--- /dev/null
+++ b/app/code/Magento/Catalog/Model/Indexer/Category/Product/Plugin/TableResolver.php
@@ -0,0 +1,74 @@
+storeManager = $storeManager;
+ $this->tableResolver = $tableResolver;
+ }
+
+ /**
+ * replacing catalog_category_product_index table name on the table name segmented per store
+ *
+ * @param ResourceConnection $subject
+ * @param string $result
+ * @param string|string[] $modelEntity
+ *
+ * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+ *
+ * @return string
+ */
+ public function afterGetTableName(
+ \Magento\Framework\App\ResourceConnection $subject,
+ string $result,
+ $modelEntity
+ ) {
+ if (!is_array($modelEntity) && $modelEntity === AbstractAction::MAIN_INDEX_TABLE) {
+ $catalogCategoryProductDimension = new Dimension(
+ \Magento\Store\Model\Store::ENTITY,
+ $this->storeManager->getStore()->getId()
+ );
+
+ $tableName = $this->tableResolver->resolve(
+ AbstractAction::MAIN_INDEX_TABLE,
+ [
+ $catalogCategoryProductDimension
+ ]
+ );
+ return $tableName;
+ }
+ return $result;
+ }
+}
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
new file mode 100644
index 0000000000000..90abb0415288d
--- /dev/null
+++ b/app/code/Magento/Catalog/Model/Indexer/Category/Product/Plugin/Website.php
@@ -0,0 +1,45 @@
+tableMaintainer = $tableMaintainer;
+ }
+
+ /**
+ * Delete catalog_category_product indexer tables for deleted website
+ *
+ * @param AbstractDb $subject
+ * @param AbstractDb $objectResource
+ * @param AbstractModel $website
+ *
+ * @return AbstractDb
+ * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+ */
+ public function afterDelete(AbstractDb $subject, AbstractDb $objectResource, AbstractModel $website)
+ {
+ foreach ($website->getStoreIds() as $storeId) {
+ $this->tableMaintainer->dropTablesForStore($storeId);
+ }
+ return $objectResource;
+ }
+}
diff --git a/app/code/Magento/Catalog/Model/Indexer/Category/Product/TableMaintainer.php b/app/code/Magento/Catalog/Model/Indexer/Category/Product/TableMaintainer.php
new file mode 100644
index 0000000000000..3c2629bc570f2
--- /dev/null
+++ b/app/code/Magento/Catalog/Model/Indexer/Category/Product/TableMaintainer.php
@@ -0,0 +1,222 @@
+resource = $resource;
+ $this->tableResolver = $tableResolver;
+ }
+
+ /**
+ * Get connection
+ *
+ * @return AdapterInterface
+ */
+ private function getConnection()
+ {
+ if (!isset($this->connection)) {
+ $this->connection = $this->resource->getConnection();
+ }
+ return $this->connection;
+ }
+
+ /**
+ * Return validated table name
+ *
+ * @param string|string[] $table
+ * @return string
+ */
+ private function getTable($table)
+ {
+ return $this->resource->getTableName($table);
+ }
+
+ /**
+ * Create table based on main table
+ *
+ * @param string $mainTableName
+ * @param string $newTableName
+ *
+ * @return void
+ *
+ * @throws \Zend_Db_Exception
+ */
+ private function createTable($mainTableName, $newTableName)
+ {
+ if (!$this->getConnection()->isTableExists($newTableName)) {
+ $this->getConnection()->createTable(
+ $this->getConnection()->createTableByDdl($mainTableName, $newTableName)
+ );
+ }
+ }
+
+ /**
+ * Drop table
+ *
+ * @param string $tableName
+ *
+ * @return void
+ */
+ private function dropTable($tableName)
+ {
+ if ($this->getConnection()->isTableExists($tableName)) {
+ $this->getConnection()->dropTable($tableName);
+ }
+ }
+
+ /**
+ * Return main index table name
+ *
+ * @param $storeId
+ *
+ * @return string
+ */
+ public function getMainTable(int $storeId)
+ {
+ $catalogCategoryProductDimension = new Dimension(\Magento\Store\Model\Store::ENTITY, $storeId);
+
+ return $this->tableResolver->resolve(AbstractAction::MAIN_INDEX_TABLE, [$catalogCategoryProductDimension]);
+ }
+
+ /**
+ * Create main and replica index tables for store
+ *
+ * @param $storeId
+ *
+ * @return void
+ *
+ * @throws \Zend_Db_Exception
+ */
+ public function createTablesForStore(int $storeId)
+ {
+ $mainTableName = $this->getMainTable($storeId);
+ //Create index table for store based on main replica table
+ //Using main replica table is necessary for backward capability and TableResolver plugin work
+ $this->createTable(
+ $this->getTable(AbstractAction::MAIN_INDEX_TABLE . $this->additionalTableSuffix),
+ $mainTableName
+ );
+
+ $mainReplicaTableName = $this->getMainTable($storeId) . $this->additionalTableSuffix;
+ //Create replica table for store based on main replica table
+ $this->createTable(
+ $this->getTable(AbstractAction::MAIN_INDEX_TABLE . $this->additionalTableSuffix),
+ $mainReplicaTableName
+ );
+ }
+
+ /**
+ * Drop main and replica index tables for store
+ *
+ * @param $storeId
+ *
+ * @return void
+ */
+ public function dropTablesForStore(int $storeId)
+ {
+ $mainTableName = $this->getMainTable($storeId);
+ $this->dropTable($mainTableName);
+
+ $mainReplicaTableName = $this->getMainTable($storeId) . $this->additionalTableSuffix;
+ $this->dropTable($mainReplicaTableName);
+ }
+
+ /**
+ * Return replica index table name
+ *
+ * @param $storeId
+ *
+ * @return string
+ */
+ public function getMainReplicaTable(int $storeId)
+ {
+ return $this->getMainTable($storeId) . $this->additionalTableSuffix;
+ }
+
+ /**
+ * Create temporary index table for store
+ *
+ * @param $storeId
+ *
+ * @return void
+ */
+ public function createMainTmpTable(int $storeId)
+ {
+ if (!isset($this->mainTmpTable[$storeId])) {
+ $originTableName = $this->getMainTable($storeId);
+ $temporaryTableName = $this->getMainTable($storeId) . $this->tmpTableSuffix;
+ $this->getConnection()->createTemporaryTableLike($temporaryTableName, $originTableName, true);
+ $this->mainTmpTable[$storeId] = $temporaryTableName;
+ }
+ }
+
+ /**
+ * Return temporary index table name
+ *
+ * @param $storeId
+ *
+ * @return string
+ *
+ * @throws \Magento\Framework\Exception\NoSuchEntityException
+ */
+ public function getMainTmpTable(int $storeId)
+ {
+ if (!isset($this->mainTmpTable[$storeId])) {
+ throw new \Magento\Framework\Exception\NoSuchEntityException('Temporary table does not exist');
+ }
+ return $this->mainTmpTable[$storeId];
+ }
+}
diff --git a/app/code/Magento/Catalog/Model/Indexer/Product/Category/Action/Rows.php b/app/code/Magento/Catalog/Model/Indexer/Product/Category/Action/Rows.php
index 1b988534328e9..182f04de4ab0e 100644
--- a/app/code/Magento/Catalog/Model/Indexer/Product/Category/Action/Rows.php
+++ b/app/code/Magento/Catalog/Model/Indexer/Product/Category/Action/Rows.php
@@ -6,9 +6,19 @@
namespace Magento\Catalog\Model\Indexer\Product\Category\Action;
use Magento\Catalog\Model\Category;
+use Magento\Catalog\Model\Config;
use Magento\Catalog\Model\Product;
+use Magento\Framework\App\ObjectManager;
+use Magento\Framework\App\ResourceConnection;
+use Magento\Framework\DB\Query\Generator as QueryGenerator;
+use Magento\Framework\EntityManager\MetadataPool;
+use Magento\Framework\Event\ManagerInterface as EventManagerInterface;
use Magento\Framework\Indexer\CacheContext;
+use Magento\Store\Model\StoreManagerInterface;
+/**
+ * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
+ */
class Rows extends \Magento\Catalog\Model\Indexer\Category\Product\AbstractAction
{
/**
@@ -19,32 +29,102 @@ class Rows extends \Magento\Catalog\Model\Indexer\Category\Product\AbstractActio
protected $limitationByProducts;
/**
- * @var \Magento\Framework\Indexer\CacheContext
+ * @var CacheContext
*/
private $cacheContext;
+ /**
+ * @var EventManagerInterface|null
+ */
+ private $eventManager;
+
+ /**
+ * @param ResourceConnection $resource
+ * @param StoreManagerInterface $storeManager
+ * @param Config $config
+ * @param QueryGenerator|null $queryGenerator
+ * @param MetadataPool|null $metadataPool
+ * @param CacheContext|null $cacheContext
+ * @param EventManagerInterface|null $eventManager
+ */
+ public function __construct(
+ ResourceConnection $resource,
+ StoreManagerInterface $storeManager,
+ Config $config,
+ QueryGenerator $queryGenerator = null,
+ MetadataPool $metadataPool = null,
+ CacheContext $cacheContext = null,
+ EventManagerInterface $eventManager = null
+ ) {
+ parent::__construct($resource, $storeManager, $config, $queryGenerator, $metadataPool);
+ $this->cacheContext = $cacheContext ?: ObjectManager::getInstance()->get(CacheContext::class);
+ $this->eventManager = $eventManager ?: ObjectManager::getInstance()->get(EventManagerInterface::class);
+ }
+
/**
* Refresh entities index
*
* @param int[] $entityIds
* @param bool $useTempTable
* @return $this
+ * @throws \Exception if metadataPool doesn't contain metadata for ProductInterface
+ * @throws \DomainException
*/
public function execute(array $entityIds = [], $useTempTable = false)
{
- $this->limitationByProducts = $entityIds;
+ $idsToBeReIndexed = $this->getProductIdsWithParents($entityIds);
+
+ $this->limitationByProducts = $idsToBeReIndexed;
$this->useTempTable = $useTempTable;
+ $affectedCategories = $this->getCategoryIdsFromIndex($idsToBeReIndexed);
+
$this->removeEntries();
$this->reindex();
- $this->registerProducts($entityIds);
- $this->registerCategories($entityIds);
+ $affectedCategories = array_merge($affectedCategories, $this->getCategoryIdsFromIndex($idsToBeReIndexed));
+
+ $this->registerProducts($idsToBeReIndexed);
+ $this->registerCategories($affectedCategories);
+ $this->eventManager->dispatch('clean_cache_by_tags', ['object' => $this->cacheContext]);
return $this;
}
+ /**
+ * Get IDs of parent products by their child IDs.
+ *
+ * Returns identifiers of parent product from the catalog_product_relation.
+ * Please note that returned ids don't contain ids of passed child products.
+ *
+ * @param int[] $childProductIds
+ * @return int[]
+ * @throws \Exception if metadataPool doesn't contain metadata for ProductInterface
+ * @throws \DomainException
+ */
+ private function getProductIdsWithParents(array $childProductIds)
+ {
+ /** @var \Magento\Framework\EntityManager\EntityMetadataInterface $metadata */
+ $metadata = $this->metadataPool->getMetadata(\Magento\Catalog\Api\Data\ProductInterface::class);
+ $fieldForParent = $metadata->getLinkField();
+
+ $select = $this->connection
+ ->select()
+ ->from(['relation' => $this->getTable('catalog_product_relation')], [])
+ ->distinct(true)
+ ->where('child_id IN (?)', $childProductIds)
+ ->join(
+ ['cpe' => $this->getTable('catalog_product_entity')],
+ 'relation.parent_id = cpe.' . $fieldForParent,
+ ['cpe.entity_id']
+ );
+
+ $parentProductIds = $this->connection->fetchCol($select);
+
+ return array_unique(array_merge($childProductIds, $parentProductIds));
+ }
+
/**
* Register affected products
*
@@ -53,26 +133,19 @@ public function execute(array $entityIds = [], $useTempTable = false)
*/
private function registerProducts($entityIds)
{
- $this->getCacheContext()->registerEntities(Product::CACHE_TAG, $entityIds);
+ $this->cacheContext->registerEntities(Product::CACHE_TAG, $entityIds);
}
/**
* Register categories assigned to products
*
- * @param array $entityIds
+ * @param array $categoryIds
* @return void
*/
- private function registerCategories($entityIds)
+ private function registerCategories(array $categoryIds)
{
- $categories = $this->connection->fetchCol(
- $this->connection->select()
- ->from($this->getMainTable(), ['category_id'])
- ->where('product_id IN (?)', $entityIds)
- ->distinct()
- );
-
- if ($categories) {
- $this->getCacheContext()->registerEntities(Category::CACHE_TAG, $categories);
+ if ($categoryIds) {
+ $this->cacheContext->registerEntities(Category::CACHE_TAG, $categoryIds);
}
}
@@ -83,10 +156,12 @@ private function registerCategories($entityIds)
*/
protected function removeEntries()
{
- $this->connection->delete(
- $this->getMainTable(),
- ['product_id IN (?)' => $this->limitationByProducts]
- );
+ foreach ($this->storeManager->getStores() as $store) {
+ $this->connection->delete(
+ $this->getIndexTable($store->getId()),
+ ['product_id IN (?)' => $this->limitationByProducts]
+ );
+ };
}
/**
@@ -98,7 +173,7 @@ protected function removeEntries()
protected function getNonAnchorCategoriesSelect(\Magento\Store\Model\Store $store)
{
$select = parent::getNonAnchorCategoriesSelect($store);
- return $select->where('ccp.product_id IN (?)', $this->limitationByProducts);
+ return $select->where('ccp.product_id IN (?) OR relation.child_id IN (?)', $this->limitationByProducts);
}
/**
@@ -136,16 +211,31 @@ protected function isRangingNeeded()
}
/**
- * Get cache context
+ * Returns a list of category ids which are assigned to product ids in the index
*
* @return \Magento\Framework\Indexer\CacheContext
- * @deprecated 101.0.0
*/
- private function getCacheContext()
+ private function getCategoryIdsFromIndex(array $productIds)
{
- if ($this->cacheContext === null) {
- $this->cacheContext = \Magento\Framework\App\ObjectManager::getInstance()->get(CacheContext::class);
+ $categoryIds = [];
+ foreach ($this->storeManager->getStores() as $store) {
+ $categoryIds = array_merge(
+ $categoryIds,
+ $this->connection->fetchCol(
+ $this->connection->select()
+ ->from($this->getIndexTable($store->getId()), ['category_id'])
+ ->where('product_id IN (?)', $productIds)
+ ->distinct()
+ )
+ );
+ };
+ $parentCategories = $categoryIds;
+ foreach ($categoryIds as $categoryId) {
+ $parentIds = explode('/', $this->getPathFromCategoryId($categoryId));
+ $parentCategories = array_merge($parentCategories, $parentIds);
}
- return $this->cacheContext;
+ $categoryIds = array_unique($parentCategories);
+
+ return $categoryIds;
}
}
diff --git a/app/code/Magento/Catalog/Model/Indexer/Product/Eav/AbstractAction.php b/app/code/Magento/Catalog/Model/Indexer/Product/Eav/AbstractAction.php
index 6a2642a8568f4..43b4b5518c16f 100644
--- a/app/code/Magento/Catalog/Model/Indexer/Product/Eav/AbstractAction.php
+++ b/app/code/Magento/Catalog/Model/Indexer/Product/Eav/AbstractAction.php
@@ -3,6 +3,8 @@
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
+declare(strict_types=1);
+
namespace Magento\Catalog\Model\Indexer\Product\Eav;
/**
@@ -10,6 +12,11 @@
*/
abstract class AbstractAction
{
+ /**
+ * Config path for enable EAV indexer
+ */
+ const ENABLE_EAV_INDEXER = 'catalog/search/enable_eav_indexer';
+
/**
* EAV Indexers by type
*
@@ -27,17 +34,27 @@ abstract class AbstractAction
*/
protected $_eavDecimalFactory;
+ /**
+ * @var \Magento\Framework\App\Config\ScopeConfigInterface
+ */
+ private $scopeConfig;
+
/**
* AbstractAction constructor.
* @param \Magento\Catalog\Model\ResourceModel\Product\Indexer\Eav\DecimalFactory $eavDecimalFactory
* @param \Magento\Catalog\Model\ResourceModel\Product\Indexer\Eav\SourceFactory $eavSourceFactory
+ * @param \Magento\Framework\App\Config\ScopeConfigInterface|null $scopeConfig
*/
public function __construct(
\Magento\Catalog\Model\ResourceModel\Product\Indexer\Eav\DecimalFactory $eavDecimalFactory,
- \Magento\Catalog\Model\ResourceModel\Product\Indexer\Eav\SourceFactory $eavSourceFactory
+ \Magento\Catalog\Model\ResourceModel\Product\Indexer\Eav\SourceFactory $eavSourceFactory,
+ \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig = null
) {
$this->_eavDecimalFactory = $eavDecimalFactory;
$this->_eavSourceFactory = $eavSourceFactory;
+ $this->scopeConfig = $scopeConfig ?: \Magento\Framework\App\ObjectManager::getInstance()->get(
+ \Magento\Framework\App\Config\ScopeConfigInterface::class
+ );
}
/**
@@ -90,6 +107,9 @@ public function getIndexer($type)
*/
public function reindex($ids = null)
{
+ if (!$this->isEavIndexerEnabled()) {
+ return;
+ }
foreach ($this->getIndexers() as $indexer) {
if ($ids === null) {
$indexer->reindexAll();
@@ -143,7 +163,23 @@ protected function syncData($indexer, $destinationTable, $ids)
protected function processRelations($indexer, $ids, $onlyParents = false)
{
$parentIds = $indexer->getRelationsByChild($ids);
+ $parentIds = array_unique(array_merge($parentIds, $ids));
$childIds = $onlyParents ? [] : $indexer->getRelationsByParent($parentIds);
return array_unique(array_merge($ids, $childIds, $parentIds));
}
+
+ /**
+ * Get EAV indexer status
+ *
+ * @return bool
+ */
+ private function isEavIndexerEnabled(): bool
+ {
+ $eavIndexerStatus = $this->scopeConfig->getValue(
+ self::ENABLE_EAV_INDEXER,
+ \Magento\Store\Model\ScopeInterface::SCOPE_STORE
+ );
+
+ return (bool)$eavIndexerStatus;
+ }
}
diff --git a/app/code/Magento/Catalog/Model/Indexer/Product/Eav/Action/Full.php b/app/code/Magento/Catalog/Model/Indexer/Product/Eav/Action/Full.php
index bc747e62f641e..802176092d147 100644
--- a/app/code/Magento/Catalog/Model/Indexer/Product/Eav/Action/Full.php
+++ b/app/code/Magento/Catalog/Model/Indexer/Product/Eav/Action/Full.php
@@ -3,12 +3,15 @@
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
+declare(strict_types=1);
+
namespace Magento\Catalog\Model\Indexer\Product\Eav\Action;
use Magento\Catalog\Model\ResourceModel\Indexer\ActiveTableSwitcher;
/**
* Class Full reindex action
+ * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
*/
class Full extends \Magento\Catalog\Model\Indexer\Product\Eav\AbstractAction
{
@@ -32,6 +35,11 @@ class Full extends \Magento\Catalog\Model\Indexer\Product\Eav\AbstractAction
*/
private $activeTableSwitcher;
+ /**
+ * @var \Magento\Framework\App\Config\ScopeConfigInterface
+ */
+ private $scopeConfig;
+
/**
* @param \Magento\Catalog\Model\ResourceModel\Product\Indexer\Eav\DecimalFactory $eavDecimalFactory
* @param \Magento\Catalog\Model\ResourceModel\Product\Indexer\Eav\SourceFactory $eavSourceFactory
@@ -39,6 +47,7 @@ class Full extends \Magento\Catalog\Model\Indexer\Product\Eav\AbstractAction
* @param \Magento\Framework\Indexer\BatchProviderInterface|null $batchProvider
* @param \Magento\Catalog\Model\ResourceModel\Product\Indexer\Eav\BatchSizeCalculator $batchSizeCalculator
* @param ActiveTableSwitcher|null $activeTableSwitcher
+ * @param \Magento\Framework\App\Config\ScopeConfigInterface|null $scopeConfig
*/
public function __construct(
\Magento\Catalog\Model\ResourceModel\Product\Indexer\Eav\DecimalFactory $eavDecimalFactory,
@@ -46,9 +55,13 @@ public function __construct(
\Magento\Framework\EntityManager\MetadataPool $metadataPool = null,
\Magento\Framework\Indexer\BatchProviderInterface $batchProvider = null,
\Magento\Catalog\Model\ResourceModel\Product\Indexer\Eav\BatchSizeCalculator $batchSizeCalculator = null,
- ActiveTableSwitcher $activeTableSwitcher = null
+ ActiveTableSwitcher $activeTableSwitcher = null,
+ \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig = null
) {
- parent::__construct($eavDecimalFactory, $eavSourceFactory);
+ $this->scopeConfig = $scopeConfig ?: \Magento\Framework\App\ObjectManager::getInstance()->get(
+ \Magento\Framework\App\Config\ScopeConfigInterface::class
+ );
+ parent::__construct($eavDecimalFactory, $eavSourceFactory, $scopeConfig);
$this->metadataPool = $metadataPool ?: \Magento\Framework\App\ObjectManager::getInstance()->get(
\Magento\Framework\EntityManager\MetadataPool::class
);
@@ -73,6 +86,9 @@ public function __construct(
*/
public function execute($ids = null)
{
+ if (!$this->isEavIndexerEnabled()) {
+ return;
+ }
try {
foreach ($this->getIndexers() as $indexerName => $indexer) {
$connection = $indexer->getConnection();
@@ -129,4 +145,19 @@ protected function syncData($indexer, $destinationTable, $ids = null)
throw $e;
}
}
+
+ /**
+ * Get EAV indexer status
+ *
+ * @return bool
+ */
+ private function isEavIndexerEnabled(): bool
+ {
+ $eavIndexerStatus = $this->scopeConfig->getValue(
+ self::ENABLE_EAV_INDEXER,
+ \Magento\Store\Model\ScopeInterface::SCOPE_STORE
+ );
+
+ return (bool)$eavIndexerStatus;
+ }
}
diff --git a/app/code/Magento/Catalog/Model/Indexer/Product/Flat/Action/Indexer.php b/app/code/Magento/Catalog/Model/Indexer/Product/Flat/Action/Indexer.php
index 9dd312e9da801..f95807f615390 100644
--- a/app/code/Magento/Catalog/Model/Indexer/Product/Flat/Action/Indexer.php
+++ b/app/code/Magento/Catalog/Model/Indexer/Product/Flat/Action/Indexer.php
@@ -56,39 +56,36 @@ public function __construct(
* @return \Magento\Catalog\Model\Indexer\Product\Flat
* @SuppressWarnings(PHPMD.CyclomaticComplexity)
* @SuppressWarnings(PHPMD.ExcessiveMethodLength)
+ * @SuppressWarnings(PHPMD.NPathComplexity)
*/
public function write($storeId, $productId, $valueFieldSuffix = '')
{
$flatTable = $this->_productIndexerHelper->getFlatTableName($storeId);
+ $entityTableName = $this->_productIndexerHelper->getTable('catalog_product_entity');
$attributes = $this->_productIndexerHelper->getAttributes();
$eavAttributes = $this->_productIndexerHelper->getTablesStructure($attributes);
$updateData = [];
$describe = $this->_connection->describeTable($flatTable);
+ $metadata = $this->getMetadataPool()->getMetadata(ProductInterface::class);
+ $linkField = $metadata->getLinkField();
foreach ($eavAttributes as $tableName => $tableColumns) {
$columnsChunks = array_chunk($tableColumns, self::ATTRIBUTES_CHUNK_SIZE, true);
foreach ($columnsChunks as $columns) {
$select = $this->_connection->select();
- $selectValue = $this->_connection->select();
- $keyColumns = [
- 'entity_id' => 'e.entity_id',
- 'attribute_id' => 't.attribute_id',
- 'value' => $this->_connection->getIfNullSql('`t2`.`value`', '`t`.`value`'),
- ];
-
- if ($tableName != $this->_productIndexerHelper->getTable('catalog_product_entity')) {
+
+ if ($tableName != $entityTableName) {
$valueColumns = [];
$ids = [];
$select->from(
- ['e' => $this->_productIndexerHelper->getTable('catalog_product_entity')],
- $keyColumns
- );
-
- $selectValue->from(
- ['e' => $this->_productIndexerHelper->getTable('catalog_product_entity')],
- $keyColumns
+ ['e' => $entityTableName],
+ [
+ 'entity_id' => 'e.entity_id',
+ 'attribute_id' => 't.attribute_id',
+ 'value' => $this->_connection->getIfNullSql('`t2`.`value`', '`t`.`value`'),
+ ]
);
/** @var $attribute \Magento\Catalog\Model\ResourceModel\Eav\Attribute */
@@ -97,8 +94,7 @@ public function write($storeId, $productId, $valueFieldSuffix = '')
$ids[$attribute->getId()] = $columnName;
}
}
- $linkField = $this->getMetadataPool()->getMetadata(ProductInterface::class)->getLinkField();
- $select->joinLeft(
+ $select->joinInner(
['t' => $tableName],
sprintf('e.%s = t.%s ', $linkField, $linkField) . $this->_connection->quoteInto(
' AND t.attribute_id IN (?)',
@@ -116,8 +112,6 @@ public function write($storeId, $productId, $valueFieldSuffix = '')
[]
)->where(
'e.entity_id = ' . $productId
- )->where(
- 't.attribute_id IS NOT NULL'
);
$cursor = $this->_connection->query($select);
while ($row = $cursor->fetch(\Zend_Db::FETCH_ASSOC)) {
@@ -157,7 +151,7 @@ public function write($storeId, $productId, $valueFieldSuffix = '')
$columnNames[] = 'attribute_set_id';
$columnNames[] = 'type_id';
$select->from(
- ['e' => $this->_productIndexerHelper->getTable('catalog_product_entity')],
+ ['e' => $entityTableName],
$columnNames
)->where(
'e.entity_id = ' . $productId
@@ -175,6 +169,9 @@ public function write($storeId, $productId, $valueFieldSuffix = '')
if (!empty($updateData)) {
$updateData += ['entity_id' => $productId];
+ if ($linkField !== $metadata->getIdentifierField()) {
+ $updateData += [$linkField => $productId];
+ }
$updateFields = [];
foreach ($updateData as $key => $value) {
$updateFields[$key] = $key;
diff --git a/app/code/Magento/Catalog/Model/Indexer/Product/Flat/Action/Row.php b/app/code/Magento/Catalog/Model/Indexer/Product/Flat/Action/Row.php
index b5dbdb68606ff..6d0727259d9db 100644
--- a/app/code/Magento/Catalog/Model/Indexer/Product/Flat/Action/Row.php
+++ b/app/code/Magento/Catalog/Model/Indexer/Product/Flat/Action/Row.php
@@ -5,16 +5,20 @@
*/
namespace Magento\Catalog\Model\Indexer\Product\Flat\Action;
+use Magento\Catalog\Api\Data\ProductInterface;
use Magento\Catalog\Model\Indexer\Product\Flat\FlatTableBuilder;
use Magento\Catalog\Model\Indexer\Product\Flat\TableBuilder;
+use Magento\Framework\EntityManager\MetadataPool;
/**
- * Class Row reindex action
+ * Class Row reindex action.
+ *
+ * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
*/
class Row extends \Magento\Catalog\Model\Indexer\Product\Flat\AbstractAction
{
/**
- * @var \Magento\Catalog\Model\Indexer\Product\Flat\Action\Indexer
+ * @var Indexer
*/
protected $flatItemWriter;
@@ -23,6 +27,11 @@ class Row extends \Magento\Catalog\Model\Indexer\Product\Flat\AbstractAction
*/
protected $flatItemEraser;
+ /**
+ * @var MetadataPool
+ */
+ private $metadataPool;
+
/**
* @param \Magento\Framework\App\ResourceConnection $resource
* @param \Magento\Store\Model\StoreManagerInterface $storeManager
@@ -32,6 +41,7 @@ class Row extends \Magento\Catalog\Model\Indexer\Product\Flat\AbstractAction
* @param FlatTableBuilder $flatTableBuilder
* @param Indexer $flatItemWriter
* @param Eraser $flatItemEraser
+ * @param MetadataPool|null $metadataPool
*/
public function __construct(
\Magento\Framework\App\ResourceConnection $resource,
@@ -41,7 +51,8 @@ public function __construct(
TableBuilder $tableBuilder,
FlatTableBuilder $flatTableBuilder,
Indexer $flatItemWriter,
- Eraser $flatItemEraser
+ Eraser $flatItemEraser,
+ MetadataPool $metadataPool = null
) {
parent::__construct(
$resource,
@@ -53,6 +64,8 @@ public function __construct(
);
$this->flatItemWriter = $flatItemWriter;
$this->flatItemEraser = $flatItemEraser;
+ $this->metadataPool = $metadataPool ?:
+ \Magento\Framework\App\ObjectManager::getInstance()->get(MetadataPool::class);
}
/**
@@ -70,24 +83,47 @@ public function execute($id = null)
);
}
$ids = [$id];
- foreach ($this->_storeManager->getStores() as $store) {
+ $linkField = $this->metadataPool->getMetadata(ProductInterface::class)->getLinkField();
+
+ $stores = $this->_storeManager->getStores();
+ foreach ($stores as $store) {
$tableExists = $this->_isFlatTableExists($store->getId());
if ($tableExists) {
$this->flatItemEraser->removeDeletedProducts($ids, $store->getId());
}
- if (isset($ids[0])) {
+
+ /* @var $status \Magento\Eav\Model\Entity\Attribute */
+ $status = $this->_productIndexerHelper->getAttribute(ProductInterface::STATUS);
+ $statusTable = $status->getBackend()->getTable();
+ $statusConditions = [
+ 'store_id IN(0,' . (int)$store->getId() . ')',
+ 'attribute_id = ' . (int)$status->getId(),
+ $linkField . ' = ' . (int)$id,
+ ];
+ $select = $this->_connection->select();
+ $select->from($statusTable, ['value'])
+ ->where(implode(' AND ', $statusConditions))
+ ->order('store_id DESC')
+ ->limit(1);
+ $result = $this->_connection->query($select);
+ $status = $result->fetchColumn(0);
+
+ if ($status == \Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED) {
if (!$tableExists) {
$this->_flatTableBuilder->build(
$store->getId(),
- [$ids[0]],
+ $ids,
$this->_valueFieldSuffix,
$this->_tableDropSuffix,
false
);
}
- $this->flatItemWriter->write($store->getId(), $ids[0], $this->_valueFieldSuffix);
+ $this->flatItemWriter->write($store->getId(), $id, $this->_valueFieldSuffix);
+ } else {
+ $this->flatItemEraser->deleteProductsFromStore($id, $store->getId());
}
}
+
return $this;
}
}
diff --git a/app/code/Magento/Catalog/Model/Indexer/Product/Flat/FlatTableBuilder.php b/app/code/Magento/Catalog/Model/Indexer/Product/Flat/FlatTableBuilder.php
index 05dd94dbe6e57..fbe0d4b550fa6 100644
--- a/app/code/Magento/Catalog/Model/Indexer/Product/Flat/FlatTableBuilder.php
+++ b/app/code/Magento/Catalog/Model/Indexer/Product/Flat/FlatTableBuilder.php
@@ -179,6 +179,11 @@ protected function _createTemporaryFlatTable($storeId)
$columnComment = isset($fieldProp['comment']) ? $fieldProp['comment'] : $fieldName;
+ if ($fieldName == 'created_at') {
+ $columnDefinition['nullable'] = true;
+ $columnDefinition['default'] = null;
+ }
+
$table->addColumn($fieldName, $fieldProp['type'], $columnLength, $columnDefinition, $columnComment);
}
diff --git a/app/code/Magento/Catalog/Model/Indexer/Product/Price/AbstractAction.php b/app/code/Magento/Catalog/Model/Indexer/Product/Price/AbstractAction.php
index 6db52f969d273..e9a907f0b5097 100644
--- a/app/code/Magento/Catalog/Model/Indexer/Product/Price/AbstractAction.php
+++ b/app/code/Magento/Catalog/Model/Indexer/Product/Price/AbstractAction.php
@@ -5,6 +5,12 @@
*/
namespace Magento\Catalog\Model\Indexer\Product\Price;
+use Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\DefaultPrice;
+use Magento\Customer\Model\Indexer\CustomerGroupDimensionProvider;
+use Magento\Framework\App\ObjectManager;
+use Magento\Framework\Indexer\DimensionalIndexerInterface;
+use Magento\Store\Model\Indexer\WebsiteDimensionProvider;
+
/**
* Abstract action reindex class
*
@@ -15,7 +21,7 @@ abstract class AbstractAction
/**
* Default Product Type Price indexer resource model
*
- * @var \Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\DefaultPrice
+ * @var DefaultPrice
*/
protected $_defaultIndexerResource;
@@ -71,9 +77,19 @@ abstract class AbstractAction
protected $_indexers;
/**
- * @var \Magento\Catalog\Model\ResourceModel\Product
+ * @var \Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\TierPrice
+ */
+ private $tierPriceIndexResource;
+
+ /**
+ * @var \Magento\Catalog\Model\Indexer\Product\Price\DimensionCollectionFactory
+ */
+ private $dimensionCollectionFactory;
+
+ /**
+ * @var TableMaintainer
*/
- private $productResource;
+ private $tableMaintainer;
/**
* @param \Magento\Framework\App\Config\ScopeConfigInterface $config
@@ -83,7 +99,13 @@ abstract class AbstractAction
* @param \Magento\Framework\Stdlib\DateTime $dateTime
* @param \Magento\Catalog\Model\Product\Type $catalogProductType
* @param \Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\Factory $indexerPriceFactory
- * @param \Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\DefaultPrice $defaultIndexerResource
+ * @param DefaultPrice $defaultIndexerResource
+ * @param \Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\TierPrice|null $tierPriceIndexResource
+ * @param DimensionCollectionFactory|null $dimensionCollectionFactory
+ * @param TableMaintainer|null $tableMaintainer
+ * @SuppressWarnings(PHPMD.NPathComplexity)
+ * @SuppressWarnings(PHPMD.CyclomaticComplexity)
+ * @SuppressWarnings(PHPMD.ExcessiveParameterList)
*/
public function __construct(
\Magento\Framework\App\Config\ScopeConfigInterface $config,
@@ -93,7 +115,10 @@ public function __construct(
\Magento\Framework\Stdlib\DateTime $dateTime,
\Magento\Catalog\Model\Product\Type $catalogProductType,
\Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\Factory $indexerPriceFactory,
- \Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\DefaultPrice $defaultIndexerResource
+ DefaultPrice $defaultIndexerResource,
+ \Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\TierPrice $tierPriceIndexResource = null,
+ \Magento\Catalog\Model\Indexer\Product\Price\DimensionCollectionFactory $dimensionCollectionFactory = null,
+ \Magento\Catalog\Model\Indexer\Product\Price\TableMaintainer $tableMaintainer = null
) {
$this->_config = $config;
$this->_storeManager = $storeManager;
@@ -104,6 +129,15 @@ public function __construct(
$this->_indexerPriceFactory = $indexerPriceFactory;
$this->_defaultIndexerResource = $defaultIndexerResource;
$this->_connection = $this->_defaultIndexerResource->getConnection();
+ $this->tierPriceIndexResource = $tierPriceIndexResource ?? ObjectManager::getInstance()->get(
+ \Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\TierPrice::class
+ );
+ $this->dimensionCollectionFactory = $dimensionCollectionFactory ?? ObjectManager::getInstance()->get(
+ \Magento\Catalog\Model\Indexer\Product\Price\DimensionCollectionFactory::class
+ );
+ $this->tableMaintainer = $tableMaintainer ?? ObjectManager::getInstance()->get(
+ \Magento\Catalog\Model\Indexer\Product\Price\TableMaintainer::class
+ );
}
/**
@@ -119,30 +153,29 @@ abstract public function execute($ids);
*
* @param array $processIds
* @return \Magento\Catalog\Model\Indexer\Product\Price\AbstractAction
+ * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+ * @deprecated Used only for backward compatibility for indexer, which not support indexation by dimensions
*/
protected function _syncData(array $processIds = [])
{
- // delete invalid rows
- $select = $this->_connection->select()->from(
- ['index_price' => $this->getIndexTargetTable()],
- null
- )->joinLeft(
- ['ip_tmp' => $this->_defaultIndexerResource->getIdxTable()],
- 'index_price.entity_id = ip_tmp.entity_id AND index_price.website_id = ip_tmp.website_id',
- []
- )->where(
- 'ip_tmp.entity_id IS NULL'
- );
- if (!empty($processIds)) {
- $select->where('index_price.entity_id IN(?)', $processIds);
- }
- $sql = $select->deleteFromSelect('index_price');
- $this->_connection->query($sql);
+ // for backward compatibility split data from old idx table on dimension tables
+ foreach ($this->dimensionCollectionFactory->create() as $dimensions) {
+ $insertSelect = $this->getConnection()->select()->from(
+ ['ip_tmp' => $this->_defaultIndexerResource->getIdxTable()]
+ );
- $this->_insertFromTable(
- $this->_defaultIndexerResource->getIdxTable(),
- $this->getIndexTargetTable()
- );
+ foreach ($dimensions as $dimension) {
+ if ($dimension->getName() === WebsiteDimensionProvider::DIMENSION_NAME) {
+ $insertSelect->where('ip_tmp.website_id = ?', $dimension->getValue());
+ }
+ if ($dimension->getName() === CustomerGroupDimensionProvider::DIMENSION_NAME) {
+ $insertSelect->where('ip_tmp.customer_group_id = ?', $dimension->getValue());
+ }
+ }
+
+ $query = $insertSelect->insertFromSelect($this->tableMaintainer->getMainTable($dimensions));
+ $this->getConnection()->query($query);
+ }
return $this;
}
@@ -150,12 +183,15 @@ protected function _syncData(array $processIds = [])
* Prepare website current dates table
*
* @return \Magento\Catalog\Model\Indexer\Product\Price\AbstractAction
+ *
+ * @throws \Magento\Framework\Exception\LocalizedException
+ * @throws \Magento\Framework\Exception\NoSuchEntityException
*/
protected function _prepareWebsiteDateTable()
{
$baseCurrency = $this->_config->getValue(\Magento\Directory\Model\Currency::XML_PATH_CURRENCY_BASE);
- $select = $this->_connection->select()->from(
+ $select = $this->getConnection()->select()->from(
['cw' => $this->_defaultIndexerResource->getTable('store_website')],
['website_id']
)->join(
@@ -167,7 +203,7 @@ protected function _prepareWebsiteDateTable()
);
$data = [];
- foreach ($this->_connection->fetchAll($select) as $item) {
+ foreach ($this->getConnection()->fetchAll($select) as $item) {
/** @var $website \Magento\Store\Model\Website */
$website = $this->_storeManager->getWebsite($item['website_id']);
@@ -192,6 +228,7 @@ protected function _prepareWebsiteDateTable()
'website_id' => $website->getId(),
'website_date' => $this->_dateTime->formatDate($timestamp, false),
'rate' => $rate,
+ 'default_store_id' => $store->getId()
];
}
}
@@ -200,7 +237,7 @@ protected function _prepareWebsiteDateTable()
$this->_emptyTable($table);
if ($data) {
foreach ($data as $row) {
- $this->_connection->insertOnDuplicate($table, $row, array_keys($row));
+ $this->getConnection()->insertOnDuplicate($table, $row, array_keys($row));
}
}
@@ -215,101 +252,21 @@ protected function _prepareWebsiteDateTable()
*/
protected function _prepareTierPriceIndex($entityIds = null)
{
- $table = $this->_defaultIndexerResource->getTable('catalog_product_index_tier_price');
- $this->_emptyTable($table);
- if (empty($entityIds)) {
- return $this;
- }
- $linkField = $this->getProductIdFieldName();
- $priceAttribute = $this->getProductResource()->getAttribute('price');
- $baseColumns = [
- 'cpe.entity_id',
- 'tp.customer_group_id',
- 'tp.website_id'
- ];
- if ($linkField !== 'entity_id') {
- $baseColumns[] = 'cpe.' . $linkField;
- };
- $subSelect = $this->_connection->select()->from(
- ['cpe' => $this->_defaultIndexerResource->getTable('catalog_product_entity')],
- array_merge_recursive(
- $baseColumns,
- [
- 'min(tp.value) AS value',
- 'min(tp.percentage_value) AS percentage_value'
- ]
- )
- )->joinInner(
- ['tp' => $this->_defaultIndexerResource->getTable(['catalog_product_entity', 'tier_price'])],
- 'tp.' . $linkField . ' = cpe.' . $linkField,
- []
- )->where("cpe.entity_id IN(?)", $entityIds)
- ->where("tp.website_id != 0")
- ->group(['cpe.entity_id', 'tp.customer_group_id', 'tp.website_id']);
-
- $subSelect2 = $this->_connection->select()
- ->from(
- ['cpe' => $this->_defaultIndexerResource->getTable('catalog_product_entity')],
- array_merge_recursive(
- $baseColumns,
- [
- 'MIN(ROUND(tp.value * cwd.rate, 4)) AS value',
- 'MIN(ROUND(tp.percentage_value * cwd.rate, 4)) AS percentage_value'
+ $this->tierPriceIndexResource->reindexEntity((array) $entityIds);
- ]
- )
- )
- ->joinInner(
- ['tp' => $this->_defaultIndexerResource->getTable(['catalog_product_entity', 'tier_price'])],
- 'tp.' . $linkField . ' = cpe.' . $linkField,
- []
- )->join(
- ['cw' => $this->_defaultIndexerResource->getTable('store_website')],
- true,
- []
- )
- ->joinInner(
- ['cwd' => $this->_defaultIndexerResource->getTable('catalog_product_index_website')],
- 'cw.website_id = cwd.website_id',
- []
- )
- ->where("cpe.entity_id IN(?)", $entityIds)
- ->where("tp.website_id = 0")
- ->group(
- ['cpe.entity_id', 'tp.customer_group_id', 'tp.website_id']
- );
-
- $unionSelect = $this->_connection->select()
- ->union([$subSelect, $subSelect2], \Magento\Framework\DB\Select::SQL_UNION_ALL);
- $select = $this->_connection->select()
- ->from(
- ['b' => new \Zend_Db_Expr(sprintf('(%s)', $unionSelect->assemble()))],
- [
- 'b.entity_id',
- 'b.customer_group_id',
- 'b.website_id',
- 'MIN(IF(b.value = 0, product_price.value * (1 - b.percentage_value / 100), b.value))'
- ]
- )
- ->joinInner(
- ['product_price' => $priceAttribute->getBackend()->getTable()],
- 'b.' . $linkField . ' = product_price.' . $linkField,
- []
- )
- ->group(['b.entity_id', 'b.customer_group_id', 'b.website_id']);
-
- $query = $select->insertFromSelect($table, [], false);
-
- $this->_connection->query($query);
return $this;
}
/**
* Retrieve price indexers per product type
*
+ * @param bool $fullReindexAction
+ *
* @return \Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\PriceInterface[]
+ *
+ * @throws \Magento\Framework\Exception\LocalizedException
*/
- public function getTypeIndexers()
+ public function getTypeIndexers($fullReindexAction = false)
{
if ($this->_indexers === null) {
$this->_indexers = [];
@@ -319,14 +276,20 @@ public function getTypeIndexers()
$typeInfo['price_indexer']
) ? $typeInfo['price_indexer'] : get_class($this->_defaultIndexerResource);
- $isComposite = !empty($typeInfo['composite']);
$indexer = $this->_indexerPriceFactory->create(
- $modelName
- )->setTypeId(
- $typeId
- )->setIsComposite(
- $isComposite
+ $modelName,
+ [
+ 'fullReindexAction' => $fullReindexAction
+ ]
);
+ // left setters for backward compatibility
+ if ($indexer instanceof DefaultPrice) {
+ $indexer->setTypeId(
+ $typeId
+ )->setIsComposite(
+ !empty($typeInfo['composite'])
+ );
+ }
$this->_indexers[$typeId] = $indexer;
}
}
@@ -339,7 +302,9 @@ public function getTypeIndexers()
*
* @param string $productTypeId
* @return \Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\PriceInterface
+ *
* @throws \Magento\Framework\Exception\InputException
+ * @throws \Magento\Framework\Exception\LocalizedException
*/
protected function _getIndexer($productTypeId)
{
@@ -360,19 +325,19 @@ protected function _getIndexer($productTypeId)
*/
protected function _insertFromTable($sourceTable, $destTable, $where = null)
{
- $sourceColumns = array_keys($this->_connection->describeTable($sourceTable));
- $targetColumns = array_keys($this->_connection->describeTable($destTable));
- $select = $this->_connection->select()->from($sourceTable, $sourceColumns);
+ $sourceColumns = array_keys($this->getConnection()->describeTable($sourceTable));
+ $targetColumns = array_keys($this->getConnection()->describeTable($destTable));
+ $select = $this->getConnection()->select()->from($sourceTable, $sourceColumns);
if ($where) {
$select->where($where);
}
- $query = $this->_connection->insertFromSelect(
+ $query = $this->getConnection()->insertFromSelect(
$select,
$destTable,
$targetColumns,
\Magento\Framework\DB\Adapter\AdapterInterface::INSERT_ON_DUPLICATE
);
- $this->_connection->query($query);
+ $this->getConnection()->query($query);
}
/**
@@ -383,7 +348,7 @@ protected function _insertFromTable($sourceTable, $destTable, $where = null)
*/
protected function _emptyTable($table)
{
- $this->_connection->delete($table);
+ $this->getConnection()->delete($table);
}
/**
@@ -391,74 +356,64 @@ protected function _emptyTable($table)
*
* @param array $changedIds
* @return array Affected ids
- * @SuppressWarnings(PHPMD.CyclomaticComplexity)
+ *
+ * @throws \Magento\Framework\Exception\InputException
+ * @throws \Magento\Framework\Exception\LocalizedException
+ * @throws \Magento\Framework\Exception\NoSuchEntityException
*/
protected function _reindexRows($changedIds = [])
{
- $this->_emptyTable($this->_defaultIndexerResource->getIdxTable());
$this->_prepareWebsiteDateTable();
- $select = $this->_connection->select()->from(
- $this->_defaultIndexerResource->getTable('catalog_product_entity'),
- ['entity_id', 'type_id']
- )->where(
- 'entity_id IN(?)',
- $changedIds
- );
- $pairs = $this->_connection->fetchPairs($select);
- $byType = [];
- foreach ($pairs as $productId => $productType) {
- $byType[$productType][$productId] = $productId;
- }
+ $productsTypes = $this->getProductsTypes($changedIds);
+ $parentProductsTypes = $this->getParentProductsTypes($changedIds);
- $compositeIds = [];
- $notCompositeIds = [];
+ $changedIds = array_merge($changedIds, ...array_values($parentProductsTypes));
+ $productsTypes = array_merge_recursive($productsTypes, $parentProductsTypes);
- foreach ($byType as $productType => $entityIds) {
- $indexer = $this->_getIndexer($productType);
- if ($indexer->getIsComposite()) {
- $compositeIds += $entityIds;
- } else {
- $notCompositeIds += $entityIds;
- }
+ if ($changedIds) {
+ $this->deleteIndexData($changedIds);
}
-
- if (!empty($notCompositeIds)) {
- $select = $this->_connection->select()->from(
- ['l' => $this->_defaultIndexerResource->getTable('catalog_product_relation')],
- ''
- )->join(
- ['e' => $this->_defaultIndexerResource->getTable('catalog_product_entity')],
- 'e.' . $this->getProductIdFieldName() . ' = l.parent_id',
- ['e.entity_id as parent_id', 'type_id']
- )->where(
- 'l.child_id IN(?)',
- $notCompositeIds
- );
- $pairs = $this->_connection->fetchPairs($select);
- foreach ($pairs as $productId => $productType) {
- if (!in_array($productId, $changedIds)) {
- $changedIds[] = (string) $productId;
- $byType[$productType][$productId] = $productId;
- $compositeIds[$productId] = $productId;
+ foreach ($productsTypes as $productType => $entityIds) {
+ $indexer = $this->_getIndexer($productType);
+ if ($indexer instanceof DimensionalIndexerInterface) {
+ foreach ($this->dimensionCollectionFactory->create() as $dimensions) {
+ $this->tableMaintainer->createMainTmpTable($dimensions);
+ $temporaryTable = $this->tableMaintainer->getMainTmpTable($dimensions);
+ $this->_emptyTable($temporaryTable);
+ $indexer->executeByDimensions($dimensions, \SplFixedArray::fromArray($entityIds, false));
+ // copy to index
+ $this->_insertFromTable(
+ $temporaryTable,
+ $this->tableMaintainer->getMainTable($dimensions)
+ );
}
+ } else {
+ // handle 3d-party indexers for backward compatibility
+ $this->_emptyTable($this->_defaultIndexerResource->getIdxTable());
+ $this->_copyRelationIndexData($entityIds);
+ $indexer->reindexEntity($entityIds);
+ $this->_syncData($entityIds);
}
}
- if (!empty($compositeIds)) {
- $this->_copyRelationIndexData($compositeIds, $notCompositeIds);
- }
- $this->_prepareTierPriceIndex($compositeIds + $notCompositeIds);
+ return $changedIds;
+ }
- $indexers = $this->getTypeIndexers();
- foreach ($indexers as $indexer) {
- if (!empty($byType[$indexer->getTypeId()])) {
- $indexer->reindexEntity($byType[$indexer->getTypeId()]);
- }
+ /**
+ * @param array $entityIds
+ * @return void
+ */
+ private function deleteIndexData(array $entityIds)
+ {
+ foreach ($this->dimensionCollectionFactory->create() as $dimensions) {
+ $select = $this->getConnection()->select()->from(
+ ['index_price' => $this->tableMaintainer->getMainTable($dimensions)],
+ null
+ )->where('index_price.entity_id IN (?)', $entityIds);
+ $query = $select->deleteFromSelect('index_price');
+ $this->getConnection()->query($query);
}
- $this->_syncData($changedIds);
-
- return $compositeIds + $notCompositeIds;
}
/**
@@ -467,16 +422,21 @@ protected function _reindexRows($changedIds = [])
* @param null|array $parentIds
* @param array $excludeIds
* @return \Magento\Catalog\Model\Indexer\Product\Price\AbstractAction
+ * @deprecated Used only for backward compatibility for do not broke custom indexer implementation
+ * which do not work by dimensions.
+ * For indexers, which support dimensions all composite products read data directly from main price indexer table
+ * or replica table for partial or full reindex correspondingly.
*/
protected function _copyRelationIndexData($parentIds, $excludeIds = null)
{
$linkField = $this->getProductIdFieldName();
- $select = $this->_connection->select()->from(
+ $select = $this->getConnection()->select()->from(
$this->_defaultIndexerResource->getTable('catalog_product_relation'),
['child_id']
)->join(
['e' => $this->_defaultIndexerResource->getTable('catalog_product_entity')],
- 'e.' . $linkField . ' = parent_id'
+ 'e.' . $linkField . ' = parent_id',
+ []
)->where(
'e.entity_id IN(?)',
$parentIds
@@ -485,22 +445,45 @@ protected function _copyRelationIndexData($parentIds, $excludeIds = null)
$select->where('child_id NOT IN(?)', $excludeIds);
}
- $children = $this->_connection->fetchCol($select);
+ $children = $this->getConnection()->fetchCol($select);
if ($children) {
- $select = $this->_connection->select()->from(
- $this->getIndexTargetTable()
- )->where(
- 'entity_id IN(?)',
- $children
- );
- $query = $select->insertFromSelect($this->_defaultIndexerResource->getIdxTable(), [], false);
- $this->_connection->query($query);
+ foreach ($this->dimensionCollectionFactory->create() as $dimensions) {
+ $select = $this->getConnection()->select()->from(
+ $this->getIndexTargetTableByDimension($dimensions)
+ )->where(
+ 'entity_id IN(?)',
+ $children
+ );
+ $query = $select->insertFromSelect($this->_defaultIndexerResource->getIdxTable(), [], false);
+ $this->getConnection()->query($query);
+ }
}
return $this;
}
+ /**
+ * Retrieve index table by dimension that will be used for write operations.
+ *
+ * This method is used during both partial and full reindex to identify the table.
+ *
+ * @param \Magento\Framework\Search\Request\Dimension[] $dimensions
+ *
+ * @return string
+ */
+ private function getIndexTargetTableByDimension(array $dimensions)
+ {
+ $indexTargetTable = $this->getIndexTargetTable();
+ if ($indexTargetTable === self::getIndexTargetTable()) {
+ $indexTargetTable = $this->tableMaintainer->getMainTable($dimensions);
+ }
+ if ($indexTargetTable === self::getIndexTargetTable() . '_replica') {
+ $indexTargetTable = $this->tableMaintainer->getMainReplicaTable($dimensions);
+ }
+ return $indexTargetTable;
+ }
+
/**
* Retrieve index table that will be used for write operations.
*
@@ -519,20 +502,72 @@ protected function getIndexTargetTable()
protected function getProductIdFieldName()
{
$table = $this->_defaultIndexerResource->getTable('catalog_product_entity');
- $indexList = $this->_connection->getIndexList($table);
- return $indexList[$this->_connection->getPrimaryKeyName($table)]['COLUMNS_LIST'][0];
+ $indexList = $this->getConnection()->getIndexList($table);
+ return $indexList[$this->getConnection()->getPrimaryKeyName($table)]['COLUMNS_LIST'][0];
+ }
+
+ /**
+ * Get products types.
+ *
+ * @param array $changedIds
+ * @return array
+ */
+ private function getProductsTypes(array $changedIds = [])
+ {
+ $select = $this->getConnection()->select()->from(
+ $this->_defaultIndexerResource->getTable('catalog_product_entity'),
+ ['entity_id', 'type_id']
+ );
+ if ($changedIds) {
+ $select->where('entity_id IN (?)', $changedIds);
+ }
+ $pairs = $this->getConnection()->fetchPairs($select);
+
+ $byType = [];
+ foreach ($pairs as $productId => $productType) {
+ $byType[$productType][$productId] = $productId;
+ }
+
+ return $byType;
}
/**
- * @return \Magento\Catalog\Model\ResourceModel\Product
- * @deprecated 101.1.0
+ * Get parent products types
+ * Used for add composite products to reindex if we have only simple products in changed ids set
+ *
+ * @param array $productsIds
+ * @return array
*/
- private function getProductResource()
+ private function getParentProductsTypes(array $productsIds)
{
- if (null === $this->productResource) {
- $this->productResource = \Magento\Framework\App\ObjectManager::getInstance()
- ->get(\Magento\Catalog\Model\ResourceModel\Product::class);
+ $select = $this->getConnection()->select()->from(
+ ['l' => $this->_defaultIndexerResource->getTable('catalog_product_relation')],
+ ''
+ )->join(
+ ['e' => $this->_defaultIndexerResource->getTable('catalog_product_entity')],
+ 'e.' . $this->getProductIdFieldName() . ' = l.parent_id',
+ ['e.entity_id as parent_id', 'type_id']
+ )->where(
+ 'l.child_id IN(?)',
+ $productsIds
+ );
+ $pairs = $this->getConnection()->fetchPairs($select);
+
+ $byType = [];
+ foreach ($pairs as $productId => $productType) {
+ $byType[$productType][$productId] = $productId;
}
- return $this->productResource;
+
+ return $byType;
+ }
+
+ /**
+ * Get connection
+ *
+ * @return \Magento\Framework\DB\Adapter\AdapterInterface
+ */
+ private function getConnection()
+ {
+ return $this->_defaultIndexerResource->getConnection();
}
}
diff --git a/app/code/Magento/Catalog/Model/Indexer/Product/Price/Action/Full.php b/app/code/Magento/Catalog/Model/Indexer/Product/Price/Action/Full.php
index eb15833a7d0b2..1a75751570658 100644
--- a/app/code/Magento/Catalog/Model/Indexer/Product/Price/Action/Full.php
+++ b/app/code/Magento/Catalog/Model/Indexer/Product/Price/Action/Full.php
@@ -6,9 +6,17 @@
namespace Magento\Catalog\Model\Indexer\Product\Price\Action;
use Magento\Framework\App\ObjectManager;
+use Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\PriceInterface;
+use Magento\Framework\EntityManager\EntityMetadataInterface;
+use Magento\Catalog\Api\Data\ProductInterface;
+use Magento\Framework\Exception\LocalizedException;
+use Magento\Framework\Indexer\DimensionalIndexerInterface;
+use Magento\Customer\Model\Indexer\CustomerGroupDimensionProvider;
+use Magento\Store\Model\Indexer\WebsiteDimensionProvider;
/**
* Class Full reindex action
+ *
* @SuppressWarnings(PHPMD.CouplingBetweenObjects)
*/
class Full extends \Magento\Catalog\Model\Indexer\Product\Price\AbstractAction
@@ -33,6 +41,26 @@ class Full extends \Magento\Catalog\Model\Indexer\Product\Price\AbstractAction
*/
private $activeTableSwitcher;
+ /**
+ * @var EntityMetadataInterface
+ */
+ private $productMetaDataCached;
+
+ /**
+ * @var \Magento\Catalog\Model\Indexer\Product\Price\DimensionCollectionFactory
+ */
+ private $dimensionCollectionFactory;
+
+ /**
+ * @var \Magento\Catalog\Model\Indexer\Product\Price\TableMaintainer
+ */
+ private $dimensionTableMaintainer;
+
+ /**
+ * @var \Magento\Indexer\Model\ProcessManager
+ */
+ private $processManager;
+
/**
* @param \Magento\Framework\App\Config\ScopeConfigInterface $config
* @param \Magento\Store\Model\StoreManagerInterface $storeManager
@@ -46,7 +74,9 @@ class Full extends \Magento\Catalog\Model\Indexer\Product\Price\AbstractAction
* @param \Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\BatchSizeCalculator|null $batchSizeCalculator
* @param \Magento\Framework\Indexer\BatchProviderInterface|null $batchProvider
* @param \Magento\Catalog\Model\ResourceModel\Indexer\ActiveTableSwitcher|null $activeTableSwitcher
- *
+ * @param \Magento\Catalog\Model\Indexer\Product\Price\DimensionCollectionFactory|null $dimensionCollectionFactory
+ * @param \Magento\Catalog\Model\Indexer\Product\Price\TableMaintainer|null $dimensionTableMaintainer
+ * @param \Magento\Indexer\Model\ProcessManager $processManager
* @SuppressWarnings(PHPMD.ExcessiveParameterList)
*/
public function __construct(
@@ -61,7 +91,10 @@ public function __construct(
\Magento\Framework\EntityManager\MetadataPool $metadataPool = null,
\Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\BatchSizeCalculator $batchSizeCalculator = null,
\Magento\Framework\Indexer\BatchProviderInterface $batchProvider = null,
- \Magento\Catalog\Model\ResourceModel\Indexer\ActiveTableSwitcher $activeTableSwitcher = null
+ \Magento\Catalog\Model\ResourceModel\Indexer\ActiveTableSwitcher $activeTableSwitcher = null,
+ \Magento\Catalog\Model\Indexer\Product\Price\DimensionCollectionFactory $dimensionCollectionFactory = null,
+ \Magento\Catalog\Model\Indexer\Product\Price\TableMaintainer $dimensionTableMaintainer = null,
+ \Magento\Indexer\Model\ProcessManager $processManager = null
) {
parent::__construct(
$config,
@@ -85,6 +118,15 @@ public function __construct(
$this->activeTableSwitcher = $activeTableSwitcher ?: ObjectManager::getInstance()->get(
\Magento\Catalog\Model\ResourceModel\Indexer\ActiveTableSwitcher::class
);
+ $this->dimensionCollectionFactory = $dimensionCollectionFactory ?: ObjectManager::getInstance()->get(
+ \Magento\Catalog\Model\Indexer\Product\Price\DimensionCollectionFactory::class
+ );
+ $this->dimensionTableMaintainer = $dimensionTableMaintainer ?: ObjectManager::getInstance()->get(
+ \Magento\Catalog\Model\Indexer\Product\Price\TableMaintainer::class
+ );
+ $this->processManager = $processManager ?: ObjectManager::getInstance()->get(
+ \Magento\Indexer\Model\ProcessManager::class
+ );
}
/**
@@ -92,75 +134,335 @@ public function __construct(
*
* @param array|int|null $ids
* @return void
- * @throws \Magento\Framework\Exception\LocalizedException
+ * @throws \Exception
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
*/
public function execute($ids = null)
{
try {
- $this->_defaultIndexerResource->getTableStrategy()->setUseIdxTable(false);
- $this->_prepareWebsiteDateTable();
+ //Prepare indexer tables before full reindex
+ $this->prepareTables();
+
+ /** @var \Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\DefaultPrice $indexer */
+ foreach ($this->getTypeIndexers(true) as $typeId => $priceIndexer) {
+ if ($priceIndexer instanceof DimensionalIndexerInterface) {
+ //New price reindex mechanism
+ $this->reindexProductTypeWithDimensions($priceIndexer, $typeId);
+ continue;
+ }
+
+ $priceIndexer->getTableStrategy()->setUseIdxTable(false);
+
+ //Old price reindex mechanism
+ $this->reindexProductType($priceIndexer, $typeId);
+ }
+
+ //Final replacement of tables from replica to main
+ $this->switchTables();
+ } catch (\Exception $e) {
+ throw new LocalizedException(__($e->getMessage()), $e);
+ }
+ }
+
+ /**
+ * Prepare indexer tables before full reindex
+ *
+ * @return void
+ * @throws \Exception
+ */
+ private function prepareTables()
+ {
+ $this->_defaultIndexerResource->getTableStrategy()->setUseIdxTable(false);
+
+ $this->_prepareWebsiteDateTable();
- $entityMetadata = $this->metadataPool->getMetadata(\Magento\Catalog\Api\Data\ProductInterface::class);
- $replicaTable = $this->activeTableSwitcher->getAdditionalTableName(
- $this->_defaultIndexerResource->getMainTable()
+ $this->truncateReplicaTables();
+ }
+
+ /**
+ * Truncate replica tables by dimensions
+ *
+ * @return void
+ * @throws \Exception
+ */
+ private function truncateReplicaTables()
+ {
+ foreach ($this->dimensionCollectionFactory->create() as $dimension) {
+ $dimensionTable = $this->dimensionTableMaintainer->getMainReplicaTable($dimension);
+ $this->_defaultIndexerResource->getConnection()->truncateTable($dimensionTable);
+ }
+ }
+
+ /**
+ * Reindex new 'Dimensional' price indexer by product type
+ *
+ * @param DimensionalIndexerInterface $priceIndexer
+ * @param string $typeId
+ *
+ * @return void
+ * @throws \Exception
+ */
+ private function reindexProductTypeWithDimensions(DimensionalIndexerInterface $priceIndexer, string $typeId)
+ {
+ $userFunctions = [];
+ foreach ($this->dimensionCollectionFactory->create() as $dimensions) {
+ $userFunctions[] = function () use ($priceIndexer, $dimensions, $typeId) {
+ return $this->reindexByBatches($priceIndexer, $dimensions, $typeId);
+ };
+ }
+ $this->processManager->execute($userFunctions);
+ }
+
+ /**
+ * Reindex new 'Dimensional' price indexer by batches
+ *
+ * @param DimensionalIndexerInterface $priceIndexer
+ * @param array $dimensions
+ * @param string $typeId
+ *
+ * @return void
+ * @throws \Exception
+ */
+ private function reindexByBatches(DimensionalIndexerInterface $priceIndexer, array $dimensions, string $typeId)
+ {
+ foreach ($this->getBatchesForIndexer($typeId) as $batch) {
+ $this->reindexByBatchWithDimensions($priceIndexer, $batch, $dimensions, $typeId);
+ }
+ }
+
+ /**
+ * Get batches for new 'Dimensional' price indexer
+ *
+ * @param string $typeId
+ *
+ * @return \Generator
+ * @throws \Exception
+ */
+ private function getBatchesForIndexer(string $typeId)
+ {
+ $connection = $this->_defaultIndexerResource->getConnection();
+ return $this->batchProvider->getBatches(
+ $connection,
+ $this->getProductMetaData()->getEntityTable(),
+ $this->getProductMetaData()->getIdentifierField(),
+ $this->batchSizeCalculator->estimateBatchSize(
+ $connection,
+ $typeId
+ )
+ );
+ }
+
+ /**
+ * Reindex by batch for new 'Dimensional' price indexer
+ *
+ * @param DimensionalIndexerInterface $priceIndexer
+ * @param array $batch
+ * @param array $dimensions
+ * @param string $typeId
+ *
+ * @return void
+ * @throws \Exception
+ */
+ private function reindexByBatchWithDimensions(
+ DimensionalIndexerInterface $priceIndexer,
+ array $batch,
+ array $dimensions,
+ string $typeId
+ ) {
+ $entityIds = $this->getEntityIdsFromBatch($typeId, $batch);
+
+ if (!empty($entityIds)) {
+ $this->dimensionTableMaintainer->createMainTmpTable($dimensions);
+ $temporaryTable = $this->dimensionTableMaintainer->getMainTmpTable($dimensions);
+ $this->_emptyTable($temporaryTable);
+
+ $priceIndexer->executeByDimensions($dimensions, \SplFixedArray::fromArray($entityIds, false));
+
+ // Sync data from temp table to index table
+ $this->_insertFromTable(
+ $temporaryTable,
+ $this->dimensionTableMaintainer->getMainReplicaTable($dimensions)
);
+ }
+ }
- // Prepare replica table for indexation.
- $this->_defaultIndexerResource->getConnection()->truncateTable($replicaTable);
-
- /** @var \Magento\Catalog\Model\ResourceModel\Product\Indexer\AbstractIndexer $indexer */
- foreach ($this->getTypeIndexers() as $indexer) {
- $indexer->getTableStrategy()->setUseIdxTable(false);
- $connection = $indexer->getConnection();
-
- $batches = $this->batchProvider->getBatches(
- $connection,
- $entityMetadata->getEntityTable(),
- $entityMetadata->getIdentifierField(),
- $this->batchSizeCalculator->estimateBatchSize($connection, $indexer->getTypeId())
- );
-
- foreach ($batches as $batch) {
- // Get entity ids from batch
- $select = $connection->select();
- $select->distinct(true);
- $select->from(['e' => $entityMetadata->getEntityTable()], $entityMetadata->getIdentifierField());
- $select->where('type_id = ?', $indexer->getTypeId());
-
- $entityIds = $this->batchProvider->getBatchIds($connection, $select, $batch);
-
- if (!empty($entityIds)) {
- // Temporary table will created if not exists
- $idxTableName = $this->_defaultIndexerResource->getIdxTable();
- $this->_emptyTable($idxTableName);
-
- if ($indexer->getIsComposite()) {
- $this->_copyRelationIndexData($entityIds);
- }
- $this->_prepareTierPriceIndex($entityIds);
-
- // Reindex entities by id
- $indexer->reindexEntity($entityIds);
-
- // Sync data from temp table to index table
- $this->_insertFromTable($idxTableName, $replicaTable);
-
- // Drop temporary index table
- $connection->dropTable($idxTableName);
- }
- }
+ /**
+ * Reindex old price indexer by product type
+ *
+ * @param PriceInterface $priceIndexer
+ * @param string $typeId
+ *
+ * @return void
+ * @throws \Exception
+ */
+ private function reindexProductType(PriceInterface $priceIndexer, string $typeId)
+ {
+ foreach ($this->getBatchesForIndexer($typeId) as $batch) {
+ $this->reindexBatch($priceIndexer, $batch, $typeId);
+ }
+ }
+
+ /**
+ * Reindex by batch for old price indexer
+ *
+ * @param PriceInterface $priceIndexer
+ * @param array $batch
+ * @param string $typeId
+ *
+ * @return void
+ * @throws \Exception
+ */
+ private function reindexBatch(PriceInterface $priceIndexer, array $batch, string $typeId)
+ {
+ $entityIds = $this->getEntityIdsFromBatch($typeId, $batch);
+
+ if (!empty($entityIds)) {
+ // Temporary table will created if not exists
+ $idxTableName = $this->_defaultIndexerResource->getIdxTable();
+ $this->_emptyTable($idxTableName);
+
+ if ($priceIndexer->getIsComposite()) {
+ $this->_copyRelationIndexData($entityIds);
}
+
+ // Reindex entities by id
+ $priceIndexer->reindexEntity($entityIds);
+
+ // Sync data from temp table to index table
+ $this->_insertFromTable($idxTableName, $this->getReplicaTable());
+
+ // Drop temporary index table
+ $this->_defaultIndexerResource->getConnection()->dropTable($idxTableName);
+ }
+ }
+
+ /**
+ * Get Entity Ids from batch
+ *
+ * @param string $typeId
+ * @param array $batch
+ *
+ * @return array
+ * @throws \Exception
+ */
+ private function getEntityIdsFromBatch(string $typeId, array $batch)
+ {
+ $connection = $this->_defaultIndexerResource->getConnection();
+
+ // Get entity ids from batch
+ $select = $connection
+ ->select()
+ ->distinct(true)
+ ->from(
+ ['e' => $this->getProductMetaData()->getEntityTable()],
+ $this->getProductMetaData()->getIdentifierField()
+ )
+ ->where('type_id = ?', $typeId);
+
+ return $this->batchProvider->getBatchIds($connection, $select, $batch);
+ }
+
+ /**
+ * Get product meta data
+ *
+ * @return EntityMetadataInterface
+ * @throws \Exception
+ */
+ private function getProductMetaData()
+ {
+ if ($this->productMetaDataCached === null) {
+ $this->productMetaDataCached = $this->metadataPool->getMetadata(ProductInterface::class);
+ }
+
+ return $this->productMetaDataCached;
+ }
+
+ /**
+ * Get replica table
+ *
+ * @return string
+ * @throws \Exception
+ */
+ private function getReplicaTable()
+ {
+ return $this->activeTableSwitcher->getAdditionalTableName(
+ $this->_defaultIndexerResource->getMainTable()
+ );
+ }
+
+ /**
+ * Replacement of tables from replica to main
+ *
+ * @return void
+ */
+ private function switchTables()
+ {
+ // Switch dimension tables
+ $mainTablesByDimension = [];
+
+ foreach ($this->dimensionCollectionFactory->create() as $dimensions) {
+ $mainTablesByDimension[] = $this->dimensionTableMaintainer->getMainTable($dimensions);
+
+ //Move data from indexers with old realisation
+ $this->moveDataFromReplicaTableToReplicaTables($dimensions);
+ }
+
+ if (count($mainTablesByDimension) > 0) {
$this->activeTableSwitcher->switchTable(
$this->_defaultIndexerResource->getConnection(),
- [$this->_defaultIndexerResource->getMainTable()]
+ $mainTablesByDimension
);
- } catch (\Exception $e) {
- throw new \Magento\Framework\Exception\LocalizedException(__($e->getMessage()), $e);
}
}
/**
+ * Move data from old price indexer mechanism to new indexer mechanism by dimensions.
+ * Used only for backward compatibility
+ *
+ * @param array $dimensions
+ *
+ * @return void
+ */
+ private function moveDataFromReplicaTableToReplicaTables(array $dimensions)
+ {
+ if (!$dimensions) {
+ return;
+ }
+ $select = $this->dimensionTableMaintainer->getConnection()->select()->from(
+ $this->dimensionTableMaintainer->getMainReplicaTable([])
+ );
+
+ $check = clone $select;
+ $check->reset('columns')->columns('count(*)');
+
+ if (!$this->dimensionTableMaintainer->getConnection()->query($check)->fetchColumn()) {
+ return;
+ }
+
+ $replicaTablesByDimension = $this->dimensionTableMaintainer->getMainReplicaTable($dimensions);
+
+ foreach ($dimensions as $dimension) {
+ if ($dimension->getName() === WebsiteDimensionProvider::DIMENSION_NAME) {
+ $select->where('website_id = ?', $dimension->getValue());
+ }
+ if ($dimension->getName() === CustomerGroupDimensionProvider::DIMENSION_NAME) {
+ $select->where('customer_group_id = ?', $dimension->getValue());
+ }
+ }
+
+ $this->dimensionTableMaintainer->getConnection()->query(
+ $this->dimensionTableMaintainer->getConnection()->insertFromSelect(
+ $select,
+ $replicaTablesByDimension,
+ [],
+ \Magento\Framework\DB\Adapter\AdapterInterface::INSERT_ON_DUPLICATE
+ )
+ );
+ }
+
+ /**
+ * @deprecated
+ *
* @inheritdoc
*/
protected function getIndexTargetTable()
diff --git a/app/code/Magento/Catalog/Model/Indexer/Product/Price/DimensionCollectionFactory.php b/app/code/Magento/Catalog/Model/Indexer/Product/Price/DimensionCollectionFactory.php
new file mode 100644
index 0000000000000..62a129f0ff448
--- /dev/null
+++ b/app/code/Magento/Catalog/Model/Indexer/Product/Price/DimensionCollectionFactory.php
@@ -0,0 +1,70 @@
+multiDimensionProviderFactory = $multiDimensionProviderFactory;
+ $this->dimensionProviders = $dimensionProviders;
+ $this->dimensionModeConfiguration = $dimensionModeConfiguration;
+ }
+
+ /**
+ * Create MultiDimensionProvider for specified "dimension mode".
+ * By default return multiplication of dimensions by current set mode
+ *
+ * @param string|null $dimensionsMode
+ * @return MultiDimensionProvider
+ */
+ public function create(string $dimensionsMode = null): MultiDimensionProvider
+ {
+ $dimensionConfiguration = $this->dimensionModeConfiguration->getDimensionConfiguration($dimensionsMode);
+
+ $providers = [];
+ foreach ($dimensionConfiguration as $dimensionName) {
+ if (!isset($this->dimensionProviders[$dimensionName])) {
+ throw new \LogicException(
+ 'Dimension Provider is missing. Cannot handle unknown dimension: ' . $dimensionName
+ );
+ }
+ $providers[] = clone $this->dimensionProviders[$dimensionName];
+ }
+
+ return $this->multiDimensionProviderFactory->create(
+ [
+ 'dimensionProviders' => $providers
+ ]
+ );
+ }
+}
diff --git a/app/code/Magento/Catalog/Model/Indexer/Product/Price/DimensionModeConfiguration.php b/app/code/Magento/Catalog/Model/Indexer/Product/Price/DimensionModeConfiguration.php
new file mode 100644
index 0000000000000..7a4d8e313462d
--- /dev/null
+++ b/app/code/Magento/Catalog/Model/Indexer/Product/Price/DimensionModeConfiguration.php
@@ -0,0 +1,102 @@
+ [
+ ],
+ self::DIMENSION_WEBSITE => [
+ WebsiteDimensionProvider::DIMENSION_NAME
+ ],
+ self::DIMENSION_CUSTOMER_GROUP => [
+ CustomerGroupDimensionProvider::DIMENSION_NAME
+ ],
+ self::DIMENSION_WEBSITE_AND_CUSTOMER_GROUP => [
+ WebsiteDimensionProvider::DIMENSION_NAME,
+ CustomerGroupDimensionProvider::DIMENSION_NAME
+ ],
+ ];
+
+ /**
+ * @var ScopeConfigInterface
+ */
+ private $scopeConfig;
+
+ /**
+ * @var string
+ */
+ private $currentMode;
+
+ /**
+ * @param ScopeConfigInterface $scopeConfig
+ */
+ public function __construct(ScopeConfigInterface $scopeConfig)
+ {
+ $this->scopeConfig = $scopeConfig;
+ }
+
+ /**
+ * Return dimension modes configuration.
+ *
+ * @return array
+ */
+ public function getDimensionModes(): array
+ {
+ return $this->modesMapping;
+ }
+
+ /**
+ * Get names of dimensions which used for provided mode.
+ * By default return dimensions for current enabled mode
+ *
+ * @param string|null $mode
+ * @return string[]
+ * @throws \InvalidArgumentException
+ */
+ public function getDimensionConfiguration(string $mode = null): array
+ {
+ if ($mode && !isset($this->modesMapping[$mode])) {
+ throw new \InvalidArgumentException(
+ sprintf('Undefined dimension mode "%s".', $mode)
+ );
+ }
+ return $this->modesMapping[$mode ?? $this->getCurrentMode()];
+ }
+
+ /**
+ * @return string
+ */
+ private function getCurrentMode(): string
+ {
+ if (null === $this->currentMode) {
+ $this->currentMode = $this->scopeConfig->getValue(ModeSwitcherConfiguration::XML_PATH_PRICE_DIMENSIONS_MODE)
+ ?: self::DIMENSION_NONE;
+ }
+
+ return $this->currentMode;
+ }
+}
diff --git a/app/code/Magento/Catalog/Model/Indexer/Product/Price/ModeSwitcher.php b/app/code/Magento/Catalog/Model/Indexer/Product/Price/ModeSwitcher.php
new file mode 100644
index 0000000000000..e71031489fa0e
--- /dev/null
+++ b/app/code/Magento/Catalog/Model/Indexer/Product/Price/ModeSwitcher.php
@@ -0,0 +1,211 @@
+tableMaintainer = $tableMaintainer;
+ $this->dimensionCollectionFactory = $dimensionCollectionFactory;
+ $this->dimensionModeConfiguration = $dimensionModeConfiguration;
+ $this->modeSwitcherConfiguration = $modeSwitcherConfiguration;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function getDimensionModes(): DimensionModes
+ {
+ $dimensionsList = [];
+ foreach ($this->dimensionModeConfiguration->getDimensionModes() as $dimension => $modes) {
+ $dimensionsList[] = new DimensionMode($dimension, $modes);
+ }
+
+ return new DimensionModes($dimensionsList);
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function switchMode(string $currentMode, string $previousMode)
+ {
+ //Create new tables and move data
+ $this->createTables($currentMode);
+ $this->moveData($currentMode, $previousMode);
+
+ //Change config options
+ $this->modeSwitcherConfiguration->saveMode($currentMode);
+
+ //Delete old tables
+ $this->dropTables($previousMode);
+ }
+
+ /**
+ * Create new tables
+ *
+ * @param string $currentMode
+ *
+ * @return void
+ * @throws \Zend_Db_Exception
+ */
+ public function createTables(string $currentMode)
+ {
+ foreach ($this->getDimensionsArray($currentMode) as $dimensions) {
+ if (!empty($dimensions)) {
+ $this->tableMaintainer->createTablesForDimensions($dimensions);
+ }
+ }
+ }
+
+ /**
+ * Move data from old tables to new
+ *
+ * @param string $currentMode
+ * @param string $previousMode
+ *
+ * @return void
+ */
+ public function moveData(string $currentMode, string $previousMode)
+ {
+ $dimensionsArrayForCurrentMode = $this->getDimensionsArray($currentMode);
+ $dimensionsArrayForPreviousMode = $this->getDimensionsArray($previousMode);
+
+ foreach ($dimensionsArrayForCurrentMode as $dimensionsForCurrentMode) {
+ $newTable = $this->tableMaintainer->getMainTable($dimensionsForCurrentMode);
+ if (empty($dimensionsForCurrentMode)) {
+ // new mode is 'none'
+ foreach ($dimensionsArrayForPreviousMode as $dimensionsForPreviousMode) {
+ $oldTable = $this->tableMaintainer->getMainTable($dimensionsForPreviousMode);
+ $this->insertFromOldTablesToNew($newTable, $oldTable);
+ }
+ } else {
+ // new mode is not 'none'
+ foreach ($dimensionsArrayForPreviousMode as $dimensionsForPreviousMode) {
+ $oldTable = $this->tableMaintainer->getMainTable($dimensionsForPreviousMode);
+ $this->insertFromOldTablesToNew($newTable, $oldTable, $dimensionsForCurrentMode);
+ }
+ }
+ }
+ }
+
+ /**
+ * Drop old tables
+ *
+ * @param string $previousMode
+ *
+ * @return void
+ */
+ public function dropTables(string $previousMode)
+ {
+ foreach ($this->getDimensionsArray($previousMode) as $dimensions) {
+ if (empty($dimensions)) {
+ $this->tableMaintainer->truncateTablesForDimensions($dimensions);
+ } else {
+ $this->tableMaintainer->dropTablesForDimensions($dimensions);
+ }
+ }
+ }
+
+ /**
+ * Get dimensions array
+ *
+ * @param string $mode
+ *
+ * @return \Magento\Framework\Indexer\MultiDimensionProvider
+ */
+ private function getDimensionsArray(string $mode): \Magento\Framework\Indexer\MultiDimensionProvider
+ {
+ if (isset($this->dimensionsArray[$mode])) {
+ return $this->dimensionsArray[$mode];
+ }
+
+ $this->dimensionsArray[$mode] = $this->dimensionCollectionFactory->create($mode);
+
+ return $this->dimensionsArray[$mode];
+ }
+
+ /**
+ * Insert from old tables data to new
+ *
+ * @param string $newTable
+ * @param string $oldTable
+ * @param Dimension[] $dimensions
+ *
+ * @return void
+ */
+ private function insertFromOldTablesToNew(string $newTable, string $oldTable, array $dimensions = [])
+ {
+ $select = $this->tableMaintainer->getConnection()->select()->from($oldTable);
+
+ foreach ($dimensions as $dimension) {
+ if ($dimension->getName() === WebsiteDimensionProvider::DIMENSION_NAME) {
+ $select->where('website_id = ?', $dimension->getValue());
+ }
+ if ($dimension->getName() === CustomerGroupDimensionProvider::DIMENSION_NAME) {
+ $select->where('customer_group_id = ?', $dimension->getValue());
+ }
+ }
+ $this->tableMaintainer->getConnection()->query(
+ $this->tableMaintainer->getConnection()->insertFromSelect(
+ $select,
+ $newTable,
+ [],
+ \Magento\Framework\DB\Adapter\AdapterInterface::INSERT_ON_DUPLICATE
+ )
+ );
+ }
+}
diff --git a/app/code/Magento/Catalog/Model/Indexer/Product/Price/ModeSwitcherConfiguration.php b/app/code/Magento/Catalog/Model/Indexer/Product/Price/ModeSwitcherConfiguration.php
new file mode 100644
index 0000000000000..66b7147a8db76
--- /dev/null
+++ b/app/code/Magento/Catalog/Model/Indexer/Product/Price/ModeSwitcherConfiguration.php
@@ -0,0 +1,70 @@
+configWriter = $configWriter;
+ $this->cacheTypeList = $cacheTypeList;
+ $this->indexer = $indexer;
+ }
+
+ /**
+ * Save switcher mode and invalidate reindex.
+ *
+ * @param string $mode
+ * @return void
+ * @throws \InvalidArgumentException
+ */
+ public function saveMode(string $mode)
+ {
+ //Change config options
+ $this->configWriter->saveConfig(self::XML_PATH_PRICE_DIMENSIONS_MODE, $mode);
+ $this->cacheTypeList->cleanType('config');
+ $this->indexer->load(\Magento\Catalog\Model\Indexer\Product\Price\Processor::INDEXER_ID);
+ $this->indexer->invalidate();
+ }
+}
diff --git a/app/code/Magento/Catalog/Model/Indexer/Product/Price/Plugin/CustomerGroup.php b/app/code/Magento/Catalog/Model/Indexer/Product/Price/Plugin/CustomerGroup.php
index 32b2db8a7008c..9b99ee8c8dc8c 100644
--- a/app/code/Magento/Catalog/Model/Indexer/Product/Price/Plugin/CustomerGroup.php
+++ b/app/code/Magento/Catalog/Model/Indexer/Product/Price/Plugin/CustomerGroup.php
@@ -5,9 +5,15 @@
*/
namespace Magento\Catalog\Model\Indexer\Product\Price\Plugin;
+use Magento\Catalog\Model\Indexer\Product\Price\DimensionModeConfiguration;
use Magento\Customer\Api\GroupRepositoryInterface;
use Magento\Customer\Api\Data\GroupInterface;
-use \Magento\Catalog\Model\Indexer\Product\Price\UpdateIndexInterface;
+use Magento\Catalog\Model\Indexer\Product\Price\UpdateIndexInterface;
+use Magento\Catalog\Model\Indexer\Product\Price\TableMaintainer;
+use Magento\Framework\Indexer\Dimension;
+use Magento\Framework\Indexer\DimensionFactory;
+use Magento\Customer\Model\Indexer\CustomerGroupDimensionProvider;
+use Magento\Store\Model\Indexer\WebsiteDimensionProvider;
class CustomerGroup
{
@@ -17,14 +23,46 @@ class CustomerGroup
private $updateIndex;
/**
- * Constructor
+ * @var TableMaintainer
+ */
+ private $tableMaintainer;
+
+ /**
+ * DimensionFactory
*
+ * @var DimensionFactory
+ */
+ private $dimensionFactory;
+
+ /**
+ * @var DimensionModeConfiguration
+ */
+ private $dimensionModeConfiguration;
+
+ /**
+ * @var WebsiteDimensionProvider
+ */
+ private $websiteDimensionProvider;
+
+ /**
* @param UpdateIndexInterface $updateIndex
+ * @param TableMaintainer $tableMaintainer
+ * @param DimensionFactory $dimensionFactory
+ * @param DimensionModeConfiguration $dimensionModeConfiguration
+ * @param WebsiteDimensionProvider $websiteDimensionProvider
*/
public function __construct(
- UpdateIndexInterface $updateIndex
+ UpdateIndexInterface $updateIndex,
+ TableMaintainer $tableMaintainer,
+ DimensionFactory $dimensionFactory,
+ DimensionModeConfiguration $dimensionModeConfiguration,
+ WebsiteDimensionProvider $websiteDimensionProvider
) {
$this->updateIndex = $updateIndex;
+ $this->tableMaintainer = $tableMaintainer;
+ $this->dimensionFactory = $dimensionFactory;
+ $this->dimensionModeConfiguration = $dimensionModeConfiguration;
+ $this->websiteDimensionProvider = $websiteDimensionProvider;
}
/**
@@ -32,7 +70,8 @@ public function __construct(
*
* @param GroupRepositoryInterface $subject
* @param \Closure $proceed
- * @param GroupInterface $result
+ * @param GroupInterface $group
+ *
* @return GroupInterface
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
*/
@@ -43,7 +82,64 @@ public function aroundSave(
) {
$isGroupNew = !$group->getId();
$group = $proceed($group);
+ if ($isGroupNew) {
+ foreach ($this->getAffectedDimensions((string)$group->getId()) as $dimensions) {
+ $this->tableMaintainer->createTablesForDimensions($dimensions);
+ }
+ }
$this->updateIndex->update($group, $isGroupNew);
return $group;
}
+
+ /**
+ * Update price index after customer group deleted
+ *
+ * @param GroupRepositoryInterface $subject
+ * @param bool $result
+ * @param string $groupId
+ *
+ * @return bool
+ * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+ */
+ public function afterDeleteById(GroupRepositoryInterface $subject, bool $result, string $groupId)
+ {
+ foreach ($this->getAffectedDimensions($groupId) as $dimensions) {
+ $this->tableMaintainer->dropTablesForDimensions($dimensions);
+ }
+
+ return $result;
+ }
+
+ /**
+ * Get affected dimensions
+ *
+ * @param string $groupId
+ * @return Dimension[][]
+ */
+ private function getAffectedDimensions(string $groupId): array
+ {
+ $currentDimensions = $this->dimensionModeConfiguration->getDimensionConfiguration();
+ // do not return dimensions if Customer Group dimension is not present in configuration
+ if (!in_array(CustomerGroupDimensionProvider::DIMENSION_NAME, $currentDimensions, true)) {
+ return [];
+ }
+ $customerGroupDimension = $this->dimensionFactory->create(
+ CustomerGroupDimensionProvider::DIMENSION_NAME,
+ $groupId
+ );
+
+ $dimensions = [];
+ if (in_array(WebsiteDimensionProvider::DIMENSION_NAME, $currentDimensions, true)) {
+ foreach ($this->websiteDimensionProvider as $websiteDimension) {
+ $dimensions[] = [
+ $customerGroupDimension,
+ $websiteDimension
+ ];
+ }
+ } else {
+ $dimensions[] = [$customerGroupDimension];
+ }
+
+ return $dimensions;
+ }
}
diff --git a/app/code/Magento/Catalog/Model/Indexer/Product/Price/Plugin/TableResolver.php b/app/code/Magento/Catalog/Model/Indexer/Product/Price/Plugin/TableResolver.php
new file mode 100644
index 0000000000000..fbeec22783090
--- /dev/null
+++ b/app/code/Magento/Catalog/Model/Indexer/Product/Price/Plugin/TableResolver.php
@@ -0,0 +1,140 @@
+priceTableResolver = $priceTableResolver;
+ $this->storeManager = $storeManager;
+ $this->httpContext = $context;
+ $this->dimensionFactory = $dimensionFactory;
+ $this->dimensionModeConfiguration = $dimensionModeConfiguration;
+ }
+
+ /**
+ * Replacing catalog_product_index_price table name on the table name segmented per dimension.
+ *
+ * @param ResourceConnection $subject
+ * @param string $result
+ * @param string|string[] $tableName
+ * @return string
+ * @throws \Magento\Framework\Exception\NoSuchEntityException
+ * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+ */
+ public function afterGetTableName(
+ ResourceConnection $subject,
+ string $result,
+ $tableName
+ ) {
+ if (!is_array($tableName)
+ && $tableName === 'catalog_product_index_price'
+ && $this->dimensionModeConfiguration->getDimensionConfiguration()
+ ) {
+ return $this->priceTableResolver->resolve('catalog_product_index_price', $this->getDimensions());
+ }
+
+ return $result;
+ }
+
+ /**
+ * @return Dimension[]
+ * @throws \Magento\Framework\Exception\NoSuchEntityException
+ */
+ private function getDimensions(): array
+ {
+ $dimensions = [];
+ foreach ($this->dimensionModeConfiguration->getDimensionConfiguration() as $dimensionName) {
+ if ($dimensionName === WebsiteDimensionProvider::DIMENSION_NAME) {
+ $dimensions[] = $this->createDimensionFromWebsite();
+ }
+ if ($dimensionName === CustomerGroupDimensionProvider::DIMENSION_NAME) {
+ $dimensions[] = $this->createDimensionFromCustomerGroup();
+ }
+ }
+
+ return $dimensions;
+ }
+
+ /**
+ * @return Dimension
+ * @throws \Magento\Framework\Exception\NoSuchEntityException
+ */
+ private function createDimensionFromWebsite(): Dimension
+ {
+ $storeKey = $this->httpContext->getValue(StoreManagerInterface::CONTEXT_STORE);
+ return $this->dimensionFactory->create(
+ WebsiteDimensionProvider::DIMENSION_NAME,
+ (string)$this->storeManager->getStore($storeKey)->getWebsiteId()
+ );
+ }
+
+ /**
+ * @return Dimension
+ */
+ private function createDimensionFromCustomerGroup(): Dimension
+ {
+ return $this->dimensionFactory->create(
+ CustomerGroupDimensionProvider::DIMENSION_NAME,
+ (string)$this->httpContext->getValue(CustomerContext::CONTEXT_GROUP)
+ );
+ }
+}
diff --git a/app/code/Magento/Catalog/Model/Indexer/Product/Price/Plugin/Website.php b/app/code/Magento/Catalog/Model/Indexer/Product/Price/Plugin/Website.php
index 269515e292e17..4831680f07c33 100644
--- a/app/code/Magento/Catalog/Model/Indexer/Product/Price/Plugin/Website.php
+++ b/app/code/Magento/Catalog/Model/Indexer/Product/Price/Plugin/Website.php
@@ -5,33 +5,128 @@
*/
namespace Magento\Catalog\Model\Indexer\Product\Price\Plugin;
+use Magento\Catalog\Model\Indexer\Product\Price\DimensionModeConfiguration;
+use Magento\Catalog\Model\Indexer\Product\Price\TableMaintainer;
+use Magento\Framework\Indexer\Dimension;
+use Magento\Framework\Indexer\DimensionFactory;
+use Magento\Customer\Model\Indexer\CustomerGroupDimensionProvider;
+use Magento\Store\Model\Indexer\WebsiteDimensionProvider;
+use Magento\Framework\Model\ResourceModel\Db\AbstractDb;
+use Magento\Framework\Model\AbstractModel;
+
class Website
{
/**
- * @var \Magento\Catalog\Model\Indexer\Product\Price\Processor
+ * @var TableMaintainer
+ */
+ private $tableMaintainer;
+
+ /**
+ * DimensionFactory
+ *
+ * @var DimensionFactory
+ */
+ private $dimensionFactory;
+
+ /**
+ * @var DimensionModeConfiguration
*/
- protected $_processor;
+ private $dimensionModeConfiguration;
/**
- * @param \Magento\Catalog\Model\Indexer\Product\Price\Processor $processor
+ * @var CustomerGroupDimensionProvider
*/
- public function __construct(\Magento\Catalog\Model\Indexer\Product\Price\Processor $processor)
+ private $customerGroupDimensionProvider;
+
+ /**
+ * @param TableMaintainer $tableMaintainer
+ * @param DimensionFactory $dimensionFactory
+ * @param DimensionModeConfiguration $dimensionModeConfiguration
+ * @param CustomerGroupDimensionProvider $customerGroupDimensionProvider
+ */
+ public function __construct(
+ TableMaintainer $tableMaintainer,
+ DimensionFactory $dimensionFactory,
+ DimensionModeConfiguration $dimensionModeConfiguration,
+ CustomerGroupDimensionProvider $customerGroupDimensionProvider
+ ) {
+ $this->tableMaintainer = $tableMaintainer;
+ $this->dimensionFactory = $dimensionFactory;
+ $this->dimensionModeConfiguration = $dimensionModeConfiguration;
+ $this->customerGroupDimensionProvider = $customerGroupDimensionProvider;
+ }
+
+ /**
+ * Update price index after website deleted
+ *
+ * @param AbstractDb $subject
+ * @param AbstractDb $objectResource
+ * @param AbstractModel $website
+ *
+ * @return AbstractDb
+ * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+ */
+ public function afterDelete(AbstractDb $subject, AbstractDb $objectResource, AbstractModel $website)
{
- $this->_processor = $processor;
+ foreach ($this->getAffectedDimensions($website->getId()) as $dimensions) {
+ $this->tableMaintainer->dropTablesForDimensions($dimensions);
+ }
+
+ return $objectResource;
}
/**
- * Invalidate price indexer
+ * Update price index after website created
*
- * @param \Magento\Store\Model\ResourceModel\Website $subject
- * @param \Magento\Store\Model\ResourceModel\Website $result
- * @return \Magento\Store\Model\ResourceModel\Website
+ * @param AbstractDb $subject
+ * @param AbstractDb $objectResource
+ * @param AbstractModel $website
*
+ * @return AbstractDb
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
*/
- public function afterDelete(\Magento\Store\Model\ResourceModel\Website $subject, $result)
+ public function afterSave(AbstractDb $subject, AbstractDb $objectResource, AbstractModel $website)
+ {
+ if ($website->isObjectNew()) {
+ foreach ($this->getAffectedDimensions($website->getId()) as $dimensions) {
+ $this->tableMaintainer->createTablesForDimensions($dimensions);
+ }
+ }
+
+ return $objectResource;
+ }
+
+ /**
+ * Get affected dimensions
+ *
+ * @param string $websiteId
+ *
+ * @return Dimension[][]
+ */
+ private function getAffectedDimensions(string $websiteId): array
{
- $this->_processor->markIndexerAsInvalid();
- return $result;
+ $currentDimensions = $this->dimensionModeConfiguration->getDimensionConfiguration();
+ // do not return dimensions if Website dimension is not present in configuration
+ if (!in_array(WebsiteDimensionProvider::DIMENSION_NAME, $currentDimensions, true)) {
+ return [];
+ }
+ $websiteDimension = $this->dimensionFactory->create(
+ WebsiteDimensionProvider::DIMENSION_NAME,
+ $websiteId
+ );
+
+ $dimensions = [];
+ if (in_array(CustomerGroupDimensionProvider::DIMENSION_NAME, $currentDimensions, true)) {
+ foreach ($this->customerGroupDimensionProvider as $customerGroupDimension) {
+ $dimensions[] = [
+ $customerGroupDimension,
+ $websiteDimension
+ ];
+ }
+ } else {
+ $dimensions[] = [$websiteDimension];
+ }
+
+ return $dimensions;
}
}
diff --git a/app/code/Magento/Catalog/Model/Indexer/Product/Price/PriceTableResolver.php b/app/code/Magento/Catalog/Model/Indexer/Product/Price/PriceTableResolver.php
new file mode 100644
index 0000000000000..0e4850d1f9541
--- /dev/null
+++ b/app/code/Magento/Catalog/Model/Indexer/Product/Price/PriceTableResolver.php
@@ -0,0 +1,77 @@
+indexScopeResolver = $indexScopeResolver;
+ $this->dimensionModeConfiguration = $dimensionModeConfiguration;
+ }
+
+ /**
+ * Return price table name based on dimension
+ * @param string $index
+ * @param array $dimensions
+ * @return string
+ */
+ public function resolve($index, array $dimensions)
+ {
+ if ($index === 'catalog_product_index_price') {
+ $dimensions = $this->filterDimensions($dimensions);
+ }
+ return $this->indexScopeResolver->resolve($index, $dimensions);
+ }
+
+ /**
+ * @param Dimension[] $dimensions
+ * @return array
+ * @throws \Exception
+ */
+ private function filterDimensions($dimensions): array
+ {
+ $existDimensions = [];
+ $currentDimensions = $this->dimensionModeConfiguration->getDimensionConfiguration();
+ foreach ($dimensions as $dimension) {
+ if ((string)$dimension->getValue() === '') {
+ throw new \InvalidArgumentException(
+ sprintf('Dimension value of "%s" can not be empty', $dimension->getName())
+ );
+ }
+ if (in_array($dimension->getName(), $currentDimensions, true)) {
+ $existDimensions[] = $dimension;
+ }
+ }
+
+ return $existDimensions;
+ }
+}
diff --git a/app/code/Magento/Catalog/Model/Indexer/Product/Price/TableMaintainer.php b/app/code/Magento/Catalog/Model/Indexer/Product/Price/TableMaintainer.php
new file mode 100644
index 0000000000000..999eaaa2a8025
--- /dev/null
+++ b/app/code/Magento/Catalog/Model/Indexer/Product/Price/TableMaintainer.php
@@ -0,0 +1,280 @@
+resource = $resource;
+ $this->tableResolver = $tableResolver;
+ $this->connectionName = $connectionName;
+ }
+
+ /**
+ * Get connection for work with price indexer
+ *
+ * @return AdapterInterface
+ */
+ public function getConnection(): AdapterInterface
+ {
+ if (null === $this->connection) {
+ $this->connection = $this->resource->getConnection($this->connectionName);
+ }
+ return $this->connection;
+ }
+
+ /**
+ * Return validated table name
+ *
+ * @param string $table
+ * @return string
+ */
+ private function getTable(string $table): string
+ {
+ return $this->resource->getTableName($table);
+ }
+
+ /**
+ * Create table based on main table
+ *
+ * @param string $mainTableName
+ * @param string $newTableName
+ *
+ * @return void
+ *
+ * @throws \Zend_Db_Exception
+ */
+ private function createTable(string $mainTableName, string $newTableName)
+ {
+ if (!$this->getConnection()->isTableExists($newTableName)) {
+ $this->getConnection()->createTable(
+ $this->getConnection()->createTableByDdl($mainTableName, $newTableName)
+ );
+ }
+ }
+
+ /**
+ * Drop table
+ *
+ * @param string $tableName
+ *
+ * @return void
+ */
+ private function dropTable(string $tableName)
+ {
+ if ($this->getConnection()->isTableExists($tableName)) {
+ $this->getConnection()->dropTable($tableName);
+ }
+ }
+
+ /**
+ * Truncate table
+ *
+ * @param string $tableName
+ *
+ * @return void
+ */
+ private function truncateTable(string $tableName)
+ {
+ if ($this->getConnection()->isTableExists($tableName)) {
+ $this->getConnection()->truncateTable($tableName);
+ }
+ }
+
+ /**
+ * Get array key for tmp table
+ *
+ * @param Dimension[] $dimensions
+ *
+ * @return string
+ */
+ private function getArrayKeyForTmpTable(array $dimensions): string
+ {
+ $key = 'temp';
+ foreach ($dimensions as $dimension) {
+ $key .= $dimension->getName() . '_' . $dimension->getValue();
+ }
+ return $key;
+ }
+
+ /**
+ * Return main index table name
+ *
+ * @param Dimension[] $dimensions
+ *
+ * @return string
+ */
+ public function getMainTable(array $dimensions): string
+ {
+ return $this->tableResolver->resolve(self::MAIN_INDEX_TABLE, $dimensions);
+ }
+
+ /**
+ * Create main and replica index tables for dimensions
+ *
+ * @param Dimension[] $dimensions
+ *
+ * @return void
+ *
+ * @throws \Zend_Db_Exception
+ */
+ public function createTablesForDimensions(array $dimensions)
+ {
+ $mainTableName = $this->getMainTable($dimensions);
+ //Create index table for dimensions based on main replica table
+ //Using main replica table is necessary for backward capability and TableResolver plugin work
+ $this->createTable(
+ $this->getTable(self::MAIN_INDEX_TABLE . $this->additionalTableSuffix),
+ $mainTableName
+ );
+
+ $mainReplicaTableName = $this->getMainTable($dimensions) . $this->additionalTableSuffix;
+ //Create replica table for dimensions based on main replica table
+ $this->createTable(
+ $this->getTable(self::MAIN_INDEX_TABLE . $this->additionalTableSuffix),
+ $mainReplicaTableName
+ );
+ }
+
+ /**
+ * Drop main and replica index tables for dimensions
+ *
+ * @param Dimension[] $dimensions
+ *
+ * @return void
+ */
+ public function dropTablesForDimensions(array $dimensions)
+ {
+ $mainTableName = $this->getMainTable($dimensions);
+ $this->dropTable($mainTableName);
+
+ $mainReplicaTableName = $this->getMainTable($dimensions) . $this->additionalTableSuffix;
+ $this->dropTable($mainReplicaTableName);
+ }
+
+ /**
+ * Truncate main and replica index tables for dimensions
+ *
+ * @param Dimension[] $dimensions
+ *
+ * @return void
+ */
+ public function truncateTablesForDimensions(array $dimensions)
+ {
+ $mainTableName = $this->getMainTable($dimensions);
+ $this->truncateTable($mainTableName);
+
+ $mainReplicaTableName = $this->getMainTable($dimensions) . $this->additionalTableSuffix;
+ $this->truncateTable($mainReplicaTableName);
+ }
+
+ /**
+ * Return replica index table name
+ *
+ * @param Dimension[] $dimensions
+ *
+ * @return string
+ */
+ public function getMainReplicaTable(array $dimensions): string
+ {
+ return $this->getMainTable($dimensions) . $this->additionalTableSuffix;
+ }
+
+ /**
+ * Create temporary index table for dimensions
+ *
+ * @param Dimension[] $dimensions
+ *
+ * @return void
+ */
+ public function createMainTmpTable(array $dimensions)
+ {
+ // Create temporary table based on template table catalog_product_index_price_tmp without indexes
+ $templateTableName = $this->resource->getTableName(self::MAIN_INDEX_TABLE . '_tmp');
+ $temporaryTableName = $this->getMainTable($dimensions) . $this->tmpTableSuffix;
+ $this->getConnection()->createTemporaryTableLike($temporaryTableName, $templateTableName, true);
+ $this->mainTmpTable[$this->getArrayKeyForTmpTable($dimensions)] = $temporaryTableName;
+ }
+
+ /**
+ * Return temporary index table name
+ *
+ * @param Dimension[] $dimensions
+ *
+ * @return string
+ *
+ * @throws \LogicException
+ */
+ public function getMainTmpTable(array $dimensions): string
+ {
+ $cacheKey = $this->getArrayKeyForTmpTable($dimensions);
+ if (!isset($this->mainTmpTable[$cacheKey])) {
+ throw new \LogicException(
+ sprintf('Temporary table for provided dimensions "%s" does not exist', $cacheKey)
+ );
+ }
+ return $this->mainTmpTable[$cacheKey];
+ }
+}
diff --git a/app/code/Magento/Catalog/Model/Layer/Filter/AbstractFilter.php b/app/code/Magento/Catalog/Model/Layer/Filter/AbstractFilter.php
index a4db630f0234b..d21a8666ec0ac 100644
--- a/app/code/Magento/Catalog/Model/Layer/Filter/AbstractFilter.php
+++ b/app/code/Magento/Catalog/Model/Layer/Filter/AbstractFilter.php
@@ -241,7 +241,7 @@ protected function _createItem($label, $value, $count = 0)
}
/**
- * Get all product ids from from collection with applied filters
+ * Get all product ids from collection with applied filters
*
* @return array
*/
diff --git a/app/code/Magento/Catalog/Model/Layer/Filter/DataProvider/Price.php b/app/code/Magento/Catalog/Model/Layer/Filter/DataProvider/Price.php
index 8a17a3b6c8cfa..d1aee8c4c5ba6 100644
--- a/app/code/Magento/Catalog/Model/Layer/Filter/DataProvider/Price.php
+++ b/app/code/Magento/Catalog/Model/Layer/Filter/DataProvider/Price.php
@@ -282,7 +282,8 @@ public function getMaxPrice()
public function getPriorFilters($filterParams)
{
$priorFilters = [];
- for ($i = 1; $i < count($filterParams); ++$i) {
+ $count = count($filterParams);
+ for ($i = 1; $i < $count; ++$i) {
$priorFilter = $this->validateFilter($filterParams[$i]);
if ($priorFilter) {
$priorFilters[] = $priorFilter;
diff --git a/app/code/Magento/Catalog/Model/Layer/Filter/Price.php b/app/code/Magento/Catalog/Model/Layer/Filter/Price.php
index ec5e2bff81ab3..68ef96c0f36a1 100644
--- a/app/code/Magento/Catalog/Model/Layer/Filter/Price.php
+++ b/app/code/Magento/Catalog/Model/Layer/Filter/Price.php
@@ -150,7 +150,7 @@ public function apply(\Magento\Framework\App\RequestInterface $request)
public function getCustomerGroupId()
{
$customerGroupId = $this->_getData('customer_group_id');
- if (is_null($customerGroupId)) {
+ if ($customerGroupId === null) {
$customerGroupId = $this->_customerSession->getCustomerGroupId();
}
@@ -176,7 +176,7 @@ public function setCustomerGroupId($customerGroupId)
public function getCurrencyRate()
{
$rate = $this->_getData('currency_rate');
- if (is_null($rate)) {
+ if ($rate === null) {
$rate = $this->_storeManager->getStore($this->getStoreId())
->getCurrentCurrencyRate();
}
diff --git a/app/code/Magento/Catalog/Model/Product.php b/app/code/Magento/Catalog/Model/Product.php
index cf1392a7e9e8c..e4dacf275fc9b 100644
--- a/app/code/Magento/Catalog/Model/Product.php
+++ b/app/code/Magento/Catalog/Model/Product.php
@@ -12,6 +12,7 @@
use Magento\Catalog\Model\Product\Attribute\Backend\Media\EntryConverterPool;
use Magento\Framework\Api\AttributeValueFactory;
use Magento\Framework\App\Filesystem\DirectoryList;
+use Magento\Framework\App\ObjectManager;
use Magento\Framework\DataObject\IdentityInterface;
use Magento\Framework\Pricing\SaleableInterface;
@@ -117,6 +118,11 @@ class Product extends \Magento\Catalog\Model\AbstractModel implements
*/
protected $_urlModel = null;
+ /**
+ * @var ResourceModel\Product
+ */
+ protected $_resource;
+
/**
* @var string
*/
@@ -270,6 +276,7 @@ class Product extends \Magento\Catalog\Model\AbstractModel implements
/**
* @var \Magento\Catalog\Api\ProductAttributeRepositoryInterface
+ * @deprecated Not used anymore due to performance issue (loaded all product attributes)
*/
protected $metadataService;
@@ -346,6 +353,11 @@ class Product extends \Magento\Catalog\Model\AbstractModel implements
*/
protected $linkTypeProvider;
+ /**
+ * @var \Magento\Eav\Model\Config
+ */
+ private $eavConfig;
+
/**
* Product constructor.
* @param \Magento\Framework\Model\Context $context
@@ -383,7 +395,7 @@ class Product extends \Magento\Catalog\Model\AbstractModel implements
* @param \Magento\Framework\Api\DataObjectHelper $dataObjectHelper
* @param \Magento\Framework\Api\ExtensionAttribute\JoinProcessorInterface $joinProcessor
* @param array $data
- *
+ * @param \Magento\Eav\Model\Config|null $config
* @SuppressWarnings(PHPMD.ExcessiveParameterList)
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
*/
@@ -422,7 +434,8 @@ public function __construct(
EntryConverterPool $mediaGalleryEntryConverterPool,
\Magento\Framework\Api\DataObjectHelper $dataObjectHelper,
\Magento\Framework\Api\ExtensionAttribute\JoinProcessorInterface $joinProcessor,
- array $data = []
+ array $data = [],
+ \Magento\Eav\Model\Config $config = null
) {
$this->metadataService = $metadataService;
$this->_itemOptionFactory = $itemOptionFactory;
@@ -461,6 +474,7 @@ public function __construct(
$resourceCollection,
$data
);
+ $this->eavConfig = $config ?? ObjectManager::getInstance()->get(\Magento\Eav\Model\Config::class);
}
/**
@@ -474,12 +488,30 @@ protected function _construct()
}
/**
- * {@inheritdoc}
+ * Get resource instance
+ *
+ * @throws \Magento\Framework\Exception\LocalizedException
+ * @return \Magento\Catalog\Model\ResourceModel\Product
+ * @deprecated because resource models should be used directly
+ */
+ protected function _getResource()
+ {
+ return parent::_getResource();
+ }
+
+ /**
+ * Get a list of custom attribute codes that belongs to product attribute set. If attribute set not specified for
+ * product will return all attribute codes
+ *
+ * @return string[]
*/
protected function getCustomAttributesCodes()
{
if ($this->customAttributesCodes === null) {
- $this->customAttributesCodes = $this->getEavAttributesCodes($this->metadataService);
+ $this->customAttributesCodes = array_keys($this->eavConfig->getEntityAttributes(
+ self::ENTITY,
+ $this
+ ));
$this->customAttributesCodes = array_diff($this->customAttributesCodes, $this->interfaceAttributes);
}
return $this->customAttributesCodes;
@@ -493,22 +525,9 @@ protected function getCustomAttributesCodes()
public function getStoreId()
{
if ($this->hasData(self::STORE_ID)) {
- return $this->getData(self::STORE_ID);
+ return (int)$this->getData(self::STORE_ID);
}
- return $this->_storeManager->getStore()->getId();
- }
-
- /**
- * Get collection instance
- *
- * @return object
- * @deprecated 101.1.0 because collections should be used directly via factory
- */
- public function getResourceCollection()
- {
- $collection = parent::getResourceCollection();
- $collection->setStoreId($this->getStoreId());
- return $collection;
+ return (int)$this->_storeManager->getStore()->getId();
}
/**
@@ -610,6 +629,7 @@ public function getUpdatedAt()
*
* @param bool $calculate
* @return void
+ * @deprecated
*/
public function setPriceCalculation($calculate = true)
{
@@ -789,6 +809,9 @@ public function getStoreIds()
if (!$this->hasStoreIds()) {
$storeIds = [];
if ($websiteIds = $this->getWebsiteIds()) {
+ if ($this->_storeManager->isSingleStoreMode()) {
+ $websiteIds = array_keys($websiteIds);
+ }
foreach ($websiteIds as $websiteId) {
$websiteStores = $this->_storeManager->getWebsite($websiteId)->getStoreIds();
$storeIds = array_merge($storeIds, $websiteStores);
@@ -1061,12 +1084,13 @@ protected function _afterLoad()
/**
* Clear cache related with product id
*
+ * @deprecated
+ * @see \Magento\Framework\Model\AbstractModel::cleanModelCache
* @return $this
*/
public function cleanCache()
{
- $this->_cacheManager->clean('catalog_product_' . $this->getId());
- return $this;
+ return $this->cleanModelCache();
}
/**
@@ -1128,11 +1152,24 @@ public function getTierPrice($qty = null)
/**
* Get formatted by currency product price
*
- * @return array || double
+ * @return array|double
+ */
+ public function getFormattedPrice()
+ {
+ return $this->getPriceModel()->getFormattedPrice($this);
+ }
+
+ /**
+ * Get formatted by currency product price
+ *
+ * @return array|double
+ *
+ * @deprecated
+ * @see getFormattedPrice()
*/
public function getFormatedPrice()
{
- return $this->getPriceModel()->getFormatedPrice($this);
+ return $this->getFormattedPrice();
}
/**
@@ -1159,10 +1196,11 @@ public function setFinalPrice($price)
*/
public function getFinalPrice($qty = null)
{
- if ($this->_getData('final_price') === null) {
- $this->setFinalPrice($this->getPriceModel()->getFinalPrice($qty, $this));
+ if ($this->_calculatePrice || $this->_getData('final_price') === null) {
+ return $this->getPriceModel()->getFinalPrice($qty, $this);
+ } else {
+ return $this->_getData('final_price');
}
- return $this->_getData('final_price');
}
/**
@@ -1712,7 +1750,7 @@ public function isInStock()
* Get attribute text by its code
*
* @param string $attributeCode Code of the attribute
- * @return string
+ * @return string|array|null
*/
public function getAttributeText($attributeCode)
{
@@ -1991,7 +2029,7 @@ public function getIsVirtual()
*/
public function addCustomOption($code, $value, $product = null)
{
- $product = $product ? $product : $this;
+ $product = $product ?: $this;
$option = $this->_itemOptionFactory->create()->addData(
['product_id' => $product->getId(), 'product' => $product, 'code' => $code, 'value' => $value]
);
@@ -2094,6 +2132,8 @@ public function reset()
/**
* Get cache tags associated with object id
*
+ * @deprecated
+ * @see \Magento\Catalog\Model\Product::getIdentities
* @return string[]
*/
public function getCacheIdTags()
@@ -2287,7 +2327,12 @@ public function getImage()
*/
public function getIdentities()
{
- $identities = [self::CACHE_TAG . '_' . $this->getId()];
+ $identities = [];
+
+ if ($this->getId()) {
+ $identities[] = self::CACHE_TAG . '_' . $this->getId();
+ }
+
if ($this->getIsChangedCategories()) {
foreach ($this->getAffectedCategoryIds() as $categoryId) {
$identities[] = self::CACHE_PRODUCT_CATEGORY_TAG . '_' . $categoryId;
@@ -2299,6 +2344,7 @@ public function getIdentities()
$identities[] = self::CACHE_PRODUCT_CATEGORY_TAG . '_' . $categoryId;
}
}
+
if ($this->_appState->getAreaCode() == \Magento\Framework\App\Area::AREA_FRONTEND) {
$identities[] = self::CACHE_TAG;
}
@@ -2718,4 +2764,18 @@ public function setStockData($stockData)
$this->setData('stock_data', $stockData);
return $this;
}
+
+ /**
+ * {@inheritDoc}
+ */
+ public function getCacheTags()
+ {
+ //Preferring individual tags over broad ones.
+ $individualTags = $this->getIdentities();
+ if ($individualTags) {
+ return $individualTags;
+ }
+
+ return parent::getCacheTags();
+ }
}
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 f7ab3f5a8b665..4c7c1c5bcf592 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
@@ -91,7 +91,7 @@ public function __construct(
*/
protected function _getWebsiteCurrencyRates()
{
- if (is_null($this->_rates)) {
+ if ($this->_rates === null) {
$this->_rates = [];
$baseCurrency = $this->_config->getValue(
\Magento\Directory\Model\Currency::XML_PATH_CURRENCY_BASE,
@@ -360,7 +360,7 @@ protected function modifyPriceData($object, $data)
{
/** @var array $priceItem */
foreach ($data as $key => $priceItem) {
- if (isset($priceItem['price']) && $priceItem['price'] > 0) {
+ if (array_key_exists('price', $priceItem)) {
$data[$key]['website_price'] = $priceItem['price'];
}
if ($priceItem['all_groups']) {
@@ -375,118 +375,10 @@ protected function modifyPriceData($object, $data)
*
* @param \Magento\Catalog\Model\Product $object
* @return $this
- * @SuppressWarnings(PHPMD.CyclomaticComplexity)
- * @SuppressWarnings(PHPMD.NPathComplexity)
- * @SuppressWarnings(PHPMD.ExcessiveMethodLength)
+ * @SuppressWarnings(PHPMD.UnusedFormalParameter)
*/
public function afterSave($object)
{
- $websiteId = $this->_storeManager->getStore($object->getStoreId())->getWebsiteId();
- $isGlobal = $this->getAttribute()->isScopeGlobal() || $websiteId == 0;
-
- $priceRows = $object->getData($this->getAttribute()->getName());
- if (null === $priceRows) {
- return $this;
- }
-
- $priceRows = array_filter((array)$priceRows);
-
- $old = [];
- $new = [];
-
- // prepare original data for compare
- $origPrices = $object->getOrigData($this->getAttribute()->getName());
- if (!is_array($origPrices)) {
- $origPrices = [];
- }
- foreach ($origPrices as $data) {
- if ($data['website_id'] > 0 || $data['website_id'] == '0' && $isGlobal) {
- $key = implode(
- '-',
- array_merge(
- [$data['website_id'], $data['cust_group']],
- $this->_getAdditionalUniqueFields($data)
- )
- );
- $old[$key] = $data;
- }
- }
-
- // prepare data for save
- foreach ($priceRows as $data) {
- $hasEmptyData = false;
- foreach ($this->_getAdditionalUniqueFields($data) as $field) {
- if (empty($field)) {
- $hasEmptyData = true;
- break;
- }
- }
-
- if ($hasEmptyData || !isset($data['cust_group']) || !empty($data['delete'])) {
- continue;
- }
- if ($this->getAttribute()->isScopeGlobal() && $data['website_id'] > 0) {
- continue;
- }
- if (!$isGlobal && (int)$data['website_id'] == 0) {
- continue;
- }
-
- $key = implode(
- '-',
- array_merge([$data['website_id'], $data['cust_group']], $this->_getAdditionalUniqueFields($data))
- );
-
- $useForAllGroups = $data['cust_group'] == $this->_groupManagement->getAllCustomersGroup()->getId();
- $customerGroupId = !$useForAllGroups ? $data['cust_group'] : 0;
- $new[$key] = array_merge(
- $this->getAdditionalFields($data),
- [
- 'website_id' => $data['website_id'],
- 'all_groups' => $useForAllGroups ? 1 : 0,
- 'customer_group_id' => $customerGroupId,
- 'value' => isset($data['price']) ? $data['price'] : null,
- ],
- $this->_getAdditionalUniqueFields($data)
- );
- }
-
- $delete = array_diff_key($old, $new);
- $insert = array_diff_key($new, $old);
- $update = array_intersect_key($new, $old);
-
- $isChanged = false;
- $productId = $object->getData($this->getMetadataPool()->getMetadata(ProductInterface::class)->getLinkField());
-
- if (!empty($delete)) {
- foreach ($delete as $data) {
- $this->_getResource()->deletePriceData($productId, null, $data['price_id']);
- $isChanged = true;
- }
- }
-
- if (!empty($insert)) {
- foreach ($insert as $data) {
- $price = new \Magento\Framework\DataObject($data);
- $price->setData(
- $this->getMetadataPool()->getMetadata(ProductInterface::class)->getLinkField(),
- $productId
- );
- $this->_getResource()->savePriceData($price);
-
- $isChanged = true;
- }
- }
-
- if (!empty($update)) {
- $isChanged |= $this->updateValues($update, $old);
- }
-
- if ($isChanged) {
- $valueChangedKey = $this->getAttribute()->getName() . '_changed';
- $object->setData($valueChangedKey, 1);
- }
-
return $this;
}
diff --git a/app/code/Magento/Catalog/Model/Product/Attribute/Backend/TierPrice/SaveHandler.php b/app/code/Magento/Catalog/Model/Product/Attribute/Backend/TierPrice/SaveHandler.php
new file mode 100644
index 0000000000000..587865414129a
--- /dev/null
+++ b/app/code/Magento/Catalog/Model/Product/Attribute/Backend/TierPrice/SaveHandler.php
@@ -0,0 +1,168 @@
+storeManager = $storeManager;
+ $this->attributeRepository = $attributeRepository;
+ $this->groupManagement = $groupManagement;
+ $this->metadataPoll = $metadataPool;
+ $this->tierPriceResource = $tierPriceResource;
+ }
+
+ /**
+ * Set tier price data for product entity
+ *
+ * @param \Magento\Catalog\Api\Data\ProductInterface|object $entity
+ * @param array $arguments
+ * @return \Magento\Catalog\Api\Data\ProductInterface|object
+ * @throws \Magento\Framework\Exception\NoSuchEntityException
+ * @throws \Magento\Framework\Exception\LocalizedException
+ * @throws \Magento\Framework\Exception\InputException
+ * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+ */
+ public function execute($entity, $arguments = [])
+ {
+ $attribute = $this->attributeRepository->get('tier_price');
+ $priceRows = $entity->getData($attribute->getName());
+ if (null !== $priceRows) {
+ if (!is_array($priceRows)) {
+ throw new \Magento\Framework\Exception\InputException(
+ __('Tier prices data should be array, but actually other type is received')
+ );
+ }
+ $websiteId = $this->storeManager->getStore($entity->getStoreId())->getWebsiteId();
+ $isGlobal = $attribute->isScopeGlobal() || $websiteId === 0;
+ $identifierField = $this->metadataPoll->getMetadata(ProductInterface::class)->getLinkField();
+ $priceRows = array_filter($priceRows);
+ $productId = $entity->getData($identifierField);
+
+ // prepare and save data
+ foreach ($priceRows as $data) {
+ $isPriceWebsiteGlobal = (int)$data['website_id'] === 0;
+ if ($isGlobal === $isPriceWebsiteGlobal
+ || !empty($data['price_qty'])
+ || isset($data['cust_group'])
+ ) {
+ $tierPrice = $this->prepareTierPrice($data);
+ $price = new \Magento\Framework\DataObject($tierPrice);
+ $price->setData(
+ $identifierField,
+ $productId
+ );
+ $this->tierPriceResource->savePriceData($price);
+ $valueChangedKey = $attribute->getName() . '_changed';
+ $entity->setData($valueChangedKey, 1);
+ }
+ }
+ }
+
+ return $entity;
+ }
+
+ /**
+ * Get additional tier price fields
+ *
+ * @return array
+ */
+ private function getAdditionalFields(array $objectArray): array
+ {
+ $percentageValue = $this->getPercentage($objectArray);
+ return [
+ 'value' => $percentageValue ? null : $objectArray['price'],
+ 'percentage_value' => $percentageValue ?: null,
+ ];
+ }
+
+ /**
+ * Check whether price has percentage value.
+ *
+ * @param array $priceRow
+ * @return integer|null
+ */
+ private function getPercentage(array $priceRow)
+ {
+ return isset($priceRow['percentage_value']) && is_numeric($priceRow['percentage_value'])
+ ? (int)$priceRow['percentage_value']
+ : null;
+ }
+
+ /**
+ * Prepare tier price data by provided price row data
+ *
+ * @param array $data
+ * @return array
+ * @throws \Magento\Framework\Exception\LocalizedException
+ */
+ private function prepareTierPrice(array $data): array
+ {
+ $useForAllGroups = (int)$data['cust_group'] === $this->groupManagement->getAllCustomersGroup()->getId();
+ $customerGroupId = $useForAllGroups ? 0 : $data['cust_group'];
+ $tierPrice = array_merge(
+ $this->getAdditionalFields($data),
+ [
+ 'website_id' => $data['website_id'],
+ 'all_groups' => (int)$useForAllGroups,
+ 'customer_group_id' => $customerGroupId,
+ 'value' => $data['price'] ?? null,
+ 'qty' => (int)$data['price_qty']
+ ]
+ );
+
+ return $tierPrice;
+ }
+}
diff --git a/app/code/Magento/Catalog/Model/Product/Attribute/Backend/TierPrice/UpdateHandler.php b/app/code/Magento/Catalog/Model/Product/Attribute/Backend/TierPrice/UpdateHandler.php
new file mode 100644
index 0000000000000..b23dc6f30f8fa
--- /dev/null
+++ b/app/code/Magento/Catalog/Model/Product/Attribute/Backend/TierPrice/UpdateHandler.php
@@ -0,0 +1,307 @@
+storeManager = $storeManager;
+ $this->attributeRepository = $attributeRepository;
+ $this->groupManagement = $groupManagement;
+ $this->metadataPoll = $metadataPool;
+ $this->tierPriceResource = $tierPriceResource;
+ }
+
+ /**
+ * @param \Magento\Catalog\Api\Data\ProductInterface|object $entity
+ * @param array $arguments
+ * @return \Magento\Catalog\Api\Data\ProductInterface|object
+ * @throws \Magento\Framework\Exception\NoSuchEntityException
+ * @throws \Magento\Framework\Exception\LocalizedException
+ * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+ */
+ public function execute($entity, $arguments = [])
+ {
+ $attribute = $this->attributeRepository->get('tier_price');
+ $priceRows = $entity->getData($attribute->getName());
+ if (null !== $priceRows) {
+ if (!is_array($priceRows)) {
+ throw new \Magento\Framework\Exception\InputException(
+ __('Tier prices data should be array, but actually other type is received')
+ );
+ }
+ $websiteId = $this->storeManager->getStore($entity->getStoreId())->getWebsiteId();
+ $isGlobal = $attribute->isScopeGlobal() || $websiteId === 0;
+ $identifierField = $this->metadataPoll->getMetadata(ProductInterface::class)->getLinkField();
+ $productId = $entity->getData($identifierField);
+
+ // prepare original data to compare
+ $origPrices = $entity->getOrigData($attribute->getName());
+ $old = $this->prepareOriginalDataToCompare($origPrices, $isGlobal);
+ // prepare data for save
+ $new = $this->prepareNewDataForSave($priceRows, $isGlobal);
+
+ $delete = array_diff_key($old, $new);
+ $insert = array_diff_key($new, $old);
+ $update = array_intersect_key($new, $old);
+
+ $isAttributeChanged = $this->deleteValues($productId, $delete);
+ $isAttributeChanged |= $this->insertValues($productId, $insert);
+ $isAttributeChanged |= $this->updateValues($update, $old);
+
+ if ($isAttributeChanged) {
+ $valueChangedKey = $attribute->getName() . '_changed';
+ $entity->setData($valueChangedKey, 1);
+ }
+ }
+
+ return $entity;
+ }
+
+ /**
+ * Get additional tier price fields
+ *
+ * @param array $objectArray
+ * @return array
+ */
+ private function getAdditionalFields(array $objectArray): array
+ {
+ $percentageValue = $this->getPercentage($objectArray);
+ return [
+ 'value' => $percentageValue ? null : $objectArray['price'],
+ 'percentage_value' => $percentageValue ?: null,
+ ];
+ }
+
+ /**
+ * Check whether price has percentage value.
+ *
+ * @param array $priceRow
+ * @return integer|null
+ */
+ private function getPercentage(array $priceRow)
+ {
+ return isset($priceRow['percentage_value']) && is_numeric($priceRow['percentage_value'])
+ ? (int)$priceRow['percentage_value']
+ : null;
+ }
+
+ /**
+ * Update existing tier prices for processed product
+ *
+ * @param array $valuesToUpdate
+ * @param array $oldValues
+ * @return boolean
+ */
+ private function updateValues(array $valuesToUpdate, array $oldValues): bool
+ {
+ $isChanged = false;
+ foreach ($valuesToUpdate as $key => $value) {
+ if ((!empty($value['value']) && (float)$oldValues[$key]['price'] !== (float)$value['value'])
+ || $this->getPercentage($oldValues[$key]) !== $this->getPercentage($value)
+ ) {
+ $price = new \Magento\Framework\DataObject(
+ [
+ 'value_id' => $oldValues[$key]['price_id'],
+ 'value' => $value['value'],
+ 'percentage_value' => $this->getPercentage($value)
+ ]
+ );
+ $this->tierPriceResource->savePriceData($price);
+ $isChanged = true;
+ }
+ }
+
+ return $isChanged;
+ }
+
+ /**
+ * Insert new tier prices for processed product
+ *
+ * @param int $productId
+ * @param array $valuesToInsert
+ * @return bool
+ */
+ private function insertValues(int $productId, array $valuesToInsert): bool
+ {
+ $isChanged = false;
+ $identifierField = $this->metadataPoll->getMetadata(ProductInterface::class)->getLinkField();
+ foreach ($valuesToInsert as $data) {
+ $price = new \Magento\Framework\DataObject($data);
+ $price->setData(
+ $identifierField,
+ $productId
+ );
+ $this->tierPriceResource->savePriceData($price);
+ $isChanged = true;
+ }
+
+ return $isChanged;
+ }
+
+ /**
+ * Delete tier price values for processed product
+ *
+ * @param int $productId
+ * @param array $valuesToDelete
+ * @return bool
+ */
+ private function deleteValues(int $productId, array $valuesToDelete): bool
+ {
+ $isChanged = false;
+ foreach ($valuesToDelete as $data) {
+ $this->tierPriceResource->deletePriceData($productId, null, $data['price_id']);
+ $isChanged = true;
+ }
+
+ return $isChanged;
+ }
+
+ /**
+ * Get generated price key based on price data
+ *
+ * @param array $priceData
+ * @return string
+ */
+ private function getPriceKey(array $priceData): string
+ {
+ $key = implode(
+ '-',
+ array_merge([$priceData['website_id'], $priceData['cust_group']], [(int)$priceData['price_qty']])
+ );
+
+ return $key;
+ }
+
+ /**
+ * Prepare tier price data by provided price row data
+ *
+ * @param array $data
+ * @return array
+ * @throws \Magento\Framework\Exception\LocalizedException
+ */
+ private function prepareTierPrice(array $data): array
+ {
+ $useForAllGroups = (int)$data['cust_group'] === $this->groupManagement->getAllCustomersGroup()->getId();
+ $customerGroupId = $useForAllGroups ? 0 : $data['cust_group'];
+ $tierPrice = array_merge(
+ $this->getAdditionalFields($data),
+ [
+ 'website_id' => $data['website_id'],
+ 'all_groups' => (int)$useForAllGroups,
+ 'customer_group_id' => $customerGroupId,
+ 'value' => $data['price'] ?? null,
+ 'qty' => (int)$data['price_qty']
+ ]
+ );
+
+ return $tierPrice;
+ }
+
+ /**
+ * Check by id is website global
+ *
+ * @param int $websiteId
+ * @return bool
+ */
+ private function isWebsiteGlobal(int $websiteId): bool
+ {
+ return $websiteId === 0;
+ }
+
+ /**
+ * @param array|null $origPrices
+ * @param bool $isGlobal
+ * @return array
+ */
+ private function prepareOriginalDataToCompare($origPrices, $isGlobal = true): array
+ {
+ $old = [];
+ if (is_array($origPrices)) {
+ foreach ($origPrices as $data) {
+ if ($isGlobal === $this->isWebsiteGlobal((int)$data['website_id'])) {
+ $key = $this->getPriceKey($data);
+ $old[$key] = $data;
+ }
+ }
+ }
+
+ return $old;
+ }
+
+ /**
+ * @param array $priceRows
+ * @param bool $isGlobal
+ * @return array
+ * @throws \Magento\Framework\Exception\LocalizedException
+ */
+ private function prepareNewDataForSave($priceRows, $isGlobal = true): array
+ {
+ $new = [];
+ $priceRows = array_filter($priceRows);
+ foreach ($priceRows as $data) {
+ if (empty($data['delete'])
+ && (!empty($data['price_qty'])
+ || isset($data['cust_group'])
+ || $isGlobal === $this->isWebsiteGlobal((int)$data['website_id']))
+ ) {
+ $key = $this->getPriceKey($data);
+ $new[$key] = $this->prepareTierPrice($data);
+ }
+ }
+
+ return $new;
+ }
+}
diff --git a/app/code/Magento/Catalog/Model/Product/Attribute/Frontend/Image.php b/app/code/Magento/Catalog/Model/Product/Attribute/Frontend/Image.php
index 6173a76eca421..cdd6da7019da5 100644
--- a/app/code/Magento/Catalog/Model/Product/Attribute/Frontend/Image.php
+++ b/app/code/Magento/Catalog/Model/Product/Attribute/Frontend/Image.php
@@ -9,23 +9,28 @@
*
* @author Magento Core Team
*/
+
namespace Magento\Catalog\Model\Product\Attribute\Frontend;
-class Image extends \Magento\Eav\Model\Entity\Attribute\Frontend\AbstractFrontend
+use Magento\Eav\Model\Entity\Attribute\Frontend\AbstractFrontend;
+use Magento\Framework\UrlInterface;
+use Magento\Store\Model\StoreManagerInterface;
+
+class Image extends AbstractFrontend
{
/**
* Store manager
*
- * @var \Magento\Store\Model\StoreManagerInterface
+ * @var StoreManagerInterface
*/
protected $_storeManager;
/**
* Construct
*
- * @param \Magento\Store\Model\StoreManagerInterface $storeManager
+ * @param StoreManagerInterface $storeManager
*/
- public function __construct(\Magento\Store\Model\StoreManagerInterface $storeManager)
+ public function __construct(StoreManagerInterface $storeManager)
{
$this->_storeManager = $storeManager;
}
@@ -42,9 +47,9 @@ public function getUrl($product)
$image = $product->getData($this->getAttribute()->getAttributeCode());
$url = false;
if (!empty($image)) {
- $url = $this->_storeManager->getStore($product->getStore())
- ->getBaseUrl(\Magento\Framework\UrlInterface::URL_TYPE_MEDIA)
- . 'catalog/product/' . $image;
+ $url = $this->_storeManager
+ ->getStore($product->getStore())
+ ->getBaseUrl(UrlInterface::URL_TYPE_MEDIA) . 'catalog/product/' . ltrim($image, '/');
}
return $url;
}
diff --git a/app/code/Magento/Catalog/Model/Product/Attribute/OptionManagement.php b/app/code/Magento/Catalog/Model/Product/Attribute/OptionManagement.php
index a840498dfbf2b..f2039a5002dcc 100644
--- a/app/code/Magento/Catalog/Model/Product/Attribute/OptionManagement.php
+++ b/app/code/Magento/Catalog/Model/Product/Attribute/OptionManagement.php
@@ -40,6 +40,17 @@ public function getItems($attributeCode)
*/
public function add($attributeCode, $option)
{
+ /** @var \Magento\Eav\Api\Data\AttributeOptionInterface[] $currentOptions */
+ $currentOptions = $this->getItems($attributeCode);
+ if (is_array($currentOptions)) {
+ array_walk($currentOptions, function (&$attributeOption) {
+ /** @var \Magento\Eav\Api\Data\AttributeOptionInterface $attributeOption */
+ $attributeOption = $attributeOption->getLabel();
+ });
+ if (in_array($option->getLabel(), $currentOptions)) {
+ return false;
+ }
+ }
return $this->eavOptionManagement->add(
\Magento\Catalog\Api\Data\ProductAttributeInterface::ENTITY_TYPE_CODE,
$attributeCode,
diff --git a/app/code/Magento/Catalog/Model/Product/Attribute/Repository.php b/app/code/Magento/Catalog/Model/Product/Attribute/Repository.php
index 270a2f229678b..c36d5ffcaa9e9 100644
--- a/app/code/Magento/Catalog/Model/Product/Attribute/Repository.php
+++ b/app/code/Magento/Catalog/Model/Product/Attribute/Repository.php
@@ -118,17 +118,11 @@ public function save(\Magento\Catalog\Api\Data\ProductAttributeInterface $attrib
$attribute->setAttributeId($existingModel->getAttributeId());
$attribute->setIsUserDefined($existingModel->getIsUserDefined());
$attribute->setFrontendInput($existingModel->getFrontendInput());
-
- if (is_array($attribute->getFrontendLabels())) {
- $defaultFrontendLabel = $attribute->getDefaultFrontendLabel();
- $frontendLabel[0] = !empty($defaultFrontendLabel)
- ? $defaultFrontendLabel
- : $existingModel->getDefaultFrontendLabel();
- foreach ($attribute->getFrontendLabels() as $item) {
- $frontendLabel[$item->getStoreId()] = $item->getLabel();
- }
- $attribute->setDefaultFrontendLabel($frontendLabel);
+ if ($attribute->getIsUserDefined()) {
+ $this->processAttributeData($attribute);
}
+
+ $this->updateDefaultFrontendLabel($attribute, $existingModel);
} else {
$attribute->setAttributeId(null);
@@ -136,35 +130,15 @@ public function save(\Magento\Catalog\Api\Data\ProductAttributeInterface $attrib
throw InputException::requiredField('frontend_label');
}
- $frontendLabels = [];
- if ($attribute->getDefaultFrontendLabel()) {
- $frontendLabels[0] = $attribute->getDefaultFrontendLabel();
- }
- if ($attribute->getFrontendLabels() && is_array($attribute->getFrontendLabels())) {
- foreach ($attribute->getFrontendLabels() as $label) {
- $frontendLabels[$label->getStoreId()] = $label->getLabel();
- }
- if (!isset($frontendLabels[0]) || !$frontendLabels[0]) {
- throw InputException::invalidFieldValue('frontend_label', null);
- }
+ $frontendLabel = $this->updateDefaultFrontendLabel($attribute, null);
- $attribute->setDefaultFrontendLabel($frontendLabels);
- }
$attribute->setAttributeCode(
- $attribute->getAttributeCode() ?: $this->generateCode($frontendLabels[0])
+ $attribute->getAttributeCode() ?: $this->generateCode($frontendLabel)
);
$this->validateCode($attribute->getAttributeCode());
$this->validateFrontendInput($attribute->getFrontendInput());
- $attribute->setBackendType(
- $attribute->getBackendTypeByInput($attribute->getFrontendInput())
- );
- $attribute->setSourceModel(
- $this->productHelper->getAttributeSourceModelByInputType($attribute->getFrontendInput())
- );
- $attribute->setBackendModel(
- $this->productHelper->getAttributeBackendModelByInputType($attribute->getFrontendInput())
- );
+ $this->processAttributeData($attribute);
$attribute->setEntityTypeId(
$this->eavConfig
->getEntityType(\Magento\Catalog\Api\Data\ProductAttributeInterface::ENTITY_TYPE_CODE)
@@ -275,4 +249,71 @@ protected function validateFrontendInput($frontendInput)
throw InputException::invalidFieldValue('frontend_input', $frontendInput);
}
}
+
+ /**
+ * Process attribute data based on attribute frontend input type.
+ *
+ * @param \Magento\Catalog\Api\Data\ProductAttributeInterface $attribute
+ * @return void
+ */
+ private function processAttributeData(\Magento\Catalog\Api\Data\ProductAttributeInterface $attribute)
+ {
+ $attribute->setBackendType(
+ $attribute->getBackendTypeByInput($attribute->getFrontendInput())
+ );
+ $attribute->setSourceModel(
+ $this->productHelper->getAttributeSourceModelByInputType($attribute->getFrontendInput())
+ );
+ $attribute->setBackendModel(
+ $this->productHelper->getAttributeBackendModelByInputType($attribute->getFrontendInput())
+ );
+ }
+
+ /**
+ * This method sets default frontend value using given default frontend value or frontend value from admin store
+ * if default frontend value is not presented.
+ * If both default frontend label and admin store frontend label are not given it throws exception
+ * for attribute creation process or sets existing attribute value for attribute update action.
+ *
+ * @param \Magento\Catalog\Api\Data\ProductAttributeInterface $attribute
+ * @param \Magento\Catalog\Api\Data\ProductAttributeInterface|null $existingModel
+ * @return string|null
+ * @throws InputException
+ */
+ private function updateDefaultFrontendLabel($attribute, $existingModel)
+ {
+ $frontendLabel = $attribute->getDefaultFrontendLabel();
+ if (empty($frontendLabel)) {
+ $frontendLabel = $this->extractAdminStoreFrontendLabel($attribute);
+ if (empty($frontendLabel)) {
+ if ($existingModel) {
+ $frontendLabel = $existingModel->getDefaultFrontendLabel();
+ } else {
+ throw InputException::invalidFieldValue('frontend_label', null);
+ }
+ }
+ $attribute->setDefaultFrontendLabel($frontendLabel);
+ }
+ return $frontendLabel;
+ }
+
+ /**
+ * This method extracts frontend label from FrontendLabel object for admin store.
+ *
+ * @param \Magento\Catalog\Api\Data\ProductAttributeInterface $attribute
+ * @return string|null
+ */
+ private function extractAdminStoreFrontendLabel($attribute)
+ {
+ $frontendLabel = [];
+ $frontendLabels = $attribute->getFrontendLabels();
+ if (isset($frontendLabels[0])
+ && $frontendLabels[0] instanceof \Magento\Eav\Api\Data\AttributeFrontendLabelInterface
+ ) {
+ foreach ($attribute->getFrontendLabels() as $label) {
+ $frontendLabel[$label->getStoreId()] = $label->getLabel();
+ }
+ }
+ return $frontendLabel[0] ?? null;
+ }
}
diff --git a/app/code/Magento/Catalog/Model/Product/Attribute/SetRepository.php b/app/code/Magento/Catalog/Model/Product/Attribute/SetRepository.php
index ab6cdb3b464a4..73a8a1e25957f 100644
--- a/app/code/Magento/Catalog/Model/Product/Attribute/SetRepository.php
+++ b/app/code/Magento/Catalog/Model/Product/Attribute/SetRepository.php
@@ -6,8 +6,6 @@
*/
namespace Magento\Catalog\Model\Product\Attribute;
-use Magento\Framework\Exception\InputException;
-
class SetRepository implements \Magento\Catalog\Api\AttributeSetRepositoryInterface
{
/**
@@ -53,7 +51,7 @@ public function __construct(
*/
public function save(\Magento\Eav\Api\Data\AttributeSetInterface $attributeSet)
{
- $this->validate($attributeSet);
+ $this->validateBeforeSave($attributeSet);
return $this->attributeSetRepository->save($attributeSet);
}
@@ -62,6 +60,7 @@ public function save(\Magento\Eav\Api\Data\AttributeSetInterface $attributeSet)
*/
public function getList(\Magento\Framework\Api\SearchCriteriaInterface $searchCriteria)
{
+ $this->searchCriteriaBuilder->setFilterGroups((array)$searchCriteria->getFilterGroups());
$this->searchCriteriaBuilder->addFilters(
[
$this->filterBuilder
@@ -71,9 +70,15 @@ public function getList(\Magento\Framework\Api\SearchCriteriaInterface $searchCr
->create(),
]
);
+
+ $this->searchCriteriaBuilder->setSortOrders((array)$searchCriteria->getSortOrders());
$this->searchCriteriaBuilder->setCurrentPage($searchCriteria->getCurrentPage());
$this->searchCriteriaBuilder->setPageSize($searchCriteria->getPageSize());
- return $this->attributeSetRepository->getList($this->searchCriteriaBuilder->create());
+
+ $searchResult = $this->attributeSetRepository->getList($this->searchCriteriaBuilder->create());
+ $searchResult->setSearchCriteria($searchCriteria);
+
+ return $searchResult;
}
/**
@@ -120,4 +125,29 @@ protected function validate(\Magento\Eav\Api\Data\AttributeSetInterface $attribu
);
}
}
+
+ /**
+ * Validate attribute set entity type id.
+ *
+ * @param \Magento\Eav\Api\Data\AttributeSetInterface $attributeSet
+ * @return void
+ * @throws \Magento\Framework\Exception\StateException
+ * @throws \Magento\Framework\Exception\NoSuchEntityException
+ * @throws \Magento\Framework\Exception\LocalizedException
+ */
+ private function validateBeforeSave(\Magento\Eav\Api\Data\AttributeSetInterface $attributeSet)
+ {
+ $productEntityId = $this->eavConfig->getEntityType(\Magento\Catalog\Model\Product::ENTITY)->getId();
+ $result = $attributeSet->getEntityTypeId() === $productEntityId;
+ if (!$result && $attributeSet->getAttributeSetId()) {
+ $existingAttributeSet = $this->attributeSetRepository->get($attributeSet->getAttributeSetId());
+ $attributeSet->setEntityTypeId($existingAttributeSet->getEntityTypeId());
+ $result = $existingAttributeSet->getEntityTypeId() === $productEntityId;
+ }
+ if (!$result) {
+ throw new \Magento\Framework\Exception\StateException(
+ __('Provided Attribute set non product Attribute set.')
+ );
+ }
+ }
}
diff --git a/app/code/Magento/Catalog/Model/Product/Compare/Item.php b/app/code/Magento/Catalog/Model/Product/Compare/Item.php
index 8f18780ef8f71..777ed0fbe393b 100644
--- a/app/code/Magento/Catalog/Model/Product/Compare/Item.php
+++ b/app/code/Magento/Catalog/Model/Product/Compare/Item.php
@@ -158,8 +158,8 @@ public function addProductData($product)
{
if ($product instanceof Product) {
$this->setProductId($product->getId());
- } elseif (intval($product)) {
- $this->setProductId(intval($product));
+ } elseif ((int)$product) {
+ $this->setProductId((int)$product);
}
return $this;
diff --git a/app/code/Magento/Catalog/Model/Product/Configuration/Item/ItemResolverComposite.php b/app/code/Magento/Catalog/Model/Product/Configuration/Item/ItemResolverComposite.php
new file mode 100644
index 0000000000000..ec500ac2f2030
--- /dev/null
+++ b/app/code/Magento/Catalog/Model/Product/Configuration/Item/ItemResolverComposite.php
@@ -0,0 +1,68 @@
+itemResolvers = $itemResolvers;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getFinalProduct(ItemInterface $item): ProductInterface
+ {
+ $finalProduct = $item->getProduct();
+
+ foreach ($this->itemResolvers as $resolver) {
+ $resolvedProduct = $this->getItemResolverInstance($resolver)->getFinalProduct($item);
+ if ($resolvedProduct !== $finalProduct) {
+ $finalProduct = $resolvedProduct;
+ break;
+ }
+ }
+
+ return $finalProduct;
+ }
+
+ /**
+ * Get the instance of the item resolver by class name.
+ *
+ * @param string $className
+ * @return ItemResolverInterface
+ */
+ private function getItemResolverInstance(string $className): ItemResolverInterface
+ {
+ if (!isset($this->itemResolversInstances[$className])) {
+ $this->itemResolversInstances[$className] = ObjectManager::getInstance()->get($className);
+ }
+
+ return $this->itemResolversInstances[$className];
+ }
+}
diff --git a/app/code/Magento/Catalog/Model/Product/Configuration/Item/ItemResolverInterface.php b/app/code/Magento/Catalog/Model/Product/Configuration/Item/ItemResolverInterface.php
new file mode 100644
index 0000000000000..9e6de01bb5112
--- /dev/null
+++ b/app/code/Magento/Catalog/Model/Product/Configuration/Item/ItemResolverInterface.php
@@ -0,0 +1,26 @@
+productFactory = $productFactory;
$this->copyConstructor = $copyConstructor;
@@ -46,18 +52,16 @@ public function __construct(
/**
* Create product duplicate
*
- * @param \Magento\Catalog\Model\Product $product
- * @return \Magento\Catalog\Model\Product
+ * @param Product $product
+ * @return Product
*/
- public function copy(\Magento\Catalog\Model\Product $product)
+ public function copy(Product $product)
{
$product->getWebsiteIds();
$product->getCategoryIds();
- /** @var \Magento\Framework\EntityManager\EntityMetadataInterface $metadata */
$metadata = $this->getMetadataPool()->getMetadata(ProductInterface::class);
- /** @var \Magento\Catalog\Model\Product $duplicate */
$duplicate = $this->productFactory->create();
$productData = $product->getData();
$productData = $this->removeStockItem($productData);
@@ -83,6 +87,7 @@ public function copy(\Magento\Catalog\Model\Product $product)
$duplicate->save();
$isDuplicateSaved = true;
} catch (\Magento\Framework\Exception\AlreadyExistsException $e) {
+ } catch (\Magento\UrlRewrite\Model\Exception\UrlAlreadyExistsException $e) {
}
} while (!$isDuplicateSaved);
$this->getOptionRepository()->duplicate($product, $duplicate);
@@ -94,27 +99,25 @@ public function copy(\Magento\Catalog\Model\Product $product)
}
/**
- * @return Option\Repository
+ * @return OptionRepository
* @deprecated 101.0.0
*/
private function getOptionRepository()
{
if (null === $this->optionRepository) {
- $this->optionRepository = \Magento\Framework\App\ObjectManager::getInstance()
- ->get(\Magento\Catalog\Model\Product\Option\Repository::class);
+ $this->optionRepository = ObjectManager::getInstance()->get(OptionRepository::class);
}
return $this->optionRepository;
}
/**
- * @return \Magento\Framework\EntityManager\MetadataPool
+ * @return MetadataPool
* @deprecated 101.0.0
*/
private function getMetadataPool()
{
if (null === $this->metadataPool) {
- $this->metadataPool = \Magento\Framework\App\ObjectManager::getInstance()
- ->get(\Magento\Framework\EntityManager\MetadataPool::class);
+ $this->metadataPool = ObjectManager::getInstance()->get(MetadataPool::class);
}
return $this->metadataPool;
}
diff --git a/app/code/Magento/Catalog/Model/Product/Gallery/CreateHandler.php b/app/code/Magento/Catalog/Model/Product/Gallery/CreateHandler.php
index 03d418f3ba0d9..1a3d03bf2c353 100644
--- a/app/code/Magento/Catalog/Model/Product/Gallery/CreateHandler.php
+++ b/app/code/Magento/Catalog/Model/Product/Gallery/CreateHandler.php
@@ -167,23 +167,19 @@ public function execute($product, $arguments = [])
if (empty($attrData) && empty($clearImages) && empty($newImages) && empty($existImages)) {
continue;
}
- if (in_array($attrData, $clearImages)) {
- $product->setData($mediaAttrCode, 'no_selection');
- }
-
- if (in_array($attrData, array_keys($newImages))) {
- $product->setData($mediaAttrCode, $newImages[$attrData]['new_file']);
- $product->setData($mediaAttrCode . '_label', $newImages[$attrData]['label']);
- }
-
- if (in_array($attrData, array_keys($existImages)) && isset($existImages[$attrData]['label'])) {
- $product->setData($mediaAttrCode . '_label', $existImages[$attrData]['label']);
- }
- if (!empty($product->getData($mediaAttrCode))) {
- $product->addAttributeUpdate(
+ $this->processMediaAttribute(
+ $product,
+ $mediaAttrCode,
+ $clearImages,
+ $newImages
+ );
+ if (in_array($mediaAttrCode, ['image', 'small_image', 'thumbnail'])) {
+ $this->processMediaAttributeLabel(
+ $product,
$mediaAttrCode,
- $product->getData($mediaAttrCode),
- $product->getStoreId()
+ $clearImages,
+ $newImages,
+ $existImages
);
}
}
@@ -245,12 +241,13 @@ protected function processNewAndExistingImages($product, array &$images)
if (empty($image['removed'])) {
$data = $this->processNewImage($product, $image);
- $this->resourceModel->deleteGalleryValueInStore(
- $image['value_id'],
- $product->getData($this->metadata->getLinkField()),
- $product->getStoreId()
- );
-
+ if (!$product->isObjectNew()) {
+ $this->resourceModel->deleteGalleryValueInStore(
+ $image['value_id'],
+ $product->getData($this->metadata->getLinkField()),
+ $product->getStoreId()
+ );
+ }
// Add per store labels, position, disabled
$data['value_id'] = $image['value_id'];
$data['label'] = isset($image['label']) ? $image['label'] : '';
@@ -448,4 +445,77 @@ private function getMediaAttributeCodes()
}
return $this->mediaAttributeCodes;
}
+
+ /**
+ * @param \Magento\Catalog\Model\Product $product
+ * @param $mediaAttrCode
+ * @param array $clearImages
+ * @param array $newImages
+ */
+ private function processMediaAttribute(
+ \Magento\Catalog\Model\Product $product,
+ $mediaAttrCode,
+ array $clearImages,
+ array $newImages
+ ) {
+ $attrData = $product->getData($mediaAttrCode);
+ if (in_array($attrData, $clearImages)) {
+ $product->setData($mediaAttrCode, 'no_selection');
+ }
+
+ if (in_array($attrData, array_keys($newImages))) {
+ $product->setData($mediaAttrCode, $newImages[$attrData]['new_file']);
+ }
+ if (!empty($product->getData($mediaAttrCode))) {
+ $product->addAttributeUpdate(
+ $mediaAttrCode,
+ $product->getData($mediaAttrCode),
+ $product->getStoreId()
+ );
+ }
+ }
+
+ /**
+ * @param \Magento\Catalog\Model\Product $product
+ * @param $mediaAttrCode
+ * @param array $clearImages
+ * @param array $newImages
+ * @param array $existImages
+ */
+ private function processMediaAttributeLabel(
+ \Magento\Catalog\Model\Product $product,
+ $mediaAttrCode,
+ array $clearImages,
+ array $newImages,
+ array $existImages
+ ) {
+ $resetLabel = false;
+ $attrData = $product->getData($mediaAttrCode);
+ if (in_array($attrData, $clearImages)) {
+ $product->setData($mediaAttrCode . '_label', null);
+ $resetLabel = true;
+ }
+
+ if (in_array($attrData, array_keys($newImages))) {
+ $product->setData($mediaAttrCode . '_label', $newImages[$attrData]['label']);
+ }
+
+ if (in_array($attrData, array_keys($existImages)) && isset($existImages[$attrData]['label'])) {
+ $product->setData($mediaAttrCode . '_label', $existImages[$attrData]['label']);
+ }
+
+ if ($attrData === 'no_selection' && !empty($product->getData($mediaAttrCode . '_label'))) {
+ $product->setData($mediaAttrCode . '_label', null);
+ $resetLabel = true;
+ }
+ if (!empty($product->getData($mediaAttrCode . '_label'))
+ || $resetLabel === true
+ ) {
+ $product->addAttributeUpdate(
+ $mediaAttrCode . '_label',
+ $product->getData($mediaAttrCode . '_label'),
+ $product->getStoreId()
+ );
+ }
+ }
}
diff --git a/app/code/Magento/Catalog/Model/Product/Gallery/Processor.php b/app/code/Magento/Catalog/Model/Product/Gallery/Processor.php
index 977d93f350145..73d0f1fa6795b 100644
--- a/app/code/Magento/Catalog/Model/Product/Gallery/Processor.php
+++ b/app/code/Magento/Catalog/Model/Product/Gallery/Processor.php
@@ -149,7 +149,7 @@ public function addImage(
}
$fileName = \Magento\MediaStorage\Model\File\Uploader::getCorrectFileName($pathinfo['basename']);
- $dispretionPath = \Magento\MediaStorage\Model\File\Uploader::getDispretionPath($fileName);
+ $dispretionPath = \Magento\MediaStorage\Model\File\Uploader::getDispersionPath($fileName);
$fileName = $dispretionPath . '/' . $fileName;
$fileName = $this->getNotDuplicatedFilename($fileName, $dispretionPath);
@@ -192,6 +192,7 @@ public function addImage(
$mediaGalleryData['images'][] = [
'file' => $fileName,
'position' => $position,
+ 'media_type' => 'image',
'label' => '',
'disabled' => (int)$exclude,
];
@@ -339,9 +340,9 @@ public function setMediaAttribute(\Magento\Catalog\Model\Product $product, $medi
$mediaAttributeCodes = $this->mediaConfig->getMediaAttributeCodes();
if (is_array($mediaAttribute)) {
- foreach ($mediaAttribute as $atttribute) {
- if (in_array($atttribute, $mediaAttributeCodes)) {
- $product->setData($atttribute, $value);
+ foreach ($mediaAttribute as $attribute) {
+ if (in_array($attribute, $mediaAttributeCodes)) {
+ $product->setData($attribute, $value);
}
}
} elseif (in_array($mediaAttribute, $mediaAttributeCodes)) {
diff --git a/app/code/Magento/Catalog/Model/Product/Gallery/ReadHandler.php b/app/code/Magento/Catalog/Model/Product/Gallery/ReadHandler.php
index c785d08e64b7f..4ad275bc70f90 100644
--- a/app/code/Magento/Catalog/Model/Product/Gallery/ReadHandler.php
+++ b/app/code/Magento/Catalog/Model/Product/Gallery/ReadHandler.php
@@ -55,9 +55,6 @@ public function __construct(
*/
public function execute($entity, $arguments = [])
{
- $value = [];
- $value['images'] = [];
-
$mediaEntries = $this->resourceModel->loadProductGalleryByAttributeId(
$entity,
$this->getAttribute()->getAttributeId()
@@ -79,37 +76,13 @@ public function execute($entity, $arguments = [])
*/
public function addMediaDataToProduct(Product $product, array $mediaEntries)
{
- $attrCode = $this->getAttribute()->getAttributeCode();
- $value = [];
- $value['images'] = [];
- $value['values'] = [];
-
- foreach ($mediaEntries as $mediaEntry) {
- $mediaEntry = $this->substituteNullsWithDefaultValues($mediaEntry);
- $value['images'][$mediaEntry['value_id']] = $mediaEntry;
- }
- $product->setData($attrCode, $value);
- }
-
- /**
- * @param array $rawData
- * @return array
- */
- private function substituteNullsWithDefaultValues(array $rawData)
- {
- $processedData = [];
- foreach ($rawData as $key => $rawValue) {
- if (null !== $rawValue) {
- $processedValue = $rawValue;
- } elseif (isset($rawData[$key . '_default'])) {
- $processedValue = $rawData[$key . '_default'];
- } else {
- $processedValue = null;
- }
- $processedData[$key] = $processedValue;
- }
-
- return $processedData;
+ $product->setData(
+ $this->getAttribute()->getAttributeCode(),
+ [
+ 'images' => array_column($mediaEntries, null, 'value_id'),
+ 'values' => []
+ ]
+ );
}
/**
diff --git a/app/code/Magento/Catalog/Model/Product/Gallery/UpdateHandler.php b/app/code/Magento/Catalog/Model/Product/Gallery/UpdateHandler.php
index 626d88f1acf82..f26e55099b054 100644
--- a/app/code/Magento/Catalog/Model/Product/Gallery/UpdateHandler.php
+++ b/app/code/Magento/Catalog/Model/Product/Gallery/UpdateHandler.php
@@ -32,9 +32,14 @@ protected function processDeletedImages($product, array &$images)
foreach ($images as &$image) {
if (!empty($image['removed'])) {
if (!empty($image['value_id']) && !isset($picturesInOtherStores[$image['file']])) {
+ if (preg_match('/\.\.(\\\|\/)/', $image['file'])) {
+ continue;
+ }
$recordsToDelete[] = $image['value_id'];
- // only delete physical files if they are not used by any other products
- if (!$this->resourceModel->countImageUses($image['file']) > 1) {
+ $catalogPath = $this->mediaConfig->getBaseMediaPath();
+ $isFile = $this->mediaDirectory->isFile($catalogPath . $image['file']);
+ // only delete physical files if they are not used by any other products and if this file exists
+ if (!($this->resourceModel->countImageUses($image['file']) > 1) && $isFile) {
$filesToDelete[] = ltrim($image['file'], '/');
}
}
diff --git a/app/code/Magento/Catalog/Model/Product/Image.php b/app/code/Magento/Catalog/Model/Product/Image.php
index abd28ed3bf1ec..74e5e94889253 100644
--- a/app/code/Magento/Catalog/Model/Product/Image.php
+++ b/app/code/Magento/Catalog/Model/Product/Image.php
@@ -5,9 +5,11 @@
*/
namespace Magento\Catalog\Model\Product;
+use Magento\Catalog\Model\Product\Image\NotLoadInfoImageException;
use Magento\Framework\App\Filesystem\DirectoryList;
use Magento\Framework\App\ObjectManager;
use Magento\Framework\Image as MagentoImage;
+use Magento\Framework\Serialize\SerializerInterface;
/**
* @method string getFile()
@@ -171,6 +173,16 @@ class Image extends \Magento\Framework\Model\AbstractModel
*/
private $imageAsset;
+ /**
+ * @var string
+ */
+ private $cachePrefix = 'IMG_INFO';
+
+ /**
+ * @var SerializerInterface
+ */
+ private $serializer;
+
/**
* Constructor
*
@@ -189,6 +201,7 @@ class Image extends \Magento\Framework\Model\AbstractModel
* @param array $data
* @param \Magento\Catalog\Model\View\Asset\ImageFactory|null $viewAssetImageFactory
* @param \Magento\Catalog\Model\View\Asset\PlaceholderFactory|null $viewAssetPlaceholderFactory
+ * @param SerializerInterface|null $serializer
* @SuppressWarnings(PHPMD.ExcessiveParameterList)
* @SuppressWarnings(PHPMD.UnusedLocalVariable)
*/
@@ -207,7 +220,8 @@ public function __construct(
\Magento\Framework\Data\Collection\AbstractDb $resourceCollection = null,
array $data = [],
\Magento\Catalog\Model\View\Asset\ImageFactory $viewAssetImageFactory = null,
- \Magento\Catalog\Model\View\Asset\PlaceholderFactory $viewAssetPlaceholderFactory = null
+ \Magento\Catalog\Model\View\Asset\PlaceholderFactory $viewAssetPlaceholderFactory = null,
+ SerializerInterface $serializer = null
) {
$this->_storeManager = $storeManager;
$this->_catalogProductMediaConfig = $catalogProductMediaConfig;
@@ -222,6 +236,7 @@ public function __construct(
->get(\Magento\Catalog\Model\View\Asset\ImageFactory::class);
$this->viewAssetPlaceholderFactory = $viewAssetPlaceholderFactory ?: ObjectManager::getInstance()
->get(\Magento\Catalog\Model\View\Asset\PlaceholderFactory::class);
+ $this->serializer = $serializer ?: ObjectManager::getInstance()->get(SerializerInterface::class);
}
/**
@@ -355,86 +370,6 @@ public function setSize($size)
return $this;
}
- /**
- * @param string|null $file
- * @return bool
- */
- protected function _checkMemory($file = null)
- {
- return $this->_getMemoryLimit() > $this->_getMemoryUsage() + $this->_getNeedMemoryForFile(
- $file
- )
- || $this->_getMemoryLimit() == -1;
- }
-
- /**
- * @return string
- */
- protected function _getMemoryLimit()
- {
- $memoryLimit = trim(strtoupper(ini_get('memory_limit')));
-
- if (!isset($memoryLimit[0])) {
- $memoryLimit = "128M";
- }
-
- if (substr($memoryLimit, -1) == 'K') {
- return substr($memoryLimit, 0, -1) * 1024;
- }
- if (substr($memoryLimit, -1) == 'M') {
- return substr($memoryLimit, 0, -1) * 1024 * 1024;
- }
- if (substr($memoryLimit, -1) == 'G') {
- return substr($memoryLimit, 0, -1) * 1024 * 1024 * 1024;
- }
- return $memoryLimit;
- }
-
- /**
- * @return int
- */
- protected function _getMemoryUsage()
- {
- if (function_exists('memory_get_usage')) {
- return memory_get_usage();
- }
- return 0;
- }
-
- /**
- * @param string|null $file
- * @return float|int
- * @SuppressWarnings(PHPMD.NPathComplexity)
- */
- protected function _getNeedMemoryForFile($file = null)
- {
- $file = $file === null ? $this->getBaseFile() : $file;
- if (!$file) {
- return 0;
- }
-
- if (!$this->_mediaDirectory->isExist($file)) {
- return 0;
- }
-
- $imageInfo = getimagesize($this->_mediaDirectory->getAbsolutePath($file));
-
- if (!isset($imageInfo[0]) || !isset($imageInfo[1])) {
- return 0;
- }
- if (!isset($imageInfo['channels'])) {
- // if there is no info about this parameter lets set it for maximum
- $imageInfo['channels'] = 4;
- }
- if (!isset($imageInfo['bits'])) {
- // if there is no info about this parameter lets set it for maximum
- $imageInfo['bits'] = 8;
- }
- return round(
- ($imageInfo[0] * $imageInfo[1] * $imageInfo['bits'] * $imageInfo['channels'] / 8 + Pow(2, 16)) * 1.65
- );
- }
-
/**
* Convert array of 3 items (decimal r, g, b) to string of their hex values
*
@@ -471,9 +406,7 @@ public function setBaseFile($file)
'filePath' => $file,
]
);
- if ($file == 'no_selection' || !$this->_fileExists($this->imageAsset->getSourceFile())
- || !$this->_checkMemory($this->imageAsset->getSourceFile())
- ) {
+ if ($file == 'no_selection' || !$this->_fileExists($this->imageAsset->getSourceFile())) {
$this->_isBaseFilePlaceholder = true;
$this->imageAsset = $this->viewAssetPlaceholderFactory->create(
[
@@ -561,7 +494,7 @@ public function resize()
*/
public function rotate($angle)
{
- $angle = intval($angle);
+ $angle = (int)$angle;
$this->getImageProcessor()->rotate($angle);
return $this;
}
@@ -681,11 +614,14 @@ public function getDestinationSubdir()
}
/**
- * @return bool|void
+ * @return bool
*/
public function isCached()
{
- return file_exists($this->imageAsset->getPath());
+ return (
+ is_array($this->loadImageInfoFromCache($this->imageAsset->getPath())) ||
+ file_exists($this->imageAsset->getPath())
+ );
}
/**
@@ -855,6 +791,7 @@ public function clearCache()
$this->_mediaDirectory->delete($directory);
$this->_coreFileStorageDatabase->deleteFolder($this->_mediaDirectory->getAbsolutePath($directory));
+ $this->clearImageInfoFromCache();
}
/**
@@ -877,17 +814,26 @@ protected function _fileExists($filename)
/**
* Return resized product image information
- *
* @return array
+ * @throws NotLoadInfoImageException
*/
public function getResizedImageInfo()
{
- if ($this->isBaseFilePlaceholder() == true) {
- $image = $this->imageAsset->getSourceFile();
- } else {
- $image = $this->imageAsset->getPath();
+ try {
+ if ($this->isBaseFilePlaceholder() == true) {
+ $image = $this->imageAsset->getSourceFile();
+ } else {
+ $image = $this->imageAsset->getPath();
+ }
+
+ $imageProperties = $this->getImageSize($image);
+
+ return $imageProperties;
+ } finally {
+ if (empty($imageProperties)) {
+ throw new NotLoadInfoImageException(__('Can\'t get information about the picture: %1', $image));
+ }
}
- return getimagesize($image);
}
/**
@@ -922,4 +868,66 @@ private function getMiscParams()
return $miscParams;
}
+
+ /**
+ * Get image size
+ *
+ * @param string $imagePath
+ * @return array
+ */
+ private function getImageSize($imagePath)
+ {
+ $imageInfo = $this->loadImageInfoFromCache($imagePath);
+ if (!isset($imageInfo['size'])) {
+ $imageSize = getimagesize($imagePath);
+ $this->saveImageInfoToCache(['size' => $imageSize], $imagePath);
+ return $imageSize;
+ } else {
+ return $imageInfo['size'];
+ }
+ }
+
+ /**
+ * Save image data to cache
+ *
+ * @param array $imageInfo
+ * @param string $imagePath
+ * @return void
+ */
+ private function saveImageInfoToCache(array $imageInfo, string $imagePath)
+ {
+ $imagePath = $this->cachePrefix . $imagePath;
+ $this->_cacheManager->save(
+ $this->serializer->serialize($imageInfo),
+ $imagePath,
+ [$this->cachePrefix]
+ );
+ }
+
+ /**
+ * Load image data from cache
+ *
+ * @param string $imagePath
+ * @return array|false
+ */
+ private function loadImageInfoFromCache(string $imagePath)
+ {
+ $imagePath = $this->cachePrefix . $imagePath;
+ $cacheData = $this->_cacheManager->load($imagePath);
+ if (!$cacheData) {
+ return false;
+ } else {
+ return $this->serializer->unserialize($cacheData);
+ }
+ }
+
+ /**
+ * Clear image data from cache
+ *
+ * @return void
+ */
+ private function clearImageInfoFromCache()
+ {
+ $this->_cacheManager->clean([$this->cachePrefix]);
+ }
}
diff --git a/app/code/Magento/Catalog/Model/Product/Image/Cache.php b/app/code/Magento/Catalog/Model/Product/Image/Cache.php
index c5e5e0ecac4c0..cd66257657cb1 100644
--- a/app/code/Magento/Catalog/Model/Product/Image/Cache.php
+++ b/app/code/Magento/Catalog/Model/Product/Image/Cache.php
@@ -10,6 +10,7 @@
use Magento\Theme\Model\ResourceModel\Theme\Collection as ThemeCollection;
use Magento\Framework\App\Area;
use Magento\Framework\View\ConfigInterface;
+use Psr\Log\LoggerInterface;
class Cache
{
@@ -33,19 +34,29 @@ class Cache
*/
protected $data = [];
+ /**
+ * Logger.
+ *
+ * @var LoggerInterface
+ */
+ private $logger;
+
/**
* @param ConfigInterface $viewConfig
* @param ThemeCollection $themeCollection
* @param ImageHelper $imageHelper
+ * @param LoggerInterface $logger
*/
public function __construct(
ConfigInterface $viewConfig,
ThemeCollection $themeCollection,
- ImageHelper $imageHelper
+ ImageHelper $imageHelper,
+ LoggerInterface $logger = null
) {
$this->viewConfig = $viewConfig;
$this->themeCollection = $themeCollection;
$this->imageHelper = $imageHelper;
+ $this->logger = $logger ?: \Magento\Framework\App\ObjectManager::getInstance()->get(LoggerInterface::class);
}
/**
@@ -74,21 +85,37 @@ protected function getData()
}
/**
- * Resize product images and save results to image cache
+ * Resize product images and save results to image cache.
*
* @param Product $product
+ *
* @return $this
+ * @throws \Exception
*/
public function generate(Product $product)
{
+ $isException = false;
$galleryImages = $product->getMediaGalleryImages();
if ($galleryImages) {
foreach ($galleryImages as $image) {
foreach ($this->getData() as $imageData) {
- $this->processImageData($product, $imageData, $image->getFile());
+ try {
+ $this->processImageData($product, $imageData, $image->getFile());
+ } catch (\Exception $e) {
+ $isException = true;
+ $this->logger->error($e->getMessage());
+ $this->logger->error(__('The image could not be resized: ') . $image->getPath());
+ }
}
}
}
+
+ if ($isException === true) {
+ throw new \Magento\Framework\Exception\RuntimeException(
+ __('Some images could not be resized. See log file for details.')
+ );
+ }
+
return $this;
}
diff --git a/app/code/Magento/Catalog/Model/Product/Image/NotLoadInfoImageException.php b/app/code/Magento/Catalog/Model/Product/Image/NotLoadInfoImageException.php
new file mode 100644
index 0000000000000..0feec11c62a59
--- /dev/null
+++ b/app/code/Magento/Catalog/Model/Product/Image/NotLoadInfoImageException.php
@@ -0,0 +1,10 @@
+getData($this->metadataPool->getMetadata($entityType)->getLinkField());
if ($this->linkResource->hasProductLinks($link)) {
- /** @var \Magento\Catalog\Api\Data\ProductInterface $entity*/
+ /** @var \Magento\Catalog\Api\Data\ProductInterface $entity */
foreach ($this->productLinkRepository->getList($entity) as $link) {
$this->productLinkRepository->delete($link);
}
}
- $productLinks = $entity->getProductLinks();
+
+ // Build links per type
+ $linksByType = [];
+ foreach ($entity->getProductLinks() as $link) {
+ $linksByType[$link->getLinkType()][] = $link;
+ }
+
+ // Set array position as a fallback position if necessary
+ foreach ($linksByType as $linkType => $links) {
+ if (!$this->hasPosition($links)) {
+ array_walk($linksByType[$linkType], function ($productLink, $position) {
+ $productLink->setPosition(++$position);
+ });
+ }
+ }
+
+ // Flatten multi-dimensional linksByType in ProductLinks
+ $productLinks = array_reduce($linksByType, 'array_merge', []);
+
if (count($productLinks) > 0) {
foreach ($entity->getProductLinks() as $link) {
$this->productLinkRepository->save($link);
@@ -68,4 +87,19 @@ public function execute($entityType, $entity)
}
return $entity;
}
+
+ /**
+ * Check if at least one link without position
+ * @param array $links
+ * @return bool
+ */
+ private function hasPosition(array $links)
+ {
+ foreach ($links as $link) {
+ if (!array_key_exists('position', $link->getData())) {
+ return false;
+ }
+ }
+ return true;
+ }
}
diff --git a/app/code/Magento/Catalog/Model/Product/Option.php b/app/code/Magento/Catalog/Model/Product/Option.php
index 67d9074d91382..2b72a6ca4e8ae 100644
--- a/app/code/Magento/Catalog/Model/Product/Option.php
+++ b/app/code/Magento/Catalog/Model/Product/Option.php
@@ -8,6 +8,7 @@
use Magento\Catalog\Api\Data\ProductCustomOptionInterface;
use Magento\Catalog\Api\Data\ProductCustomOptionValuesInterface;
+use Magento\Catalog\Api\Data\ProductCustomOptionValuesInterfaceFactory;
use Magento\Catalog\Api\Data\ProductInterface;
use Magento\Catalog\Model\Product;
use Magento\Catalog\Model\ResourceModel\Product\Option\Value\Collection;
@@ -102,6 +103,11 @@ class Option extends AbstractExtensibleModel implements ProductCustomOptionInter
*/
private $metadataPool;
+ /**
+ * @var ProductCustomOptionValuesInterfaceFactory
+ */
+ private $customOptionValuesFactory;
+
/**
* @param \Magento\Framework\Model\Context $context
* @param \Magento\Framework\Registry $registry
@@ -114,6 +120,7 @@ class Option extends AbstractExtensibleModel implements ProductCustomOptionInter
* @param \Magento\Framework\Model\ResourceModel\AbstractResource $resource
* @param \Magento\Framework\Data\Collection\AbstractDb $resourceCollection
* @param array $data
+ * @param ProductCustomOptionValuesInterfaceFactory|null $customOptionValuesFactory
* @SuppressWarnings(PHPMD.ExcessiveParameterList)
*/
public function __construct(
@@ -127,12 +134,16 @@ public function __construct(
Option\Validator\Pool $validatorPool,
\Magento\Framework\Model\ResourceModel\AbstractResource $resource = null,
\Magento\Framework\Data\Collection\AbstractDb $resourceCollection = null,
- array $data = []
+ array $data = [],
+ ProductCustomOptionValuesInterfaceFactory $customOptionValuesFactory = null
) {
$this->productOptionValue = $productOptionValue;
$this->optionTypeFactory = $optionFactory;
$this->validatorPool = $validatorPool;
$this->string = $string;
+ $this->customOptionValuesFactory = $customOptionValuesFactory ?:
+ \Magento\Framework\App\ObjectManager::getInstance()->get(ProductCustomOptionValuesInterfaceFactory::class);
+
parent::__construct(
$context,
$registry,
@@ -312,7 +323,7 @@ public function getGroupByType($type = null)
self::OPTION_TYPE_TIME => self::OPTION_GROUP_DATE,
];
- return isset($optionGroupsToTypes[$type]) ? $optionGroupsToTypes[$type] : '';
+ return $optionGroupsToTypes[$type] ?? '';
}
/**
@@ -377,6 +388,11 @@ public function beforeSave()
}
}
}
+
+ if ($this->getGroupByType($this->getData('type')) === self::OPTION_GROUP_FILE) {
+ $this->cleanFileExtensions();
+ }
+
return $this;
}
@@ -386,20 +402,21 @@ public function beforeSave()
*/
public function afterSave()
{
- $this->getValueInstance()->unsetValues();
$values = $this->getValues() ?: $this->getData('values');
if (is_array($values)) {
foreach ($values as $value) {
- if ($value instanceof \Magento\Catalog\Api\Data\ProductCustomOptionValuesInterface) {
+ if ($value instanceof ProductCustomOptionValuesInterface) {
$data = $value->getData();
} else {
$data = $value;
}
- $this->getValueInstance()->addValue($data);
- }
- $this->getValueInstance()->setOption($this)->saveValues();
- } elseif ($this->getGroupByType($this->getType()) == self::OPTION_GROUP_SELECT) {
+ $this->customOptionValuesFactory->create()
+ ->addValue($data)
+ ->setOption($this)
+ ->saveValues();
+ }
+ } elseif ($this->getGroupByType($this->getType()) === self::OPTION_GROUP_SELECT) {
throw new LocalizedException(__('Select type options required values rows.'));
}
@@ -800,7 +817,7 @@ public function setImageSizeY($imageSizeY)
}
/**
- * @param \Magento\Catalog\Api\Data\ProductCustomOptionValuesInterface[] $values
+ * @param ProductCustomOptionValuesInterface[] $values
* @return $this
*/
public function setValues(array $values = null)
@@ -901,5 +918,22 @@ private function getMetadataPool()
return $this->metadataPool;
}
+ /**
+ * Clears all non-accepted characters from file_extension field.
+ *
+ * @return void
+ */
+ private function cleanFileExtensions()
+ {
+ $extensions = '';
+ $rawExtensions = $this->getFileExtension();
+ $matches = [];
+ preg_match_all('/(?[a-z0-9]+)/i', strtolower($rawExtensions), $matches);
+ if (!empty($matches)) {
+ $extensions = implode(', ', array_unique($matches['extensions']));
+ }
+ $this->setFileExtension($extensions);
+ }
+
//@codeCoverageIgnoreEnd
}
diff --git a/app/code/Magento/Catalog/Model/Product/Option/Type/Date.php b/app/code/Magento/Catalog/Model/Product/Option/Type/Date.php
index cb6e76aebaadb..f6884a8d17f1f 100644
--- a/app/code/Magento/Catalog/Model/Product/Option/Type/Date.php
+++ b/app/code/Magento/Catalog/Model/Product/Option/Type/Date.php
@@ -101,11 +101,11 @@ public function validateUserValue($values)
$this->setUserValue(
[
'date' => isset($value['date']) ? $value['date'] : '',
- 'year' => isset($value['year']) ? intval($value['year']) : 0,
- 'month' => isset($value['month']) ? intval($value['month']) : 0,
- 'day' => isset($value['day']) ? intval($value['day']) : 0,
- 'hour' => isset($value['hour']) ? intval($value['hour']) : 0,
- 'minute' => isset($value['minute']) ? intval($value['minute']) : 0,
+ 'year' => isset($value['year']) ? (int)$value['year'] : 0,
+ 'month' => isset($value['month']) ? (int)$value['month'] : 0,
+ 'day' => isset($value['day']) ? (int)$value['day'] : 0,
+ 'hour' => isset($value['hour']) ? (int)$value['hour'] : 0,
+ 'minute' => isset($value['minute']) ? (int)$value['minute'] : 0,
'day_part' => isset($value['day_part']) ? $value['day_part'] : '',
'date_internal' => isset($value['date_internal']) ? $value['date_internal'] : '',
]
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 690809332de4a..0bb193107e65d 100644
--- a/app/code/Magento/Catalog/Model/Product/Option/Type/DefaultType.php
+++ b/app/code/Magento/Catalog/Model/Product/Option/Type/DefaultType.php
@@ -276,7 +276,7 @@ public function getFormattedOptionValue($optionValue)
*/
public function getCustomizedView($optionInfo)
{
- return isset($optionInfo['value']) ? $optionInfo['value'] : $optionInfo;
+ return $optionInfo['value'] ?? $optionInfo;
}
/**
@@ -338,7 +338,7 @@ public function getOptionPrice($optionValue, $basePrice)
{
$option = $this->getOption();
- return $this->_getChargableOptionPrice($option->getPrice(), $option->getPriceType() == 'percent', $basePrice);
+ return $this->_getChargeableOptionPrice($option->getPrice(), $option->getPriceType() == 'percent', $basePrice);
}
/**
@@ -392,14 +392,27 @@ public function getProductOptions()
}
/**
- * Return final chargable price for option
- *
* @param float $price Price of option
* @param boolean $isPercent Price type - percent or fixed
* @param float $basePrice For percent price type
* @return float
+ * @deprecated 102.0.4 typo in method name
+ * @see _getChargeableOptionPrice
*/
protected function _getChargableOptionPrice($price, $isPercent, $basePrice)
+ {
+ return $this->_getChargeableOptionPrice($price, $isPercent, $basePrice);
+ }
+
+ /**
+ * Return final chargeable price for option
+ *
+ * @param float $price Price of option
+ * @param boolean $isPercent Price type - percent or fixed
+ * @param float $basePrice For percent price type
+ * @return float
+ */
+ protected function _getChargeableOptionPrice($price, $isPercent, $basePrice)
{
if ($isPercent) {
return $basePrice * $price / 100;
diff --git a/app/code/Magento/Catalog/Model/Product/Option/Type/File.php b/app/code/Magento/Catalog/Model/Product/Option/Type/File.php
index a7304c9b67bb2..26e6a92852720 100644
--- a/app/code/Magento/Catalog/Model/Product/Option/Type/File.php
+++ b/app/code/Magento/Catalog/Model/Product/Option/Type/File.php
@@ -126,7 +126,7 @@ public function __construct(
$this->mediaDirectory = $this->filesystem->getDirectoryWrite(DirectoryList::MEDIA);
$this->validatorInfo = $validatorInfo;
$this->validatorFile = $validatorFile;
- $this->serializer = $serializer ? $serializer : ObjectManager::getInstance()->get(Json::class);
+ $this->serializer = $serializer ?: ObjectManager::getInstance()->get(Json::class);
parent::__construct($checkoutSession, $scopeConfig, $data);
}
diff --git a/app/code/Magento/Catalog/Model/Product/Option/Type/File/ExistingValidate.php b/app/code/Magento/Catalog/Model/Product/Option/Type/File/ExistingValidate.php
new file mode 100644
index 0000000000000..8d4aea135eabb
--- /dev/null
+++ b/app/code/Magento/Catalog/Model/Product/Option/Type/File/ExistingValidate.php
@@ -0,0 +1,50 @@
+_messages = [];
+ $this->_errors = [];
+
+ if (!is_string($value)) {
+ $this->_messages[] = __('Full file path is expected.')->render();
+ return false;
+ }
+
+ $result = true;
+ $fileInfo = null;
+ if ($originalName) {
+ $fileInfo = ['name' => $originalName];
+ }
+ foreach ($this->_validators as $element) {
+ $validator = $element['instance'];
+ if ($validator->isValid($value, $fileInfo)) {
+ continue;
+ }
+ $result = false;
+ $messages = $validator->getMessages();
+ $this->_messages = array_merge($this->_messages, $messages);
+ $this->_errors = array_merge($this->_errors, array_keys($messages));
+ if ($element['breakChainOnFailure']) {
+ break;
+ }
+ }
+ return $result;
+ }
+}
diff --git a/app/code/Magento/Catalog/Model/Product/Option/Type/File/ValidateFactory.php b/app/code/Magento/Catalog/Model/Product/Option/Type/File/ValidateFactory.php
index 32c901afe8e74..c0d10c720f6f6 100644
--- a/app/code/Magento/Catalog/Model/Product/Option/Type/File/ValidateFactory.php
+++ b/app/code/Magento/Catalog/Model/Product/Option/Type/File/ValidateFactory.php
@@ -13,6 +13,6 @@ class ValidateFactory
*/
public function create()
{
- return new \Zend_Validate();
+ return new ExistingValidate();
}
}
diff --git a/app/code/Magento/Catalog/Model/Product/Option/Type/File/ValidatorFile.php b/app/code/Magento/Catalog/Model/Product/Option/Type/File/ValidatorFile.php
index af6c4dba784f0..49e062c5fd465 100644
--- a/app/code/Magento/Catalog/Model/Product/Option/Type/File/ValidatorFile.php
+++ b/app/code/Magento/Catalog/Model/Product/Option/Type/File/ValidatorFile.php
@@ -10,6 +10,8 @@
use Magento\Framework\App\Filesystem\DirectoryList;
use Magento\Catalog\Model\Product\Exception as ProductException;
use Magento\Framework\Exception\LocalizedException;
+use Magento\Framework\Math\Random;
+use Magento\Framework\App\ObjectManager;
/**
* @SuppressWarnings(PHPMD.CouplingBetweenObjects)
@@ -62,12 +64,18 @@ class ValidatorFile extends Validator
*/
protected $isImageValidator;
+ /**
+ * @var Random
+ */
+ private $random;
+
/**
* @param \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig
* @param \Magento\Framework\Filesystem $filesystem
* @param \Magento\Framework\File\Size $fileSize
* @param \Magento\Framework\HTTP\Adapter\FileTransferFactory $httpFactory
* @param \Magento\Framework\Validator\File\IsImage $isImageValidator
+ * @param Random|null $random
* @throws \Magento\Framework\Exception\FileSystemException
*/
public function __construct(
@@ -75,12 +83,15 @@ public function __construct(
\Magento\Framework\Filesystem $filesystem,
\Magento\Framework\File\Size $fileSize,
\Magento\Framework\HTTP\Adapter\FileTransferFactory $httpFactory,
- \Magento\Framework\Validator\File\IsImage $isImageValidator
+ \Magento\Framework\Validator\File\IsImage $isImageValidator,
+ Random $random = null
) {
$this->mediaDirectory = $filesystem->getDirectoryWrite(DirectoryList::MEDIA);
$this->filesystem = $filesystem;
$this->httpFactory = $httpFactory;
$this->isImageValidator = $isImageValidator;
+ $this->random = $random
+ ?? ObjectManager::getInstance()->get(Random::class);
parent::__construct($scopeConfig, $filesystem, $fileSize);
}
@@ -147,16 +158,15 @@ public function validate($processingParams, $option)
$userValue = [];
if ($upload->isUploaded($file) && $upload->isValid($file)) {
- $extension = pathinfo(strtolower($fileInfo['name']), PATHINFO_EXTENSION);
-
$fileName = \Magento\MediaStorage\Model\File\Uploader::getCorrectFileName($fileInfo['name']);
- $dispersion = \Magento\MediaStorage\Model\File\Uploader::getDispretionPath($fileName);
+ $dispersion = \Magento\MediaStorage\Model\File\Uploader::getDispersionPath($fileName);
$filePath = $dispersion;
$tmpDirectory = $this->filesystem->getDirectoryRead(DirectoryList::SYS_TMP);
$fileHash = md5($tmpDirectory->readFile($tmpDirectory->getRelativePath($fileInfo['tmp_name'])));
- $filePath .= '/' . $fileHash . '.' . $extension;
+ $fileRandomName = $this->random->getRandomString(32);
+ $filePath .= '/' .$fileRandomName;
$fileFullPath = $this->mediaDirectory->getAbsolutePath($this->quotePath . $filePath);
$upload->addFilter(new \Zend_Filter_File_Rename(['target' => $fileFullPath, 'overwrite' => true]));
diff --git a/app/code/Magento/Catalog/Model/Product/Option/Type/File/ValidatorInfo.php b/app/code/Magento/Catalog/Model/Product/Option/Type/File/ValidatorInfo.php
index 30c3de932c3e6..b9946b99c5fdc 100644
--- a/app/code/Magento/Catalog/Model/Product/Option/Type/File/ValidatorInfo.php
+++ b/app/code/Magento/Catalog/Model/Product/Option/Type/File/ValidatorInfo.php
@@ -6,6 +6,9 @@
namespace Magento\Catalog\Model\Product\Option\Type\File;
+/**
+ * Validator for existing files.
+ */
class ValidatorInfo extends Validator
{
/**
@@ -90,7 +93,7 @@ public function validate($optionValue, $option)
}
$result = false;
- if ($validatorChain->isValid($this->fileFullPath)) {
+ if ($validatorChain->isValid($this->fileFullPath, $optionValue['title'])) {
$result = $this->rootDirectory->isReadable($this->fileRelativePath)
&& isset($optionValue['secret_key'])
&& $this->buildSecretKey($this->fileRelativePath) == $optionValue['secret_key'];
diff --git a/app/code/Magento/Catalog/Model/Product/Option/Type/Select.php b/app/code/Magento/Catalog/Model/Product/Option/Type/Select.php
index 78cce7bd76163..d14f56718d159 100644
--- a/app/code/Magento/Catalog/Model/Product/Option/Type/Select.php
+++ b/app/code/Magento/Catalog/Model/Product/Option/Type/Select.php
@@ -222,7 +222,7 @@ public function getOptionPrice($optionValue, $basePrice)
foreach (explode(',', $optionValue) as $value) {
$_result = $option->getValueById($value);
if ($_result) {
- $result += $this->_getChargableOptionPrice(
+ $result += $this->_getChargeableOptionPrice(
$_result->getPrice(),
$_result->getPriceType() == 'percent',
$basePrice
@@ -237,7 +237,7 @@ public function getOptionPrice($optionValue, $basePrice)
} elseif ($this->_isSingleSelection()) {
$_result = $option->getValueById($optionValue);
if ($_result) {
- $result = $this->_getChargableOptionPrice(
+ $result = $this->_getChargeableOptionPrice(
$_result->getPrice(),
$_result->getPriceType() == 'percent',
$basePrice
diff --git a/app/code/Magento/Catalog/Model/Product/Option/Type/Text.php b/app/code/Magento/Catalog/Model/Product/Option/Type/Text.php
index 79ee37c51671d..5624733831c1a 100644
--- a/app/code/Magento/Catalog/Model/Product/Option/Type/Text.php
+++ b/app/code/Magento/Catalog/Model/Product/Option/Type/Text.php
@@ -81,7 +81,7 @@ public function validateUserValue($values)
*/
public function prepareForCart()
{
- if ($this->getIsValid() && strlen($this->getUserValue()) > 0) {
+ if ($this->getIsValid() && ($this->getUserValue() !== '')) {
return $this->getUserValue();
} else {
return null;
diff --git a/app/code/Magento/Catalog/Model/Product/Option/Validator/DefaultValidator.php b/app/code/Magento/Catalog/Model/Product/Option/Validator/DefaultValidator.php
index 1e5c7f76d829b..73bbc9cc88d3d 100644
--- a/app/code/Magento/Catalog/Model/Product/Option/Validator/DefaultValidator.php
+++ b/app/code/Magento/Catalog/Model/Product/Option/Validator/DefaultValidator.php
@@ -106,7 +106,9 @@ protected function isValidOptionTitle($title, $storeId)
if ($storeId > \Magento\Store\Model\Store::DEFAULT_STORE_ID && $title === null) {
return true;
}
- if ($this->isEmpty($title)) {
+
+ // checking whether title is null and is empty string
+ if ($title === null || $title === '') {
return false;
}
@@ -132,7 +134,7 @@ protected function validateOptionType(Option $option)
*/
protected function validateOptionValue(Option $option)
{
- return $this->isInRange($option->getPriceType(), $this->priceTypes) && !$this->isNegative($option->getPrice());
+ return $this->isInRange($option->getPriceType(), $this->priceTypes);
}
/**
@@ -166,6 +168,6 @@ protected function isInRange($value, array $range)
*/
protected function isNegative($value)
{
- return intval($value) < 0;
+ return (int)$value < 0;
}
}
diff --git a/app/code/Magento/Catalog/Model/Product/Option/Validator/Pool.php b/app/code/Magento/Catalog/Model/Product/Option/Validator/Pool.php
index 1e00654249556..2256f031098f1 100644
--- a/app/code/Magento/Catalog/Model/Product/Option/Validator/Pool.php
+++ b/app/code/Magento/Catalog/Model/Product/Option/Validator/Pool.php
@@ -29,6 +29,6 @@ public function __construct(array $validators)
*/
public function get($type)
{
- return isset($this->validators[$type]) ? $this->validators[$type] : $this->validators['default'];
+ return $this->validators[$type] ?? $this->validators['default'];
}
}
diff --git a/app/code/Magento/Catalog/Model/Product/Option/Validator/Select.php b/app/code/Magento/Catalog/Model/Product/Option/Validator/Select.php
index f04ab497e1d4f..44756890b6ed7 100644
--- a/app/code/Magento/Catalog/Model/Product/Option/Validator/Select.php
+++ b/app/code/Magento/Catalog/Model/Product/Option/Validator/Select.php
@@ -83,7 +83,7 @@ protected function isValidOptionPrice($priceType, $price, $storeId)
if (!$priceType && !$price) {
return true;
}
- if (!$this->isInRange($priceType, $this->priceTypes) || $this->isNegative($price)) {
+ if (!$this->isInRange($priceType, $this->priceTypes)) {
return false;
}
diff --git a/app/code/Magento/Catalog/Model/Product/Option/Value.php b/app/code/Magento/Catalog/Model/Product/Option/Value.php
index 0e86510ebcee7..e7c770d065824 100644
--- a/app/code/Magento/Catalog/Model/Product/Option/Value.php
+++ b/app/code/Magento/Catalog/Model/Product/Option/Value.php
@@ -11,6 +11,9 @@
use Magento\Catalog\Model\Product;
use Magento\Catalog\Model\Product\Option;
use Magento\Framework\Model\AbstractModel;
+use Magento\Catalog\Pricing\Price\BasePrice;
+use Magento\Catalog\Pricing\Price\CustomOptionPriceCalculator;
+use Magento\Catalog\Pricing\Price\RegularPrice;
/**
* Catalog product option select type model
@@ -20,6 +23,9 @@
* @method \Magento\Catalog\Model\Product\Option\Value setOptionId(int $value)
*
* @SuppressWarnings(PHPMD.LongVariable)
+ * @SuppressWarnings(PHPMD.CouplingBetweenObjects) - added use of constants instead of string literals:
+ * BasePrice::PRICE_CODE - instead of 'base_price'
+ * RegularPrice::PRICE_CODE - instead of 'regular_price'
* @since 100.0.2
*/
class Value extends AbstractModel implements \Magento\Catalog\Api\Data\ProductCustomOptionValuesInterface
@@ -60,6 +66,11 @@ class Value extends AbstractModel implements \Magento\Catalog\Api\Data\ProductCu
*/
protected $_valueCollectionFactory;
+ /**
+ * @var CustomOptionPriceCalculator
+ */
+ private $customOptionPriceCalculator;
+
/**
* @param \Magento\Framework\Model\Context $context
* @param \Magento\Framework\Registry $registry
@@ -67,6 +78,7 @@ class Value extends AbstractModel implements \Magento\Catalog\Api\Data\ProductCu
* @param \Magento\Framework\Model\ResourceModel\AbstractResource $resource
* @param \Magento\Framework\Data\Collection\AbstractDb $resourceCollection
* @param array $data
+ * @param CustomOptionPriceCalculator|null $customOptionPriceCalculator
*/
public function __construct(
\Magento\Framework\Model\Context $context,
@@ -74,9 +86,13 @@ public function __construct(
\Magento\Catalog\Model\ResourceModel\Product\Option\Value\CollectionFactory $valueCollectionFactory,
\Magento\Framework\Model\ResourceModel\AbstractResource $resource = null,
\Magento\Framework\Data\Collection\AbstractDb $resourceCollection = null,
- array $data = []
+ array $data = [],
+ CustomOptionPriceCalculator $customOptionPriceCalculator = null
) {
$this->_valueCollectionFactory = $valueCollectionFactory;
+ $this->customOptionPriceCalculator = $customOptionPriceCalculator
+ ?? \Magento\Framework\App\ObjectManager::getInstance()->get(CustomOptionPriceCalculator::class);
+
parent::__construct(
$context,
$registry,
@@ -178,7 +194,7 @@ public function setProduct($product)
*/
public function getProduct()
{
- if (is_null($this->_product)) {
+ if ($this->_product === null) {
$this->_product = $this->getOption()->getProduct();
}
return $this->_product;
@@ -189,18 +205,15 @@ public function getProduct()
*/
public function saveValues()
{
+ $option = $this->getOption();
+
foreach ($this->getValues() as $value) {
- $this->setData(
- $value
- )->setData(
- 'option_id',
- $this->getOption()->getId()
- )->setData(
- 'store_id',
- $this->getOption()->getStoreId()
- );
-
- if ($this->getData('is_delete') == '1') {
+ $this->isDeleted(false);
+ $this->setData($value)
+ ->setData('option_id', $option->getId())
+ ->setData('store_id', $option->getStoreId());
+
+ if ((bool) $this->getData('is_delete') === true) {
if ($this->getId()) {
$this->deleteValues($this->getId());
$this->delete();
@@ -209,7 +222,7 @@ public function saveValues()
$this->save();
}
}
- //eof foreach()
+
return $this;
}
@@ -222,10 +235,8 @@ public function saveValues()
*/
public function getPrice($flag = false)
{
- if ($flag && $this->getPriceType() == self::TYPE_PERCENT) {
- $basePrice = $this->getOption()->getProduct()->getFinalPrice();
- $price = $basePrice * ($this->_getData(self::KEY_PRICE) / 100);
- return $price;
+ if ($flag) {
+ return $this->customOptionPriceCalculator->getOptionPriceByPriceCode($this, BasePrice::PRICE_CODE);
}
return $this->_getData(self::KEY_PRICE);
}
@@ -237,12 +248,7 @@ public function getPrice($flag = false)
*/
public function getRegularPrice()
{
- if ($this->getPriceType() == self::TYPE_PERCENT) {
- $basePrice = $this->getOption()->getProduct()->getPriceInfo()->getPrice('regular_price')->getAmount()->getValue();
- $price = $basePrice * ($this->_getData(self::KEY_PRICE) / 100);
- return $price;
- }
- return $this->_getData(self::KEY_PRICE);
+ return $this->customOptionPriceCalculator->getOptionPriceByPriceCode($this, RegularPrice::PRICE_CODE);
}
/**
diff --git a/app/code/Magento/Catalog/Model/Product/Price/TierPriceStorage.php b/app/code/Magento/Catalog/Model/Product/Price/TierPriceStorage.php
index 3bb6bba69bfb4..1bddd2d07cd81 100644
--- a/app/code/Magento/Catalog/Model/Product/Price/TierPriceStorage.php
+++ b/app/code/Magento/Catalog/Model/Product/Price/TierPriceStorage.php
@@ -172,8 +172,10 @@ private function getExistingPrices(array $skus, $groupBySku = false)
$rawPrices = $this->tierPricePersistence->get($ids);
$prices = [];
+ $linkField = $this->tierPricePersistence->getEntityLinkField();
+ $skuByIdLookup = $this->buildSkuByIdLookup($skus);
foreach ($rawPrices as $rawPrice) {
- $sku = $this->retrieveSkuById($rawPrice[$this->tierPricePersistence->getEntityLinkField()], $skus);
+ $sku = $skuByIdLookup[$rawPrice[$linkField]];
$price = $this->tierPriceFactory->create($rawPrice, $sku);
if ($groupBySku) {
$prices[$sku][] = $price;
@@ -300,21 +302,21 @@ private function isCorrectPriceValue(array $existingPrice, array $price)
}
/**
- * Retrieve SKU by product ID.
+ * Generate lookup to retrieve SKU by product ID.
*
- * @param int $id
* @param array $skus
- * @return string|null
+ * @return array
*/
- private function retrieveSkuById($id, $skus)
+ private function buildSkuByIdLookup($skus)
{
+ $lookup = [];
foreach ($this->productIdLocator->retrieveProductIdsBySkus($skus) as $sku => $ids) {
- if (isset($ids[$id])) {
- return $sku;
+ foreach (array_keys($ids) as $id) {
+ $lookup[$id] = $sku;
}
}
- return null;
+ return $lookup;
}
/**
diff --git a/app/code/Magento/Catalog/Model/Product/PriceModifier.php b/app/code/Magento/Catalog/Model/Product/PriceModifier.php
index 4d81000501cff..ed9b3597c0dc9 100644
--- a/app/code/Magento/Catalog/Model/Product/PriceModifier.php
+++ b/app/code/Magento/Catalog/Model/Product/PriceModifier.php
@@ -46,11 +46,11 @@ public function removeTierPrice(\Magento\Catalog\Model\Product $product, $custom
foreach ($prices as $key => $tierPrice) {
if ($customerGroupId == 'all' && $tierPrice['price_qty'] == $qty
- && $tierPrice['all_groups'] == 1 && intval($tierPrice['website_id']) === intval($websiteId)
+ && $tierPrice['all_groups'] == 1 && (int)$tierPrice['website_id'] === (int)$websiteId
) {
unset($prices[$key]);
} elseif ($tierPrice['price_qty'] == $qty && $tierPrice['cust_group'] == $customerGroupId
- && intval($tierPrice['website_id']) === intval($websiteId)
+ && (int)$tierPrice['website_id'] === (int)$websiteId
) {
unset($prices[$key]);
}
diff --git a/app/code/Magento/Catalog/Model/Product/ProductList/Toolbar.php b/app/code/Magento/Catalog/Model/Product/ProductList/Toolbar.php
index c2046bea550e6..af0772251e235 100644
--- a/app/code/Magento/Catalog/Model/Product/ProductList/Toolbar.php
+++ b/app/code/Magento/Catalog/Model/Product/ProductList/Toolbar.php
@@ -102,6 +102,6 @@ public function getLimit()
public function getCurrentPage()
{
$page = (int) $this->request->getParam(self::PAGE_PARM_NAME);
- return $page ? $page : 1;
+ return $page ?: 1;
}
}
diff --git a/app/code/Magento/Catalog/Model/Product/TierPriceManagement.php b/app/code/Magento/Catalog/Model/Product/TierPriceManagement.php
index 7cecd2f37bb84..18dc49a852a94 100644
--- a/app/code/Magento/Catalog/Model/Product/TierPriceManagement.php
+++ b/app/code/Magento/Catalog/Model/Product/TierPriceManagement.php
@@ -181,7 +181,7 @@ public function getList($sku, $customerGroupId)
$prices = [];
foreach ($product->getData('tier_price') as $price) {
- if ((is_numeric($customerGroupId) && intval($price['cust_group']) === intval($customerGroupId))
+ if ((is_numeric($customerGroupId) && (int)$price['cust_group'] === (int)$customerGroupId)
|| ($customerGroupId === 'all' && $price['all_groups'])
) {
/** @var \Magento\Catalog\Api\Data\ProductTierPriceInterface $tierPrice */
diff --git a/app/code/Magento/Catalog/Model/Product/Type.php b/app/code/Magento/Catalog/Model/Product/Type.php
index dc3971397acb2..0bc5216d2d238 100644
--- a/app/code/Magento/Catalog/Model/Product/Type.php
+++ b/app/code/Magento/Catalog/Model/Product/Type.php
@@ -232,7 +232,7 @@ public function getOptions()
public function getOptionText($optionId)
{
$options = $this->getOptionArray();
- return isset($options[$optionId]) ? $options[$optionId] : null;
+ return $options[$optionId] ?? null;
}
/**
@@ -285,7 +285,7 @@ public function getTypesByPriority()
$types = $this->getTypes();
foreach ($types as $typeId => $typeInfo) {
- $priority = isset($typeInfo['index_priority']) ? abs(intval($typeInfo['index_priority'])) : 0;
+ $priority = isset($typeInfo['index_priority']) ? abs((int)$typeInfo['index_priority']) : 0;
if (!empty($typeInfo['composite'])) {
$compositePriority[$typeId] = $priority;
} else {
diff --git a/app/code/Magento/Catalog/Model/Product/Type/AbstractType.php b/app/code/Magento/Catalog/Model/Product/Type/AbstractType.php
index 33ff3ecccd4dd..83057ad00c668 100644
--- a/app/code/Magento/Catalog/Model/Product/Type/AbstractType.php
+++ b/app/code/Magento/Catalog/Model/Product/Type/AbstractType.php
@@ -251,7 +251,7 @@ public function getChildrenIds($parentId, $required = true)
}
/**
- * Retrieve parent ids array by requered child
+ * Retrieve parent ids array by required child
*
* @param int|array $childId
* @return array
@@ -291,13 +291,7 @@ public function attributesCompare($attributeOne, $attributeTwo)
$sortOne = $attributeOne->getGroupSortPath() * 1000 + $attributeOne->getSortPath() * 0.0001;
$sortTwo = $attributeTwo->getGroupSortPath() * 1000 + $attributeTwo->getSortPath() * 0.0001;
- if ($sortOne > $sortTwo) {
- return 1;
- } elseif ($sortOne < $sortTwo) {
- return -1;
- }
-
- return 0;
+ return $sortOne <=> $sortTwo;
}
/**
@@ -938,7 +932,7 @@ public function getForceChildItemQtyChanges($product)
*/
public function prepareQuoteItemQty($qty, $product)
{
- return floatval($qty);
+ return (float)$qty;
}
/**
diff --git a/app/code/Magento/Catalog/Model/Product/Type/Price.php b/app/code/Magento/Catalog/Model/Product/Type/Price.php
index aa28a3478ebf7..f6caa299d66d7 100644
--- a/app/code/Magento/Catalog/Model/Product/Type/Price.php
+++ b/app/code/Magento/Catalog/Model/Product/Type/Price.php
@@ -341,7 +341,7 @@ public function getTierPrice($qty, $product)
}
}
- return $prices ? $prices : [];
+ return $prices ?: [];
}
/**
@@ -474,14 +474,15 @@ public function getTierPriceCount($product)
*
* @param float $qty
* @param Product $product
+ *
* @return array|float
*/
- public function getFormatedTierPrice($qty, $product)
+ public function getFormattedTierPrice($qty, $product)
{
$price = $product->getTierPrice($qty);
if (is_array($price)) {
foreach (array_keys($price) as $index) {
- $price[$index]['formated_price'] = $this->priceCurrency->convertAndFormat(
+ $price[$index]['formatted_price'] = $this->priceCurrency->convertAndFormat(
$price[$index]['website_price']
);
}
@@ -492,15 +493,45 @@ public function getFormatedTierPrice($qty, $product)
return $price;
}
+ /**
+ * Get formatted by currency tier price
+ *
+ * @param float $qty
+ * @param Product $product
+ *
+ * @return array|float
+ *
+ * @deprecated
+ * @see getFormattedTierPrice()
+ */
+ public function getFormatedTierPrice($qty, $product)
+ {
+ return $this->getFormattedTierPrice($qty, $product);
+ }
+
+ /**
+ * Get formatted by currency product price
+ *
+ * @param Product $product
+ * @return array|float
+ */
+ public function getFormattedPrice($product)
+ {
+ return $this->priceCurrency->format($product->getFinalPrice());
+ }
+
/**
* Get formatted by currency product price
*
* @param Product $product
* @return array || float
+ *
+ * @deprecated
+ * @see getFormattedPrice()
*/
public function getFormatedPrice($product)
{
- return $this->priceCurrency->format($product->getFinalPrice());
+ return $this->getFormattedPrice($product);
}
/**
diff --git a/app/code/Magento/Catalog/Model/ProductCategoryList.php b/app/code/Magento/Catalog/Model/ProductCategoryList.php
index ae875453be938..68d2c0606e567 100644
--- a/app/code/Magento/Catalog/Model/ProductCategoryList.php
+++ b/app/code/Magento/Catalog/Model/ProductCategoryList.php
@@ -8,6 +8,9 @@
use Magento\Catalog\Model\Indexer\Category\Product\AbstractAction;
use Magento\Framework\DB\Select;
use Magento\Framework\DB\Sql\UnionExpression;
+use Magento\Framework\App\ObjectManager;
+use Magento\Store\Model\StoreManagerInterface;
+use Magento\Catalog\Model\Indexer\Category\Product\TableMaintainer;
/**
* Provides info about product categories.
@@ -29,16 +32,32 @@ class ProductCategoryList
*/
private $category;
+ /**
+ * @var StoreManagerInterface
+ */
+ private $storeManager;
+
+ /**
+ * @var TableMaintainer
+ */
+ private $tableMaintainer;
+
/**
* @param ResourceModel\Product $productResource
* @param ResourceModel\Category $category
+ * @param StoreManagerInterface $storeManager
+ * @param TableMaintainer|null $tableMaintainer
*/
public function __construct(
ResourceModel\Product $productResource,
- ResourceModel\Category $category
+ ResourceModel\Category $category,
+ StoreManagerInterface $storeManager = null,
+ TableMaintainer $tableMaintainer = null
) {
$this->productResource = $productResource;
$this->category = $category;
+ $this->tableMaintainer = $tableMaintainer ?: ObjectManager::getInstance()->get(TableMaintainer::class);
+ $this->storeManager = $storeManager ?: ObjectManager::getInstance()->get(StoreManagerInterface::class);
}
/**
@@ -50,14 +69,15 @@ public function __construct(
public function getCategoryIds($productId)
{
if (!isset($this->categoryIdList[$productId])) {
+ $unionTables[] = $this->getCategorySelect($productId, $this->category->getCategoryProductTable());
+ foreach ($this->storeManager->getStores() as $store) {
+ $unionTables[] = $this->getCategorySelect(
+ $productId,
+ $this->tableMaintainer->getMainTable($store->getId())
+ );
+ }
$unionSelect = new UnionExpression(
- [
- $this->getCategorySelect($productId, $this->category->getCategoryProductTable()),
- $this->getCategorySelect(
- $productId,
- $this->productResource->getTable(AbstractAction::MAIN_INDEX_TABLE)
- )
- ],
+ $unionTables,
Select::SQL_UNION_ALL
);
diff --git a/app/code/Magento/Catalog/Model/ProductLink/CollectionProvider.php b/app/code/Magento/Catalog/Model/ProductLink/CollectionProvider.php
index b635d854c9e2b..d397dc515db61 100644
--- a/app/code/Magento/Catalog/Model/ProductLink/CollectionProvider.php
+++ b/app/code/Magento/Catalog/Model/ProductLink/CollectionProvider.php
@@ -47,22 +47,20 @@ public function getCollection(\Magento\Catalog\Model\Product $product, $type)
$products = $this->providers[$type]->getLinkedProducts($product);
$converter = $this->converterPool->getConverter($type);
- $output = [];
$sorterItems = [];
foreach ($products as $item) {
- $output[$item->getId()] = $converter->convert($item);
+ $itemId = $item->getId();
+ $sorterItems[$itemId] = $converter->convert($item);
+ $sorterItems[$itemId]['position'] = $sorterItems[$itemId]['position'] ?? 0;
}
- foreach ($output as $item) {
- $itemPosition = $item['position'];
- if (!isset($sorterItems[$itemPosition])) {
- $sorterItems[$itemPosition] = $item;
- } else {
- $newPosition = $itemPosition + 1;
- $sorterItems[$newPosition] = $item;
- }
- }
- ksort($sorterItems);
+ usort($sorterItems, function ($itemA, $itemB) {
+ $posA = intval($itemA['position']);
+ $posB = intval($itemB['position']);
+
+ return $posA <=> $posB;
+ });
+
return $sorterItems;
}
}
diff --git a/app/code/Magento/Catalog/Model/ProductLink/Repository.php b/app/code/Magento/Catalog/Model/ProductLink/Repository.php
index f8dee9216ddcf..9571ab91c3278 100644
--- a/app/code/Magento/Catalog/Model/ProductLink/Repository.php
+++ b/app/code/Magento/Catalog/Model/ProductLink/Repository.php
@@ -10,6 +10,7 @@
use Magento\Catalog\Api\Data\ProductLinkExtensionFactory;
use Magento\Catalog\Model\Product\Initialization\Helper\ProductLinks as LinksInitializer;
use Magento\Catalog\Model\Product\LinkTypeProvider;
+use Magento\Framework\Api\SimpleDataObjectConverter;
use Magento\Framework\Exception\CouldNotSaveException;
use Magento\Framework\Exception\NoSuchEntityException;
use Magento\Framework\EntityManager\MetadataPool;
@@ -170,7 +171,7 @@ public function getList(\Magento\Catalog\Api\Data\ProductInterface $product)
foreach ($item['custom_attributes'] as $option) {
$name = $option['attribute_code'];
$value = $option['value'];
- $setterName = 'set'.ucfirst($name);
+ $setterName = 'set' . SimpleDataObjectConverter::snakeCaseToUpperCamelCase($name);
// Check if setter exists
if (method_exists($productLinkExtension, $setterName)) {
call_user_func([$productLinkExtension, $setterName], $value);
diff --git a/app/code/Magento/Catalog/Model/ProductRepository.php b/app/code/Magento/Catalog/Model/ProductRepository.php
index 4a72539e982f2..dd941680b8a4d 100644
--- a/app/code/Magento/Catalog/Model/ProductRepository.php
+++ b/app/code/Magento/Catalog/Model/ProductRepository.php
@@ -6,10 +6,11 @@
*/
namespace Magento\Catalog\Model;
+use Magento\Catalog\Api\Data\ProductExtension;
use Magento\Catalog\Api\Data\ProductInterface;
use Magento\Catalog\Model\Product\Gallery\MimeTypeExtensionMap;
use Magento\Catalog\Model\ResourceModel\Product\Collection;
-use Magento\Framework\Api\Data\ImageContentInterface;
+use Magento\Eav\Model\Entity\Attribute\Exception as AttributeException;
use Magento\Framework\Api\Data\ImageContentInterfaceFactory;
use Magento\Framework\Api\ImageContentValidatorInterface;
use Magento\Framework\Api\ImageProcessorInterface;
@@ -21,7 +22,7 @@
use Magento\Framework\Exception\InputException;
use Magento\Framework\Exception\LocalizedException;
use Magento\Framework\Exception\NoSuchEntityException;
-use Magento\Framework\Exception\StateException;
+use Magento\Framework\Exception\TemporaryState\CouldNotSaveException as TemporaryCouldNotSaveException;
use Magento\Framework\Exception\ValidatorException;
/**
@@ -116,11 +117,15 @@ class ProductRepository implements \Magento\Catalog\Api\ProductRepositoryInterfa
protected $fileSystem;
/**
+ * @deprecated
+ * @see \Magento\Catalog\Model\MediaGalleryProcessor
* @var ImageContentInterfaceFactory
*/
protected $contentFactory;
/**
+ * @deprecated
+ * @see \Magento\Catalog\Model\MediaGalleryProcessor
* @var ImageProcessorInterface
*/
protected $imageProcessor;
@@ -131,7 +136,7 @@ class ProductRepository implements \Magento\Catalog\Api\ProductRepositoryInterfa
protected $extensionAttributesJoinProcessor;
/**
- * @var \Magento\Catalog\Model\Product\Gallery\Processor
+ * @var ProductRepository\MediaGalleryProcessor
*/
protected $mediaGalleryProcessor;
@@ -232,26 +237,19 @@ public function __construct(
public function get($sku, $editMode = false, $storeId = null, $forceReload = false)
{
$cacheKey = $this->getCacheKey([$editMode, $storeId]);
- if (!isset($this->instances[$sku][$cacheKey]) || $forceReload) {
- $product = $this->productFactory->create();
-
+ $cachedProduct = $this->getProductFromLocalCache($sku, $cacheKey);
+ if ($cachedProduct === null || $forceReload) {
$productId = $this->resourceModel->getIdBySku($sku);
if (!$productId) {
throw new NoSuchEntityException(__('Requested product doesn\'t exist'));
}
- if ($editMode) {
- $product->setData('_edit_mode', true);
- }
- if ($storeId !== null) {
- $product->setData('store_id', $storeId);
- }
- $product->load($productId);
+
+ $product = $this->getById($productId, $editMode, $storeId, $forceReload);
+
$this->cacheProduct($cacheKey, $product);
+ $cachedProduct = $product;
}
- if (!isset($this->instances[$sku])) {
- $sku = trim($sku);
- }
- return $this->instances[$sku][$cacheKey];
+ return $cachedProduct;
}
/**
@@ -301,18 +299,18 @@ protected function getCacheKey($data)
* Add product to internal cache and truncate cache if it has more than cacheLimit elements.
*
* @param string $cacheKey
- * @param \Magento\Catalog\Api\Data\ProductInterface $product
+ * @param ProductInterface $product
* @return void
*/
- private function cacheProduct($cacheKey, \Magento\Catalog\Api\Data\ProductInterface $product)
+ private function cacheProduct($cacheKey, ProductInterface $product)
{
$this->instancesById[$product->getId()][$cacheKey] = $product;
- $this->instances[$product->getSku()][$cacheKey] = $product;
+ $this->saveProductInLocalCache($product, $cacheKey);
if ($this->cacheLimit && count($this->instances) > $this->cacheLimit) {
$offset = round($this->cacheLimit / -2);
$this->instancesById = array_slice($this->instancesById, $offset, null, true);
- $this->instances = array_slice($this->instances, $offset);
+ $this->instances = array_slice($this->instances, $offset, null, true);
}
}
@@ -321,7 +319,7 @@ private function cacheProduct($cacheKey, \Magento\Catalog\Api\Data\ProductInterf
*
* @param array $productData
* @param bool $createNew
- * @return \Magento\Catalog\Api\Data\ProductInterface|Product
+ * @return ProductInterface|Product
* @throws NoSuchEntityException
*/
protected function initializeProductData(array $productData, $createNew)
@@ -329,18 +327,23 @@ protected function initializeProductData(array $productData, $createNew)
unset($productData['media_gallery']);
if ($createNew) {
$product = $this->productFactory->create();
- if ($this->storeManager->hasSingleStore()) {
- $product->setWebsiteIds([$this->storeManager->getStore(true)->getWebsiteId()]);
+ $this->assignProductToWebsites($product);
+ if (isset($productData['price']) && !isset($productData['product_type'])) {
+ $product->setTypeId(Product\Type::TYPE_SIMPLE);
}
} else {
- unset($this->instances[$productData['sku']]);
- $product = $this->get($productData['sku']);
+ if (!empty($productData['id'])) {
+ unset($this->instancesById[$productData['id']]);
+ $product = $this->getById($productData['id']);
+ } else {
+ $this->removeProductFromLocalCache($productData['sku']);
+ $product = $this->get($productData['sku']);
+ }
}
foreach ($productData as $key => $value) {
$product->setData($key, $value);
}
- $this->assignProductToWebsites($product);
return $product;
}
@@ -351,80 +354,24 @@ protected function initializeProductData(array $productData, $createNew)
*/
private function assignProductToWebsites(\Magento\Catalog\Model\Product $product)
{
- $websiteIds = $product->getWebsiteIds();
-
- if (!$this->storeManager->hasSingleStore()) {
- $websiteIds = array_unique(
- array_merge(
- $websiteIds,
- [$this->storeManager->getStore()->getWebsiteId()]
- )
- );
- }
-
- if ($this->storeManager->getStore(true)->getCode() == \Magento\Store\Model\Store::ADMIN_CODE) {
+ if ($this->storeManager->getStore(true)->getCode() === \Magento\Store\Model\Store::ADMIN_CODE) {
$websiteIds = array_keys($this->storeManager->getWebsites());
+ } else {
+ $websiteIds = [$this->storeManager->getStore()->getWebsiteId()];
}
$product->setWebsiteIds($websiteIds);
}
- /**
- * @param ProductInterface $product
- * @param array $newEntry
- * @return $this
- * @throws InputException
- * @throws StateException
- * @throws \Magento\Framework\Exception\LocalizedException
- */
- protected function processNewMediaGalleryEntry(
- ProductInterface $product,
- array $newEntry
- ) {
- /** @var ImageContentInterface $contentDataObject */
- $contentDataObject = $newEntry['content'];
-
- /** @var \Magento\Catalog\Model\Product\Media\Config $mediaConfig */
- $mediaConfig = $product->getMediaConfig();
- $mediaTmpPath = $mediaConfig->getBaseTmpMediaPath();
-
- $relativeFilePath = $this->imageProcessor->processImageContent($mediaTmpPath, $contentDataObject);
- $tmpFilePath = $mediaConfig->getTmpMediaShortUrl($relativeFilePath);
-
- if (!$product->hasGalleryAttribute()) {
- throw new StateException(__('Requested product does not support images.'));
- }
-
- $imageFileUri = $this->getMediaGalleryProcessor()->addImage(
- $product,
- $tmpFilePath,
- isset($newEntry['types']) ? $newEntry['types'] : [],
- true,
- isset($newEntry['disabled']) ? $newEntry['disabled'] : true
- );
- // Update additional fields that are still empty after addImage call
- $this->getMediaGalleryProcessor()->updateImage(
- $product,
- $imageFileUri,
- [
- 'label' => $newEntry['label'],
- 'position' => $newEntry['position'],
- 'disabled' => $newEntry['disabled'],
- 'media_type' => $newEntry['media_type'],
- ]
- );
- return $this;
- }
-
/**
* Process product links, creating new links, updating and deleting existing links
*
- * @param \Magento\Catalog\Api\Data\ProductInterface $product
+ * @param ProductInterface $product
* @param \Magento\Catalog\Api\Data\ProductLinkInterface[] $newLinks
* @return $this
* @throws NoSuchEntityException
*/
- private function processLinks(\Magento\Catalog\Api\Data\ProductInterface $product, $newLinks)
+ private function processLinks(ProductInterface $product, $newLinks)
{
if ($newLinks === null) {
// If product links were not specified, don't do anything
@@ -474,94 +421,17 @@ private function processLinks(\Magento\Catalog\Api\Data\ProductInterface $produc
return $this;
}
- /**
- * Process Media gallery data before save product.
- *
- * Compare Media Gallery Entries Data with existing Media Gallery
- * * If Media entry has not value_id set it as new
- * * If Existing entry 'value_id' absent in Media Gallery set 'removed' flag
- * * Merge Existing and new media gallery
- *
- * @param ProductInterface $product contains only existing media gallery items
- * @param array $mediaGalleryEntries array which contains all media gallery items
- * @return $this
- * @throws InputException
- * @throws StateException
- * @SuppressWarnings(PHPMD.CyclomaticComplexity)
- */
- protected function processMediaGallery(ProductInterface $product, $mediaGalleryEntries)
- {
- $existingMediaGallery = $product->getMediaGallery('images');
- $newEntries = [];
- $entriesById = [];
- if (!empty($existingMediaGallery)) {
- foreach ($mediaGalleryEntries as $entry) {
- if (isset($entry['value_id'])) {
- $entriesById[$entry['value_id']] = $entry;
- } else {
- $newEntries[] = $entry;
- }
- }
- foreach ($existingMediaGallery as $key => &$existingEntry) {
- if (isset($entriesById[$existingEntry['value_id']])) {
- $updatedEntry = $entriesById[$existingEntry['value_id']];
- if ($updatedEntry['file'] === null) {
- unset($updatedEntry['file']);
- }
- $existingMediaGallery[$key] = array_merge($existingEntry, $updatedEntry);
- } else {
- //set the removed flag
- $existingEntry['removed'] = true;
- }
- }
- $product->setData('media_gallery', ["images" => $existingMediaGallery]);
- } else {
- $newEntries = $mediaGalleryEntries;
- }
-
- $this->getMediaGalleryProcessor()->clearMediaAttribute($product, array_keys($product->getMediaAttributes()));
- $images = $product->getMediaGallery('images');
- if ($images) {
- foreach ($images as $image) {
- if (!isset($image['removed']) && !empty($image['types'])) {
- $this->getMediaGalleryProcessor()->setMediaAttribute($product, $image['types'], $image['file']);
- }
- }
- }
-
- foreach ($newEntries as $newEntry) {
- if (!isset($newEntry['content'])) {
- throw new InputException(__('The image content is not valid.'));
- }
- /** @var ImageContentInterface $contentDataObject */
- $contentDataObject = $this->contentFactory->create()
- ->setName($newEntry['content']['data'][ImageContentInterface::NAME])
- ->setBase64EncodedData($newEntry['content']['data'][ImageContentInterface::BASE64_ENCODED_DATA])
- ->setType($newEntry['content']['data'][ImageContentInterface::TYPE]);
- $newEntry['content'] = $contentDataObject;
- $this->processNewMediaGalleryEntry($product, $newEntry);
-
- $finalGallery = $product->getData('media_gallery');
- $newEntryId = key(array_diff_key($product->getData('media_gallery')['images'], $entriesById));
- $newEntry = array_replace_recursive($newEntry, $finalGallery['images'][$newEntryId]);
- $entriesById[$newEntryId] = $newEntry;
- $finalGallery['images'][$newEntryId] = $newEntry;
- $product->setData('media_gallery', $finalGallery);
- }
- return $this;
- }
-
/**
* {@inheritdoc}
* @SuppressWarnings(PHPMD.CyclomaticComplexity)
* @SuppressWarnings(PHPMD.NPathComplexity)
*/
- public function save(\Magento\Catalog\Api\Data\ProductInterface $product, $saveOptions = false)
+ public function save(ProductInterface $product, $saveOptions = false)
{
$tierPrices = $product->getData('tier_price');
try {
- $existingProduct = $this->get($product->getSku());
+ $existingProduct = $product->getId() ? $this->getById($product->getId()) : $this->get($product->getSku());
$product->setData(
$this->resourceModel->getLinkField(),
@@ -570,24 +440,35 @@ public function save(\Magento\Catalog\Api\Data\ProductInterface $product, $saveO
if (!$product->hasData(Product::STATUS)) {
$product->setStatus($existingProduct->getStatus());
}
+
+ /** @var ProductExtension $extensionAttributes */
+ $extensionAttributes = $product->getExtensionAttributes();
+ if (empty($extensionAttributes->__toArray())) {
+ $product->setExtensionAttributes($existingProduct->getExtensionAttributes());
+ }
} catch (NoSuchEntityException $e) {
$existingProduct = null;
}
$productDataArray = $this->extensibleDataObjectConverter
- ->toNestedArray($product, [], \Magento\Catalog\Api\Data\ProductInterface::class);
+ ->toNestedArray($product, [], ProductInterface::class);
$productDataArray = array_replace($productDataArray, $product->getData());
$ignoreLinksFlag = $product->getData('ignore_links_flag');
$productLinks = null;
if (!$ignoreLinksFlag && $ignoreLinksFlag !== null) {
$productLinks = $product->getProductLinks();
}
- $productDataArray['store_id'] = (int)$this->storeManager->getStore()->getId();
+ if (!isset($productDataArray['store_id'])) {
+ $productDataArray['store_id'] = (int)$this->storeManager->getStore()->getId();
+ }
$product = $this->initializeProductData($productDataArray, empty($existingProduct));
$this->processLinks($product, $productLinks);
- if (isset($productDataArray['media_gallery'])) {
- $this->processMediaGallery($product, $productDataArray['media_gallery']['images']);
+ if (isset($productDataArray['media_gallery_entries'])) {
+ $this->getMediaGalleryProcessor()->processMediaGallery(
+ $product,
+ $productDataArray['media_gallery_entries']
+ );
}
if (!$product->getOptionsReadonly()) {
@@ -601,58 +482,26 @@ public function save(\Magento\Catalog\Api\Data\ProductInterface $product, $saveO
);
}
- try {
- if ($tierPrices !== null) {
- $product->setData('tier_price', $tierPrices);
- }
- unset($this->instances[$product->getSku()]);
- unset($this->instancesById[$product->getId()]);
- $this->resourceModel->save($product);
- } catch (ConnectionException $exception) {
- throw new \Magento\Framework\Exception\TemporaryState\CouldNotSaveException(
- __('Database connection error'),
- $exception,
- $exception->getCode()
- );
- } catch (DeadlockException $exception) {
- throw new \Magento\Framework\Exception\TemporaryState\CouldNotSaveException(
- __('Database deadlock found when trying to get lock'),
- $exception,
- $exception->getCode()
- );
- } catch (LockWaitException $exception) {
- throw new \Magento\Framework\Exception\TemporaryState\CouldNotSaveException(
- __('Database lock wait timeout exceeded'),
- $exception,
- $exception->getCode()
- );
- } catch (\Magento\Eav\Model\Entity\Attribute\Exception $exception) {
- throw \Magento\Framework\Exception\InputException::invalidFieldValue(
- $exception->getAttributeCode(),
- $product->getData($exception->getAttributeCode()),
- $exception
- );
- } catch (ValidatorException $e) {
- throw new CouldNotSaveException(__($e->getMessage()));
- } catch (LocalizedException $e) {
- throw $e;
- } catch (\Exception $e) {
- throw new \Magento\Framework\Exception\CouldNotSaveException(__('Unable to save product'), $e);
+ if ($tierPrices !== null) {
+ $product->setData('tier_price', $tierPrices);
}
- unset($this->instances[$product->getSku()]);
+
+ $this->saveProduct($product);
+ $this->removeProductFromLocalCache($product->getSku());
unset($this->instancesById[$product->getId()]);
+
return $this->get($product->getSku(), false, $product->getStoreId());
}
/**
* {@inheritdoc}
*/
- public function delete(\Magento\Catalog\Api\Data\ProductInterface $product)
+ public function delete(ProductInterface $product)
{
$sku = $product->getSku();
$productId = $product->getId();
try {
- unset($this->instances[$product->getSku()]);
+ $this->removeProductFromLocalCache($product->getSku());
unset($this->instancesById[$product->getId()]);
$this->resourceModel->delete($product);
} catch (ValidatorException $e) {
@@ -662,7 +511,7 @@ public function delete(\Magento\Catalog\Api\Data\ProductInterface $product)
__('Unable to remove product %1', $sku)
);
}
- unset($this->instances[$sku]);
+ $this->removeProductFromLocalCache($sku);
unset($this->instancesById[$productId]);
return true;
}
@@ -729,7 +578,7 @@ protected function addFilterGroupToCollection(
$fields = [];
$categoryFilter = [];
foreach ($filterGroup->getFilters() as $filter) {
- $conditionType = $filter->getConditionType() ? $filter->getConditionType() : 'eq';
+ $conditionType = $filter->getConditionType() ?: 'eq';
if ($filter->getField() == 'category_id') {
$categoryFilter[$conditionType][] = $filter->getValue();
@@ -759,13 +608,13 @@ public function cleanCache()
}
/**
- * @return Product\Gallery\Processor
+ * @return ProductRepository\MediaGalleryProcessor
*/
private function getMediaGalleryProcessor()
{
if (null === $this->mediaGalleryProcessor) {
$this->mediaGalleryProcessor = \Magento\Framework\App\ObjectManager::getInstance()
- ->get(\Magento\Catalog\Model\Product\Gallery\Processor::class);
+ ->get(ProductRepository\MediaGalleryProcessor::class);
}
return $this->mediaGalleryProcessor;
}
@@ -785,4 +634,104 @@ private function getCollectionProcessor()
}
return $this->collectionProcessor;
}
+
+ /**
+ * Gets product from the local cache by SKU.
+ *
+ * @param string $sku
+ * @param string $cacheKey
+ * @return Product|null
+ */
+ private function getProductFromLocalCache(string $sku, string $cacheKey)
+ {
+ $preparedSku = $this->prepareSku($sku);
+ if (!isset($this->instances[$preparedSku])) {
+ return null;
+ }
+
+ return $this->instances[$preparedSku][$cacheKey] ?? null;
+ }
+
+ /**
+ * Removes product in the local cache.
+ *
+ * @param string $sku
+ * @return void
+ */
+ private function removeProductFromLocalCache(string $sku)
+ {
+ $preparedSku = $this->prepareSku($sku);
+ unset($this->instances[$preparedSku]);
+ }
+
+ /**
+ * Saves product in the local cache.
+ *
+ * @param Product $product
+ * @param string $cacheKey
+ */
+ private function saveProductInLocalCache(Product $product, string $cacheKey)
+ {
+ $preparedSku = $this->prepareSku($product->getSku());
+ $this->instances[$preparedSku][$cacheKey] = $product;
+ }
+
+ /**
+ * Converts SKU to lower case and trims.
+ *
+ * @param string $sku
+ * @return string
+ */
+ private function prepareSku(string $sku): string
+ {
+ return mb_strtolower(trim($sku));
+ }
+
+ /**
+ * Save product resource model.
+ *
+ * @param ProductInterface|Product $product
+ * @throws TemporaryCouldNotSaveException
+ * @throws InputException
+ * @throws CouldNotSaveException
+ * @throws LocalizedException
+ */
+ private function saveProduct($product)
+ {
+ try {
+ $this->removeProductFromLocalCache($product->getSku());
+ unset($this->instancesById[$product->getId()]);
+ $this->resourceModel->save($product);
+ } catch (ConnectionException $exception) {
+ throw new TemporaryCouldNotSaveException(
+ __('Database connection error'),
+ $exception,
+ $exception->getCode()
+ );
+ } catch (DeadlockException $exception) {
+ throw new TemporaryCouldNotSaveException(
+ __('Database deadlock found when trying to get lock'),
+ $exception,
+ $exception->getCode()
+ );
+ } catch (LockWaitException $exception) {
+ throw new TemporaryCouldNotSaveException(
+ __('Database lock wait timeout exceeded'),
+ $exception,
+ $exception->getCode()
+ );
+ } catch (AttributeException $exception) {
+ throw InputException::invalidFieldValue(
+ $exception->getAttributeCode(),
+ $product->getData($exception->getAttributeCode()),
+ $exception
+ );
+ } catch (ValidatorException $e) {
+ throw new CouldNotSaveException(__($e->getMessage()));
+ } catch (LocalizedException $e) {
+ throw $e;
+ } catch (\Exception $e) {
+ throw new CouldNotSaveException(__('Unable to save product'), $e);
+ }
+ }
}
diff --git a/app/code/Magento/Catalog/Model/ProductRepository/MediaGalleryProcessor.php b/app/code/Magento/Catalog/Model/ProductRepository/MediaGalleryProcessor.php
new file mode 100644
index 0000000000000..56d1cfda8bce5
--- /dev/null
+++ b/app/code/Magento/Catalog/Model/ProductRepository/MediaGalleryProcessor.php
@@ -0,0 +1,249 @@
+processor = $processor;
+ $this->contentFactory = $contentFactory;
+ $this->imageProcessor = $imageProcessor;
+ }
+
+ /**
+ * Process Media gallery data before save product.
+ *
+ * Compare Media Gallery Entries Data with existing Media Gallery
+ * * If Media entry has not value_id set it as new
+ * * If Existing entry 'value_id' absent in Media Gallery set 'removed' flag
+ * * Merge Existing and new media gallery
+ *
+ * @param ProductInterface $product contains only existing media gallery items.
+ * @param array $mediaGalleryEntries array which contains all media gallery items.
+ * @return void
+ * @throws InputException
+ * @throws StateException
+ * @throws LocalizedException
+ * @SuppressWarnings(PHPMD.CyclomaticComplexity)
+ */
+ public function processMediaGallery(ProductInterface $product, array $mediaGalleryEntries)
+ {
+ $existingMediaGallery = $product->getMediaGallery('images');
+ $newEntries = [];
+ $entriesById = [];
+ if (!empty($existingMediaGallery)) {
+ foreach ($mediaGalleryEntries as $entry) {
+ if (isset($entry['id'])) {
+ $entriesById[$entry['id']] = $entry;
+ } else {
+ $newEntries[] = $entry;
+ }
+ }
+ foreach ($existingMediaGallery as $key => &$existingEntry) {
+ if (isset($entriesById[$existingEntry['value_id']])) {
+ $updatedEntry = $entriesById[$existingEntry['value_id']];
+ if (array_key_exists('file', $updatedEntry) && $updatedEntry['file'] === null) {
+ unset($updatedEntry['file']);
+ }
+ $existingMediaGallery[$key] = array_merge($existingEntry, $updatedEntry);
+ } else {
+ //set the removed flag.
+ $existingEntry['removed'] = true;
+ }
+ }
+ unset($existingEntry);
+ $product->setData('media_gallery', ["images" => $existingMediaGallery]);
+ } else {
+ $newEntries = $mediaGalleryEntries;
+ }
+
+ $images = $product->getMediaGallery('images');
+
+ if ($images) {
+ $images = $this->determineImageRoles($product, $images);
+ }
+
+ $this->processor->clearMediaAttribute($product, array_keys($product->getMediaAttributes()));
+ if ($images) {
+ foreach ($images as $image) {
+ if (!isset($image['removed']) && !empty($image['types'])) {
+ $this->processor->setMediaAttribute($product, $image['types'], $image['file']);
+ }
+ }
+ }
+ $this->processEntries($product, $newEntries, $entriesById);
+ }
+
+ /**
+ * Ascertain image roles, if they are not set against the gallery entries
+ *
+ * @param ProductInterface $product
+ * @param array $images
+ * @return array
+ */
+ private function determineImageRoles(ProductInterface $product, array $images)
+ {
+ $imagesWithRoles = [];
+ foreach ($images as $image) {
+ if (!isset($image['types'])) {
+ $image['types'] = [];
+ if (isset($image['file'])) {
+ foreach (array_keys($product->getMediaAttributes()) as $attribute) {
+ if ($image['file'] == $product->getData($attribute)) {
+ $image['types'][] = $attribute;
+ }
+ }
+ }
+ }
+ $imagesWithRoles[] = $image;
+ }
+ return $imagesWithRoles;
+ }
+
+ /**
+ * Convert entries into product media gallery data and set to product.
+ *
+ * @param ProductInterface $product
+ * @param array $newEntries
+ * @param array $entriesById
+ * @throws InputException
+ * @throws LocalizedException
+ * @throws StateException
+ * @return void
+ */
+ private function processEntries(ProductInterface $product, array $newEntries, array $entriesById)
+ {
+ foreach ($newEntries as $newEntry) {
+ if (!isset($newEntry['content'])) {
+ throw new InputException(__('The image content is not valid.'));
+ }
+ /** @var ImageContentInterface $contentDataObject */
+ $contentDataObject = $this->contentFactory->create()
+ ->setName($newEntry['content'][ImageContentInterface::NAME])
+ ->setBase64EncodedData($newEntry['content'][ImageContentInterface::BASE64_ENCODED_DATA])
+ ->setType($newEntry['content'][ImageContentInterface::TYPE]);
+ $newEntry['content'] = $contentDataObject;
+ $this->processNewMediaGalleryEntry($product, $newEntry);
+
+ $finalGallery = $product->getData('media_gallery');
+ $newEntryId = key(array_diff_key($product->getData('media_gallery')['images'], $entriesById));
+ if (isset($newEntry['extension_attributes'])) {
+ $this->processExtensionAttributes($newEntry, $newEntry['extension_attributes']);
+ }
+ $newEntry = array_replace_recursive($newEntry, $finalGallery['images'][$newEntryId]);
+ $entriesById[$newEntryId] = $newEntry;
+ $finalGallery['images'][$newEntryId] = $newEntry;
+ $product->setData('media_gallery', $finalGallery);
+ }
+ }
+
+ /**
+ * Save gallery entry as image.
+ *
+ * @param ProductInterface $product
+ * @param array $newEntry
+ * @return void
+ * @throws InputException
+ * @throws StateException
+ * @throws \Magento\Framework\Exception\LocalizedException
+ */
+ private function processNewMediaGalleryEntry(
+ ProductInterface $product,
+ array $newEntry
+ ) {
+ /** @var ImageContentInterface $contentDataObject */
+ $contentDataObject = $newEntry['content'];
+
+ /** @var \Magento\Catalog\Model\Product\Media\Config $mediaConfig */
+ $mediaConfig = $product->getMediaConfig();
+ $mediaTmpPath = $mediaConfig->getBaseTmpMediaPath();
+
+ $relativeFilePath = $this->imageProcessor->processImageContent($mediaTmpPath, $contentDataObject);
+ $tmpFilePath = $mediaConfig->getTmpMediaShortUrl($relativeFilePath);
+
+ if (!$product->hasGalleryAttribute()) {
+ throw new StateException(__('Requested product does not support images.'));
+ }
+
+ $imageFileUri = $this->processor->addImage(
+ $product,
+ $tmpFilePath,
+ isset($newEntry['types']) ? $newEntry['types'] : [],
+ true,
+ isset($newEntry['disabled']) ? $newEntry['disabled'] : true
+ );
+ // Update additional fields that are still empty after addImage call.
+ $this->processor->updateImage(
+ $product,
+ $imageFileUri,
+ [
+ 'label' => $newEntry['label'],
+ 'position' => $newEntry['position'],
+ 'disabled' => $newEntry['disabled'],
+ 'media_type' => $newEntry['media_type'],
+ ]
+ );
+ }
+
+ /**
+ * Convert extension attribute for product media gallery.
+ *
+ * @param array $newEntry
+ * @param array $extensionAttributes
+ * @return void
+ */
+ private function processExtensionAttributes(array &$newEntry, array $extensionAttributes)
+ {
+ foreach ($extensionAttributes as $code => $value) {
+ if (is_array($value)) {
+ $this->processExtensionAttributes($newEntry, $value);
+ } else {
+ $newEntry[$code] = $value;
+ }
+ }
+ unset($newEntry['extension_attributes']);
+ }
+}
diff --git a/app/code/Magento/Catalog/Model/ResourceModel/AbstractResource.php b/app/code/Magento/Catalog/Model/ResourceModel/AbstractResource.php
index 1296ca62be7ef..12009e62fd27e 100644
--- a/app/code/Magento/Catalog/Model/ResourceModel/AbstractResource.php
+++ b/app/code/Magento/Catalog/Model/ResourceModel/AbstractResource.php
@@ -177,7 +177,10 @@ protected function _prepareLoadSelect(array $selects)
protected function _saveAttributeValue($object, $attribute, $value)
{
$connection = $this->getConnection();
- $storeId = (int) $this->_storeManager->getStore($object->getStoreId())->getId();
+ $hasSingleStore = $this->_storeManager->hasSingleStore();
+ $storeId = $hasSingleStore
+ ? $this->getDefaultStoreId()
+ : (int) $this->_storeManager->getStore($object->getStoreId())->getId();
$table = $attribute->getBackend()->getTable();
/**
@@ -186,15 +189,18 @@ protected function _saveAttributeValue($object, $attribute, $value)
* In this case we clear all not default values
*/
$entityIdField = $this->getLinkField();
- if ($this->_storeManager->hasSingleStore()) {
- $storeId = $this->getDefaultStoreId();
+ $conditions = [
+ 'attribute_id = ?' => $attribute->getAttributeId(),
+ "{$entityIdField} = ?" => $object->getData($entityIdField),
+ 'store_id <> ?' => $storeId
+ ];
+ if ($hasSingleStore
+ && !$object->isObjectNew()
+ && $this->isAttributePresentForNonDefaultStore($attribute, $conditions)
+ ) {
$connection->delete(
$table,
- [
- 'attribute_id = ?' => $attribute->getAttributeId(),
- "{$entityIdField} = ?" => $object->getData($entityIdField),
- 'store_id <> ?' => $storeId
- ]
+ $conditions
);
}
@@ -233,6 +239,27 @@ protected function _saveAttributeValue($object, $attribute, $value)
return $this;
}
+ /**
+ * Check if attribute present for non default Store View.
+ * Prevent "delete" query locking in a case when nothing to delete
+ *
+ * @param AbstractAttribute $attribute
+ * @param array $conditions
+ *
+ * @return boolean
+ */
+ private function isAttributePresentForNonDefaultStore($attribute, $conditions)
+ {
+ $connection = $this->getConnection();
+ $select = $connection->select()->from($attribute->getBackend()->getTable());
+ foreach ($conditions as $condition => $conditionValue) {
+ $select->where($condition, $conditionValue);
+ }
+ $select->limit(1);
+
+ return !empty($connection->fetchRow($select));
+ }
+
/**
* Insert entity attribute value
*
@@ -568,8 +595,7 @@ public function getAttributeRawValue($entityId, $attribute, $store)
}
if (is_array($attributesData) && sizeof($attributesData) == 1) {
- $_data = each($attributesData);
- $attributesData = $_data[1];
+ $attributesData = array_shift($attributesData);
}
return $attributesData === false ? false : $attributesData;
diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Attribute.php b/app/code/Magento/Catalog/Model/ResourceModel/Attribute.php
index bdb3cdab617ac..8457e5d0eaa5c 100644
--- a/app/code/Magento/Catalog/Model/ResourceModel/Attribute.php
+++ b/app/code/Magento/Catalog/Model/ResourceModel/Attribute.php
@@ -141,19 +141,17 @@ public function deleteEntity(\Magento\Framework\Model\AbstractModel $object)
->getMetadata(ProductInterface::class)
->getLinkField();
- $select = $this->getConnection()->select()->from(
- $attribute->getEntity()->getEntityTable(),
- $linkField
- )->where(
- 'attribute_set_id = ?',
- $result['attribute_set_id']
- );
+ $backendLinkField = $attribute->getBackend()->getEntityIdField();
- $clearCondition = [
- 'attribute_id =?' => $attribute->getId(),
- $linkField . ' IN (?)' => $select,
- ];
- $this->getConnection()->delete($backendTable, $clearCondition);
+ $select = $this->getConnection()->select()
+ ->from(['b' => $backendTable])
+ ->join(
+ ['e' => $attribute->getEntity()->getEntityTable()],
+ "b.$backendLinkField = e.$linkField"
+ )->where('b.attribute_id = ?', $attribute->getId())
+ ->where('e.attribute_set_id = ?', $result['attribute_set_id']);
+
+ $this->getConnection()->query($select->deleteFromSelect('b'));
}
}
diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Category.php b/app/code/Magento/Catalog/Model/ResourceModel/Category.php
index a9c705697b268..56b8a19d14255 100644
--- a/app/code/Magento/Catalog/Model/ResourceModel/Category.php
+++ b/app/code/Magento/Catalog/Model/ResourceModel/Category.php
@@ -11,6 +11,8 @@
*/
namespace Magento\Catalog\Model\ResourceModel;
+use Magento\Catalog\Model\Indexer\Category\Product\Processor;
+use Magento\Framework\DataObject;
use Magento\Framework\EntityManager\EntityManager;
/**
@@ -82,6 +84,11 @@ class Category extends AbstractResource
*/
protected $aggregateCount;
+ /**
+ * @var Processor
+ */
+ private $indexerProcessor;
+
/**
* Category constructor.
* @param \Magento\Eav\Model\Entity\Context $context
@@ -92,6 +99,7 @@ class Category extends AbstractResource
* @param Category\CollectionFactory $categoryCollectionFactory
* @param array $data
* @param \Magento\Framework\Serialize\Serializer\Json|null $serializer
+ * @param Processor|null $indexerProcessor
*/
public function __construct(
\Magento\Eav\Model\Entity\Context $context,
@@ -101,7 +109,8 @@ public function __construct(
\Magento\Catalog\Model\ResourceModel\Category\TreeFactory $categoryTreeFactory,
\Magento\Catalog\Model\ResourceModel\Category\CollectionFactory $categoryCollectionFactory,
$data = [],
- \Magento\Framework\Serialize\Serializer\Json $serializer = null
+ \Magento\Framework\Serialize\Serializer\Json $serializer = null,
+ Processor $indexerProcessor = null
) {
parent::__construct(
$context,
@@ -115,6 +124,8 @@ public function __construct(
$this->connectionName = 'catalog';
$this->serializer = $serializer ?: \Magento\Framework\App\ObjectManager::getInstance()
->get(\Magento\Framework\Serialize\Serializer\Json::class);
+ $this->indexerProcessor = $indexerProcessor ?: \Magento\Framework\App\ObjectManager::getInstance()
+ ->get(Processor::class);
}
/**
@@ -197,6 +208,19 @@ protected function _beforeDelete(\Magento\Framework\DataObject $object)
$this->deleteChildren($object);
}
+ /**
+ * Mark Category indexer as invalid to be picked up by cron.
+ *
+ * @param DataObject $object
+ * @return $this
+ */
+ protected function _afterDelete(DataObject $object): Category
+ {
+ $this->indexerProcessor->markIndexerAsInvalid();
+
+ return parent::_afterDelete($object);
+ }
+
/**
* Delete children categories of specific category
*
@@ -410,9 +434,18 @@ protected function _saveCategoryProducts($category)
* Update product positions in category
*/
if (!empty($update)) {
+ $newPositions = [];
foreach ($update as $productId => $position) {
- $where = ['category_id = ?' => (int)$id, 'product_id = ?' => (int)$productId];
- $bind = ['position' => (int)$position];
+ $delta = $position - $oldProducts[$productId];
+ if (!isset($newPositions[$delta])) {
+ $newPositions[$delta] = [];
+ }
+ $newPositions[$delta][] = $productId;
+ }
+
+ foreach ($newPositions as $delta => $productIds) {
+ $bind = ['position' => new \Zend_Db_Expr("position + ({$delta})")];
+ $where = ['category_id = ?' => (int)$id, 'product_id IN (?)' => $productIds];
$connection->update($this->getCategoryProductTable(), $bind, $where);
}
}
@@ -423,6 +456,8 @@ protected function _saveCategoryProducts($category)
'catalog_category_change_products',
['category' => $category, 'product_ids' => $productIds]
);
+
+ $category->setChangedProductIds($productIds);
}
if (!empty($insert) || !empty($update) || !empty($delete)) {
@@ -631,7 +666,7 @@ public function getProductCount($category)
$bind = ['category_id' => (int)$category->getId()];
$counts = $this->getConnection()->fetchOne($select, $bind);
- return intval($counts);
+ return (int)$counts;
}
/**
@@ -885,7 +920,7 @@ public function changeParent(
$childrenCount = $this->getChildrenCount($category->getId()) + 1;
$table = $this->getEntityTable();
$connection = $this->getConnection();
- $levelFiled = $connection->quoteIdentifier('level');
+ $levelField = $connection->quoteIdentifier('level');
$pathField = $connection->quoteIdentifier('path');
/**
@@ -925,7 +960,7 @@ public function changeParent(
$newPath . '/'
) . ')'
),
- 'level' => new \Zend_Db_Expr($levelFiled . ' + ' . $levelDisposition)
+ 'level' => new \Zend_Db_Expr($levelField . ' + ' . $levelDisposition)
],
[$pathField . ' LIKE ?' => $category->getPath() . '/%']
);
diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Category/Collection.php b/app/code/Magento/Catalog/Model/ResourceModel/Category/Collection.php
index eed1ab136cce1..6de2cf7788779 100644
--- a/app/code/Magento/Catalog/Model/ResourceModel/Category/Collection.php
+++ b/app/code/Magento/Catalog/Model/ResourceModel/Category/Collection.php
@@ -313,7 +313,7 @@ public function joinUrlRewrite()
['request_path'],
sprintf(
'{{table}}.is_autogenerated = 1 AND {{table}}.store_id = %d AND {{table}}.entity_type = \'%s\'',
- $this->_storeManager->getStore()->getId(),
+ $this->getStoreId(),
CategoryUrlRewriteGenerator::ENTITY_TYPE
),
'left'
diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Category/Flat.php b/app/code/Magento/Catalog/Model/ResourceModel/Category/Flat.php
index d7f8bd9d789a2..9db2c8248ce52 100644
--- a/app/code/Magento/Catalog/Model/ResourceModel/Category/Flat.php
+++ b/app/code/Magento/Catalog/Model/ResourceModel/Category/Flat.php
@@ -173,7 +173,7 @@ public function getMainTable()
public function getMainStoreTable($storeId = \Magento\Store\Model\Store::DEFAULT_STORE_ID)
{
if (is_string($storeId)) {
- $storeId = intval($storeId);
+ $storeId = (int) $storeId;
}
if ($storeId) {
@@ -602,9 +602,10 @@ public function isInRootCategoryList($category)
* @param \Magento\Catalog\Model\Category $category
* @param bool $recursive
* @param bool $isActive
+ * @param bool $sortByPosition
* @return array
*/
- public function getChildren($category, $recursive = true, $isActive = true)
+ public function getChildren($category, $recursive = true, $isActive = true, $sortByPosition = false)
{
$select = $this->getConnection()->select()->from(
$this->getMainStoreTable($category->getStoreId()),
@@ -619,6 +620,9 @@ public function getChildren($category, $recursive = true, $isActive = true)
if ($isActive) {
$select->where('is_active = ?', '1');
}
+ if ($sortByPosition) {
+ $select->order('position ASC');
+ }
$_categories = $this->getConnection()->fetchAll($select);
$categoriesIds = [];
foreach ($_categories as $_category) {
diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Category/StateDependentCollectionFactory.php b/app/code/Magento/Catalog/Model/ResourceModel/Category/StateDependentCollectionFactory.php
new file mode 100644
index 0000000000000..fc476ab6ff286
--- /dev/null
+++ b/app/code/Magento/Catalog/Model/ResourceModel/Category/StateDependentCollectionFactory.php
@@ -0,0 +1,55 @@
+objectManager = $objectManager;
+ $this->catalogCategoryFlatState = $catalogCategoryFlatState;
+ }
+
+ /**
+ * Create class instance with specified parameters
+ *
+ * @param array $data
+ * @return \Magento\Framework\Data\Collection\AbstractDb
+ */
+ public function create(array $data = [])
+ {
+ return $this->objectManager->create(
+ ($this->catalogCategoryFlatState->isAvailable()) ? Flat\Collection::class : Collection::class,
+ $data
+ );
+ }
+}
diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Collection/AbstractCollection.php b/app/code/Magento/Catalog/Model/ResourceModel/Collection/AbstractCollection.php
index 0d8c3992ddbb8..9ab863cde2704 100644
--- a/app/code/Magento/Catalog/Model/ResourceModel/Collection/AbstractCollection.php
+++ b/app/code/Magento/Catalog/Model/ResourceModel/Collection/AbstractCollection.php
@@ -140,7 +140,7 @@ public function getDefaultStoreId()
*
* @param string $table
* @param array|int $attributeIds
- * @return \Magento\Eav\Model\Entity\Collection\AbstractCollection
+ * @return \Magento\Framework\DB\Select
*/
protected function _getLoadAttributesSelect($table, $attributeIds = [])
{
diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Config.php b/app/code/Magento/Catalog/Model/ResourceModel/Config.php
index 7fb13265cd130..7b5d4e09a3599 100644
--- a/app/code/Magento/Catalog/Model/ResourceModel/Config.php
+++ b/app/code/Magento/Catalog/Model/ResourceModel/Config.php
@@ -149,8 +149,7 @@ public function getAttributesUsedForSortBy()
['main_table' => $this->getTable('eav_attribute')]
)->join(
['additional_table' => $this->getTable('catalog_eav_attribute')],
- 'main_table.attribute_id = additional_table.attribute_id',
- []
+ 'main_table.attribute_id = additional_table.attribute_id'
)->joinLeft(
['al' => $this->getTable('eav_attribute_label')],
'al.attribute_id = main_table.attribute_id AND al.store_id = ' . $this->getStoreId(),
diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Eav/Attribute.php b/app/code/Magento/Catalog/Model/ResourceModel/Eav/Attribute.php
index 4b3f81b551b30..f9ec6500b1635 100644
--- a/app/code/Magento/Catalog/Model/ResourceModel/Eav/Attribute.php
+++ b/app/code/Magento/Catalog/Model/ResourceModel/Eav/Attribute.php
@@ -239,11 +239,11 @@ public function afterSave()
*/
protected function _isEnabledInFlat()
{
- return $this->getData('backend_type') == 'static'
+ return $this->_getData('backend_type') == 'static'
|| $this->_productFlatIndexerHelper->isAddFilterableAttributes()
- && $this->getData('is_filterable') > 0
- || $this->getData('used_in_product_listing') == 1
- || $this->getData('used_for_sort_by') == 1;
+ && $this->_getData('is_filterable') > 0
+ || $this->_getData('used_in_product_listing') == 1
+ || $this->_getData('used_for_sort_by') == 1;
}
/**
@@ -341,7 +341,7 @@ public function getStoreId()
if ($dataObject) {
return $dataObject->getStoreId();
}
- return $this->getData('store_id');
+ return $this->_getData('store_id');
}
/**
@@ -366,7 +366,7 @@ public function getApplyTo()
*/
public function getSourceModel()
{
- $model = $this->getData('source_model');
+ $model = $this->_getData('source_model');
if (empty($model)) {
if ($this->getBackendType() == 'int' && $this->getFrontendInput() == 'select') {
return $this->_getDefaultSourceModel();
@@ -462,7 +462,7 @@ protected function _isOriginalIndexable()
$backendType = $this->getOrigData('backend_type');
$frontendInput = $this->getOrigData('frontend_input');
- if ($backendType == 'int' && $frontendInput == 'select') {
+ if ($backendType == 'int' && ($frontendInput == 'select' || $frontendInput == 'boolean')) {
return true;
} elseif ($backendType == 'varchar' && $frontendInput == 'multiselect') {
return true;
@@ -496,7 +496,7 @@ public function getIndexType()
*/
public function getIsWysiwygEnabled()
{
- return $this->getData(self::IS_WYSIWYG_ENABLED);
+ return $this->_getData(self::IS_WYSIWYG_ENABLED);
}
/**
@@ -504,7 +504,7 @@ public function getIsWysiwygEnabled()
*/
public function getIsHtmlAllowedOnFront()
{
- return $this->getData(self::IS_HTML_ALLOWED_ON_FRONT);
+ return $this->_getData(self::IS_HTML_ALLOWED_ON_FRONT);
}
/**
@@ -512,7 +512,7 @@ public function getIsHtmlAllowedOnFront()
*/
public function getUsedForSortBy()
{
- return $this->getData(self::USED_FOR_SORT_BY);
+ return $this->_getData(self::USED_FOR_SORT_BY);
}
/**
@@ -520,7 +520,7 @@ public function getUsedForSortBy()
*/
public function getIsFilterable()
{
- return $this->getData(self::IS_FILTERABLE);
+ return $this->_getData(self::IS_FILTERABLE);
}
/**
@@ -528,7 +528,7 @@ public function getIsFilterable()
*/
public function getIsFilterableInSearch()
{
- return $this->getData(self::IS_FILTERABLE_IN_SEARCH);
+ return $this->_getData(self::IS_FILTERABLE_IN_SEARCH);
}
/**
@@ -536,7 +536,7 @@ public function getIsFilterableInSearch()
*/
public function getIsUsedInGrid()
{
- return (bool)$this->getData(self::IS_USED_IN_GRID);
+ return (bool)$this->_getData(self::IS_USED_IN_GRID);
}
/**
@@ -544,7 +544,7 @@ public function getIsUsedInGrid()
*/
public function getIsVisibleInGrid()
{
- return (bool)$this->getData(self::IS_VISIBLE_IN_GRID);
+ return (bool)$this->_getData(self::IS_VISIBLE_IN_GRID);
}
/**
@@ -552,7 +552,7 @@ public function getIsVisibleInGrid()
*/
public function getIsFilterableInGrid()
{
- return (bool)$this->getData(self::IS_FILTERABLE_IN_GRID);
+ return (bool)$this->_getData(self::IS_FILTERABLE_IN_GRID);
}
/**
@@ -560,7 +560,7 @@ public function getIsFilterableInGrid()
*/
public function getPosition()
{
- return $this->getData(self::POSITION);
+ return $this->_getData(self::POSITION);
}
/**
@@ -568,7 +568,7 @@ public function getPosition()
*/
public function getIsSearchable()
{
- return $this->getData(self::IS_SEARCHABLE);
+ return $this->_getData(self::IS_SEARCHABLE);
}
/**
@@ -576,7 +576,7 @@ public function getIsSearchable()
*/
public function getIsVisibleInAdvancedSearch()
{
- return $this->getData(self::IS_VISIBLE_IN_ADVANCED_SEARCH);
+ return $this->_getData(self::IS_VISIBLE_IN_ADVANCED_SEARCH);
}
/**
@@ -584,7 +584,7 @@ public function getIsVisibleInAdvancedSearch()
*/
public function getIsComparable()
{
- return $this->getData(self::IS_COMPARABLE);
+ return $this->_getData(self::IS_COMPARABLE);
}
/**
@@ -592,7 +592,7 @@ public function getIsComparable()
*/
public function getIsUsedForPromoRules()
{
- return $this->getData(self::IS_USED_FOR_PROMO_RULES);
+ return $this->_getData(self::IS_USED_FOR_PROMO_RULES);
}
/**
@@ -600,7 +600,7 @@ public function getIsUsedForPromoRules()
*/
public function getIsVisibleOnFront()
{
- return $this->getData(self::IS_VISIBLE_ON_FRONT);
+ return $this->_getData(self::IS_VISIBLE_ON_FRONT);
}
/**
@@ -608,7 +608,7 @@ public function getIsVisibleOnFront()
*/
public function getUsedInProductListing()
{
- return $this->getData(self::USED_IN_PRODUCT_LISTING);
+ return $this->_getData(self::USED_IN_PRODUCT_LISTING);
}
/**
@@ -616,7 +616,7 @@ public function getUsedInProductListing()
*/
public function getIsVisible()
{
- return $this->getData(self::IS_VISIBLE);
+ return $this->_getData(self::IS_VISIBLE);
}
//@codeCoverageIgnoreEnd
diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Layer/Filter/Price.php b/app/code/Magento/Catalog/Model/ResourceModel/Layer/Filter/Price.php
index bed129e19168f..585da2af529a4 100644
--- a/app/code/Magento/Catalog/Model/ResourceModel/Layer/Filter/Price.php
+++ b/app/code/Magento/Catalog/Model/ResourceModel/Layer/Filter/Price.php
@@ -5,6 +5,15 @@
*/
namespace Magento\Catalog\Model\ResourceModel\Layer\Filter;
+use Magento\Framework\App\Http\Context;
+use Magento\Framework\App\ObjectManager;
+use Magento\Framework\Indexer\DimensionFactory;
+use Magento\Framework\Search\Request\IndexScopeResolverInterface;
+use Magento\Store\Model\StoreManagerInterface;
+use Magento\Customer\Model\Context as CustomerContext;
+use Magento\Customer\Model\Indexer\CustomerGroupDimensionProvider;
+use Magento\Store\Model\Indexer\WebsiteDimensionProvider;
+
/**
* Catalog Layer Price Filter resource model
*
@@ -41,6 +50,21 @@ class Price extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb
*/
private $storeManager;
+ /**
+ * @var IndexScopeResolverInterface|null
+ */
+ private $priceTableResolver;
+
+ /**
+ * @var Context
+ */
+ private $httpContext;
+
+ /**
+ * @var DimensionFactory|null
+ */
+ private $dimensionFactory;
+
/**
* @param \Magento\Framework\Model\ResourceModel\Db\Context $context
* @param \Magento\Framework\Event\ManagerInterface $eventManager
@@ -48,6 +72,9 @@ class Price extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb
* @param \Magento\Customer\Model\Session $session
* @param \Magento\Store\Model\StoreManagerInterface $storeManager
* @param null $connectionName
+ * @param IndexScopeResolverInterface|null $priceTableResolver
+ * @param Context|null $httpContext
+ * @param DimensionFactory|null $dimensionFactory
*/
public function __construct(
\Magento\Framework\Model\ResourceModel\Db\Context $context,
@@ -55,12 +82,19 @@ public function __construct(
\Magento\Catalog\Model\Layer\Resolver $layerResolver,
\Magento\Customer\Model\Session $session,
\Magento\Store\Model\StoreManagerInterface $storeManager,
- $connectionName = null
+ $connectionName = null,
+ IndexScopeResolverInterface $priceTableResolver = null,
+ Context $httpContext = null,
+ DimensionFactory $dimensionFactory = null
) {
$this->layer = $layerResolver->get();
$this->session = $session;
$this->storeManager = $storeManager;
$this->_eventManager = $eventManager;
+ $this->priceTableResolver = $priceTableResolver
+ ?? ObjectManager::getInstance()->get(IndexScopeResolverInterface::class);
+ $this->httpContext = $httpContext ?? ObjectManager::getInstance()->get(Context::class);
+ $this->dimensionFactory = $dimensionFactory ?? ObjectManager::getInstance()->get(DimensionFactory::class);
parent::__construct($context, $connectionName);
}
@@ -78,7 +112,7 @@ public function getCount($range)
/**
* Check and set correct variable values to prevent SQL-injections
*/
- $range = floatval($range);
+ $range = (float)$range;
if ($range == 0) {
$range = 1;
}
@@ -118,11 +152,8 @@ protected function _getSelect()
// remove join with main table
$fromPart = $select->getPart(\Magento\Framework\DB\Select::FROM);
- if (!isset(
- $fromPart[\Magento\Catalog\Model\ResourceModel\Product\Collection::INDEX_TABLE_ALIAS]
- ) || !isset(
- $fromPart[\Magento\Catalog\Model\ResourceModel\Product\Collection::MAIN_TABLE_ALIAS]
- )
+ if (!isset($fromPart[\Magento\Catalog\Model\ResourceModel\Product\Collection::INDEX_TABLE_ALIAS]) ||
+ !isset($fromPart[\Magento\Catalog\Model\ResourceModel\Product\Collection::MAIN_TABLE_ALIAS])
) {
return $select;
}
@@ -376,6 +407,30 @@ protected function _construct()
$this->_init('catalog_product_index_price', 'entity_id');
}
+ /**
+ * {@inheritdoc}
+ * @return string
+ */
+ public function getMainTable()
+ {
+ $storeKey = $this->httpContext->getValue(StoreManagerInterface::CONTEXT_STORE);
+ $priceTableName = $this->priceTableResolver->resolve(
+ 'catalog_product_index_price',
+ [
+ $this->dimensionFactory->create(
+ WebsiteDimensionProvider::DIMENSION_NAME,
+ (string)$this->storeManager->getStore($storeKey)->getWebsiteId()
+ ),
+ $this->dimensionFactory->create(
+ CustomerGroupDimensionProvider::DIMENSION_NAME,
+ (string)$this->httpContext->getValue(CustomerContext::CONTEXT_GROUP)
+ )
+ ]
+ );
+
+ return $this->getTable($priceTableName);
+ }
+
/**
* Retrieve joined price index table alias
*
diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product.php b/app/code/Magento/Catalog/Model/ResourceModel/Product.php
index a5fdc264aa19a..b4b78996f762f 100644
--- a/app/code/Magento/Catalog/Model/ResourceModel/Product.php
+++ b/app/code/Magento/Catalog/Model/ResourceModel/Product.php
@@ -7,6 +7,7 @@
use Magento\Catalog\Model\ResourceModel\Product\Website\Link as ProductWebsiteLink;
use Magento\Framework\App\ObjectManager;
+use Magento\Catalog\Model\Indexer\Category\Product\TableMaintainer;
/**
* Product entity resource model
@@ -83,6 +84,11 @@ class Product extends AbstractResource
*/
private $productCategoryLink;
+ /**
+ * @var TableMaintainer
+ */
+ private $tableMaintainer;
+
/**
* @param \Magento\Eav\Model\Entity\Context $context
* @param \Magento\Store\Model\StoreManagerInterface $storeManager
@@ -94,6 +100,7 @@ class Product extends AbstractResource
* @param \Magento\Eav\Model\Entity\TypeFactory $typeFactory
* @param \Magento\Catalog\Model\Product\Attribute\DefaultAttributes $defaultAttributes
* @param array $data
+ * @param TableMaintainer|null $tableMaintainer
*
* @SuppressWarnings(PHPMD.ExcessiveParameterList)
*/
@@ -107,7 +114,8 @@ public function __construct(
\Magento\Eav\Model\Entity\Attribute\SetFactory $setFactory,
\Magento\Eav\Model\Entity\TypeFactory $typeFactory,
\Magento\Catalog\Model\Product\Attribute\DefaultAttributes $defaultAttributes,
- $data = []
+ $data = [],
+ TableMaintainer $tableMaintainer = null
) {
$this->_categoryCollectionFactory = $categoryCollectionFactory;
$this->_catalogCategory = $catalogCategory;
@@ -122,6 +130,7 @@ public function __construct(
$data
);
$this->connectionName = 'catalog';
+ $this->tableMaintainer = $tableMaintainer ?: ObjectManager::getInstance()->get(TableMaintainer::class);
}
/**
@@ -366,22 +375,42 @@ public function getAvailableInCategories($object)
// fetching all parent IDs, including those are higher on the tree
$entityId = (int)$object->getEntityId();
if (!isset($this->availableCategoryIdsCache[$entityId])) {
- $this->availableCategoryIdsCache[$entityId] = $this->getConnection()->fetchCol(
- $this->getConnection()->select()->distinct()->from(
- $this->getTable('catalog_category_product_index'),
- ['category_id']
- )->where(
- 'product_id = ? AND is_parent = 1',
- $entityId
- )->where(
- 'visibility != ?',
- \Magento\Catalog\Model\Product\Visibility::VISIBILITY_NOT_VISIBLE
- )
+ foreach ($this->_storeManager->getStores() as $store) {
+ $unionTables[] = $this->getAvailableInCategoriesSelect(
+ $entityId,
+ $this->tableMaintainer->getMainTable($store->getId())
+ );
+ }
+ $unionSelect = new \Magento\Framework\DB\Sql\UnionExpression(
+ $unionTables,
+ \Magento\Framework\DB\Select::SQL_UNION_ALL
);
+ $this->availableCategoryIdsCache[$entityId] = array_unique($this->getConnection()->fetchCol($unionSelect));
}
return $this->availableCategoryIdsCache[$entityId];
}
+ /**
+ * Returns DB select for available categories.
+ *
+ * @param int $entityId
+ * @param string $tableName
+ * @return \Magento\Framework\DB\Select
+ */
+ private function getAvailableInCategoriesSelect($entityId, $tableName)
+ {
+ return $this->getConnection()->select()->distinct()->from(
+ $tableName,
+ ['category_id']
+ )->where(
+ 'product_id = ? AND is_parent = 1',
+ $entityId
+ )->where(
+ 'visibility != ?',
+ \Magento\Catalog\Model\Product\Visibility::VISIBILITY_NOT_VISIBLE
+ );
+ }
+
/**
* Get default attribute source model
*
@@ -402,7 +431,7 @@ public function getDefaultAttributeSourceModel()
public function canBeShowInCategory($product, $categoryId)
{
$select = $this->getConnection()->select()->from(
- $this->getTable('catalog_category_product_index'),
+ $this->tableMaintainer->getMainTable($product->getStoreId()),
'product_id'
)->where(
'product_id = ?',
diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/CategoryLink.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/CategoryLink.php
index 6e2642d09910a..b54c19a111508 100644
--- a/app/code/Magento/Catalog/Model/ResourceModel/Product/CategoryLink.php
+++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/CategoryLink.php
@@ -83,11 +83,12 @@ public function saveCategoryLinks(ProductInterface $product, array $categoryLink
$insertUpdate = $this->processCategoryLinks($categoryLinks, $oldCategoryLinks);
$deleteUpdate = $this->processCategoryLinks($oldCategoryLinks, $categoryLinks);
- list($delete, $insert) = $this->analyseUpdatedLinks($deleteUpdate, $insertUpdate);
+ list($delete, $insert, $update) = $this->analyseUpdatedLinks($deleteUpdate, $insertUpdate);
return array_merge(
- $this->updateCategoryLinks($product, $insert),
- $this->deleteCategoryLinks($product, $delete)
+ $this->deleteCategoryLinks($product, $delete),
+ $this->updateCategoryLinks($product, $insert, true),
+ $this->updateCategoryLinks($product, $update)
);
}
@@ -133,16 +134,15 @@ private function processCategoryLinks($newCategoryPositions, &$oldCategoryPositi
/**
* @param ProductInterface $product
* @param array $insertLinks
+ * @param bool $insert
* @return array
*/
- private function updateCategoryLinks(ProductInterface $product, array $insertLinks)
+ private function updateCategoryLinks(ProductInterface $product, array $insertLinks, $insert = false)
{
if (empty($insertLinks)) {
return [];
}
- $connection = $this->resourceConnection->getConnection();
-
$data = [];
foreach ($insertLinks as $categoryLink) {
$data[] = [
@@ -153,11 +153,22 @@ private function updateCategoryLinks(ProductInterface $product, array $insertLin
}
if ($data) {
- $connection->insertOnDuplicate(
- $this->getCategoryLinkMetadata()->getEntityTable(),
- $data,
- ['position']
- );
+ $connection = $this->resourceConnection->getConnection();
+ if ($insert) {
+ $connection->insertArray(
+ $this->getCategoryLinkMetadata()->getEntityTable(),
+ array_keys($data[0]),
+ $data,
+ \Magento\Framework\DB\Adapter\AdapterInterface::INSERT_IGNORE
+ );
+ } else {
+ // for mass update category links with constraint by unique key use insert on duplicate statement
+ $connection->insertOnDuplicate(
+ $this->getCategoryLinkMetadata()->getEntityTable(),
+ $data,
+ ['position']
+ );
+ }
}
return array_column($insertLinks, 'category_id');
@@ -215,7 +226,7 @@ private function verifyCategoryLinks(array $links)
}
/**
- * Analyse category links for update or/and delete
+ * Analyse category links for update or/and delete. Return array of links for delete, insert and update
*
* @param array $deleteUpdate
* @param array $insertUpdate
@@ -226,8 +237,7 @@ private function analyseUpdatedLinks($deleteUpdate, $insertUpdate)
$delete = $deleteUpdate['changed'] ?: [];
$insert = $insertUpdate['changed'] ?: [];
$insert = array_merge_recursive($insert, $deleteUpdate['updated']);
- $insert = array_merge_recursive($insert, $insertUpdate['updated']);
- return [$delete, $insert];
+ return [$delete, $insert, $insertUpdate['updated']];
}
}
diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/Collection.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/Collection.php
index 58e8424663c83..937b21a9c8421 100644
--- a/app/code/Magento/Catalog/Model/ResourceModel/Product/Collection.php
+++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/Collection.php
@@ -14,10 +14,15 @@
use Magento\Catalog\Model\ResourceModel\Product\Collection\ProductLimitationFactory;
use Magento\CatalogUrlRewrite\Model\ProductUrlRewriteGenerator;
use Magento\Customer\Api\GroupManagementInterface;
+use Magento\Customer\Model\Indexer\CustomerGroupDimensionProvider;
use Magento\Framework\App\ObjectManager;
use Magento\Framework\DB\Select;
use Magento\Framework\EntityManager\MetadataPool;
+use Magento\Catalog\Model\Indexer\Product\Price\PriceTableResolver;
+use Magento\Store\Model\Indexer\WebsiteDimensionProvider;
use Magento\Store\Model\Store;
+use Magento\Catalog\Model\Indexer\Category\Product\TableMaintainer;
+use Magento\Framework\Indexer\DimensionFactory;
/**
* Product collection
@@ -272,9 +277,23 @@ class Collection extends \Magento\Catalog\Model\ResourceModel\Collection\Abstrac
*/
private $backend;
+ /**
+ * @var TableMaintainer
+ */
+ private $tableMaintainer;
+
+ /**
+ * @var PriceTableResolver
+ */
+ private $priceTableResolver;
+
+ /**
+ * @var DimensionFactory
+ */
+ private $dimensionFactory;
+
/**
* Collection constructor
- *
* @param \Magento\Framework\Data\Collection\EntityFactory $entityFactory
* @param \Psr\Log\LoggerInterface $logger
* @param \Magento\Framework\Data\Collection\Db\FetchStrategyInterface $fetchStrategy
@@ -297,7 +316,9 @@ class Collection extends \Magento\Catalog\Model\ResourceModel\Collection\Abstrac
* @param \Magento\Framework\DB\Adapter\AdapterInterface|null $connection
* @param ProductLimitationFactory|null $productLimitationFactory
* @param MetadataPool|null $metadataPool
- *
+ * @param TableMaintainer|null $tableMaintainer
+ * @param PriceTableResolver|null $priceTableResolver
+ * @param DimensionFactory|null $dimensionFactory
* @SuppressWarnings(PHPMD.ExcessiveParameterList)
*/
public function __construct(
@@ -322,7 +343,10 @@ public function __construct(
GroupManagementInterface $groupManagement,
\Magento\Framework\DB\Adapter\AdapterInterface $connection = null,
ProductLimitationFactory $productLimitationFactory = null,
- MetadataPool $metadataPool = null
+ MetadataPool $metadataPool = null,
+ TableMaintainer $tableMaintainer = null,
+ PriceTableResolver $priceTableResolver = null,
+ DimensionFactory $dimensionFactory = null
) {
$this->moduleManager = $moduleManager;
$this->_catalogProductFlatState = $catalogProductFlatState;
@@ -352,6 +376,10 @@ public function __construct(
$storeManager,
$connection
);
+ $this->tableMaintainer = $tableMaintainer ?: ObjectManager::getInstance()->get(TableMaintainer::class);
+ $this->priceTableResolver = $priceTableResolver ?: ObjectManager::getInstance()->get(PriceTableResolver::class);
+ $this->dimensionFactory = $dimensionFactory
+ ?: ObjectManager::getInstance()->get(DimensionFactory::class);
}
/**
@@ -448,8 +476,7 @@ public function getFlatState()
}
/**
- * Retrieve is flat enabled flag
- * Return always false if magento run admin
+ * Retrieve is flat enabled. Return always false if magento run admin.
*
* @return bool
*/
@@ -477,8 +504,7 @@ protected function _construct()
}
/**
- * Standard resource collection initialization
- * Needed for child classes
+ * Standard resource collection initialization. Needed for child classes.
*
* @param string $model
* @param string $entityModel
@@ -517,8 +543,7 @@ protected function _prepareStaticFields()
}
/**
- * Retrieve collection empty item
- * Redeclared for specifying id field name without getting resource model inside model
+ * Get collection empty item. Redeclared for specifying id field name without getting resource model inside model.
*
* @return \Magento\Framework\DataObject
*/
@@ -604,8 +629,7 @@ public function _loadAttributes($printQuery = false, $logQuery = false)
}
/**
- * Add attribute to entities in collection
- * If $attribute=='*' select all attributes
+ * Add attribute to entities in collection. If $attribute=='*' select all attributes.
*
* @param array|string|integer|\Magento\Framework\App\Config\Element $attribute
* @param bool|string $joinType
@@ -641,8 +665,7 @@ public function addAttributeToSelect($attribute, $joinType = false)
}
/**
- * Processing collection items after loading
- * Adding url rewrites, minimal prices, final prices, tax percents
+ * Processing collection items after loading. Adding url rewrites, minimal prices, final prices, tax percents.
*
* @return $this
*/
@@ -653,6 +676,7 @@ protected function _afterLoad()
}
$this->_prepareUrlDataObject();
+ $this->prepareStoreId();
if (count($this)) {
$this->_eventManager->dispatch('catalog_product_collection_load_after', ['collection' => $this]);
@@ -661,6 +685,23 @@ protected function _afterLoad()
return $this;
}
+ /**
+ * Add Store ID to products from collection.
+ *
+ * @return $this
+ */
+ protected function prepareStoreId()
+ {
+ if ($this->getStoreId() !== null) {
+ /** @var $item \Magento\Catalog\Model\Product */
+ foreach ($this->_items as $item) {
+ $item->setStoreId($this->getStoreId());
+ }
+ }
+
+ return $this;
+ }
+
/**
* Prepare Url Data object
*
@@ -727,8 +768,7 @@ public function addIdFilter($productId, $exclude = false)
}
/**
- * Adding product website names to result collection
- * Add for each product websites information
+ * Adding product website names to result collection. Add for each product websites information.
*
* @return $this
*/
@@ -739,7 +779,7 @@ public function addWebsiteNamesToResult()
}
/**
- * {@inheritdoc}
+ * @inheritdoc
*/
public function load($printQuery = false, $logQuery = false)
{
@@ -790,14 +830,14 @@ protected function doAddWebsiteNamesToResult()
foreach ($this as $product) {
if (isset($productWebsites[$product->getId()])) {
$product->setData('websites', $productWebsites[$product->getId()]);
+ $product->setData('website_ids', $productWebsites[$product->getId()]);
}
}
return $this;
}
/**
- * Add store availability filter. Include availability product
- * for store website
+ * Add store availability filter. Include availability product for store website.
*
* @param null|string|bool|int|Store $store
* @return $this
@@ -903,7 +943,7 @@ private function mapConditionType($conditionType)
'eq' => 'in',
'neq' => 'nin'
];
- return isset($conditionsMap[$conditionType]) ? $conditionsMap[$conditionType] : $conditionType;
+ return $conditionsMap[$conditionType] ?? $conditionType;
}
/**
@@ -1051,14 +1091,15 @@ public function getAllAttributeValues($attribute)
$select = clone $this->getSelect();
$attribute = $this->getEntity()->getAttribute($attribute);
- $aiField = $this->getConnection()->getAutoIncrementField($this->getMainTable());
+ $fieldMainTable = $this->getConnection()->getAutoIncrementField($this->getMainTable());
+ $fieldJoinTable = $attribute->getEntity()->getLinkField();
$select->reset()
->from(
['cpe' => $this->getMainTable()],
['entity_id']
)->join(
['cpa' => $attribute->getBackend()->getTable()],
- 'cpe.' . $aiField . ' = cpa.' . $aiField,
+ 'cpe.' . $fieldMainTable . ' = cpa.' . $fieldJoinTable,
['store_id', 'value']
)->where('attribute_id = ?', (int)$attribute->getId());
@@ -1085,14 +1126,14 @@ public function getSelectCountSql()
/**
* Get SQL for get record count
*
- * @param null $select
+ * @param \Magento\Framework\DB\Select $select
* @param bool $resetLeftJoins
* @return \Magento\Framework\DB\Select
*/
protected function _getSelectCountSql($select = null, $resetLeftJoins = true)
{
$this->_renderFilters();
- $countSelect = is_null($select) ? $this->_getClearSelect() : $this->_buildClearSelect($select);
+ $countSelect = $select === null ? $this->_getClearSelect() : $this->_buildClearSelect($select);
$countSelect->columns('COUNT(DISTINCT e.entity_id)');
if ($resetLeftJoins) {
$countSelect->resetJoinLeft();
@@ -1192,7 +1233,7 @@ public function getProductCountSelect()
)->distinct(
false
)->join(
- ['count_table' => $this->getTable('catalog_category_product_index')],
+ ['count_table' => $this->tableMaintainer->getMainTable($this->getStoreId())],
'count_table.product_id = e.entity_id',
[
'count_table.category_id',
@@ -1327,8 +1368,7 @@ public function joinUrlRewrite()
}
/**
- * Add URL rewrites data to product
- * If collection loadded - run processing else set flag
+ * Add URL rewrites data to product. If collection loadded - run processing else set flag.
*
* @param int|string $categoryId
* @return $this
@@ -1435,7 +1475,7 @@ public function getAllIdsCache($resetCache = false)
$ids = $this->_allIdsCache;
}
- if (is_null($ids)) {
+ if ($ids === null) {
$ids = $this->getAllIds();
$this->setAllIdsCache($ids);
}
@@ -1466,17 +1506,17 @@ public function addPriceData($customerGroupId = null, $websiteId = null)
{
$this->_productLimitationFilters->setUsePriceIndex(true);
- if (!isset($this->_productLimitationFilters['customer_group_id']) && is_null($customerGroupId)) {
+ if (!isset($this->_productLimitationFilters['customer_group_id']) && $customerGroupId === null) {
$customerGroupId = $this->_customerSession->getCustomerGroupId();
}
- if (!isset($this->_productLimitationFilters['website_id']) && is_null($websiteId)) {
+ if (!isset($this->_productLimitationFilters['website_id']) && $websiteId === null) {
$websiteId = $this->_storeManager->getStore($this->getStoreId())->getWebsiteId();
}
- if (!is_null($customerGroupId)) {
+ if ($customerGroupId !== null) {
$this->_productLimitationFilters['customer_group_id'] = $customerGroupId;
}
- if (!is_null($websiteId)) {
+ if ($websiteId !== null) {
$this->_productLimitationFilters['website_id'] = $websiteId;
}
@@ -1551,7 +1591,8 @@ public function addAttributeToFilter($attribute, $condition = null, $joinType =
}
/**
- * {@inheritdoc}
+ * @inheritdoc
+ *
* @since 101.0.0
*/
protected function getEntityPkName(\Magento\Eav\Model\Entity\AbstractEntity $entity)
@@ -1560,7 +1601,7 @@ protected function getEntityPkName(\Magento\Eav\Model\Entity\AbstractEntity $ent
}
/**
- * Add requere tax percent flag for product collection
+ * Add require tax percent flag for product collection
*
* @return $this
*/
@@ -1623,7 +1664,7 @@ public function addOptionsToResult()
*/
public function addFilterByRequiredOptions()
{
- $this->addAttributeToFilter('required_options', [['neq' => 1], ['null' => true]], 'left');
+ $this->addAttributeToFilter('required_options', [['neq' => 1]], 'left');
return $this;
}
@@ -1670,7 +1711,7 @@ public function addAttributeToSort($attribute, $dir = self::SORT_ORDER_ASC)
return $this;
} elseif ($attribute == 'is_saleable') {
- $this->getSelect()->order("is_saleable " . $dir);
+ $this->getSelect()->order("is_salable " . $dir);
return $this;
}
@@ -1853,7 +1894,12 @@ protected function _productLimitationJoinPrice()
protected function _productLimitationPrice($joinLeft = false)
{
$filters = $this->_productLimitationFilters;
- if (!$filters->isUsingPriceIndex()) {
+ if (!$filters->isUsingPriceIndex() ||
+ !isset($filters['website_id']) ||
+ (string)$filters['website_id'] === '' ||
+ !isset($filters['customer_group_id']) ||
+ (string)$filters['customer_group_id'] === ''
+ ) {
return $this;
}
@@ -1888,7 +1934,23 @@ protected function _productLimitationPrice($joinLeft = false)
'max_price',
'tier_price',
];
- $tableName = ['price_index' => $this->getTable('catalog_product_index_price')];
+
+ $tableName = [
+ 'price_index' => $this->priceTableResolver->resolve(
+ 'catalog_product_index_price',
+ [
+ $this->dimensionFactory->create(
+ CustomerGroupDimensionProvider::DIMENSION_NAME,
+ (string)$filters['customer_group_id']
+ ),
+ $this->dimensionFactory->create(
+ WebsiteDimensionProvider::DIMENSION_NAME,
+ (string)$filters['website_id']
+ )
+ ]
+ )
+ ];
+
if ($joinLeft) {
$select->joinLeft($tableName, $joinCond, $colls);
} else {
@@ -1966,7 +2028,7 @@ protected function _applyProductLimitations()
$this->getSelect()->setPart(\Magento\Framework\DB\Select::FROM, $fromPart);
} else {
$this->getSelect()->join(
- ['cat_index' => $this->getTable('catalog_category_product_index')],
+ ['cat_index' => $this->tableMaintainer->getMainTable($this->getStoreId())],
$joinCond,
['cat_index_position' => 'position']
);
@@ -2218,6 +2280,7 @@ public function addPriceDataFieldFilter($comparisonFormat, $fields)
* @SuppressWarnings(PHPMD.CyclomaticComplexity)
* @SuppressWarnings(PHPMD.NPathComplexity)
* @since 101.0.1
+ * @throws \Magento\Framework\Exception\LocalizedException
*/
public function addMediaGalleryData()
{
@@ -2229,34 +2292,36 @@ public function addMediaGalleryData()
return $this;
}
- /** @var $attribute \Magento\Catalog\Model\ResourceModel\Eav\Attribute */
- $attribute = $this->getAttribute('media_gallery');
- $select = $this->getMediaGalleryResource()->createBatchBaseSelect(
- $this->getStoreId(),
- $attribute->getAttributeId()
- );
-
- $mediaGalleries = [];
- $linkField = $this->getProductEntityMetadata()->getLinkField();
$items = $this->getItems();
+ $linkField = $this->getProductEntityMetadata()->getLinkField();
- $select->where(
- 'entity.' . $linkField . ' IN (?)',
- array_map(
- function ($item) use ($linkField) {
- return $item->getData($linkField);
- },
- $items
- )
- );
+ $select = $this->getMediaGalleryResource()
+ ->createBatchBaseSelect(
+ $this->getStoreId(),
+ $this->getAttribute('media_gallery')->getAttributeId()
+ )->reset(
+ Select::ORDER // we don't care what order is in current scenario
+ )->where(
+ 'entity.' . $linkField . ' IN (?)',
+ array_map(
+ function ($item) use ($linkField) {
+ return (int) $item->getOrigData($linkField);
+ },
+ $items
+ )
+ );
+
+ $mediaGalleries = [];
foreach ($this->getConnection()->fetchAll($select) as $row) {
$mediaGalleries[$row[$linkField]][] = $row;
}
foreach ($items as $item) {
- $mediaEntries = isset($mediaGalleries[$item->getData($linkField)]) ?
- $mediaGalleries[$item->getData($linkField)] : [];
- $this->getGalleryReadHandler()->addMediaDataToProduct($item, $mediaEntries);
+ $this->getGalleryReadHandler()
+ ->addMediaDataToProduct(
+ $item,
+ $mediaGalleries[$item->getOrigData($linkField)] ?? []
+ );
}
$this->setFlag('media_gallery_added', true);
@@ -2289,7 +2354,10 @@ private function getGalleryReadHandler()
}
/**
+ * Retrieve Media gallery resource.
+ *
* @deprecated 101.0.1
+ *
* @return \Magento\Catalog\Model\ResourceModel\Product\Gallery
*/
private function getMediaGalleryResource()
@@ -2347,7 +2415,7 @@ public function setOrder($attribute, $dir = Select::SQL_DESC)
*/
public function getMaxPrice()
{
- if (is_null($this->_maxPrice)) {
+ if ($this->_maxPrice === null) {
$this->_prepareStatisticsData();
}
@@ -2361,7 +2429,7 @@ public function getMaxPrice()
*/
public function getMinPrice()
{
- if (is_null($this->_minPrice)) {
+ if ($this->_minPrice === null) {
$this->_prepareStatisticsData();
}
@@ -2375,7 +2443,7 @@ public function getMinPrice()
*/
public function getPriceStandardDeviation()
{
- if (is_null($this->_priceStandardDeviation)) {
+ if ($this->_priceStandardDeviation === null) {
$this->_prepareStatisticsData();
}
@@ -2389,7 +2457,7 @@ public function getPriceStandardDeviation()
*/
public function getPricesCount()
{
- if (is_null($this->_pricesCount)) {
+ if ($this->_pricesCount === null) {
$this->_prepareStatisticsData();
}
diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/Gallery.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/Gallery.php
index 2868392f85280..b68c43e40ff2f 100644
--- a/app/code/Magento/Catalog/Model/ResourceModel/Product/Gallery.php
+++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/Gallery.php
@@ -190,7 +190,7 @@ public function createBatchBaseSelect($storeId, $attributeId)
'value.' . $linkField . ' = entity.' . $linkField,
]
),
- ['label', 'position', 'disabled']
+ []
)->joinLeft(
['default_value' => $this->getTable(self::GALLERY_VALUE_TABLE)],
implode(
@@ -201,8 +201,15 @@ public function createBatchBaseSelect($storeId, $attributeId)
'default_value.' . $linkField . ' = entity.' . $linkField,
]
),
- ['label_default' => 'label', 'position_default' => 'position', 'disabled_default' => 'disabled']
- )->where(
+ []
+ )->columns([
+ 'label' => $this->getConnection()->getIfNullSql('`value`.`label`', '`default_value`.`label`'),
+ 'position' => $this->getConnection()->getIfNullSql('`value`.`position`', '`default_value`.`position`'),
+ 'disabled' => $this->getConnection()->getIfNullSql('`value`.`disabled`', '`default_value`.`disabled`'),
+ 'label_default' => 'default_value.label',
+ 'position_default' => 'default_value.position',
+ 'disabled_default' => 'default_value.disabled'
+ ])->where(
$mainTableAlias . '.attribute_id = ?',
$attributeId
)->where(
diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/Image.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/Image.php
new file mode 100644
index 0000000000000..5f83f9826abb5
--- /dev/null
+++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/Image.php
@@ -0,0 +1,100 @@
+batchQueryGenerator = $generator;
+ $this->resourceConnection = $resourceConnection;
+ $this->connection = $this->resourceConnection->getConnection();
+ $this->batchSize = $batchSize;
+ }
+
+ /**
+ * Returns product images
+ *
+ * @return \Generator
+ */
+ public function getAllProductImages(): \Generator
+ {
+ $batchSelectIterator = $this->batchQueryGenerator->generate(
+ 'value_id',
+ $this->getVisibleImagesSelect(),
+ $this->batchSize,
+ \Magento\Framework\DB\Query\BatchIteratorInterface::NON_UNIQUE_FIELD_ITERATOR
+ );
+
+ foreach ($batchSelectIterator as $select) {
+ foreach ($this->connection->fetchAll($select) as $key => $value) {
+ yield $key => $value;
+ }
+ }
+ }
+
+ /**
+ * Get the number of unique pictures of products
+ * @return int
+ */
+ public function getCountAllProductImages(): int
+ {
+ $select = $this->getVisibleImagesSelect()->reset('columns')->columns('count(*)');
+ return (int) $this->connection->fetchOne($select);
+ }
+
+ /**
+ * @return Select
+ */
+ private function getVisibleImagesSelect(): Select
+ {
+ return $this->connection->select()->distinct()
+ ->from(
+ ['images' => $this->resourceConnection->getTableName(Gallery::GALLERY_TABLE)],
+ 'value as filepath'
+ )->where(
+ 'disabled = 0'
+ );
+ }
+}
diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Eav/AbstractEav.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Eav/AbstractEav.php
index c4e3fb1bf1e70..183942acaabf7 100644
--- a/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Eav/AbstractEav.php
+++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Eav/AbstractEav.php
@@ -82,6 +82,7 @@ public function reindexEntities($processIds)
$this->_prepareIndex($processIds);
$this->_prepareRelationIndex($processIds);
$this->_removeNotVisibleEntityFromIndex();
+
return $this;
}
@@ -164,6 +165,7 @@ protected function _prepareRelationIndexSelect($parentIds = null)
$connection = $this->getConnection();
$idxTable = $this->getIdxTable();
$linkField = $this->getMetadataPool()->getMetadata(ProductInterface::class)->getLinkField();
+
$select = $connection->select()->from(
['l' => $this->getTable('catalog_product_relation')],
[]
@@ -179,6 +181,16 @@ protected function _prepareRelationIndexSelect($parentIds = null)
['i' => $idxTable],
'l.child_id = i.entity_id AND cs.store_id = i.store_id',
[]
+ )->join(
+ ['sw' => $this->getTable('store_website')],
+ "cs.website_id = sw.website_id",
+ []
+ )->joinLeft(
+ ['cpw' => $this->getTable('catalog_product_website')],
+ "i.entity_id = cpw.product_id AND sw.website_id = cpw.website_id",
+ []
+ )->where(
+ 'cpw.product_id IS NOT NULL'
)->group(
['parent_id', 'i.attribute_id', 'i.store_id', 'i.value', 'l.child_id']
)->columns(
diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Eav/Source.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Eav/Source.php
index 5b68730209b40..77836c58d5070 100644
--- a/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Eav/Source.php
+++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Eav/Source.php
@@ -84,7 +84,7 @@ protected function _getIndexableAttributes($multiSelect)
if ($multiSelect == true) {
$select->where('ea.backend_type = ?', 'varchar')->where('ea.frontend_input = ?', 'multiselect');
} else {
- $select->where('ea.backend_type = ?', 'int')->where('ea.frontend_input = ?', 'select');
+ $select->where('ea.backend_type = ?', 'int')->where('ea.frontend_input IN( ? )', ['select', 'boolean']);
}
return $this->getConnection()->fetchCol($select);
diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/LinkedProductSelectBuilderByIndexPrice.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/LinkedProductSelectBuilderByIndexPrice.php
index b4f7e43387d0e..ebe04fb63b217 100644
--- a/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/LinkedProductSelectBuilderByIndexPrice.php
+++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/LinkedProductSelectBuilderByIndexPrice.php
@@ -7,9 +7,13 @@
use Magento\Catalog\Api\Data\ProductInterface;
use Magento\Catalog\Model\ResourceModel\Product\BaseSelectProcessorInterface;
+use Magento\Customer\Model\Indexer\CustomerGroupDimensionProvider;
use Magento\Framework\App\ObjectManager;
use Magento\Framework\DB\Select;
use Magento\Catalog\Model\ResourceModel\Product\LinkedProductSelectBuilderInterface;
+use Magento\Framework\Indexer\DimensionFactory;
+use Magento\Store\Model\Indexer\WebsiteDimensionProvider;
+use Magento\Framework\Search\Request\IndexScopeResolverInterface;
class LinkedProductSelectBuilderByIndexPrice implements LinkedProductSelectBuilderInterface
{
@@ -38,6 +42,16 @@ class LinkedProductSelectBuilderByIndexPrice implements LinkedProductSelectBuild
*/
private $baseSelectProcessor;
+ /**
+ * @var IndexScopeResolverInterface|null
+ */
+ private $priceTableResolver;
+
+ /**
+ * @var DimensionFactory|null
+ */
+ private $dimensionFactory;
+
/**
* LinkedProductSelectBuilderByIndexPrice constructor.
* @param \Magento\Store\Model\StoreManagerInterface $storeManager
@@ -45,13 +59,17 @@ class LinkedProductSelectBuilderByIndexPrice implements LinkedProductSelectBuild
* @param \Magento\Customer\Model\Session $customerSession
* @param \Magento\Framework\EntityManager\MetadataPool $metadataPool
* @param BaseSelectProcessorInterface|null $baseSelectProcessor
+ * @param IndexScopeResolverInterface|null $priceTableResolver
+ * @param DimensionFactory|null $dimensionFactory
*/
public function __construct(
\Magento\Store\Model\StoreManagerInterface $storeManager,
\Magento\Framework\App\ResourceConnection $resourceConnection,
\Magento\Customer\Model\Session $customerSession,
\Magento\Framework\EntityManager\MetadataPool $metadataPool,
- BaseSelectProcessorInterface $baseSelectProcessor = null
+ BaseSelectProcessorInterface $baseSelectProcessor = null,
+ IndexScopeResolverInterface $priceTableResolver = null,
+ DimensionFactory $dimensionFactory = null
) {
$this->storeManager = $storeManager;
$this->resource = $resourceConnection;
@@ -59,6 +77,9 @@ public function __construct(
$this->metadataPool = $metadataPool;
$this->baseSelectProcessor = (null !== $baseSelectProcessor)
? $baseSelectProcessor : ObjectManager::getInstance()->get(BaseSelectProcessorInterface::class);
+ $this->priceTableResolver = $priceTableResolver
+ ?? ObjectManager::getInstance()->get(IndexScopeResolverInterface::class);
+ $this->dimensionFactory = $dimensionFactory ?? ObjectManager::getInstance()->get(DimensionFactory::class);
}
/**
@@ -68,6 +89,8 @@ public function build($productId)
{
$linkField = $this->metadataPool->getMetadata(ProductInterface::class)->getLinkField();
$productTable = $this->resource->getTableName('catalog_product_entity');
+ $websiteId = $this->storeManager->getStore()->getWebsiteId();
+ $customerGroupId = $this->customerSession->getCustomerGroupId();
$priceSelect = $this->resource->getConnection()->select()
->from(['parent' => $productTable], '')
@@ -80,13 +103,22 @@ public function build($productId)
sprintf('%s.entity_id = link.child_id', BaseSelectProcessorInterface::PRODUCT_TABLE_ALIAS),
['entity_id']
)->joinInner(
- ['t' => $this->resource->getTableName('catalog_product_index_price')],
+ [
+ 't' => $this->priceTableResolver->resolve('catalog_product_index_price', [
+ $this->dimensionFactory->create(WebsiteDimensionProvider::DIMENSION_NAME, (string)$websiteId),
+ $this->dimensionFactory->create(
+ CustomerGroupDimensionProvider::DIMENSION_NAME,
+ (string)$customerGroupId
+ ),
+ ])
+ ],
sprintf('t.entity_id = %s.entity_id', BaseSelectProcessorInterface::PRODUCT_TABLE_ALIAS),
[]
)->where('parent.entity_id = ?', $productId)
- ->where('t.website_id = ?', $this->storeManager->getStore()->getWebsiteId())
- ->where('t.customer_group_id = ?', $this->customerSession->getCustomerGroupId())
+ ->where('t.website_id = ?', $websiteId)
+ ->where('t.customer_group_id = ?', $customerGroupId)
->order('t.min_price ' . Select::SQL_ASC)
+ ->order(BaseSelectProcessorInterface::PRODUCT_TABLE_ALIAS . '.' . $linkField . ' ' . Select::SQL_ASC)
->limit(1);
$priceSelect = $this->baseSelectProcessor->process($priceSelect);
diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/BasePriceModifier.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/BasePriceModifier.php
new file mode 100644
index 0000000000000..ec967c7c7d04f
--- /dev/null
+++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/BasePriceModifier.php
@@ -0,0 +1,38 @@
+priceModifiers = $priceModifiers;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function modifyPrice(IndexTableStructure $priceTable, array $entityIds = [])
+ {
+ foreach ($this->priceModifiers as $priceModifier) {
+ $priceModifier->modifyPrice($priceTable, $entityIds);
+ }
+ }
+}
diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/CustomOptionPriceModifier.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/CustomOptionPriceModifier.php
new file mode 100644
index 0000000000000..646cd0d4c1a4c
--- /dev/null
+++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/CustomOptionPriceModifier.php
@@ -0,0 +1,464 @@
+resource = $resource;
+ $this->metadataPool = $metadataPool;
+ $this->connectionName = $connectionName;
+ $this->columnValueExpressionFactory = $columnValueExpressionFactory;
+ $this->dataHelper = $dataHelper;
+ $this->tableStrategy = $tableStrategy;
+ }
+
+ /**
+ * Apply custom option price to temporary index price table
+ *
+ * @param IndexTableStructure $priceTable
+ * @param array $entityIds
+ * @return void
+ * @throws \Exception
+ * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+ */
+ public function modifyPrice(IndexTableStructure $priceTable, array $entityIds = [])
+ {
+ // no need to run all queries if current products have no custom options
+ if (!$this->checkIfCustomOptionsExist($priceTable)) {
+ return;
+ }
+
+ $connection = $this->getConnection();
+ $finalPriceTable = $priceTable->getTableName();
+
+ $coaTable = $this->getCustomOptionAggregateTable();
+ $this->prepareCustomOptionAggregateTable();
+
+ $copTable = $this->getCustomOptionPriceTable();
+ $this->prepareCustomOptionPriceTable();
+
+ $select = $this->getSelectForOptionsWithMultipleValues($finalPriceTable);
+ $query = $select->insertFromSelect($coaTable);
+ $connection->query($query);
+
+ $select = $this->getSelectForOptionsWithOneValue($finalPriceTable);
+ $query = $select->insertFromSelect($coaTable);
+ $connection->query($query);
+
+ $select = $this->getSelectAggregated($coaTable);
+ $query = $select->insertFromSelect($copTable);
+ $connection->query($query);
+
+ // update tmp price index with prices from custom options (from previous aggregated table)
+ $select = $this->getSelectForUpdate($copTable);
+ $query = $select->crossUpdateFromSelect(['i' => $finalPriceTable]);
+ $connection->query($query);
+
+ $connection->delete($coaTable);
+ $connection->delete($copTable);
+ }
+
+ /**
+ * @param IndexTableStructure $priceTable
+ * @return bool
+ * @throws \Exception
+ */
+ private function checkIfCustomOptionsExist(IndexTableStructure $priceTable): bool
+ {
+ $metadata = $this->metadataPool->getMetadata(ProductInterface::class);
+
+ $select = $this->getConnection()
+ ->select()
+ ->from(
+ ['i' => $priceTable->getTableName()],
+ ['entity_id']
+ )->join(
+ ['e' => $this->getTable('catalog_product_entity')],
+ 'e.entity_id = i.entity_id',
+ []
+ )->join(
+ ['o' => $this->getTable('catalog_product_option')],
+ 'o.product_id = e.' . $metadata->getLinkField(),
+ ['option_id']
+ );
+
+ return !empty($this->getConnection()->fetchRow($select));
+ }
+
+ /**
+ * @return \Magento\Framework\DB\Adapter\AdapterInterface
+ */
+ private function getConnection()
+ {
+ if (null === $this->connection) {
+ $this->connection = $this->resource->getConnection($this->connectionName);
+ }
+
+ return $this->connection;
+ }
+
+ /**
+ * Prepare prices for products with custom options that has multiple values
+ *
+ * @param string $sourceTable
+ * @return \Magento\Framework\DB\Select
+ * @throws \Exception
+ */
+ private function getSelectForOptionsWithMultipleValues(string $sourceTable): Select
+ {
+ $connection = $this->resource->getConnection($this->connectionName);
+ $metadata = $this->metadataPool->getMetadata(ProductInterface::class);
+
+ $select = $connection->select()
+ ->from(
+ ['i' => $sourceTable],
+ ['entity_id', 'customer_group_id', 'website_id']
+ )->join(
+ ['e' => $this->getTable('catalog_product_entity')],
+ 'e.entity_id = i.entity_id',
+ []
+ )->join(
+ ['cwd' => $this->getTable('catalog_product_index_website')],
+ 'i.website_id = cwd.website_id',
+ []
+ )->join(
+ ['o' => $this->getTable('catalog_product_option')],
+ 'o.product_id = e.' . $metadata->getLinkField(),
+ ['option_id']
+ )->join(
+ ['ot' => $this->getTable('catalog_product_option_type_value')],
+ 'ot.option_id = o.option_id',
+ []
+ )->join(
+ ['otpd' => $this->getTable('catalog_product_option_type_price')],
+ 'otpd.option_type_id = ot.option_type_id AND otpd.store_id = 0',
+ []
+ )->group(
+ ['i.entity_id', 'i.customer_group_id', 'i.website_id', 'o.option_id']
+ );
+
+ if ($this->isPriceGlobal()) {
+ $optPriceType = 'otpd.price_type';
+ $optPriceValue = 'otpd.price';
+ } else {
+ $select->joinLeft(
+ ['otps' => $this->getTable('catalog_product_option_type_price')],
+ 'otps.option_type_id = otpd.option_type_id AND otpd.store_id = cwd.default_store_id',
+ []
+ );
+
+ $optPriceType = $connection->getCheckSql(
+ 'otps.option_type_price_id > 0',
+ 'otps.price_type',
+ 'otpd.price_type'
+ );
+ $optPriceValue = $connection->getCheckSql('otps.option_type_price_id > 0', 'otps.price', 'otpd.price');
+ }
+
+ $minPriceRound = $this->columnValueExpressionFactory
+ ->create([
+ 'expression' => "ROUND(i.final_price * ({$optPriceValue} / 100), 4)"
+ ]);
+ $minPriceExpr = $connection->getCheckSql("{$optPriceType} = 'fixed'", $optPriceValue, $minPriceRound);
+ $minPriceMin = $this->columnValueExpressionFactory
+ ->create([
+ 'expression' => "MIN({$minPriceExpr})"
+ ]);
+ $minPrice = $connection->getCheckSql("MIN(o.is_require) = 1", $minPriceMin, '0');
+
+ $tierPriceRound = $this->columnValueExpressionFactory
+ ->create([
+ 'expression' => "ROUND(i.tier_price * ({$optPriceValue} / 100), 4)"
+ ]);
+ $tierPriceExpr = $connection->getCheckSql("{$optPriceType} = 'fixed'", $optPriceValue, $tierPriceRound);
+ $tierPriceMin = $this->columnValueExpressionFactory
+ ->create([
+ 'expression' => "MIN({$tierPriceExpr})"
+ ]);
+ $tierPriceValue = $connection->getCheckSql("MIN(o.is_require) > 0", $tierPriceMin, 0);
+ $tierPrice = $connection->getCheckSql("MIN(i.tier_price) IS NOT NULL", $tierPriceValue, "NULL");
+
+ $maxPriceRound = $this->columnValueExpressionFactory
+ ->create([
+ 'expression' => "ROUND(i.final_price * ({$optPriceValue} / 100), 4)"
+ ]);
+ $maxPriceExpr = $connection->getCheckSql("{$optPriceType} = 'fixed'", $optPriceValue, $maxPriceRound);
+ $maxPrice = $connection->getCheckSql(
+ "(MIN(o.type)='radio' OR MIN(o.type)='drop_down')",
+ "MAX({$maxPriceExpr})",
+ "SUM({$maxPriceExpr})"
+ );
+
+ $select->columns(
+ [
+ 'min_price' => $minPrice,
+ 'max_price' => $maxPrice,
+ 'tier_price' => $tierPrice,
+ ]
+ );
+
+ return $select;
+ }
+
+ /**
+ * Prepare prices for products with custom options that has single value
+ *
+ * @param string $sourceTable
+ * @return \Magento\Framework\DB\Select
+ * @throws \Exception
+ */
+ private function getSelectForOptionsWithOneValue(string $sourceTable): Select
+ {
+ $connection = $this->resource->getConnection($this->connectionName);
+ $metadata = $this->metadataPool->getMetadata(ProductInterface::class);
+
+ $select = $connection->select()
+ ->from(
+ ['i' => $sourceTable],
+ ['entity_id', 'customer_group_id', 'website_id']
+ )->join(
+ ['e' => $this->getTable('catalog_product_entity')],
+ 'e.entity_id = i.entity_id',
+ []
+ )->join(
+ ['cwd' => $this->getTable('catalog_product_index_website')],
+ 'i.website_id = cwd.website_id',
+ []
+ )->join(
+ ['o' => $this->getTable('catalog_product_option')],
+ 'o.product_id = e.' . $metadata->getLinkField(),
+ ['option_id']
+ )->join(
+ ['opd' => $this->getTable('catalog_product_option_price')],
+ 'opd.option_id = o.option_id AND opd.store_id = 0',
+ []
+ );
+
+ if ($this->isPriceGlobal()) {
+ $optPriceType = 'opd.price_type';
+ $optPriceValue = 'opd.price';
+ } else {
+ $select->joinLeft(
+ ['ops' => $this->getTable('catalog_product_option_price')],
+ 'ops.option_id = opd.option_id AND ops.store_id = cwd.default_store_id',
+ []
+ );
+
+ $optPriceType = $connection->getCheckSql('ops.option_price_id > 0', 'ops.price_type', 'opd.price_type');
+ $optPriceValue = $connection->getCheckSql('ops.option_price_id > 0', 'ops.price', 'opd.price');
+ }
+
+ $minPriceRound = $this->columnValueExpressionFactory
+ ->create([
+ 'expression' => "ROUND(i.final_price * ({$optPriceValue} / 100), 4)"
+ ]);
+ $priceExpr = $connection->getCheckSql("{$optPriceType} = 'fixed'", $optPriceValue, $minPriceRound);
+ $minPrice = $connection->getCheckSql("{$priceExpr} > 0 AND o.is_require = 1", $priceExpr, 0);
+
+ $maxPrice = $priceExpr;
+
+ $tierPriceRound = $this->columnValueExpressionFactory
+ ->create([
+ 'expression' => "ROUND(i.tier_price * ({$optPriceValue} / 100), 4)"
+ ]);
+ $tierPriceExpr = $connection->getCheckSql("{$optPriceType} = 'fixed'", $optPriceValue, $tierPriceRound);
+ $tierPriceValue = $connection->getCheckSql("{$tierPriceExpr} > 0 AND o.is_require = 1", $tierPriceExpr, 0);
+ $tierPrice = $connection->getCheckSql("i.tier_price IS NOT NULL", $tierPriceValue, "NULL");
+
+ $select->columns(
+ [
+ 'min_price' => $minPrice,
+ 'max_price' => $maxPrice,
+ 'tier_price' => $tierPrice,
+ ]
+ );
+
+ return $select;
+ }
+
+ /**
+ * Aggregate prices with one and multiply options into one table
+ *
+ * @param string $sourceTable
+ * @return \Magento\Framework\DB\Select
+ */
+ private function getSelectAggregated(string $sourceTable): Select
+ {
+ $connection = $this->resource->getConnection($this->connectionName);
+
+ $select = $connection->select()
+ ->from(
+ [$sourceTable],
+ [
+ 'entity_id',
+ 'customer_group_id',
+ 'website_id',
+ 'min_price' => 'SUM(min_price)',
+ 'max_price' => 'SUM(max_price)',
+ 'tier_price' => 'SUM(tier_price)',
+ ]
+ )->group(
+ ['entity_id', 'customer_group_id', 'website_id']
+ );
+
+ return $select;
+ }
+
+ /**
+ * @param string $sourceTable
+ * @return \Magento\Framework\DB\Select
+ */
+ private function getSelectForUpdate(string $sourceTable): Select
+ {
+ $connection = $this->resource->getConnection($this->connectionName);
+
+ $select = $connection->select()->join(
+ ['io' => $sourceTable],
+ 'i.entity_id = io.entity_id AND i.customer_group_id = io.customer_group_id' .
+ ' AND i.website_id = io.website_id',
+ []
+ );
+ $select->columns(
+ [
+ 'min_price' => new ColumnValueExpression('i.min_price + io.min_price'),
+ 'max_price' => new ColumnValueExpression('i.max_price + io.max_price'),
+ 'tier_price' => $connection->getCheckSql(
+ 'i.tier_price IS NOT NULL',
+ 'i.tier_price + io.tier_price',
+ 'NULL'
+ ),
+ ]
+ );
+
+ return $select;
+ }
+
+ /**
+ * @param string $tableName
+ * @return string
+ */
+ private function getTable(string $tableName): string
+ {
+ return $this->resource->getTableName($tableName, $this->connectionName);
+ }
+
+ /**
+ * @return bool
+ */
+ private function isPriceGlobal(): bool
+ {
+ if ($this->isPriceGlobalFlag === null) {
+ $this->isPriceGlobalFlag = $this->dataHelper->isPriceGlobal();
+ }
+
+ return $this->isPriceGlobalFlag;
+ }
+
+ /**
+ * Retrieve table name for custom option temporary aggregation data
+ *
+ * @return string
+ */
+ private function getCustomOptionAggregateTable(): string
+ {
+ return $this->tableStrategy->getTableName('catalog_product_index_price_opt_agr');
+ }
+
+ /**
+ * Retrieve table name for custom option prices data
+ *
+ * @return string
+ */
+ private function getCustomOptionPriceTable(): string
+ {
+ return $this->tableStrategy->getTableName('catalog_product_index_price_opt');
+ }
+
+ /**
+ * Prepare table structure for custom option temporary aggregation data
+ *
+ * @return void
+ */
+ private function prepareCustomOptionAggregateTable()
+ {
+ $this->getConnection()->delete($this->getCustomOptionAggregateTable());
+ }
+
+ /**
+ * Prepare table structure for custom option prices data
+ *
+ * @return void
+ */
+ private function prepareCustomOptionPriceTable()
+ {
+ $this->getConnection()->delete($this->getCustomOptionPriceTable());
+ }
+}
diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/DefaultPrice.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/DefaultPrice.php
index 4761b8b1ce896..7ea85cd3f6f10 100644
--- a/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/DefaultPrice.php
+++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/DefaultPrice.php
@@ -6,6 +6,7 @@
namespace Magento\Catalog\Model\ResourceModel\Product\Indexer\Price;
use Magento\Catalog\Model\ResourceModel\Product\Indexer\AbstractIndexer;
+use Magento\Framework\Indexer\DimensionalIndexerInterface;
/**
* Default Product Type Price Indexer Resource model
@@ -16,6 +17,8 @@
* @author Magento Core Team
* @SuppressWarnings(PHPMD.CouplingBetweenObjects)
* @since 100.0.2
+ * @deprecated Not used anymore for price indexation. Class left for backward compatibility
+ * @see DimensionalIndexerInterface
*/
class DefaultPrice extends AbstractIndexer implements PriceInterface
{
@@ -52,6 +55,16 @@ class DefaultPrice extends AbstractIndexer implements PriceInterface
*/
private $hasEntity = null;
+ /**
+ * @var IndexTableStructureFactory
+ */
+ private $indexTableStructureFactory;
+
+ /**
+ * @var PriceModifierInterface[]
+ */
+ private $priceModifiers = [];
+
/**
* DefaultPrice constructor.
*
@@ -61,7 +74,8 @@ class DefaultPrice extends AbstractIndexer implements PriceInterface
* @param \Magento\Framework\Event\ManagerInterface $eventManager
* @param \Magento\Framework\Module\Manager $moduleManager
* @param string|null $connectionName
- * @param null|\Magento\Indexer\Model\Indexer\StateFactory $stateFactory
+ * @param IndexTableStructureFactory $indexTableStructureFactory
+ * @param PriceModifierInterface[] $priceModifiers
*/
public function __construct(
\Magento\Framework\Model\ResourceModel\Db\Context $context,
@@ -69,11 +83,25 @@ public function __construct(
\Magento\Eav\Model\Config $eavConfig,
\Magento\Framework\Event\ManagerInterface $eventManager,
\Magento\Framework\Module\Manager $moduleManager,
- $connectionName = null
+ $connectionName = null,
+ IndexTableStructureFactory $indexTableStructureFactory = null,
+ array $priceModifiers = []
) {
$this->_eventManager = $eventManager;
$this->moduleManager = $moduleManager;
parent::__construct($context, $tableStrategy, $eavConfig, $connectionName);
+
+ $this->indexTableStructureFactory = $indexTableStructureFactory ?:
+ \Magento\Framework\App\ObjectManager::getInstance()->get(IndexTableStructureFactory::class);
+ foreach ($priceModifiers as $priceModifier) {
+ if (!($priceModifier instanceof PriceModifierInterface)) {
+ throw new \InvalidArgumentException(
+ 'Argument \'priceModifiers\' must be of the type ' . PriceModifierInterface::class . '[]'
+ );
+ }
+
+ $this->priceModifiers[] = $priceModifier;
+ }
}
/**
@@ -209,6 +237,8 @@ protected function _getDefaultFinalPriceTable()
* Prepare final price temporary index table
*
* @return $this
+ * @deprecated
+ * @see prepareFinalPriceTable()
*/
protected function _prepareDefaultFinalPriceTable()
{
@@ -216,6 +246,32 @@ protected function _prepareDefaultFinalPriceTable()
return $this;
}
+ /**
+ * Create (if needed), clean and return structure of final price table
+ *
+ * @return IndexTableStructure
+ */
+ private function prepareFinalPriceTable()
+ {
+ $tableName = $this->_getDefaultFinalPriceTable();
+ $this->getConnection()->delete($tableName);
+
+ $finalPriceTable = $this->indexTableStructureFactory->create([
+ 'tableName' => $tableName,
+ 'entityField' => 'entity_id',
+ 'customerGroupField' => 'customer_group_id',
+ 'websiteField' => 'website_id',
+ 'taxClassField' => 'tax_class_id',
+ 'originalPriceField' => 'orig_price',
+ 'finalPriceField' => 'price',
+ 'minPriceField' => 'min_price',
+ 'maxPriceField' => 'max_price',
+ 'tierPriceField' => 'tier_price',
+ ]);
+
+ return $finalPriceTable;
+ }
+
/**
* Retrieve website current dates table name
*
@@ -248,11 +304,14 @@ protected function _prepareFinalPriceData($entityIds = null)
*/
protected function prepareFinalPriceDataForType($entityIds, $type)
{
- $this->_prepareDefaultFinalPriceTable();
+ $finalPriceTable = $this->prepareFinalPriceTable();
$select = $this->getSelect($entityIds, $type);
- $query = $select->insertFromSelect($this->_getDefaultFinalPriceTable(), [], false);
+ $query = $select->insertFromSelect($finalPriceTable->getTableName(), [], false);
$this->getConnection()->query($query);
+
+ $this->modifyPriceIndex($finalPriceTable);
+
return $this;
}
@@ -271,6 +330,7 @@ protected function prepareFinalPriceDataForType($entityIds, $type)
protected function getSelect($entityIds = null, $type = null)
{
$metadata = $this->getMetadataPool()->getMetadata(\Magento\Catalog\Api\Data\ProductInterface::class);
+ $linkField = $metadata->getLinkField();
$connection = $this->getConnection();
$select = $connection->select()->from(
['e' => $this->getTable('catalog_product_entity')],
@@ -300,9 +360,38 @@ protected function getSelect($entityIds = null, $type = null)
'pw.product_id = e.entity_id AND pw.website_id = cw.website_id',
[]
)->joinLeft(
- ['tp' => $this->_getTierPriceIndexTable()],
- 'tp.entity_id = e.entity_id AND tp.website_id = cw.website_id' .
- ' AND tp.customer_group_id = cg.customer_group_id',
+ // we need this only for BCC in case someone expects table `tp` to be present in query
+ ['tp' => $this->getTable('catalog_product_index_tier_price')],
+ 'tp.entity_id = e.entity_id AND tp.customer_group_id = cg.customer_group_id' .
+ ' AND tp.website_id = pw.website_id',
+ []
+ )->joinLeft(
+ // calculate tier price specified as Website = `All Websites` and Customer Group = `Specific Customer Group`
+ ['tier_price_1' => $this->getTable('catalog_product_entity_tier_price')],
+ 'tier_price_1.' . $linkField . ' = e.' . $linkField . ' AND tier_price_1.all_groups = 0' .
+ ' AND tier_price_1.customer_group_id = cg.customer_group_id AND tier_price_1.qty = 1' .
+ ' AND tier_price_1.website_id = 0',
+ []
+ )->joinLeft(
+ // calculate tier price specified as Website = `Specific Website`
+ //and Customer Group = `Specific Customer Group`
+ ['tier_price_2' => $this->getTable('catalog_product_entity_tier_price')],
+ 'tier_price_2.' . $linkField . ' = e.' . $linkField . ' AND tier_price_2.all_groups = 0' .
+ ' AND tier_price_2.customer_group_id = cg.customer_group_id AND tier_price_2.qty = 1' .
+ ' AND tier_price_2.website_id = cw.website_id',
+ []
+ )->joinLeft(
+ // calculate tier price specified as Website = `All Websites` and Customer Group = `ALL GROUPS`
+ ['tier_price_3' => $this->getTable('catalog_product_entity_tier_price')],
+ 'tier_price_3.' . $linkField . ' = e.' . $linkField . ' AND tier_price_3.all_groups = 1' .
+ ' AND tier_price_3.customer_group_id = 0 AND tier_price_3.qty = 1 AND tier_price_3.website_id = 0',
+ []
+ )->joinLeft(
+ // calculate tier price specified as Website = `Specific Website` and Customer Group = `ALL GROUPS`
+ ['tier_price_4' => $this->getTable('catalog_product_entity_tier_price')],
+ 'tier_price_4.' . $linkField . ' = e.' . $linkField . ' AND tier_price_4.all_groups = 1' .
+ ' AND tier_price_4.customer_group_id = 0 AND tier_price_4.qty = 1' .
+ ' AND tier_price_4.website_id = cw.website_id',
[]
);
@@ -318,7 +407,7 @@ protected function getSelect($entityIds = null, $type = null)
$this->_addAttributeToSelect(
$select,
'status',
- 'e.' . $metadata->getLinkField(),
+ 'e.' . $linkField,
'cs.store_id',
$statusCond,
true
@@ -327,7 +416,7 @@ protected function getSelect($entityIds = null, $type = null)
$taxClassId = $this->_addAttributeToSelect(
$select,
'tax_class_id',
- 'e.' . $metadata->getLinkField(),
+ 'e.' . $linkField,
'cs.store_id'
);
} else {
@@ -338,41 +427,46 @@ protected function getSelect($entityIds = null, $type = null)
$price = $this->_addAttributeToSelect(
$select,
'price',
- 'e.' . $metadata->getLinkField(),
+ 'e.' . $linkField,
'cs.store_id'
);
$specialPrice = $this->_addAttributeToSelect(
$select,
'special_price',
- 'e.' . $metadata->getLinkField(),
+ 'e.' . $linkField,
'cs.store_id'
);
$specialFrom = $this->_addAttributeToSelect(
$select,
'special_from_date',
- 'e.' . $metadata->getLinkField(),
+ 'e.' . $linkField,
'cs.store_id'
);
$specialTo = $this->_addAttributeToSelect(
$select,
'special_to_date',
- 'e.' . $metadata->getLinkField(),
+ 'e.' . $linkField,
'cs.store_id'
);
- $currentDate = $connection->getDatePartSql('cwd.website_date');
+ $currentDate = 'cwd.website_date';
+ $maxUnsignedBigint = '~0';
$specialFromDate = $connection->getDatePartSql($specialFrom);
$specialToDate = $connection->getDatePartSql($specialTo);
-
- $specialFromUse = $connection->getCheckSql("{$specialFromDate} <= {$currentDate}", '1', '0');
- $specialToUse = $connection->getCheckSql("{$specialToDate} >= {$currentDate}", '1', '0');
- $specialFromHas = $connection->getCheckSql("{$specialFrom} IS NULL", '1', "{$specialFromUse}");
- $specialToHas = $connection->getCheckSql("{$specialTo} IS NULL", '1', "{$specialToUse}");
- $finalPrice = $connection->getCheckSql(
- "{$specialFromHas} > 0 AND {$specialToHas} > 0" . " AND {$specialPrice} < {$price}",
+ $specialFromExpr = "{$specialFrom} IS NULL OR {$specialFromDate} <= {$currentDate}";
+ $specialToExpr = "{$specialTo} IS NULL OR {$specialToDate} >= {$currentDate}";
+ $specialPriceExpr = $connection->getCheckSql(
+ "{$specialPrice} IS NOT NULL AND ({$specialFromExpr}) AND ({$specialToExpr})",
$specialPrice,
- $price
+ $maxUnsignedBigint
);
+ $tierPrice = $this->getTotalTierPriceExpression($price);
+ $tierPriceExpr = $connection->getIfNullSql($tierPrice, $maxUnsignedBigint);
+ $finalPrice = $connection->getLeastSql([
+ $price,
+ $specialPriceExpr,
+ $tierPriceExpr,
+ ]);
$select->columns(
[
@@ -380,8 +474,8 @@ protected function getSelect($entityIds = null, $type = null)
'price' => $connection->getIfNullSql($finalPrice, 0),
'min_price' => $connection->getIfNullSql($finalPrice, 0),
'max_price' => $connection->getIfNullSql($finalPrice, 0),
- 'tier_price' => new \Zend_Db_Expr('tp.min_price'),
- 'base_tier' => new \Zend_Db_Expr('tp.min_price'),
+ 'tier_price' => $tierPrice,
+ 'base_tier' => $tierPrice,
]
);
@@ -401,6 +495,7 @@ protected function getSelect($entityIds = null, $type = null)
'store_field' => new \Zend_Db_Expr('cs.store_id'),
]
);
+
return $select;
}
@@ -446,6 +541,19 @@ protected function _prepareCustomOptionPriceTable()
return $this;
}
+ /**
+ * Modify data in price index table.
+ *
+ * @param IndexTableStructure $finalPriceTable
+ * @return void
+ */
+ private function modifyPriceIndex(IndexTableStructure $finalPriceTable)
+ {
+ foreach ($this->priceModifiers as $priceModifier) {
+ $priceModifier->modifyPrice($finalPriceTable);
+ }
+ }
+
/**
* Apply custom option minimal and maximal price to temporary final price index table
*
@@ -455,15 +563,21 @@ protected function _prepareCustomOptionPriceTable()
protected function _applyCustomOption()
{
$connection = $this->getConnection();
+ $finalPriceTable = $this->_getDefaultFinalPriceTable();
$coaTable = $this->_getCustomOptionAggregateTable();
$copTable = $this->_getCustomOptionPriceTable();
+ $metadata = $this->getMetadataPool()->getMetadata(\Magento\Catalog\Api\Data\ProductInterface::class);
$this->_prepareCustomOptionAggregateTable();
$this->_prepareCustomOptionPriceTable();
$select = $connection->select()->from(
- ['i' => $this->_getDefaultFinalPriceTable()],
+ ['i' => $finalPriceTable],
['entity_id', 'customer_group_id', 'website_id']
+ )->join(
+ ['e' => $this->getTable('catalog_product_entity')],
+ 'e.entity_id = i.entity_id',
+ []
)->join(
['cw' => $this->getTable('store_website')],
'cw.website_id = i.website_id',
@@ -478,7 +592,7 @@ protected function _applyCustomOption()
[]
)->join(
['o' => $this->getTable('catalog_product_option')],
- 'o.product_id = i.entity_id',
+ 'o.product_id = e.' . $metadata->getLinkField(),
['option_id']
)->join(
['ot' => $this->getTable('catalog_product_option_type_value')],
@@ -529,8 +643,12 @@ protected function _applyCustomOption()
$connection->query($query);
$select = $connection->select()->from(
- ['i' => $this->_getDefaultFinalPriceTable()],
+ ['i' => $finalPriceTable],
['entity_id', 'customer_group_id', 'website_id']
+ )->join(
+ ['e' => $this->getTable('catalog_product_entity')],
+ 'e.entity_id = i.entity_id',
+ []
)->join(
['cw' => $this->getTable('store_website')],
'cw.website_id = i.website_id',
@@ -545,7 +663,7 @@ protected function _applyCustomOption()
[]
)->join(
['o' => $this->getTable('catalog_product_option')],
- 'o.product_id = i.entity_id',
+ 'o.product_id = e.' . $metadata->getLinkField(),
['option_id']
)->join(
['opd' => $this->getTable('catalog_product_option_price')],
@@ -562,13 +680,13 @@ protected function _applyCustomOption()
$minPriceRound = new \Zend_Db_Expr("ROUND(i.price * ({$optPriceValue} / 100), 4)");
$priceExpr = $connection->getCheckSql("{$optPriceType} = 'fixed'", $optPriceValue, $minPriceRound);
- $minPrice = $connection->getCheckSql("{$priceExpr} > 0 AND o.is_require > 1", $priceExpr, 0);
+ $minPrice = $connection->getCheckSql("{$priceExpr} > 0 AND o.is_require = 1", $priceExpr, 0);
$maxPrice = $priceExpr;
$tierPriceRound = new \Zend_Db_Expr("ROUND(i.base_tier * ({$optPriceValue} / 100), 4)");
$tierPriceExpr = $connection->getCheckSql("{$optPriceType} = 'fixed'", $optPriceValue, $tierPriceRound);
- $tierPriceValue = $connection->getCheckSql("{$tierPriceExpr} > 0 AND o.is_require > 0", $tierPriceExpr, 0);
+ $tierPriceValue = $connection->getCheckSql("{$tierPriceExpr} > 0 AND o.is_require = 1", $tierPriceExpr, 0);
$tierPrice = $connection->getCheckSql("i.base_tier IS NOT NULL", $tierPriceValue, "NULL");
$select->columns(
@@ -598,7 +716,7 @@ protected function _applyCustomOption()
$query = $select->insertFromSelect($copTable);
$connection->query($query);
- $table = ['i' => $this->_getDefaultFinalPriceTable()];
+ $table = ['i' => $finalPriceTable];
$select = $connection->select()->join(
['io' => $copTable],
'i.entity_id = io.entity_id AND i.customer_group_id = io.customer_group_id' .
@@ -703,4 +821,57 @@ protected function hasEntity()
return $this->hasEntity;
}
+
+ /**
+ * @param \Zend_Db_Expr $priceExpression
+ * @return \Zend_Db_Expr
+ */
+ private function getTotalTierPriceExpression(\Zend_Db_Expr $priceExpression)
+ {
+ $maxUnsignedBigint = '~0';
+
+ return $this->getConnection()->getCheckSql(
+ implode(
+ ' AND ',
+ [
+ 'tier_price_1.value_id is NULL',
+ 'tier_price_2.value_id is NULL',
+ 'tier_price_3.value_id is NULL',
+ 'tier_price_4.value_id is NULL'
+ ]
+ ),
+ 'NULL',
+ $this->getConnection()->getLeastSql([
+ $this->getConnection()->getIfNullSql(
+ $this->getTierPriceExpressionForTable('tier_price_1', $priceExpression),
+ $maxUnsignedBigint
+ ),
+ $this->getConnection()->getIfNullSql(
+ $this->getTierPriceExpressionForTable('tier_price_2', $priceExpression),
+ $maxUnsignedBigint
+ ),
+ $this->getConnection()->getIfNullSql(
+ $this->getTierPriceExpressionForTable('tier_price_3', $priceExpression),
+ $maxUnsignedBigint
+ ),
+ $this->getConnection()->getIfNullSql(
+ $this->getTierPriceExpressionForTable('tier_price_4', $priceExpression),
+ $maxUnsignedBigint
+ ),
+ ])
+ );
+ }
+
+ private function getTierPriceExpressionForTable($tableAlias, \Zend_Db_Expr $priceExpression)
+ {
+ return $this->getConnection()->getCheckSql(
+ sprintf('%s.value = 0', $tableAlias),
+ sprintf(
+ 'ROUND(%s * (1 - ROUND(%s.percentage_value * cwd.rate, 4) / 100), 4)',
+ $priceExpression,
+ $tableAlias
+ ),
+ sprintf('ROUND(%s.value * cwd.rate, 4)', $tableAlias)
+ );
+ }
}
diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/Factory.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/Factory.php
index 21a7647214c26..9a310c7365ac9 100644
--- a/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/Factory.php
+++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/Factory.php
@@ -9,6 +9,8 @@
*/
namespace Magento\Catalog\Model\ResourceModel\Product\Indexer\Price;
+use Magento\Framework\Indexer\DimensionalIndexerInterface;
+
class Factory
{
/**
@@ -40,14 +42,17 @@ public function create($className, array $data = [])
{
$indexerPrice = $this->_objectManager->create($className, $data);
- if (!$indexerPrice instanceof \Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\DefaultPrice) {
- throw new \Magento\Framework\Exception\LocalizedException(
- __(
- '%1 doesn\'t extend \Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\DefaultPrice',
- $className
- )
- );
+ if ($indexerPrice instanceof PriceInterface || $indexerPrice instanceof DimensionalIndexerInterface) {
+ return $indexerPrice;
}
- return $indexerPrice;
+
+ throw new \Magento\Framework\Exception\LocalizedException(
+ __(
+ 'Price indexer "%1" must implement %2 or %3',
+ $className,
+ PriceInterface::class,
+ DimensionalIndexerInterface::class
+ )
+ );
}
}
diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/IndexTableRowSizeEstimator.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/IndexTableRowSizeEstimator.php
index 89df6677f2490..7707fadffe98c 100644
--- a/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/IndexTableRowSizeEstimator.php
+++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/IndexTableRowSizeEstimator.php
@@ -15,7 +15,7 @@ class IndexTableRowSizeEstimator implements \Magento\Framework\Indexer\IndexTabl
/**
* Calculated memory size for one record in catalog_product_index_price table
*/
- const MEMORY_SIZE_FOR_ONE_ROW = 120;
+ const MEMORY_SIZE_FOR_ONE_ROW = 200;
/**
* @var \Magento\Store\Api\WebsiteManagementInterface
diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/IndexTableStructure.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/IndexTableStructure.php
new file mode 100644
index 0000000000000..fb3eef2bf38eb
--- /dev/null
+++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/IndexTableStructure.php
@@ -0,0 +1,181 @@
+tableName = $tableName;
+ $this->entityField = $entityField;
+ $this->customerGroupField = $customerGroupField;
+ $this->websiteField = $websiteField;
+ $this->taxClassField = $taxClassField;
+ $this->originalPriceField = $originalPriceField;
+ $this->finalPriceField = $finalPriceField;
+ $this->minPriceField = $minPriceField;
+ $this->maxPriceField = $maxPriceField;
+ $this->tierPriceField = $tierPriceField;
+ }
+
+ /**
+ * @return string
+ */
+ public function getTableName(): string
+ {
+ return $this->tableName;
+ }
+
+ /**
+ * @return string
+ */
+ public function getEntityField(): string
+ {
+ return $this->entityField;
+ }
+
+ /**
+ * @return string
+ */
+ public function getCustomerGroupField(): string
+ {
+ return $this->customerGroupField;
+ }
+
+ /**
+ * @return string
+ */
+ public function getWebsiteField(): string
+ {
+ return $this->websiteField;
+ }
+
+ /**
+ * @return string
+ */
+ public function getTaxClassField(): string
+ {
+ return $this->taxClassField;
+ }
+
+ /**
+ * @return string
+ */
+ public function getOriginalPriceField(): string
+ {
+ return $this->originalPriceField;
+ }
+
+ /**
+ * @return string
+ */
+ public function getFinalPriceField(): string
+ {
+ return $this->finalPriceField;
+ }
+
+ /**
+ * @return string
+ */
+ public function getMinPriceField(): string
+ {
+ return $this->minPriceField;
+ }
+
+ /**
+ * @return string
+ */
+ public function getMaxPriceField(): string
+ {
+ return $this->maxPriceField;
+ }
+
+ /**
+ * @return string
+ */
+ public function getTierPriceField(): string
+ {
+ return $this->tierPriceField;
+ }
+}
diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/PriceModifierInterface.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/PriceModifierInterface.php
new file mode 100644
index 0000000000000..7aa9ec0af3856
--- /dev/null
+++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/PriceModifierInterface.php
@@ -0,0 +1,23 @@
+ 'pw.website_id',
+ CustomerGroupDimensionProvider::DIMENSION_NAME => 'cg.customer_group_id',
+ ];
+
+ /**
+ * @var \Magento\Framework\DB\Adapter\AdapterInterface
+ */
+ private $connection;
+
+ /**
+ * @var \Magento\Framework\EntityManager\MetadataPool
+ */
+ private $metadataPool;
+
+ /**
+ * BaseFinalPrice constructor.
+ * @param \Magento\Framework\App\ResourceConnection $resource
+ * @param JoinAttributeProcessor $joinAttributeProcessor
+ * @param \Magento\Framework\Module\Manager $moduleManager
+ * @param string $connectionName
+ */
+ public function __construct(
+ \Magento\Framework\App\ResourceConnection $resource,
+ JoinAttributeProcessor $joinAttributeProcessor,
+ \Magento\Framework\Module\Manager $moduleManager,
+ \Magento\Framework\Event\ManagerInterface $eventManager,
+ \Magento\Framework\EntityManager\MetadataPool $metadataPool,
+ $connectionName = 'indexer'
+ ) {
+ $this->resource = $resource;
+ $this->connectionName = $connectionName;
+ $this->joinAttributeProcessor = $joinAttributeProcessor;
+ $this->moduleManager = $moduleManager;
+ $this->eventManager = $eventManager;
+ $this->metadataPool = $metadataPool;
+ }
+
+ /**
+ * @param Dimension[] $dimensions
+ * @param string $productType
+ * @param array $entityIds
+ * @return Select
+ * @throws \LogicException
+ * @throws \Magento\Framework\Exception\LocalizedException
+ * @throws \Zend_Db_Select_Exception
+ * @SuppressWarnings(PHPMD.ExcessiveMethodLength)
+ */
+ public function getQuery(array $dimensions, string $productType, array $entityIds = []): Select
+ {
+ $connection = $this->getConnection();
+ $metadata = $this->metadataPool->getMetadata(\Magento\Catalog\Api\Data\ProductInterface::class);
+ $linkField = $metadata->getLinkField();
+
+ $select = $connection->select()->from(
+ ['e' => $this->getTable('catalog_product_entity')],
+ ['entity_id']
+ )->joinInner(
+ ['cg' => $this->getTable('customer_group')],
+ array_key_exists(CustomerGroupDimensionProvider::DIMENSION_NAME, $dimensions)
+ ? sprintf(
+ '%s = %s',
+ $this->dimensionToFieldMapper[CustomerGroupDimensionProvider::DIMENSION_NAME],
+ $dimensions[CustomerGroupDimensionProvider::DIMENSION_NAME]->getValue()
+ ) : '',
+ ['customer_group_id']
+ )->joinInner(
+ ['pw' => $this->getTable('catalog_product_website')],
+ 'pw.product_id = e.entity_id',
+ ['pw.website_id']
+ )->joinInner(
+ ['cwd' => $this->getTable('catalog_product_index_website')],
+ 'pw.website_id = cwd.website_id',
+ []
+ )->joinLeft(
+ // we need this only for BCC in case someone expects table `tp` to be present in query
+ ['tp' => $this->getTable('catalog_product_index_tier_price')],
+ 'tp.entity_id = e.entity_id AND' .
+ ' tp.customer_group_id = cg.customer_group_id AND tp.website_id = pw.website_id',
+ []
+ )->joinLeft(
+ // calculate tier price specified as Website = `All Websites` and Customer Group = `Specific Customer Group`
+ ['tier_price_1' => $this->getTable('catalog_product_entity_tier_price')],
+ 'tier_price_1.' . $linkField . ' = e.' . $linkField . ' AND tier_price_1.all_groups = 0' .
+ ' AND tier_price_1.customer_group_id = cg.customer_group_id AND tier_price_1.qty = 1' .
+ ' AND tier_price_1.website_id = 0',
+ []
+ )->joinLeft(
+ // calculate tier price specified as Website = `Specific Website`
+ //and Customer Group = `Specific Customer Group`
+ ['tier_price_2' => $this->getTable('catalog_product_entity_tier_price')],
+ 'tier_price_2.' . $linkField . ' = e.' . $linkField . ' AND tier_price_2.all_groups = 0 ' .
+ 'AND tier_price_2.customer_group_id = cg.customer_group_id AND tier_price_2.qty = 1' .
+ ' AND tier_price_2.website_id = pw.website_id',
+ []
+ )->joinLeft(
+ // calculate tier price specified as Website = `All Websites` and Customer Group = `ALL GROUPS`
+ ['tier_price_3' => $this->getTable('catalog_product_entity_tier_price')],
+ 'tier_price_3.' . $linkField . ' = e.' . $linkField . ' AND tier_price_3.all_groups = 1 ' .
+ 'AND tier_price_3.customer_group_id = 0 AND tier_price_3.qty = 1 AND tier_price_3.website_id = 0',
+ []
+ )->joinLeft(
+ // calculate tier price specified as Website = `Specific Website` and Customer Group = `ALL GROUPS`
+ ['tier_price_4' => $this->getTable('catalog_product_entity_tier_price')],
+ 'tier_price_4.' . $linkField . ' = e.' . $linkField . ' AND tier_price_4.all_groups = 1' .
+ ' AND tier_price_4.customer_group_id = 0 AND tier_price_4.qty = 1' .
+ ' AND tier_price_4.website_id = pw.website_id',
+ []
+ );
+
+ foreach ($dimensions as $dimension) {
+ if (!isset($this->dimensionToFieldMapper[$dimension->getName()])) {
+ throw new \LogicException(
+ 'Provided dimension is not valid for Price indexer: ' . $dimension->getName()
+ );
+ }
+ $select->where($this->dimensionToFieldMapper[$dimension->getName()] . ' = ?', $dimension->getValue());
+ }
+
+ if ($this->moduleManager->isEnabled('Magento_Tax')) {
+ $taxClassId = $this->joinAttributeProcessor->process($select, 'tax_class_id');
+ } else {
+ $taxClassId = new \Zend_Db_Expr(0);
+ }
+ $select->columns(['tax_class_id' => $taxClassId]);
+
+ $this->joinAttributeProcessor->process($select, 'status', Status::STATUS_ENABLED);
+
+ $price = $this->joinAttributeProcessor->process($select, 'price');
+ $specialPrice = $this->joinAttributeProcessor->process($select, 'special_price');
+ $specialFrom = $this->joinAttributeProcessor->process($select, 'special_from_date');
+ $specialTo = $this->joinAttributeProcessor->process($select, 'special_to_date');
+ $currentDate = 'cwd.website_date';
+
+ $maxUnsignedBigint = '~0';
+ $specialFromDate = $connection->getDatePartSql($specialFrom);
+ $specialToDate = $connection->getDatePartSql($specialTo);
+ $specialFromExpr = "{$specialFrom} IS NULL OR {$specialFromDate} <= {$currentDate}";
+ $specialToExpr = "{$specialTo} IS NULL OR {$specialToDate} >= {$currentDate}";
+ $specialPriceExpr = $connection->getCheckSql(
+ "{$specialPrice} IS NOT NULL AND {$specialFromExpr} AND {$specialToExpr}",
+ $specialPrice,
+ $maxUnsignedBigint
+ );
+ $tierPrice = $this->getTotalTierPriceExpression($price);
+ $tierPriceExpr = $connection->getIfNullSql($tierPrice, $maxUnsignedBigint);
+ $finalPrice = $connection->getLeastSql([
+ $price,
+ $specialPriceExpr,
+ $tierPriceExpr,
+ ]);
+
+ $select->columns(
+ [
+ //orig_price in catalog_product_index_price_final_tmp
+ 'price' => $connection->getIfNullSql($price, 0),
+ //price in catalog_product_index_price_final_tmp
+ 'final_price' => $connection->getIfNullSql($finalPrice, 0),
+ 'min_price' => $connection->getIfNullSql($finalPrice, 0),
+ 'max_price' => $connection->getIfNullSql($finalPrice, 0),
+ 'tier_price' => $tierPrice,
+ ]
+ );
+
+ $select->where("e.type_id = ?", $productType);
+
+ if ($entityIds !== null) {
+ if (count($entityIds) > 1) {
+ $select->where(sprintf('e.entity_id BETWEEN %s AND %s', min($entityIds), max($entityIds)));
+ } else {
+ $select->where('e.entity_id = ?', $entityIds);
+ }
+ }
+
+ /**
+ * throw event for backward compatibility
+ */
+ $this->eventManager->dispatch(
+ 'prepare_catalog_product_index_select',
+ [
+ 'select' => $select,
+ 'entity_field' => new ColumnValueExpression('e.entity_id'),
+ 'website_field' => new ColumnValueExpression('pw.website_id'),
+ 'store_field' => new ColumnValueExpression('cwd.default_store_id'),
+ ]
+ );
+
+ return $select;
+ }
+
+ /**
+ * Get total tier price expression
+ *
+ * @param \Zend_Db_Expr $priceExpression
+ * @return \Zend_Db_Expr
+ */
+ private function getTotalTierPriceExpression(\Zend_Db_Expr $priceExpression)
+ {
+ $maxUnsignedBigint = '~0';
+
+ return $this->getConnection()->getCheckSql(
+ implode(
+ ' AND ',
+ [
+ 'tier_price_1.value_id is NULL',
+ 'tier_price_2.value_id is NULL',
+ 'tier_price_3.value_id is NULL',
+ 'tier_price_4.value_id is NULL'
+ ]
+ ),
+ 'NULL',
+ $this->getConnection()->getLeastSql([
+ $this->getConnection()->getIfNullSql(
+ $this->getTierPriceExpressionForTable('tier_price_1', $priceExpression),
+ $maxUnsignedBigint
+ ),
+ $this->getConnection()->getIfNullSql(
+ $this->getTierPriceExpressionForTable('tier_price_2', $priceExpression),
+ $maxUnsignedBigint
+ ),
+ $this->getConnection()->getIfNullSql(
+ $this->getTierPriceExpressionForTable('tier_price_3', $priceExpression),
+ $maxUnsignedBigint
+ ),
+ $this->getConnection()->getIfNullSql(
+ $this->getTierPriceExpressionForTable('tier_price_4', $priceExpression),
+ $maxUnsignedBigint
+ ),
+ ])
+ );
+ }
+
+ /**
+ * Get tier price expression for table
+ *
+ * @param $tableAlias
+ * @param \Zend_Db_Expr $priceExpression
+ * @return \Zend_Db_Expr
+ */
+ private function getTierPriceExpressionForTable($tableAlias, \Zend_Db_Expr $priceExpression): \Zend_Db_Expr
+ {
+ return $this->getConnection()->getCheckSql(
+ sprintf('%s.value = 0', $tableAlias),
+ sprintf(
+ 'ROUND(%s * (1 - ROUND(%s.percentage_value * cwd.rate, 4) / 100), 4)',
+ $priceExpression,
+ $tableAlias
+ ),
+ sprintf('ROUND(%s.value * cwd.rate, 4)', $tableAlias)
+ );
+ }
+
+ /**
+ * Get connection
+ *
+ * return \Magento\Framework\DB\Adapter\AdapterInterface
+ * @throws \DomainException
+ */
+ private function getConnection(): \Magento\Framework\DB\Adapter\AdapterInterface
+ {
+ if ($this->connection === null) {
+ $this->connection = $this->resource->getConnection($this->connectionName);
+ }
+
+ return $this->connection;
+ }
+
+ /**
+ * Get table
+ *
+ * @param string $tableName
+ * @return string
+ */
+ private function getTable($tableName)
+ {
+ return $this->resource->getTableName($tableName, $this->connectionName);
+ }
+}
diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/Query/JoinAttributeProcessor.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/Query/JoinAttributeProcessor.php
new file mode 100644
index 0000000000000..888e68a817081
--- /dev/null
+++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/Query/JoinAttributeProcessor.php
@@ -0,0 +1,112 @@
+eavConfig = $eavConfig;
+ $this->metadataPool = $metadataPool;
+ $this->resource = $resource;
+ $this->connectionName = $connectionName;
+ }
+
+ /**
+ * @param Select $select
+ * @param string $attributeCode
+ * @param string|null $attributeValue
+ * @return \Zend_Db_Expr
+ * @throws \Magento\Framework\Exception\LocalizedException
+ * @throws \Zend_Db_Select_Exception
+ */
+ public function process(Select $select, $attributeCode, $attributeValue = null): \Zend_Db_Expr
+ {
+ $attribute = $this->eavConfig->getAttribute(\Magento\Catalog\Model\Product::ENTITY, $attributeCode);
+ $attributeId = $attribute->getAttributeId();
+ $attributeTable = $attribute->getBackend()->getTable();
+ $connection = $this->resource->getConnection($this->connectionName);
+ $joinType = $attributeValue !== null ? 'join' : 'joinLeft';
+ $productIdField = $this->metadataPool->getMetadata(ProductInterface::class)->getLinkField();
+
+ if ($attribute->isScopeGlobal()) {
+ $alias = 'ta_' . $attributeCode;
+ $select->{$joinType}(
+ [$alias => $attributeTable],
+ "{$alias}.{$productIdField} = e.{$productIdField} AND {$alias}.attribute_id = {$attributeId}" .
+ " AND {$alias}.store_id = 0",
+ []
+ );
+ $whereExpression = new Expression("{$alias}.value");
+ } else {
+ $dAlias = 'tad_' . $attributeCode;
+ $sAlias = 'tas_' . $attributeCode;
+
+ $select->{$joinType}(
+ [$dAlias => $attributeTable],
+ "{$dAlias}.{$productIdField} = e.{$productIdField} AND {$dAlias}.attribute_id = {$attributeId}" .
+ " AND {$dAlias}.store_id = 0",
+ []
+ );
+ $select->joinLeft(
+ [$sAlias => $attributeTable],
+ "{$sAlias}.{$productIdField} = e.{$productIdField} AND {$sAlias}.attribute_id = {$attributeId}" .
+ " AND {$sAlias}.store_id = cwd.default_store_id",
+ []
+ );
+ $whereExpression = $connection->getCheckSql(
+ $connection->getIfNullSql("{$sAlias}.value_id", -1) . ' > 0',
+ "{$sAlias}.value",
+ "{$dAlias}.value"
+ );
+ }
+
+ if ($attributeValue !== null) {
+ $select->where("{$whereExpression} = ?", $attributeValue);
+ }
+
+ return $whereExpression;
+ }
+}
diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/SimpleProductPrice.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/SimpleProductPrice.php
new file mode 100644
index 0000000000000..5a055e5ed9603
--- /dev/null
+++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/SimpleProductPrice.php
@@ -0,0 +1,89 @@
+baseFinalPrice = $baseFinalPrice;
+ $this->indexTableStructureFactory = $indexTableStructureFactory;
+ $this->tableMaintainer = $tableMaintainer;
+ $this->productType = $productType;
+ $this->basePriceModifier = $basePriceModifier;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function executeByDimensions(array $dimensions, \Traversable $entityIds)
+ {
+ $this->tableMaintainer->createMainTmpTable($dimensions);
+
+ $temporaryPriceTable = $this->indexTableStructureFactory->create([
+ 'tableName' => $this->tableMaintainer->getMainTmpTable($dimensions),
+ 'entityField' => 'entity_id',
+ 'customerGroupField' => 'customer_group_id',
+ 'websiteField' => 'website_id',
+ 'taxClassField' => 'tax_class_id',
+ 'originalPriceField' => 'price',
+ 'finalPriceField' => 'final_price',
+ 'minPriceField' => 'min_price',
+ 'maxPriceField' => 'max_price',
+ 'tierPriceField' => 'tier_price',
+ ]);
+ $select = $this->baseFinalPrice->getQuery($dimensions, $this->productType, iterator_to_array($entityIds));
+ $query = $select->insertFromSelect($temporaryPriceTable->getTableName(), [], false);
+ $this->tableMaintainer->getConnection()->query($query);
+
+ $this->basePriceModifier->modifyPrice($temporaryPriceTable, iterator_to_array($entityIds));
+ }
+}
diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/TierPrice.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/TierPrice.php
new file mode 100644
index 0000000000000..a866c1eaa413f
--- /dev/null
+++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/TierPrice.php
@@ -0,0 +1,220 @@
+tierPriceResourceModel = $tierPriceResourceModel;
+ $this->metadataPool = $metadataPool;
+ $this->attributeRepository = $attributeRepository;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ protected function _construct()
+ {
+ $this->_init('catalog_product_index_tier_price', 'entity_id');
+ }
+
+ /**
+ * Reindex tier price for entities.
+ *
+ * @param array $entityIds
+ * @return void
+ */
+ public function reindexEntity(array $entityIds = [])
+ {
+ $this->getConnection()->delete($this->getMainTable(), ['entity_id IN (?)' => $entityIds]);
+
+ //separate by variations for increase performance
+ $tierPriceVariations = [
+ [true, true], //all websites; all customer groups
+ [true, false], //all websites; specific customer group
+ [false, true], //specific website; all customer groups
+ [false, false], //specific website; specific customer group
+ ];
+ foreach ($tierPriceVariations as $variation) {
+ list ($isAllWebsites, $isAllCustomerGroups) = $variation;
+ $select = $this->getTierPriceSelect($isAllWebsites, $isAllCustomerGroups, $entityIds);
+ $query = $select->insertFromSelect($this->getMainTable());
+ $this->getConnection()->query($query);
+ }
+ }
+
+ /**
+ * Join websites table.
+ * If $isAllWebsites is true, for each website will be used default value for all websites,
+ * otherwise per each website will be used their own values.
+ *
+ * @param Select $select
+ * @param bool $isAllWebsites
+ */
+ private function joinWebsites(Select $select, bool $isAllWebsites)
+ {
+ $websiteTable = ['website' => $this->getTable('store_website')];
+ if ($isAllWebsites) {
+ $select->joinCross($websiteTable, [])
+ ->where('website.website_id > ?', 0)
+ ->where('tier_price.website_id = ?', 0);
+ } else {
+ $select->join($websiteTable, 'website.website_id = tier_price.website_id', [])
+ ->where('tier_price.website_id > 0');
+ }
+ }
+
+ /**
+ * Join customer groups table.
+ * If $isAllCustomerGroups is true, for each customer group will be used default value for all customer groups,
+ * otherwise per each customer group will be used their own values.
+ *
+ * @param Select $select
+ * @param bool $isAllCustomerGroups
+ */
+ private function joinCustomerGroups(Select $select, bool $isAllCustomerGroups)
+ {
+ $customerGroupTable = ['customer_group' => $this->getTable('customer_group')];
+ if ($isAllCustomerGroups) {
+ $select->joinCross($customerGroupTable, [])
+ ->where('tier_price.all_groups = ?', 1)
+ ->where('tier_price.customer_group_id = ?', 0);
+ } else {
+ $select->join($customerGroupTable, 'customer_group.customer_group_id = tier_price.customer_group_id', [])
+ ->where('tier_price.all_groups = ?', 0);
+ }
+ }
+
+ /**
+ * Join price table and return price value.
+ *
+ * @param Select $select
+ * @param string $linkField
+ * @return string
+ */
+ private function joinPrice(Select $select, string $linkField): string
+ {
+ /** @var \Magento\Catalog\Model\ResourceModel\Eav\Attribute $priceAttribute */
+ $priceAttribute = $this->attributeRepository->get('price');
+ $select->joinLeft(
+ ['entity_price_default' => $priceAttribute->getBackend()->getTable()],
+ 'entity_price_default.' . $linkField . ' = entity.' . $linkField
+ . ' AND entity_price_default.attribute_id = ' . $priceAttribute->getAttributeId()
+ . ' AND entity_price_default.store_id = 0',
+ []
+ );
+ $priceValue = 'entity_price_default.value';
+
+ if (!$priceAttribute->isScopeGlobal()) {
+ $select->joinLeft(
+ ['store_group' => $this->getTable('store_group')],
+ 'store_group.group_id = website.default_group_id',
+ []
+ )->joinLeft(
+ ['entity_price_store' => $priceAttribute->getBackend()->getTable()],
+ 'entity_price_store.' . $linkField . ' = entity.' . $linkField
+ . ' AND entity_price_store.attribute_id = ' . $priceAttribute->getAttributeId()
+ . ' AND entity_price_store.store_id = store_group.default_store_id',
+ []
+ );
+ $priceValue = $this->getConnection()
+ ->getIfNullSql('entity_price_store.value', 'entity_price_default.value');
+ }
+
+ return (string) $priceValue;
+ }
+
+ /**
+ * Build select for getting tier price data.
+ *
+ * @param bool $isAllWebsites
+ * @param bool $isAllCustomerGroups
+ * @param array $entityIds
+ * @return Select
+ */
+ private function getTierPriceSelect(bool $isAllWebsites, bool $isAllCustomerGroups, array $entityIds = []): Select
+ {
+ $entityMetadata = $this->metadataPool->getMetadata(ProductInterface::class);
+ $linkField = $entityMetadata->getLinkField();
+
+ $select = $this->getConnection()->select();
+ $select->from(['tier_price' => $this->tierPriceResourceModel->getMainTable()], [])
+ ->where('tier_price.qty = ?', 1);
+
+ $select->join(
+ ['entity' => $this->getTable('catalog_product_entity')],
+ "entity.{$linkField} = tier_price.{$linkField}",
+ []
+ );
+ if (!empty($entityIds)) {
+ $select->where('entity.entity_id IN (?)', $entityIds);
+ }
+ $this->joinWebsites($select, $isAllWebsites);
+ $this->joinCustomerGroups($select, $isAllCustomerGroups);
+
+ $priceValue = $this->joinPrice($select, $linkField);
+ $tierPriceValue = 'tier_price.value';
+ $tierPricePercentageValue = 'tier_price.percentage_value';
+ $tierPriceValueExpr = $this->getConnection()->getCheckSql(
+ $tierPriceValue,
+ $tierPriceValue,
+ sprintf('(1 - %s / 100) * %s', $tierPricePercentageValue, $priceValue)
+ );
+ $select->columns(
+ [
+ 'entity.entity_id',
+ 'customer_group.customer_group_id',
+ 'website.website_id',
+ 'tier_price' => $tierPriceValueExpr,
+ ]
+ );
+
+ return $select;
+ }
+}
diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/Link.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/Link.php
index 4b420329eef99..e6eb4804f56b2 100644
--- a/app/code/Magento/Catalog/Model/ResourceModel/Product/Link.php
+++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/Link.php
@@ -5,6 +5,8 @@
*/
namespace Magento\Catalog\Model\ResourceModel\Product;
+use Magento\Framework\DB\Adapter\AdapterInterface;
+
/**
* Catalog product link resource model
*
@@ -110,7 +112,7 @@ public function hasProductLinks($parentId)
['count' => new \Zend_Db_Expr('COUNT(*)')]
)->where(
'product_id = :product_id'
- ) ;
+ );
return $connection->fetchOne(
$select,
@@ -136,7 +138,6 @@ public function saveProductLinks($parentId, $data, $typeId)
$data = [];
}
- $attributes = $this->getAttributesByType($typeId);
$connection = $this->getConnection();
$bind = [':product_id' => (int)$parentId, ':link_type_id' => (int)$typeId];
@@ -151,42 +152,38 @@ public function saveProductLinks($parentId, $data, $typeId)
$links = $connection->fetchPairs($select, $bind);
- foreach ($data as $linkedProductId => $linkInfo) {
- $linkId = null;
- if (isset($links[$linkedProductId])) {
- $linkId = $links[$linkedProductId];
- unset($links[$linkedProductId]);
- } else {
- $bind = [
- 'product_id' => $parentId,
- 'linked_product_id' => $linkedProductId,
- 'link_type_id' => $typeId,
- ];
- $connection->insert($this->getMainTable(), $bind);
- $linkId = $connection->lastInsertId($this->getMainTable());
- }
+ list($insertData, $updateData, $deleteConditions) = $this->prepareProductLinksData(
+ $parentId,
+ $data,
+ $typeId,
+ $links
+ );
- foreach ($attributes as $attributeInfo) {
- $attributeTable = $this->getAttributeTypeTable($attributeInfo['type']);
- if ($attributeTable) {
- if (isset($linkInfo[$attributeInfo['code']])) {
- $value = $this->_prepareAttributeValue(
- $attributeInfo['type'],
- $linkInfo[$attributeInfo['code']]
- );
- $bind = [
- 'product_link_attribute_id' => $attributeInfo['id'],
- 'link_id' => $linkId,
- 'value' => $value,
- ];
- $connection->insertOnDuplicate($attributeTable, $bind, ['value']);
- } else {
- $connection->delete(
- $attributeTable,
- ['link_id = ?' => $linkId, 'product_link_attribute_id = ?' => $attributeInfo['id']]
- );
- }
- }
+ if ($insertData) {
+ $insertColumns = [
+ 'product_link_attribute_id',
+ 'link_id',
+ 'value',
+ ];
+ foreach ($insertData as $table => $values) {
+ $connection->insertArray($table, $insertColumns, $values, AdapterInterface::INSERT_IGNORE);
+ }
+ }
+ if ($updateData) {
+ // for mass update product links with constraint by unique key use insert on duplicate statement
+ foreach ($updateData as $table => $values) {
+ $connection->insertOnDuplicate($table, $values, ['value']);
+ }
+ }
+ if ($deleteConditions) {
+ foreach ($deleteConditions as $table => $deleteCondition) {
+ $connection->delete(
+ $table,
+ [
+ 'link_id = ?' => $deleteCondition['link_id'],
+ 'product_link_attribute_id = ?' => $deleteCondition['product_link_attribute_id']
+ ]
+ );
}
}
@@ -302,4 +299,69 @@ public function getParentIdsByChild($childId, $typeId)
return $parentIds;
}
+
+ /**
+ * Prepare data for insert, update or delete product link attributes
+ *
+ * @param int $parentId
+ * @param array $data
+ * @param int $typeId
+ * @param array $links
+ * @return array
+ */
+ private function prepareProductLinksData($parentId, $data, $typeId, $links)
+ {
+ $connection = $this->getConnection();
+ $attributes = $this->getAttributesByType($typeId);
+
+ $insertData = [];
+ $updateData = [];
+ $deleteConditions = [];
+
+ foreach ($data as $linkedProductId => $linkInfo) {
+ $linkId = null;
+ if (isset($links[$linkedProductId])) {
+ $linkId = $links[$linkedProductId];
+ } else {
+ $bind = [
+ 'product_id' => $parentId,
+ 'linked_product_id' => $linkedProductId,
+ 'link_type_id' => $typeId,
+ ];
+ $connection->insert($this->getMainTable(), $bind);
+ $linkId = $connection->lastInsertId($this->getMainTable());
+ }
+
+ foreach ($attributes as $attributeInfo) {
+ $attributeTable = $this->getAttributeTypeTable($attributeInfo['type']);
+ if (!$attributeTable) {
+ continue;
+ }
+ if (isset($linkInfo[$attributeInfo['code']])) {
+ $value = $this->_prepareAttributeValue(
+ $attributeInfo['type'],
+ $linkInfo[$attributeInfo['code']]
+ );
+ if (isset($links[$linkedProductId])) {
+ $updateData[$attributeTable][] = [
+ 'product_link_attribute_id' => $attributeInfo['id'],
+ 'link_id' => $linkId,
+ 'value' => $value,
+ ];
+ } else {
+ $insertData[$attributeTable][] = [
+ 'product_link_attribute_id' => $attributeInfo['id'],
+ 'link_id' => $linkId,
+ 'value' => $value,
+ ];
+ }
+ } else {
+ $deleteConditions[$attributeTable]['link_id'][] = $linkId;
+ $deleteConditions[$attributeTable]['product_link_attribute_id'][] = $attributeInfo['id'];
+ }
+ }
+ }
+
+ return [$insertData, $updateData, $deleteConditions];
+ }
}
diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/LinkedProductSelectBuilderByBasePrice.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/LinkedProductSelectBuilderByBasePrice.php
index f018e2b148f15..8841b6059c46f 100644
--- a/app/code/Magento/Catalog/Model/ResourceModel/Product/LinkedProductSelectBuilderByBasePrice.php
+++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/LinkedProductSelectBuilderByBasePrice.php
@@ -95,6 +95,7 @@ public function build($productId)
->where('t.attribute_id = ?', $priceAttribute->getAttributeId())
->where('t.value IS NOT NULL')
->order('t.value ' . Select::SQL_ASC)
+ ->order(BaseSelectProcessorInterface::PRODUCT_TABLE_ALIAS . '.' . $linkField . ' ' . Select::SQL_ASC)
->limit(1);
$priceSelect = $this->baseSelectProcessor->process($priceSelect);
diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/LinkedProductSelectBuilderBySpecialPrice.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/LinkedProductSelectBuilderBySpecialPrice.php
index b4459cd1eea07..5c47185a85bf4 100644
--- a/app/code/Magento/Catalog/Model/ResourceModel/Product/LinkedProductSelectBuilderBySpecialPrice.php
+++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/LinkedProductSelectBuilderBySpecialPrice.php
@@ -139,6 +139,7 @@ public function build($productId)
'special_to.value IS NULL OR ' . $connection->getDatePartSql('special_to.value') .' >= ?',
$currentDate
)->order('t.value ' . Select::SQL_ASC)
+ ->order(BaseSelectProcessorInterface::PRODUCT_TABLE_ALIAS . '.' . $linkField . ' ' . Select::SQL_ASC)
->limit(1);
$specialPrice = $this->baseSelectProcessor->process($specialPrice);
diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/LinkedProductSelectBuilderByTierPrice.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/LinkedProductSelectBuilderByTierPrice.php
index 79323e57b033e..37281193d6a1b 100644
--- a/app/code/Magento/Catalog/Model/ResourceModel/Product/LinkedProductSelectBuilderByTierPrice.php
+++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/LinkedProductSelectBuilderByTierPrice.php
@@ -97,6 +97,7 @@ public function build($productId)
->where('t.all_groups = 1 OR customer_group_id = ?', $this->customerSession->getCustomerGroupId())
->where('t.qty = ?', 1)
->order('t.value ' . Select::SQL_ASC)
+ ->order(BaseSelectProcessorInterface::PRODUCT_TABLE_ALIAS . '.' . $linkField . ' ' . Select::SQL_ASC)
->limit(1);
$priceSelect = $this->baseSelectProcessor->process($priceSelect);
diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/Option.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/Option.php
index 4775b96e3a448..179da06b59990 100644
--- a/app/code/Magento/Catalog/Model/ResourceModel/Product/Option.php
+++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/Option.php
@@ -307,7 +307,7 @@ protected function _saveValueTitles(\Magento\Framework\Model\AbstractModel $obje
}
/**
- * Get first col from from first row for option table
+ * Get first col from first row for option table
*
* @param string $tableName
* @param int $optionId
diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/Option/Value.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/Option/Value.php
index 03b70cd6d9153..5ffc9fbd575b6 100644
--- a/app/code/Magento/Catalog/Model/ResourceModel/Product/Option/Value.php
+++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/Option/Value.php
@@ -5,53 +5,66 @@
*/
namespace Magento\Catalog\Model\ResourceModel\Product\Option;
+use Magento\Catalog\Model\Product\Option\Value as OptionValue;
+use Magento\Directory\Model\Currency;
+use Magento\Directory\Model\CurrencyFactory;
+use Magento\Framework\App\Config\ScopeConfigInterface;
+use Magento\Framework\App\ObjectManager;
+use Magento\Framework\Locale\FormatInterface;
+use Magento\Framework\Model\AbstractModel;
+use Magento\Framework\Model\ResourceModel\Db\AbstractDb;
+use Magento\Framework\Model\ResourceModel\Db\Context;
+use Magento\Store\Model\ScopeInterface;
+use Magento\Store\Model\Store;
+use Magento\Store\Model\StoreManagerInterface;
+
/**
* Catalog product custom option resource model
*
* @author Magento Core Team
*/
-class Value extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb
+class Value extends AbstractDb
{
/**
* Store manager
*
- * @var \Magento\Store\Model\StoreManagerInterface
+ * @var StoreManagerInterface
*/
protected $_storeManager;
/**
* Currency factory
*
- * @var \Magento\Directory\Model\CurrencyFactory
+ * @var CurrencyFactory
*/
protected $_currencyFactory;
/**
* Core config model
*
- * @var \Magento\Framework\App\Config\ScopeConfigInterface
+ * @var ScopeConfigInterface
*/
protected $_config;
/**
- * @var \Magento\Framework\Locale\FormatInterface
+ * @var FormatInterface
*/
private $localeFormat;
/**
* Class constructor
*
- * @param \Magento\Framework\Model\ResourceModel\Db\Context $context
- * @param \Magento\Directory\Model\CurrencyFactory $currencyFactory
- * @param \Magento\Store\Model\StoreManagerInterface $storeManager
- * @param \Magento\Framework\App\Config\ScopeConfigInterface $config
+ * @param Context $context
+ * @param CurrencyFactory $currencyFactory
+ * @param StoreManagerInterface $storeManager
+ * @param ScopeConfigInterface $config
* @param string $connectionName
*/
public function __construct(
- \Magento\Framework\Model\ResourceModel\Db\Context $context,
- \Magento\Directory\Model\CurrencyFactory $currencyFactory,
- \Magento\Store\Model\StoreManagerInterface $storeManager,
- \Magento\Framework\App\Config\ScopeConfigInterface $config,
+ Context $context,
+ CurrencyFactory $currencyFactory,
+ StoreManagerInterface $storeManager,
+ ScopeConfigInterface $config,
$connectionName = null
) {
$this->_currencyFactory = $currencyFactory;
@@ -74,10 +87,10 @@ protected function _construct()
* Proceed operations after object is saved
* Save options store data
*
- * @param \Magento\Framework\Model\AbstractModel $object
- * @return \Magento\Framework\Model\ResourceModel\Db\AbstractDb
+ * @param AbstractModel $object
+ * @return AbstractDb
*/
- protected function _afterSave(\Magento\Framework\Model\AbstractModel $object)
+ protected function _afterSave(AbstractModel $object)
{
$this->_saveValuePrices($object);
$this->_saveValueTitles($object);
@@ -88,20 +101,21 @@ protected function _afterSave(\Magento\Framework\Model\AbstractModel $object)
/**
* Save option value price data
*
- * @param \Magento\Framework\Model\AbstractModel $object
+ * @param AbstractModel $object
* @return void
* @SuppressWarnings(PHPMD.CyclomaticComplexity)
* @SuppressWarnings(PHPMD.ExcessiveMethodLength)
*/
- protected function _saveValuePrices(\Magento\Framework\Model\AbstractModel $object)
+ protected function _saveValuePrices(AbstractModel $object)
{
+ $objectPrice = $object->getPrice();
$priceTable = $this->getTable('catalog_product_option_type_price');
- $formattedPrice = $this->getLocaleFormatter()->getNumber($object->getPrice());
+ $formattedPrice = $this->getLocaleFormatter()->getNumber($objectPrice);
$price = (double)sprintf('%F', $formattedPrice);
$priceType = $object->getPriceType();
- if ($object->getPrice() && $priceType) {
+ if (isset($objectPrice) && $priceType) {
//save for store_id = 0
$select = $this->getConnection()->select()->from(
$priceTable,
@@ -111,7 +125,7 @@ protected function _saveValuePrices(\Magento\Framework\Model\AbstractModel $obje
(int)$object->getId()
)->where(
'store_id = ?',
- \Magento\Store\Model\Store::DEFAULT_STORE_ID
+ Store::DEFAULT_STORE_ID
);
$optionTypeId = $this->getConnection()->fetchOne($select);
@@ -120,7 +134,7 @@ protected function _saveValuePrices(\Magento\Framework\Model\AbstractModel $obje
$bind = ['price' => $price, 'price_type' => $priceType];
$where = [
'option_type_id = ?' => $optionTypeId,
- 'store_id = ?' => \Magento\Store\Model\Store::DEFAULT_STORE_ID,
+ 'store_id = ?' => Store::DEFAULT_STORE_ID,
];
$this->getConnection()->update($priceTable, $bind, $where);
@@ -128,7 +142,7 @@ protected function _saveValuePrices(\Magento\Framework\Model\AbstractModel $obje
} else {
$bind = [
'option_type_id' => (int)$object->getId(),
- 'store_id' => \Magento\Store\Model\Store::DEFAULT_STORE_ID,
+ 'store_id' => Store::DEFAULT_STORE_ID,
'price' => $price,
'price_type' => $priceType,
];
@@ -137,28 +151,31 @@ protected function _saveValuePrices(\Magento\Framework\Model\AbstractModel $obje
}
$scope = (int)$this->_config->getValue(
- \Magento\Store\Model\Store::XML_PATH_PRICE_SCOPE,
- \Magento\Store\Model\ScopeInterface::SCOPE_STORE
+ Store::XML_PATH_PRICE_SCOPE,
+ ScopeInterface::SCOPE_STORE
);
- if ($scope == \Magento\Store\Model\Store::PRICE_SCOPE_WEBSITE
+ if ($scope == Store::PRICE_SCOPE_WEBSITE
&& $priceType
- && $object->getPrice()
- && $object->getStoreId() != \Magento\Store\Model\Store::DEFAULT_STORE_ID
+ && isset($objectPrice)
+ && $object->getStoreId() != Store::DEFAULT_STORE_ID
) {
- $baseCurrency = $this->_config->getValue(
- \Magento\Directory\Model\Currency::XML_PATH_CURRENCY_BASE,
- 'default'
+ $website = $this->_storeManager->getStore($object->getStoreId())->getWebsite();
+
+ $websiteBaseCurrency = $this->_config->getValue(
+ Currency::XML_PATH_CURRENCY_BASE,
+ ScopeInterface::SCOPE_WEBSITE,
+ $website
);
- $storeIds = $this->_storeManager->getStore($object->getStoreId())->getWebsite()->getStoreIds();
+ $storeIds = $website->getStoreIds();
if (is_array($storeIds)) {
foreach ($storeIds as $storeId) {
if ($priceType == 'fixed') {
$storeCurrency = $this->_storeManager->getStore($storeId)->getBaseCurrencyCode();
- /** @var $currencyModel \Magento\Directory\Model\Currency */
+ /** @var $currencyModel Currency */
$currencyModel = $this->_currencyFactory->create();
- $currencyModel->load($baseCurrency);
+ $currencyModel->load($websiteBaseCurrency);
$rate = $currencyModel->getRate($storeCurrency);
if (!$rate) {
$rate = 1;
@@ -198,8 +215,8 @@ protected function _saveValuePrices(\Magento\Framework\Model\AbstractModel $obje
}
}
} else {
- if ($scope == \Magento\Store\Model\Store::PRICE_SCOPE_WEBSITE
- && !$object->getPrice()
+ if ($scope == Store::PRICE_SCOPE_WEBSITE
+ && !isset($objectPrice)
&& !$priceType
) {
$storeIds = $this->_storeManager->getStore($object->getStoreId())->getWebsite()->getStoreIds();
@@ -217,13 +234,13 @@ protected function _saveValuePrices(\Magento\Framework\Model\AbstractModel $obje
/**
* Save option value title data
*
- * @param \Magento\Framework\Model\AbstractModel $object
+ * @param AbstractModel $object
* @return void
* @SuppressWarnings(PHPMD.CyclomaticComplexity)
*/
- protected function _saveValueTitles(\Magento\Framework\Model\AbstractModel $object)
+ protected function _saveValueTitles(AbstractModel $object)
{
- foreach ([\Magento\Store\Model\Store::DEFAULT_STORE_ID, $object->getStoreId()] as $storeId) {
+ foreach ([Store::DEFAULT_STORE_ID, $object->getStoreId()] as $storeId) {
$titleTable = $this->getTable('catalog_product_option_type_title');
$select = $this->getConnection()->select()->from(
$titleTable,
@@ -238,11 +255,12 @@ protected function _saveValueTitles(\Magento\Framework\Model\AbstractModel $obje
$optionTypeId = $this->getConnection()->fetchOne($select);
$existInCurrentStore = $this->getOptionIdFromOptionTable($titleTable, (int)$object->getId(), (int)$storeId);
- if ($storeId != \Magento\Store\Model\Store::DEFAULT_STORE_ID && $object->getData('is_delete_store_title')) {
+ if ($storeId != Store::DEFAULT_STORE_ID && $object->getData('is_delete_store_title')) {
$object->unsetData('title');
}
- if ($object->getTitle()) {
+ /*** Checking whether title is not null ***/
+ if ($object->getTitle()!= null) {
if ($existInCurrentStore) {
if ($storeId == $object->getStoreId()) {
$where = [
@@ -256,11 +274,11 @@ protected function _saveValueTitles(\Magento\Framework\Model\AbstractModel $obje
$existInDefaultStore = $this->getOptionIdFromOptionTable(
$titleTable,
(int)$object->getId(),
- \Magento\Store\Model\Store::DEFAULT_STORE_ID
+ Store::DEFAULT_STORE_ID
);
// we should insert record into not default store only of if it does not exist in default store
- if (($storeId == \Magento\Store\Model\Store::DEFAULT_STORE_ID && !$existInDefaultStore)
- || ($storeId != \Magento\Store\Model\Store::DEFAULT_STORE_ID && !$existInCurrentStore)
+ if (($storeId == Store::DEFAULT_STORE_ID && !$existInDefaultStore)
+ || ($storeId != Store::DEFAULT_STORE_ID && !$existInCurrentStore)
) {
$bind = [
'option_type_id' => (int)$object->getId(),
@@ -273,7 +291,7 @@ protected function _saveValueTitles(\Magento\Framework\Model\AbstractModel $obje
} else {
if ($storeId
&& $optionTypeId
- && $object->getStoreId() > \Magento\Store\Model\Store::DEFAULT_STORE_ID
+ && $object->getStoreId() > Store::DEFAULT_STORE_ID
) {
$where = [
'option_type_id = ?' => (int)$optionTypeId,
@@ -286,7 +304,7 @@ protected function _saveValueTitles(\Magento\Framework\Model\AbstractModel $obje
}
/**
- * Get first col from from first row for option table
+ * Get first col from first row for option table
*
* @param string $tableName
* @param int $optionId
@@ -353,12 +371,12 @@ public function deleteValues($optionTypeId)
/**
* Duplicate product options value
*
- * @param \Magento\Catalog\Model\Product\Option\Value $object
+ * @param OptionValue $object
* @param int $oldOptionId
* @param int $newOptionId
- * @return \Magento\Catalog\Model\Product\Option\Value
+ * @return OptionValue
*/
- public function duplicate(\Magento\Catalog\Model\Product\Option\Value $object, $oldOptionId, $newOptionId)
+ public function duplicate(OptionValue $object, $oldOptionId, $newOptionId)
{
$connection = $this->getConnection();
$select = $connection->select()->from($this->getMainTable())->where('option_id = ?', $oldOptionId);
@@ -425,14 +443,14 @@ public function duplicate(\Magento\Catalog\Model\Product\Option\Value $object, $
/**
* Get FormatInterface to convert price from string to number format
*
- * @return \Magento\Framework\Locale\FormatInterface
+ * @return FormatInterface
* @deprecated 101.0.8
*/
private function getLocaleFormatter()
{
if ($this->localeFormat === null) {
- $this->localeFormat = \Magento\Framework\App\ObjectManager::getInstance()
- ->get(\Magento\Framework\Locale\FormatInterface::class);
+ $this->localeFormat = ObjectManager::getInstance()
+ ->get(FormatInterface::class);
}
return $this->localeFormat;
}
diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Url.php b/app/code/Magento/Catalog/Model/ResourceModel/Url.php
index 1cb2fed839dde..6ca688f36c265 100644
--- a/app/code/Magento/Catalog/Model/ResourceModel/Url.php
+++ b/app/code/Magento/Catalog/Model/ResourceModel/Url.php
@@ -14,6 +14,7 @@
use Magento\Catalog\Api\Data\CategoryInterface;
use Magento\Framework\EntityManager\MetadataPool;
use Magento\Framework\App\ObjectManager;
+use Magento\Catalog\Model\Indexer\Category\Product\TableMaintainer;
/**
* Class Url
@@ -101,6 +102,11 @@ class Url extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb
*/
protected $metadataPool;
+ /**
+ * @var TableMaintainer
+ */
+ private $tableMaintainer;
+
/**
* Url constructor.
* @param \Magento\Framework\Model\ResourceModel\Db\Context $context
@@ -110,6 +116,7 @@ class Url extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb
* @param \Magento\Catalog\Model\Category $catalogCategory
* @param \Psr\Log\LoggerInterface $logger
* @param null $connectionName
+ * @param TableMaintainer|null $tableMaintainer
*/
public function __construct(
\Magento\Framework\Model\ResourceModel\Db\Context $context,
@@ -118,7 +125,8 @@ public function __construct(
Product $productResource,
\Magento\Catalog\Model\Category $catalogCategory,
\Psr\Log\LoggerInterface $logger,
- $connectionName = null
+ $connectionName = null,
+ TableMaintainer $tableMaintainer = null
) {
$this->_storeManager = $storeManager;
$this->_eavConfig = $eavConfig;
@@ -126,6 +134,7 @@ public function __construct(
$this->_catalogCategory = $catalogCategory;
$this->_logger = $logger;
parent::__construct($context, $connectionName);
+ $this->tableMaintainer = $tableMaintainer ?: ObjectManager::getInstance()->get(TableMaintainer::class);
}
/**
@@ -655,43 +664,52 @@ public function getRewriteByProductStore(array $products)
}
$connection = $this->getConnection();
- $select = $connection->select()->from(
- ['i' => $this->getTable('catalog_category_product_index')],
- ['product_id', 'store_id', 'visibility']
- )->joinLeft(
- ['u' => $this->getMainTable()],
- 'i.product_id = u.entity_id AND i.store_id = u.store_id'
- . ' AND u.entity_type = "' . ProductUrlRewriteGenerator::ENTITY_TYPE . '"',
- ['request_path']
- )->joinLeft(
- ['r' => $this->getTable('catalog_url_rewrite_product_category')],
- 'u.url_rewrite_id = r.url_rewrite_id AND r.category_id is NULL',
- []
- );
-
- $bind = [];
+ $storesProducts = [];
foreach ($products as $productId => $storeId) {
- $catId = $this->_storeManager->getStore($storeId)->getRootCategoryId();
- $productBind = 'product_id' . $productId;
- $storeBind = 'store_id' . $storeId;
- $catBind = 'category_id' . $catId;
- $cond = '(' . implode(
- ' AND ',
- ['i.product_id = :' . $productBind, 'i.store_id = :' . $storeBind, 'i.category_id = :' . $catBind]
- ) . ')';
- $bind[$productBind] = $productId;
- $bind[$storeBind] = $storeId;
- $bind[$catBind] = $catId;
- $select->orWhere($cond);
+ $storesProducts[$storeId][] = $productId;
}
- $rowSet = $connection->fetchAll($select, $bind);
- foreach ($rowSet as $row) {
- $result[$row['product_id']] = [
- 'store_id' => $row['store_id'],
- 'visibility' => $row['visibility'],
- 'url_rewrite' => $row['request_path'],
- ];
+ foreach ($storesProducts as $storeId => $productIds) {
+ $select = $connection->select()->from(
+ ['i' => $this->tableMaintainer->getMainTable($storeId)],
+ ['product_id', 'store_id', 'visibility']
+ )->joinLeft(
+ ['u' => $this->getMainTable()],
+ 'i.product_id = u.entity_id AND i.store_id = u.store_id'
+ . ' AND u.entity_type = "' . ProductUrlRewriteGenerator::ENTITY_TYPE . '"',
+ ['request_path']
+ )->joinLeft(
+ ['r' => $this->getTable('catalog_url_rewrite_product_category')],
+ 'u.url_rewrite_id = r.url_rewrite_id AND r.category_id is NULL',
+ []
+ );
+
+ $bind = [];
+ foreach ($productIds as $productId) {
+ $catId = $this->_storeManager->getStore($storeId)->getRootCategoryId();
+ $productBind = 'product_id' . $productId;
+ $storeBind = 'store_id' . $storeId;
+ $catBind = 'category_id' . $catId;
+ $bindArray = [
+ 'i.product_id = :' . $productBind,
+ 'i.store_id = :' . $storeBind,
+ 'i.category_id = :' . $catBind
+ ];
+ $cond = '(' . implode(' AND ', $bindArray) . ')';
+ $bind[$productBind] = $productId;
+ $bind[$storeBind] = $storeId;
+ $bind[$catBind] = $catId;
+ $select->orWhere($cond);
+ }
+
+ $rowSet = $connection->fetchAll($select, $bind);
+ foreach ($rowSet as $row) {
+ $result[$row['product_id']] = [
+ 'store_id' => $row['store_id'],
+ 'visibility' => $row['visibility'],
+ 'url_rewrite' => $row['request_path'],
+ ];
+ }
}
return $result;
diff --git a/app/code/Magento/Catalog/Model/View/Asset/Image.php b/app/code/Magento/Catalog/Model/View/Asset/Image.php
index ce85ba21d211f..dc81236c34cd9 100644
--- a/app/code/Magento/Catalog/Model/View/Asset/Image.php
+++ b/app/code/Magento/Catalog/Model/View/Asset/Image.php
@@ -19,6 +19,8 @@
*/
class Image implements LocalInterface
{
+ private $sourceContentType;
+
/**
* @var string
*/
@@ -67,6 +69,12 @@ public function __construct(
$filePath,
array $miscParams = []
) {
+ if (array_key_exists('image_type', $miscParams)) {
+ $this->sourceContentType = $miscParams['image_type'];
+ unset($miscParams['image_type']);
+ } else {
+ $this->sourceContentType = $this->contentType;
+ }
$this->mediaConfig = $mediaConfig;
$this->context = $context;
$this->filePath = $filePath;
@@ -129,7 +137,7 @@ public function getSourceFile()
*/
public function getSourceContentType()
{
- return $this->contentType;
+ return $this->sourceContentType;
}
/**
diff --git a/app/code/Magento/Catalog/Observer/CategoryProductIndexer.php b/app/code/Magento/Catalog/Observer/CategoryProductIndexer.php
new file mode 100644
index 0000000000000..1a131ed71973d
--- /dev/null
+++ b/app/code/Magento/Catalog/Observer/CategoryProductIndexer.php
@@ -0,0 +1,43 @@
+processor = $processor;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function execute(Observer $observer)
+ {
+ $productIds = $observer->getEvent()->getProductIds();
+ if (!empty($productIds) && $this->processor->isIndexerScheduled()) {
+ $this->processor->markIndexerAsInvalid();
+ }
+ }
+}
diff --git a/app/code/Magento/Catalog/Observer/UnsetSpecialPrice.php b/app/code/Magento/Catalog/Observer/UnsetSpecialPrice.php
deleted file mode 100644
index 0ba1251edc7d6..0000000000000
--- a/app/code/Magento/Catalog/Observer/UnsetSpecialPrice.php
+++ /dev/null
@@ -1,31 +0,0 @@
-getEvent()->getProduct();
- if ($product->getSpecialPrice() === null) {
- $product->setData('special_price', '');
- }
-
- return $this;
- }
-}
diff --git a/app/code/Magento/Catalog/Plugin/Block/Topmenu.php b/app/code/Magento/Catalog/Plugin/Block/Topmenu.php
index 5119a32d921de..debf7fd002b2d 100644
--- a/app/code/Magento/Catalog/Plugin/Block/Topmenu.php
+++ b/app/code/Magento/Catalog/Plugin/Block/Topmenu.php
@@ -22,7 +22,7 @@ class Topmenu
protected $catalogCategory;
/**
- * @var \Magento\Catalog\Model\ResourceModel\Category\CollectionFactory
+ * @var \Magento\Catalog\Model\ResourceModel\Category\StateDependentCollectionFactory
*/
private $collectionFactory;
@@ -40,13 +40,13 @@ class Topmenu
* Initialize dependencies.
*
* @param \Magento\Catalog\Helper\Category $catalogCategory
- * @param \Magento\Catalog\Model\ResourceModel\Category\CollectionFactory $categoryCollectionFactory
+ * @param \Magento\Catalog\Model\ResourceModel\Category\StateDependentCollectionFactory $categoryCollectionFactory
* @param \Magento\Store\Model\StoreManagerInterface $storeManager
* @param \Magento\Catalog\Model\Layer\Resolver $layerResolver
*/
public function __construct(
\Magento\Catalog\Helper\Category $catalogCategory,
- \Magento\Catalog\Model\ResourceModel\Category\CollectionFactory $categoryCollectionFactory,
+ \Magento\Catalog\Model\ResourceModel\Category\StateDependentCollectionFactory $categoryCollectionFactory,
\Magento\Store\Model\StoreManagerInterface $storeManager,
\Magento\Catalog\Model\Layer\Resolver $layerResolver
) {
@@ -162,6 +162,7 @@ private function getCategoryAsArray($category, $currentCategory, $isParentActive
'url' => $this->catalogCategory->getCategoryUrl($category),
'has_active' => in_array((string)$category->getId(), explode('/', $currentCategory->getPath()), true),
'is_active' => $category->getId() == $currentCategory->getId(),
+ 'is_category' => true,
'is_parent_active' => $isParentActive
];
}
diff --git a/app/code/Magento/Catalog/Plugin/Model/Attribute/Backend/AttributeValidation.php b/app/code/Magento/Catalog/Plugin/Model/Attribute/Backend/AttributeValidation.php
index 597a1466a125e..5ccec4c3a4c7b 100644
--- a/app/code/Magento/Catalog/Plugin/Model/Attribute/Backend/AttributeValidation.php
+++ b/app/code/Magento/Catalog/Plugin/Model/Attribute/Backend/AttributeValidation.php
@@ -14,6 +14,11 @@ class AttributeValidation
*/
private $storeManager;
+ /**
+ * @var array
+ */
+ private $allowedEntityTypes;
+
/**
* @param \Magento\Store\Model\StoreManagerInterface $storeManager
* @param array $allowedEntityTypes
@@ -30,6 +35,7 @@ public function __construct(
* @param \Magento\Eav\Model\Entity\Attribute\Backend\AbstractBackend $subject
* @param \Closure $proceed
* @param \Magento\Framework\DataObject $entity
+ * @throws \Magento\Framework\Exception\NoSuchEntityException
* @return bool
*/
public function aroundValidate(
@@ -41,7 +47,7 @@ public function aroundValidate(
return $entity instanceof $allowedEntity;
}, $this->allowedEntityTypes)));
- if ($isAllowedType && $this->storeManager->getStore()->getId() !== Store::DEFAULT_STORE_ID) {
+ if ($isAllowedType && (int) $this->storeManager->getStore()->getId() !== Store::DEFAULT_STORE_ID) {
$attrCode = $subject->getAttribute()->getAttributeCode();
// Null is meaning "no value" which should be overridden by value from default scope
if (array_key_exists($attrCode, $entity->getData()) && $entity->getData($attrCode) === null) {
diff --git a/app/code/Magento/Catalog/Plugin/Model/AttributeSetRepository/RemoveProducts.php b/app/code/Magento/Catalog/Plugin/Model/AttributeSetRepository/RemoveProducts.php
new file mode 100644
index 0000000000000..342b703ded0a5
--- /dev/null
+++ b/app/code/Magento/Catalog/Plugin/Model/AttributeSetRepository/RemoveProducts.php
@@ -0,0 +1,58 @@
+collectionFactory = $collectionFactory;
+ }
+
+ /**
+ * Delete related to specific attribute set products, if attribute set was removed successfully.
+ *
+ * @param AttributeSetRepositoryInterface $subject
+ * @param bool $result
+ * @param AttributeSetInterface $attributeSet
+ * @return bool
+ *
+ * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+ */
+ public function afterDelete(
+ AttributeSetRepositoryInterface $subject,
+ bool $result,
+ AttributeSetInterface $attributeSet
+ ) {
+ /** @var Collection $productCollection */
+ $productCollection = $this->collectionFactory->create();
+ $productCollection->addFieldToFilter('attribute_set_id', ['eq' => $attributeSet->getId()]);
+ $productCollection->delete();
+
+ return $result;
+ }
+}
diff --git a/app/code/Magento/Catalog/Plugin/Model/ResourceModel/ReadSnapshotPlugin.php b/app/code/Magento/Catalog/Plugin/Model/ResourceModel/ReadSnapshotPlugin.php
new file mode 100644
index 0000000000000..ff4d2f93c912a
--- /dev/null
+++ b/app/code/Magento/Catalog/Plugin/Model/ResourceModel/ReadSnapshotPlugin.php
@@ -0,0 +1,97 @@
+metadataPool = $metadataPool;
+ $this->config = $config;
+ }
+
+ /**
+ * @param ReadSnapshot $subject
+ * @param array $entityData
+ * @param string $entityType
+ * @return array
+ * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+ */
+ public function afterExecute(ReadSnapshot $subject, array $entityData, $entityType)
+ {
+ if (!in_array($entityType, [ProductInterface::class, CategoryInterface::class], true)) {
+ return $entityData;
+ }
+
+ $metadata = $this->metadataPool->getMetadata($entityType);
+ $connection = $metadata->getEntityConnection();
+ $globalAttributes = [];
+ $attributesMap = [];
+ $eavEntityType = $metadata->getEavEntityType();
+ $attributes = null === $eavEntityType
+ ? []
+ : $this->config->getEntityAttributes($eavEntityType, new \Magento\Framework\DataObject($entityData));
+
+ /** @var \Magento\Eav\Model\Entity\Attribute\AbstractAttribute $attribute */
+ foreach ($attributes as $attribute) {
+ if (!$attribute->isStatic() && $attribute->isScopeGlobal()) {
+ $globalAttributes[$attribute->getBackend()->getTable()][] = $attribute->getAttributeId();
+ $attributesMap[$attribute->getAttributeId()] = $attribute->getAttributeCode();
+ }
+ }
+
+ if ($globalAttributes) {
+ $selects = [];
+ foreach ($globalAttributes as $table => $attributeIds) {
+ $select = $connection->select()
+ ->from(
+ ['t' => $table],
+ ['value' => 't.value', 'attribute_id' => 't.attribute_id']
+ )
+ ->where($metadata->getLinkField() . ' = ?', $entityData[$metadata->getLinkField()])
+ ->where('attribute_id' . ' in (?)', $attributeIds)
+ ->where('store_id = ?', \Magento\Store\Model\Store::DEFAULT_STORE_ID);
+ $selects[] = $select;
+ }
+ $unionSelect = new \Magento\Framework\DB\Sql\UnionExpression(
+ $selects,
+ \Magento\Framework\DB\Select::SQL_UNION_ALL
+ );
+ foreach ($connection->fetchAll($unionSelect) as $attributeValue) {
+ $entityData[$attributesMap[$attributeValue['attribute_id']]] = $attributeValue['value'];
+ }
+ }
+
+ return $entityData;
+ }
+}
diff --git a/app/code/Magento/Catalog/Pricing/Price/ConfiguredOptions.php b/app/code/Magento/Catalog/Pricing/Price/ConfiguredOptions.php
new file mode 100644
index 0000000000000..212d4e7536701
--- /dev/null
+++ b/app/code/Magento/Catalog/Pricing/Price/ConfiguredOptions.php
@@ -0,0 +1,45 @@
+getProduct();
+ $value = 0.;
+ $optionIds = $item->getOptionByCode('option_ids');
+ if ($optionIds) {
+ foreach (explode(',', $optionIds->getValue()) as $optionId) {
+ $option = $product->getOptionById($optionId);
+ if ($option) {
+ $itemOption = $item->getOptionByCode('option_' . $option->getId());
+ /** @var $group \Magento\Catalog\Model\Product\Option\Type\DefaultType */
+ $group = $option->groupFactory($option->getType())
+ ->setOption($option)
+ ->setConfigurationItem($item)
+ ->setConfigurationItemOption($itemOption);
+ $value += $group->getOptionPrice($itemOption->getValue(), $basePrice);
+ }
+ }
+ }
+ return $value;
+ }
+}
diff --git a/app/code/Magento/Catalog/Pricing/Price/ConfiguredPrice.php b/app/code/Magento/Catalog/Pricing/Price/ConfiguredPrice.php
index 87d031d8d5b35..f8e89b95569a5 100644
--- a/app/code/Magento/Catalog/Pricing/Price/ConfiguredPrice.php
+++ b/app/code/Magento/Catalog/Pricing/Price/ConfiguredPrice.php
@@ -9,6 +9,7 @@
use Magento\Catalog\Model\Product;
use Magento\Catalog\Model\Product\Configuration\Item\ItemInterface;
use Magento\Framework\Pricing\Adjustment\CalculatorInterface;
+use Magento\Framework\App\ObjectManager;
/**
* Configured price model
@@ -25,21 +26,29 @@ class ConfiguredPrice extends FinalPrice implements ConfiguredPriceInterface
*/
protected $item;
+ /**
+ * @var ConfiguredOptions
+ */
+ private $configuredOptions;
+
/**
* @param Product $saleableItem
* @param float $quantity
* @param CalculatorInterface $calculator
* @param \Magento\Framework\Pricing\PriceCurrencyInterface $priceCurrency
- * @param ItemInterface $item
+ * @param ItemInterface|null $item
+ * @param ConfiguredOptions|null $configuredOptions
*/
public function __construct(
Product $saleableItem,
$quantity,
CalculatorInterface $calculator,
\Magento\Framework\Pricing\PriceCurrencyInterface $priceCurrency,
- ItemInterface $item = null
+ ItemInterface $item = null,
+ ConfiguredOptions $configuredOptions = null
) {
$this->item = $item;
+ $this->configuredOptions = $configuredOptions ?: ObjectManager::getInstance()->get(ConfiguredOptions::class);
parent::__construct($saleableItem, $quantity, $calculator, $priceCurrency);
}
@@ -56,9 +65,10 @@ public function setItem(ItemInterface $item)
/**
* Get value of configured options
*
- * @return array
+ * @deprecated ConfiguredOptions::getItemOptionsValue is used instead
+ * @return float
*/
- protected function getOptionsValue()
+ protected function getOptionsValue(): float
{
$product = $this->item->getProduct();
$value = 0.;
@@ -88,6 +98,9 @@ protected function getOptionsValue()
*/
public function getValue()
{
- return $this->item ? parent::getValue() + $this->getOptionsValue() : parent::getValue();
+ $basePrice = parent::getValue();
+ return $this->item
+ ? $basePrice + $this->configuredOptions->getItemOptionsValue($basePrice, $this->item)
+ : $basePrice;
}
}
diff --git a/app/code/Magento/Catalog/Pricing/Price/ConfiguredPriceInterface.php b/app/code/Magento/Catalog/Pricing/Price/ConfiguredPriceInterface.php
index e6d35a0f5239a..155f8d55d9a4c 100644
--- a/app/code/Magento/Catalog/Pricing/Price/ConfiguredPriceInterface.php
+++ b/app/code/Magento/Catalog/Pricing/Price/ConfiguredPriceInterface.php
@@ -18,6 +18,11 @@ interface ConfiguredPriceInterface
*/
const CONFIGURED_PRICE_CODE = 'configured_price';
+ /**
+ * Regular price type configured
+ */
+ const CONFIGURED_REGULAR_PRICE_CODE = 'configured_regular_price';
+
/**
* @param ItemInterface $item
* @return $this
diff --git a/app/code/Magento/Catalog/Pricing/Price/ConfiguredPriceSelection.php b/app/code/Magento/Catalog/Pricing/Price/ConfiguredPriceSelection.php
new file mode 100644
index 0000000000000..607bcb411c1cd
--- /dev/null
+++ b/app/code/Magento/Catalog/Pricing/Price/ConfiguredPriceSelection.php
@@ -0,0 +1,61 @@
+calculator = $calculator;
+ }
+
+ /**
+ * Get Selection pricing list.
+ *
+ * @param \Magento\Catalog\Pricing\Price\ConfiguredPriceInterface $price
+ * @return array
+ */
+ public function getSelectionPriceList(\Magento\Catalog\Pricing\Price\ConfiguredPriceInterface $price): array
+ {
+ $selectionPriceList = [];
+ foreach ($price->getOptions() as $option) {
+ $selectionPriceList = array_merge(
+ $selectionPriceList,
+ $this->createSelectionPriceList($option, $price->getProduct())
+ );
+ }
+ return $selectionPriceList;
+ }
+
+ /**
+ * Create Selection Price List
+ *
+ * @param ExtensibleDataInterface $option
+ * @param Product $product
+ * @return array
+ */
+ private function createSelectionPriceList(ExtensibleDataInterface $option, Product $product): array
+ {
+ return $this->calculator->createSelectionPriceList($option, $product);
+ }
+}
diff --git a/app/code/Magento/Catalog/Pricing/Price/ConfiguredRegularPrice.php b/app/code/Magento/Catalog/Pricing/Price/ConfiguredRegularPrice.php
new file mode 100644
index 0000000000000..75b4d13d1c8d8
--- /dev/null
+++ b/app/code/Magento/Catalog/Pricing/Price/ConfiguredRegularPrice.php
@@ -0,0 +1,77 @@
+item = $item;
+ $this->configuredOptions = $configuredOptions;
+ parent::__construct($saleableItem, $quantity, $calculator, $priceCurrency);
+ }
+
+ /**
+ * @param ItemInterface $item
+ * @return $this
+ */
+ public function setItem(ItemInterface $item)
+ {
+ $this->item = $item;
+ return $this;
+ }
+
+ /**
+ * Price value of product with configured options
+ *
+ * @return bool|float
+ */
+ public function getValue()
+ {
+ $basePrice = parent::getValue();
+ return $this->item && $basePrice !== false
+ ? $basePrice + $this->configuredOptions->getItemOptionsValue($basePrice, $this->item)
+ : $basePrice;
+ }
+}
diff --git a/app/code/Magento/Catalog/Pricing/Price/CustomOptionPrice.php b/app/code/Magento/Catalog/Pricing/Price/CustomOptionPrice.php
index 5026286610118..43697be8ccf9d 100644
--- a/app/code/Magento/Catalog/Pricing/Price/CustomOptionPrice.php
+++ b/app/code/Magento/Catalog/Pricing/Price/CustomOptionPrice.php
@@ -35,32 +35,42 @@ class CustomOptionPrice extends AbstractPrice implements CustomOptionPriceInterf
*/
protected $excludeAdjustment = null;
+ /**
+ * @var CustomOptionPriceCalculator
+ */
+ private $customOptionPriceCalculator;
+
/**
* @param SaleableInterface $saleableItem
* @param float $quantity
* @param CalculatorInterface $calculator
* @param \Magento\Framework\Pricing\PriceCurrencyInterface $priceCurrency
- * @param array $excludeAdjustment
+ * @param array|null $excludeAdjustment
+ * @param CustomOptionPriceCalculator|null $customOptionPriceCalculator
*/
public function __construct(
SaleableInterface $saleableItem,
$quantity,
CalculatorInterface $calculator,
\Magento\Framework\Pricing\PriceCurrencyInterface $priceCurrency,
- $excludeAdjustment = null
+ $excludeAdjustment = null,
+ CustomOptionPriceCalculator $customOptionPriceCalculator = null
) {
parent::__construct($saleableItem, $quantity, $calculator, $priceCurrency);
$this->excludeAdjustment = $excludeAdjustment;
+ $this->customOptionPriceCalculator = $customOptionPriceCalculator
+ ?? \Magento\Framework\App\ObjectManager::getInstance()->get(CustomOptionPriceCalculator::class);
}
/**
* Get minimal and maximal option values
*
+ * @param string $priceCode
* @return array
* @SuppressWarnings(PHPMD.CyclomaticComplexity)
* @SuppressWarnings(PHPMD.NPathComplexity)
*/
- public function getValue()
+ public function getValue($priceCode = \Magento\Catalog\Pricing\Price\BasePrice::PRICE_CODE)
{
$optionValues = [];
$options = $this->product->getOptions();
@@ -85,7 +95,8 @@ public function getValue()
} else {
/** @var $optionValue \Magento\Catalog\Model\Product\Option\Value */
foreach ($optionItem->getValues() as $optionValue) {
- $price = $optionValue->getPrice($optionValue->getPriceType() == Value::TYPE_PERCENT);
+ $price =
+ $this->customOptionPriceCalculator->getOptionPriceByPriceCode($optionValue, $priceCode);
if ($min === null) {
$min = $price;
} elseif ($price < $min) {
@@ -133,12 +144,13 @@ public function getCustomAmount($amount = null, $exclude = null, $context = [])
* Return the minimal or maximal price for custom options
*
* @param bool $getMin
+ * @param string $priceCode
* @return float
*/
- public function getCustomOptionRange($getMin)
+ public function getCustomOptionRange($getMin, $priceCode = \Magento\Catalog\Pricing\Price\BasePrice::PRICE_CODE)
{
$optionValue = 0.;
- $options = $this->getValue();
+ $options = $this->getValue($priceCode);
foreach ($options as $option) {
if ($getMin) {
$optionValue += $option['min'];
diff --git a/app/code/Magento/Catalog/Pricing/Price/CustomOptionPriceCalculator.php b/app/code/Magento/Catalog/Pricing/Price/CustomOptionPriceCalculator.php
new file mode 100644
index 0000000000000..6e2fee656e368
--- /dev/null
+++ b/app/code/Magento/Catalog/Pricing/Price/CustomOptionPriceCalculator.php
@@ -0,0 +1,42 @@
+getPriceType() === ProductOptionValue::TYPE_PERCENT) {
+ $basePrice = $optionValue->getOption()->getProduct()->getPriceInfo()->getPrice($priceCode)->getValue();
+ $price = $basePrice * ($optionValue->getData(ProductOptionValue::KEY_PRICE) / 100);
+ return $price;
+ }
+ return $optionValue->getData(ProductOptionValue::KEY_PRICE);
+ }
+}
diff --git a/app/code/Magento/Catalog/Pricing/Price/RegularPrice.php b/app/code/Magento/Catalog/Pricing/Price/RegularPrice.php
index 609255d852da3..2c4e332e71237 100644
--- a/app/code/Magento/Catalog/Pricing/Price/RegularPrice.php
+++ b/app/code/Magento/Catalog/Pricing/Price/RegularPrice.php
@@ -22,14 +22,14 @@ class RegularPrice extends AbstractPrice implements BasePriceProviderInterface
/**
* Get price value
*
- * @return float|bool
+ * @return float
*/
public function getValue()
{
if ($this->value === null) {
$price = $this->product->getPrice();
$priceInCurrentCurrency = $this->priceCurrency->convertAndRound($price);
- $this->value = $priceInCurrentCurrency ? floatval($priceInCurrentCurrency) : false;
+ $this->value = $priceInCurrentCurrency ? (float)$priceInCurrentCurrency : 0;
}
return $this->value;
}
diff --git a/app/code/Magento/Catalog/Pricing/Price/TierPrice.php b/app/code/Magento/Catalog/Pricing/Price/TierPrice.php
index 27b88873753e4..a62f3ed09ee1d 100644
--- a/app/code/Magento/Catalog/Pricing/Price/TierPrice.php
+++ b/app/code/Magento/Catalog/Pricing/Price/TierPrice.php
@@ -82,7 +82,7 @@ public function __construct(
GroupManagementInterface $groupManagement,
CustomerGroupRetrieverInterface $customerGroupRetriever = null
) {
- $quantity = $quantity ?: 1;
+ $quantity = (float)$quantity ? $quantity : 1;
parent::__construct($saleableItem, $quantity, $calculator, $priceCurrency);
$this->customerSession = $customerSession;
$this->groupManagement = $groupManagement;
diff --git a/app/code/Magento/Catalog/Pricing/Render/ConfiguredPriceBox.php b/app/code/Magento/Catalog/Pricing/Render/ConfiguredPriceBox.php
index 0722f018ae4eb..d70000cfec761 100644
--- a/app/code/Magento/Catalog/Pricing/Render/ConfiguredPriceBox.php
+++ b/app/code/Magento/Catalog/Pricing/Render/ConfiguredPriceBox.php
@@ -7,12 +7,60 @@
namespace Magento\Catalog\Pricing\Render;
use Magento\Catalog\Model\Product\Configuration\Item\ItemInterface;
+use Magento\Catalog\Model\Product\Pricing\Renderer\SalableResolverInterface;
+use Magento\Catalog\Pricing\Price\MinimalPriceCalculatorInterface;
+use Magento\Framework\Pricing\Price\PriceInterface;
+use Magento\Catalog\Pricing\Price\ConfiguredPriceInterface;
+use Magento\Catalog\Pricing\Price\FinalPrice;
+use Magento\Catalog\Pricing\Price\RegularPrice;
+use Magento\Framework\Pricing\Render\RendererPool;
+use Magento\Framework\Pricing\SaleableInterface;
+use Magento\Framework\View\Element\Template\Context;
/**
* Class for configured_price rendering
*/
class ConfiguredPriceBox extends FinalPriceBox
{
+ /**
+ * @var \Magento\Catalog\Pricing\Price\ConfiguredPriceSelection
+ */
+ private $configuredPriceSelection;
+
+ /**
+ * @param Context $context
+ * @param SaleableInterface $saleableItem
+ * @param PriceInterface $price
+ * @param RendererPool $rendererPool
+ * @param array $data
+ * @param SalableResolverInterface|null $salableResolver
+ * @param MinimalPriceCalculatorInterface|null $minimalPriceCalculator
+ * @param \Magento\Catalog\Pricing\Price\ConfiguredPriceSelection|null $configuredPriceSelection
+ */
+ public function __construct(
+ Context $context,
+ SaleableInterface $saleableItem,
+ PriceInterface $price,
+ RendererPool $rendererPool,
+ array $data = [],
+ SalableResolverInterface $salableResolver = null,
+ MinimalPriceCalculatorInterface $minimalPriceCalculator = null,
+ \Magento\Catalog\Pricing\Price\ConfiguredPriceSelection $configuredPriceSelection = null
+ ) {
+ $this->configuredPriceSelection = $configuredPriceSelection
+ ?: \Magento\Framework\App\ObjectManager::getInstance()
+ ->get(\Magento\Catalog\Pricing\Price\ConfiguredPriceSelection::class);
+ parent::__construct(
+ $context,
+ $saleableItem,
+ $price,
+ $rendererPool,
+ $data,
+ $salableResolver,
+ $minimalPriceCalculator
+ );
+ }
+
/**
* Retrieve an item instance to the configured price model
*
@@ -34,4 +82,63 @@ protected function _prepareLayout()
}
return parent::_prepareLayout();
}
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getPriceType($priceCode)
+ {
+ $price = $this->saleableItem->getPriceInfo()->getPrice($priceCode);
+ $item = $this->getData('item');
+ if ($price instanceof \Magento\Catalog\Pricing\Price\ConfiguredPriceInterface
+ && $item instanceof \Magento\Catalog\Model\Product\Configuration\Item\ItemInterface) {
+ $price->setItem($item);
+ }
+ return $price;
+ }
+
+ /**
+ * @return PriceInterface
+ */
+ public function getConfiguredPrice()
+ {
+ /** @var \Magento\Bundle\Pricing\Price\ConfiguredPrice $configuredPrice */
+ $configuredPrice = $this->getPrice();
+ if (empty($this->configuredPriceSelection->getSelectionPriceList($configuredPrice))) {
+ // If there was no selection we must show minimal regular price
+ return $this->getSaleableItem()->getPriceInfo()->getPrice(FinalPrice::PRICE_CODE);
+ }
+
+ return $configuredPrice;
+ }
+
+ /**
+ * @return PriceInterface
+ */
+ public function getConfiguredRegularPrice()
+ {
+ /** @var \Magento\Bundle\Pricing\Price\ConfiguredPrice $configuredPrice */
+ $configuredPrice = $this->getPriceType(ConfiguredPriceInterface::CONFIGURED_REGULAR_PRICE_CODE);
+ if (empty($this->configuredPriceSelection->getSelectionPriceList($configuredPrice))) {
+ // If there was no selection we must show minimal regular price
+ return $this->getSaleableItem()->getPriceInfo()->getPrice(RegularPrice::PRICE_CODE);
+ }
+
+ return $configuredPrice;
+ }
+
+ /**
+ * Define if the special price should be shown
+ *
+ * @return bool
+ */
+ public function hasSpecialPrice()
+ {
+ if ($this->price->getPriceCode() == ConfiguredPriceInterface::CONFIGURED_PRICE_CODE) {
+ $displayRegularPrice = $this->getConfiguredRegularPrice()->getAmount()->getValue();
+ $displayFinalPrice = $this->getConfiguredPrice()->getAmount()->getValue();
+ return $displayFinalPrice < $displayRegularPrice;
+ }
+ return parent::hasSpecialPrice();
+ }
}
diff --git a/app/code/Magento/Catalog/Pricing/Render/FinalPriceBox.php b/app/code/Magento/Catalog/Pricing/Render/FinalPriceBox.php
index f370c49cdfa20..e0a92ea0e0bea 100644
--- a/app/code/Magento/Catalog/Pricing/Render/FinalPriceBox.php
+++ b/app/code/Magento/Catalog/Pricing/Render/FinalPriceBox.php
@@ -115,7 +115,8 @@ protected function wrapResult($html)
{
return 'getSaleableItem()->getId() . '"' .
+ 'data-product-id="' . $this->getSaleableItem()->getId() . '" ' .
+ 'data-price-box="product-id-' . $this->getSaleableItem()->getId() . '"' .
'>' . $html . '
';
}
diff --git a/app/code/Magento/Catalog/Setup/InstallData.php b/app/code/Magento/Catalog/Setup/InstallData.php
index 5b1a10b098eb5..045ddd8a80c95 100644
--- a/app/code/Magento/Catalog/Setup/InstallData.php
+++ b/app/code/Magento/Catalog/Setup/InstallData.php
@@ -124,7 +124,6 @@ public function install(ModuleDataSetupInterface $setup, ModuleContextInterface
// update attributes group and sort
$attributes = [
'custom_design' => ['group' => 'design', 'sort' => 10],
- // 'custom_design_apply' => array('group' => 'design', 'sort' => 20),
'custom_design_from' => ['group' => 'design', 'sort' => 30],
'custom_design_to' => ['group' => 'design', 'sort' => 40],
'page_layout' => ['group' => 'design', 'sort' => 50],
diff --git a/app/code/Magento/Catalog/Setup/InstallSchema.php b/app/code/Magento/Catalog/Setup/InstallSchema.php
index a96f58ecc046a..4df65437e3a77 100644
--- a/app/code/Magento/Catalog/Setup/InstallSchema.php
+++ b/app/code/Magento/Catalog/Setup/InstallSchema.php
@@ -674,7 +674,7 @@ public function install(SchemaSetupInterface $setup, ModuleContextInterface $con
\Magento\Framework\DB\Ddl\Table::TYPE_SMALLINT,
null,
['unsigned' => true, 'nullable' => false, 'default' => '0'],
- 'Attriute Set ID'
+ 'Attribute Set ID'
)
->addColumn(
'parent_id',
@@ -2077,6 +2077,13 @@ public function install(SchemaSetupInterface $setup, ModuleContextInterface $con
['unsigned' => true, 'nullable' => false, 'default' => '0'],
'Is Disabled'
)
+ ->addIndex(
+ $installer->getIdxName(
+ 'catalog_product_entity_media_gallery_value',
+ ['entity_id', 'value_id', 'store_id']
+ ),
+ ['entity_id', 'value_id', 'store_id']
+ )
->addIndex(
$installer->getIdxName('catalog_product_entity_media_gallery_value', ['store_id']),
['store_id']
diff --git a/app/code/Magento/Catalog/Setup/UpgradeData.php b/app/code/Magento/Catalog/Setup/UpgradeData.php
index a290d4870bd49..4e0be7396a166 100644
--- a/app/code/Magento/Catalog/Setup/UpgradeData.php
+++ b/app/code/Magento/Catalog/Setup/UpgradeData.php
@@ -392,6 +392,10 @@ public function upgrade(ModuleDataSetupInterface $setup, ModuleContextInterface
$this->upgradeWebsiteAttributes->upgrade($setup);
}
+ if (version_compare($context->getVersion(), '2.2.5') < 0) {
+ $this->enableSegmentation($setup);
+ }
+
$setup->endSetup();
}
@@ -436,4 +440,44 @@ private function changePriceAttributeDefaultScope($categorySetup)
}
}
}
+
+ /**
+ * @param ModuleDataSetupInterface $setup
+ * @return void
+ */
+ private function enableSegmentation(ModuleDataSetupInterface $setup)
+ {
+ $catalogCategoryProductIndexColumns = array_keys(
+ $setup->getConnection()->describeTable($setup->getTable('catalog_category_product_index'))
+ );
+
+ $storeSelect = $setup->getConnection()->select()->from($setup->getTable('store'))->where('store_id > 0');
+ foreach ($setup->getConnection()->fetchAll($storeSelect) as $store) {
+ $catalogCategoryProductIndexSelect = $setup->getConnection()->select()
+ ->from(
+ $setup->getTable('catalog_category_product_index')
+ )->where(
+ 'store_id = ?',
+ $store['store_id']
+ );
+
+ $indexTable = $setup->getTable('catalog_category_product_index') .
+ '_' .
+ \Magento\Store\Model\Store::ENTITY .
+ $store['store_id'];
+
+ $setup->getConnection()->query(
+ $setup->getConnection()->insertFromSelect(
+ $catalogCategoryProductIndexSelect,
+ $indexTable,
+ $catalogCategoryProductIndexColumns,
+ \Magento\Framework\DB\Adapter\AdapterInterface::INSERT_ON_DUPLICATE
+ )
+ );
+ }
+
+ $setup->getConnection()->truncateTable($setup->getTable('catalog_category_product_index'));
+ $setup->getConnection()->truncateTable($setup->getTable('catalog_category_product_index_replica'));
+ $setup->getConnection()->truncateTable($setup->getTable('catalog_category_product_index_tmp'));
+ }
}
diff --git a/app/code/Magento/Catalog/Setup/UpgradeSchema.php b/app/code/Magento/Catalog/Setup/UpgradeSchema.php
index 616bee43de00e..0483fd847df18 100755
--- a/app/code/Magento/Catalog/Setup/UpgradeSchema.php
+++ b/app/code/Magento/Catalog/Setup/UpgradeSchema.php
@@ -7,6 +7,7 @@
namespace Magento\Catalog\Setup;
use Magento\Catalog\Model\Product\Attribute\Backend\Media\ImageEntryConverter;
+use Magento\Catalog\Model\Product\Exception;
use Magento\Catalog\Model\ResourceModel\Product\Gallery;
use Magento\Framework\Setup\ModuleContextInterface;
use Magento\Framework\Setup\SchemaSetupInterface;
@@ -21,10 +22,13 @@ class UpgradeSchema implements UpgradeSchemaInterface
/**
* {@inheritdoc}
* @SuppressWarnings(PHPMD.ExcessiveMethodLength)
+ * @SuppressWarnings(PHPMD.CyclomaticComplexity)
+ * @SuppressWarnings(PHPMD.NPathComplexity)
*/
public function upgrade(SchemaSetupInterface $setup, ModuleContextInterface $context)
{
$setup->startSetup();
+
if (version_compare($context->getVersion(), '2.0.1', '<')) {
$this->addSupportVideoMediaAttributes($setup);
$this->removeGroupPrice($setup);
@@ -126,6 +130,23 @@ public function upgrade(SchemaSetupInterface $setup, ModuleContextInterface $con
$this->fixCustomerGroupIdColumn($setup);
}
+ if (version_compare($context->getVersion(), '2.2.4', '<')) {
+ $this->removeAttributeSetRelation($setup);
+ }
+
+ if (version_compare($context->getVersion(), '2.2.5', '<')) {
+ $this->addGeneralIndexOnGalleryValueTable($setup);
+ }
+
+ if (version_compare($context->getVersion(), '2.2.5', '<')) {
+ $this->enableSegmentation($setup);
+ }
+
+ if (version_compare($context->getVersion(), '2.2.6', '<')) {
+ $this->addStoreIdFieldForWebsiteIndexTable($setup);
+ $this->removeIndexFromPriceIndexTable($setup);
+ }
+
$setup->endSetup();
}
@@ -517,6 +538,7 @@ private function addSupportVideoMediaAttributes(SchemaSetupInterface $setup)
),
'value_id'
);
+
$this->addForeignKeys($setup);
}
@@ -699,4 +721,124 @@ private function addReplicaTable(SchemaSetupInterface $setup, $existingTable, $r
);
$setup->getConnection()->query($sql);
}
+
+ /**
+ * Remove foreign key between catalog_product_entity and eav_attribute_set tables.
+ * Drop foreign key to delegate cascade on delete to plugin.
+ * @see \Magento\Catalog\Plugin\Model\AttributeSetRepository\RemoveProducts
+ *
+ * @param SchemaSetupInterface $setup
+ * @return void
+ */
+ private function removeAttributeSetRelation(SchemaSetupInterface $setup)
+ {
+ $setup->getConnection()->dropForeignKey(
+ $setup->getTable('catalog_product_entity'),
+ $setup->getFkName('catalog_product_entity', 'attribute_set_id', 'eav_attribute_set', 'attribute_set_id')
+ );
+ }
+
+ /**
+ * Adds index for table catalog_product_entity_media_gallery_value
+ * It was added because it suits best for selecting media data for products
+ *
+ * @see \Magento\Catalog\Model\ResourceModel\Product\Gallery::createBatchBaseSelect
+ * @param SchemaSetupInterface $setup
+ * @return void
+ * @throws \Exception
+ */
+ private function addGeneralIndexOnGalleryValueTable(SchemaSetupInterface $setup)
+ {
+ $existingKeys = $setup->getConnection()->getIndexList(
+ $setup->getTable(Gallery::GALLERY_VALUE_TABLE)
+ );
+
+ $newIndexName = $setup->getConnection()->getIndexName(
+ $setup->getTable(Gallery::GALLERY_VALUE_TABLE),
+ ['entity_id', 'value_id', 'store_id']
+ );
+
+ if (!array_key_exists($newIndexName, $existingKeys)) {
+ $entityIdKeyName = $setup->getConnection()->getIndexName(
+ $setup->getTable(Gallery::GALLERY_VALUE_TABLE),
+ ['entity_id']
+ );
+
+ if (array_key_exists($entityIdKeyName, $existingKeys)) {
+ $keyColumns = $existingKeys[$entityIdKeyName]['COLUMNS_LIST'];
+ $linkField = reset($keyColumns);
+
+ $setup->getConnection()->addIndex(
+ $setup->getTable(Gallery::GALLERY_VALUE_TABLE),
+ $newIndexName,
+ [$linkField, 'value_id', 'store_id']
+ );
+ }
+ }
+ }
+
+ /**
+ * @param SchemaSetupInterface $setup
+ * @return void
+ */
+ private function enableSegmentation(SchemaSetupInterface $setup)
+ {
+ $storeSelect = $setup->getConnection()->select()->from($setup->getTable('store'))->where('store_id > 0');
+ foreach ($setup->getConnection()->fetchAll($storeSelect) as $store) {
+ $indexTable = $setup->getTable('catalog_category_product_index') .
+ '_' .
+ \Magento\Store\Model\Store::ENTITY .
+ $store['store_id'];
+
+ $setup->getConnection()->createTable(
+ $setup->getConnection()->createTableByDdl(
+ $setup->getTable('catalog_category_product_index'),
+ $indexTable
+ )
+ );
+ $setup->getConnection()->createTable(
+ $setup->getConnection()->createTableByDdl(
+ $setup->getTable('catalog_category_product_index'),
+ $indexTable . '_replica'
+ )
+ );
+ }
+ }
+
+ /**
+ * @param SchemaSetupInterface $setup
+ */
+ private function addStoreIdFieldForWebsiteIndexTable(SchemaSetupInterface $setup)
+ {
+ $setup->getConnection()->addColumn(
+ $setup->getTable('catalog_product_index_website'),
+ 'default_store_id',
+ [
+ 'type' => \Magento\Framework\DB\Ddl\Table::TYPE_SMALLINT,
+ 'nullable' => false,
+ 'comment' => 'Default store id for website '
+ ]
+ );
+ }
+
+ /**
+ * Table "catalog_product_index_price_tmp" used as template of "catalog_product_index_price" table
+ * for create temporary tables during indexation. Indexes are removed from performance perspective
+ * @param SchemaSetupInterface $setup
+ */
+ private function removeIndexFromPriceIndexTable(SchemaSetupInterface $setup)
+ {
+ $setup->getConnection()->dropIndex(
+ $setup->getTable('catalog_product_index_price_tmp'),
+ $setup->getIdxName('catalog_product_index_price_tmp', ['customer_group_id'])
+ );
+ $setup->getConnection()->dropIndex(
+ $setup->getTable('catalog_product_index_price_tmp'),
+ $setup->getIdxName('catalog_product_index_price_tmp', ['website_id'])
+ );
+ $setup->getConnection()->dropIndex(
+ $setup->getTable('catalog_product_index_price_tmp'),
+ $setup->getIdxName('catalog_product_index_price_tmp', ['min_price'])
+ );
+ }
}
diff --git a/app/code/Magento/Catalog/Setup/UpgradeWebsiteAttributes.php b/app/code/Magento/Catalog/Setup/UpgradeWebsiteAttributes.php
index 3d300d9c849a9..05e4bb3817beb 100644
--- a/app/code/Magento/Catalog/Setup/UpgradeWebsiteAttributes.php
+++ b/app/code/Magento/Catalog/Setup/UpgradeWebsiteAttributes.php
@@ -148,6 +148,20 @@ private function processAttributeValues(ModuleDataSetupInterface $setup, array $
*/
private function fetchAttributeValues(ModuleDataSetupInterface $setup, $tableName)
{
+ $multipleStoresInWebsite = array_values(
+ array_reduce(
+ array_filter($this->getGroupedStoreViews($setup), function ($storeViews) {
+ return is_array($storeViews) && count($storeViews) > 1;
+ }),
+ 'array_merge',
+ []
+ )
+ );
+
+ if (count($multipleStoresInWebsite) < 1) {
+ return [];
+ }
+
$connection = $setup->getConnection();
$batchSelectIterator = $this->batchQueryGenerator->generate(
'value_id',
@@ -158,27 +172,18 @@ private function fetchAttributeValues(ModuleDataSetupInterface $setup, $tableNam
'*'
)
->join(
- [
- 'cea' => $setup->getTable('catalog_eav_attribute'),
- ],
+ ['cea' => $setup->getTable('catalog_eav_attribute')],
'cpei.attribute_id = cea.attribute_id',
''
)
->join(
- [
- 'st' => $setup->getTable('store'),
- ],
+ ['st' => $setup->getTable('store')],
'st.store_id = cpei.store_id',
'st.website_id'
)
- ->where(
- 'cea.is_global = ?',
- self::ATTRIBUTE_WEBSITE
- )
- ->where(
- 'cpei.store_id <> ?',
- self::GLOBAL_STORE_VIEW_ID
- )
+ ->where('cea.is_global = ?', self::ATTRIBUTE_WEBSITE)
+ ->where('cpei.store_id IN (?)', $multipleStoresInWebsite),
+ 1000
);
foreach ($batchSelectIterator as $select) {
@@ -201,17 +206,15 @@ private function getGroupedStoreViews(ModuleDataSetupInterface $setup)
->select()
->from(
$setup->getTable('store'),
- '*'
- );
+ ['store_id', 'website_id']
+ )->where('store_id <> ?', self::GLOBAL_STORE_VIEW_ID);
- $storeViews = $connection->fetchAll($query);
+ $storeViews = $connection->fetchPairs($query);
$this->groupedStoreViews = [];
- foreach ($storeViews as $storeView) {
- if ($storeView['store_id'] != 0) {
- $this->groupedStoreViews[$storeView['website_id']][] = $storeView['store_id'];
- }
+ foreach ($storeViews as $storeId => $websiteId) {
+ $this->groupedStoreViews[$websiteId][] = $storeId;
}
return $this->groupedStoreViews;
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AddProductToCartActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AddProductToCartActionGroup.xml
new file mode 100644
index 0000000000000..7dafeff34a2ea
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AddProductToCartActionGroup.xml
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
+
+
+
+
+ .
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminCategoryActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminCategoryActionGroup.xml
new file mode 100644
index 0000000000000..127b69e5c3dc4
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminCategoryActionGroup.xml
@@ -0,0 +1,90 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminCategoryProductAttributeActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminCategoryProductAttributeActionGroup.xml
new file mode 100644
index 0000000000000..b1300f63d967c
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminCategoryProductAttributeActionGroup.xml
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminCreateRootCategoryActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminCreateRootCategoryActionGroup.xml
new file mode 100644
index 0000000000000..d2301abc2085e
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminCreateRootCategoryActionGroup.xml
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductActionGroup.xml
new file mode 100644
index 0000000000000..cbc3ac876a486
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductActionGroup.xml
@@ -0,0 +1,143 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductAttributeSetActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductAttributeSetActionGroup.xml
new file mode 100644
index 0000000000000..a7e76253728a3
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductAttributeSetActionGroup.xml
@@ -0,0 +1,37 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductCheckUnsupportedFileActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductCheckUnsupportedFileActionGroup.xml
new file mode 100644
index 0000000000000..640dd9ae6d264
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductCheckUnsupportedFileActionGroup.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductGridActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductGridActionGroup.xml
new file mode 100644
index 0000000000000..408586d603835
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductGridActionGroup.xml
@@ -0,0 +1,54 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminResetProductGridToDefaultViewActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminResetProductGridToDefaultViewActionGroup.xml
new file mode 100644
index 0000000000000..8d3b9df6c2e15
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminResetProductGridToDefaultViewActionGroup.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/CheckAttributeInAdditionalInformationTabActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/CheckAttributeInAdditionalInformationTabActionGroup.xml
new file mode 100644
index 0000000000000..85537fea47e50
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/CheckAttributeInAdditionalInformationTabActionGroup.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/CheckAttributeNotInAdditionalInformationTabActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/CheckAttributeNotInAdditionalInformationTabActionGroup.xml
new file mode 100644
index 0000000000000..5806bb48d783c
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/CheckAttributeNotInAdditionalInformationTabActionGroup.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/CheckItemInLayeredNavigationActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/CheckItemInLayeredNavigationActionGroup.xml
new file mode 100644
index 0000000000000..0a0e4a0fa0ca1
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/CheckItemInLayeredNavigationActionGroup.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/CreateAttributeSetActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/CreateAttributeSetActionGroup.xml
new file mode 100644
index 0000000000000..e9e7f1c01e028
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/CreateAttributeSetActionGroup.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/CreateNewGroupInAttributeSetActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/CreateNewGroupInAttributeSetActionGroup.xml
new file mode 100644
index 0000000000000..9cc2fa03fc70a
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/CreateNewGroupInAttributeSetActionGroup.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/CustomOptionsActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/CustomOptionsActionGroup.xml
new file mode 100644
index 0000000000000..c095faa73d9b1
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/CustomOptionsActionGroup.xml
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/DeleteAttributeSetActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/DeleteAttributeSetActionGroup.xml
new file mode 100644
index 0000000000000..050c7b930a085
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/DeleteAttributeSetActionGroup.xml
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/MoveCategoryActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/MoveCategoryActionGroup.xml
new file mode 100644
index 0000000000000..929649f2971e7
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/MoveCategoryActionGroup.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/OpenEditProductOnBackendActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/OpenEditProductOnBackendActionGroup.xml
new file mode 100644
index 0000000000000..f27547a330512
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/OpenEditProductOnBackendActionGroup.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/OpenProductFromCategoryPageActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/OpenProductFromCategoryPageActionGroup.xml
new file mode 100644
index 0000000000000..e74cee080f7e3
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/OpenProductFromCategoryPageActionGroup.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/ProductsOnAdminActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/ProductsOnAdminActionGroup.xml
new file mode 100644
index 0000000000000..15c1a8079d4ac
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/ProductsOnAdminActionGroup.xml
@@ -0,0 +1,47 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/SearchAndMultiSelectActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/SearchAndMultiSelectActionGroup.xml
new file mode 100644
index 0000000000000..a55576621bd57
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/SearchAndMultiSelectActionGroup.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+ [{{options}}]
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/SearchForProductOnBackendActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/SearchForProductOnBackendActionGroup.xml
new file mode 100644
index 0000000000000..ebeee87b1c89e
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/SearchForProductOnBackendActionGroup.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAddToCartCustomOptionsProductPageActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAddToCartCustomOptionsProductPageActionGroup.xml
new file mode 100644
index 0000000000000..4938b6a9592f1
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAddToCartCustomOptionsProductPageActionGroup.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontCategoryActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontCategoryActionGroup.xml
new file mode 100644
index 0000000000000..1f9bd218b1111
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontCategoryActionGroup.xml
@@ -0,0 +1,38 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontCategoryCheckActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontCategoryCheckActionGroup.xml
new file mode 100644
index 0000000000000..e560e7facc7fd
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontCategoryCheckActionGroup.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontCompareActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontCompareActionGroup.xml
new file mode 100644
index 0000000000000..9d37f2442628e
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontCompareActionGroup.xml
@@ -0,0 +1,70 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontProductActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontProductActionGroup.xml
new file mode 100644
index 0000000000000..d3773a2b58b8e
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontProductActionGroup.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/SwitcherActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/SwitcherActionGroup.xml
new file mode 100644
index 0000000000000..3d187fb315bff
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/SwitcherActionGroup.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/VerifyEntitiesInMagentoAdminActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/VerifyEntitiesInMagentoAdminActionGroup.xml
new file mode 100644
index 0000000000000..5f50b2790a164
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/VerifyEntitiesInMagentoAdminActionGroup.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/VerifyEntitiesOnStorefrontActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/VerifyEntitiesOnStorefrontActionGroup.xml
new file mode 100644
index 0000000000000..667697d691a60
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/VerifyEntitiesOnStorefrontActionGroup.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/CatalogPriceData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/CatalogPriceData.xml
new file mode 100644
index 0000000000000..8d460fb7cbf1d
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/Data/CatalogPriceData.xml
@@ -0,0 +1,31 @@
+
+
+
+
+
+ ScopeWebsite
+ DefaultProductPrice
+
+
+ 1
+
+
+ 0
+
+
+ ScopeGlobal
+ DefaultProductPrice
+
+
+ 0
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/CategoryData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/CategoryData.xml
new file mode 100644
index 0000000000000..8d7c182d648c2
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/Data/CategoryData.xml
@@ -0,0 +1,42 @@
+
+
+
+
+
+ simpleCategory
+ simplecategory
+ true
+
+
+ ApiCategory
+ true
+
+
+ SimpleSubCategory
+ simplesubcategory
+ true
+ true
+
+
+ NewRootCategory
+ newrootcategory
+ true
+ true
+ 1
+
+
+ subCategory
+ subCategory
+ true
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/CustomAttributeData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/CustomAttributeData.xml
new file mode 100644
index 0000000000000..34fd7961fab72
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/Data/CustomAttributeData.xml
@@ -0,0 +1,39 @@
+
+
+
+
+
+ url_key
+ category
+
+
+ url_key
+ product
+
+
+ category_ids
+
+
+
+
+
+
+
+ description
+ API Product Description
+
+
+ short_description
+ API Product Short Description
+
+
+ news_from_date
+ 2018-05-17 00:00:00
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/FrontendLabelData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/FrontendLabelData.xml
new file mode 100644
index 0000000000000..2423383bc19f7
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/Data/FrontendLabelData.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
+ 0
+ attribute
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/ImageContentData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/ImageContentData.xml
new file mode 100644
index 0000000000000..36eab0b602544
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/Data/ImageContentData.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+ iVBORw0KGgoAAAANSUhEUgAAAP8AAAEsCAYAAAAM1WX/AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAACY7SURBVHhe7Z0LlBxXnd6HXWyWhx9g8HS3pJFGPVU1klmC2RB28frBIbY00y2NpBnN9GMkG4JJcOJsgCVrszZg56wJuxj2BTjLY8FsADsb20kwmEc4WYgNxPYuiwX2yg9ZxrKsefS7R5IlufP/qm+N76hbo5qa7p6qru875zs6M5quvvd/76/q3lt1/9VDhU/7Ll79ikrauL66y8pVUsZ1ezf1nan+i6KoblV+bP22uUlrT+1dG2vlycEa/pWfH5kej4+oP6Eoqpt0aGx9vJqxvnF892Dt2JUbavmMNW/8fEx+X81aXy9MxNerj1AUFWQ9eGHPy4sp4wOHJ60ZXOWLWauW08CH8TN+r0YBM8W0+f47B3vOUIegKCpoyo/HL6tkzAcA9eFdgw3Qn2z8/5z8Hf6+krEeyE/EL1WHoigqCHo+2X9+MW18dm5y8MSLV25sCvrpfEI+V520TuA4h4b6etWhKYryqwTWXTJ3fxpXbyzone5qfyrjc+WsjALeLSeBrPl0MWNOqq+gKMpPmh3rf2Mpbdxbu2pj7ejuDZ6hP9k4Do6HEUQpY34zl+r/TfWVFEWtpH52ee+rZF7/kbmsVQb4hZPgbZVxXBx/btIqldLmjQeSkVeqIlAU1Wnl0gNJAf/nGOJXlzHEd2scH99jLwhmzX8sTgwkVFEoiuqEcjvW9pWzxleO7hqsHT/pnn2njO/F95cz5pefk/KoolEU1S5VM+Y1law1hatvqck9+04Z34vvr48CrEMolyoiRVGtVG5n/0Vylf07zLvd3LPvlFEOlAflkqnA/5kdX/92VWSKopajZ8Zir53LGrdWJ61jL161oW0Lest1fUFwQ60i5ZxLG7fuT/Sdq6pAUdRSVU4ZqXLGenK59+w7ZZTP2SxUzlpPlNPGhKoKRVFuND22zpQh9N+euHJD7YXdK7Ogt1yj3Ch/JWv97fSOPktVjaKoZsK++krGuK6cNUu4emKzTTOwgmJns5CcyIqVtPkH9zJvAEU1amZi3eXVjPkQYMHmGr8P8d0a9XA2C1Wz5oOFsXX/UlWZosKt6a1rotWs9XmsmGOY3AygbjHqd3hysFbNWH+1X+qtQkBR4VMpZbynOmkewFWxlO2eq/2pjPqhnvboJms+i/qrUFBUOFRI9f+WdP7v4tbYkRZuwgmKUV/UG/WXqcB3Cjvjb1Ghoaju1L6RtedUM8YtMsw/2s5NOEGxs1moOmkdKWeMjyM+KlQU1T3KTxjb57LWL+3V7wDcs++UEQfEoz4VsH6RT63fpkJGUcFWPXGm+V+PyzD35MSZ9EI7iUQrGetriJsKIUUFS0icmU8ZH0BCTFzVmiXOpBca8XGeDTictaaLqYH/gDiqkFKU/1VKxy+Zy1h24sxuumffKSNe9mYhiV81a92fG4tfokJLUf7UoZ19vcWM8ZnqMhJn0guNONqJRFPGXyIxqQo1RflHFSTOnLT24WoVhE04QTHiaG8Werf9bMBTchLYrUJOUSurfCr+5lLG+taLV25oaeJMeqER13oi0Q21Ytr4Vn7nwD9TTUBRndWeS899dTltfFSu9hXes++cnWcD5iTuiP/By3tfpZqEotqvysRAopOJM+mFRrz1RKKVlDGsmoai2qN64kzzS0d3r1ziTHqh0Q5HdtmJRP86t7N/jWoqimqdyinj31Wz1iFcbVYycSa90GgHJ5GotM/zTCRKtUzFifjvyBDfTpyJKwyh96fRLmgftBMSiRbT639bNSFFLU3PXBF7bSVdT5yJ3WfNOhztPzuJRKXdXqhkjE8ykSi1JBVSRqqStR7nPftgGu3lPBuAdkR7qqalqOY6OBrfKJ3l7iAnzqQXGu2IRUEZBdw1s3XNBtXUFFXXnrHBM2SIf305Y5ZxtecmnO4x2lFLJFpCO+8ZHDxDNT0VZs2MDlxezVh24sxqiBb0AASemMO/zf6/24x2RfvW7woMPjjDRKLh1dT2NZG5tHkbEkqeCNEmHEAA6Oek3vm0+Rh2z4XtsWS0t51INGv+F2zGUl2CCoNKE8bV1UkrNIkzYdSxrJJnYgNSLjWQrSUjr5wZj++qZq2n8Xv8f1hi8VIiUetXTCQaAk3vjL9FzvahTJyJ9/xVZIg/lzH//Int/W9QIbF1UH6uZI2/kJOA/XfNPt+NRvs7iUQlLt/Jj8cvVCGhukUzm/vPKmaMj8sV70iYNuGgc9svzJA6y0nv/unR+KUqJE01PR6/rDpp3m9fEUO0/uFsFqpI/yimjFv2Sn9RIaGCrPzEeiTO/AU6dFgSZ6KO8yvcGTNXThkfuGN7z6+pkCyqj17c8+tyovygjBJy+HxY7nygjkwk2iWa2rbOqGSsO5AQMmyJM+uJMDfgzTjfOLTFWyLMqR39A3LiuBPHCWf87ESi30AcVEgovwsJH4sTxgfDljhTv3LJVfuX+fTADhWSZamUGhiVk8Cj9nFDOHJCP5KpwAd+ICMiFRLKj8qND1wcxsSZzpy1nDXx8otbprec8xoVkpZoZvPZZ1UzxsflpBLONZN32W8Wuh/9S4WE8ouwWl1MG39RnbSOvyids1lDdqPROZ3VagHzu+1+7RVeM1bJGN8L590S3CIdPI5+hv6mQkKtpJDQ8XDIEmeijs596nLGPFDp8H1qGQVcXZXvxfeH6jkJNa2qZq19chLYpcJBdVpI4FhKG6FMnGm/6lqGo5W0eRte8a1C0lHNjK2OyRTjr7CHvttfLa4b/cxJJCr9716ZCrxJhYRqtw4kI68sp4yPze2y7E04YZp/Os+mlzLmQ4Xx+BUqJCuqQmr9JinPw/YVMSQjL9hZZ5nLWmUkEkW/VCGh2qFieiCJhI2h62hq5bmaNYtytf+DvZt6zlQh8YX2buo7s5IxrkP57BOylLdZPbrN9glZTQWQ0LWYGUiokFCtUn5i7dpqxrr9qFz5wpQ4E50L+9ExrC6nzXtmR+MbVUh8qdlUfGM5Y/4PJx9CWE7OMPol+mc5bX0lP7J2rQoJtRzJvPLasCXORB21xaXHZVg5ocIRCMm0DJmQnkAmnXAtwjojNOsQEr6qcFBLVXE8/rZKxrITZ2JxKyxXEWcuiRx0hbRxq8wlz1MhCZRQ7lLa+BTqgfo0q2s3Gv3Ufsmo1BmJRGfHmEjUtZBwsZoxPilXjtB1Gjv7rP1AidU1nWYW2Y8n5SQu9QrfSdx+/uKFatb8k30ja89RIaGaqTAxkLaHizJ0CuNw8fCkdbCcMq+pqXh0i27o6XlZQYbBc1nr+bBO36RfM5FoM81MGIMSnLvCmDgTC0Uv7FZvnMms7VMh6Uqphdsvo75hWriFnYXbSsb879M7DEuFJLy6F7eI0sb1SKyIs2OYNuE4t4jkavgPsyF711xR6iv1/hnqH5Zbtqijs1mojESiGeM69H8VknBpdjx+hcyFHrQBCNtc0O70VjXMb5nFW47lJHBTNWvNIR5helirvlkIdwXMB5FAVoWk+4XEmdWM+QUs/oT1sVC56n0zlzJ+U4Uk1Do0PvAmice9oX1MW0Y+1bT1eXChQtKdKqSN91YnQ7ghRG3Ckavc0/kJ40oVDkpTfnzgKonPfsQpnIlEzWcLE8bVKhzdI2zCkcp9J6yJM6VT42r/508m+89XIaGa6HmJTyVj/aVMicKbSHTSvC8/Gn+zCklw9eTYurPLKeMW6fyHcc8+TPM650EPmdfdPzUWv0SFhHKh6Yn4pQJBaJOyVLODh8tp84/AjwpJsITEmQL9L9GAoUycmbVmkTiT6Z+8CXGrMJFosBKJHhpbH0fiyOOhTpxpfu35HX39KiTUMnRoIr5eLiJfC3MiUan/1wsSBxUS/8lOnClXusMhT5xZmjC2q5BQLRQSkoY+kWjafD84UyHxh/Lj8cukYew5Whg34eBlD9WMccujLU6cSS3U9BbzNSqR6NGwrSFpzwbcn59Y/OUrHRFWZ4tp47Nzk4MnXgzZyy6d1VkZkn13Zqz/rSokVAdUSPe/VaaW9USiu0J290g4q05aJ8op4zPgT4Wks0ICQ+n49Rc8hmgYNp84M2s9V8p04X3ZAKmUNt4ro4CDaI/QJRJ9tz0KeLqYMSdVONqv2bH+NyJxIYZdYU2cicSV0ylzRRJnUgs1M2bEymnj8+F9YtTO6fjNXKq/fU+M/uzy3lfJvP4jc1mrHNb5VhmJMydC9Cx2gFRIx6+Q9rETiYbx2YC5SatUSps3tjyRaA6JMzPmzxHYMCXOdFZaZY5VkCH+dX5LnEkt1A8uXv0KGZVeLyAU0G5ov2bt2m0Gj/OJRLPmPxYnWpBINLdjbV85a3wlbIkz4fnEmRnjLuQbUCGhAiC0l0wF7kb7hTE/hJ1INGN++TnhV4VkaapmzGsqWWsKZ5PQJs6cMMdVOKgAqpA2JtCOaM8wZoYSfg+BYxWO0yu3s/8iOWuEMnFm/X1s1jEZOt5aGIu9VoWECrCeHYm+TqZsn0a7on3DtFbl7C+xE4mOr3+7CkmjnpHOPpc1bq0HaUP4gvQupFiy/m7RIFGBVS5tXlS1s0GHNJGocD0nFzUkyFUhqQt54csZ68nwDY/qQ/w5GR4VJox/i0STKiRUF+qOnp5fU4lE7fdAhOkRdGc6W85aT8y/ByI3MXDHcRkahHJhROosQ/yvYGHTDgYVCiGRqEwFbg9lIlGpL3gH9z3Ht6+pFXbG6/8pV8KT/7ibjDOgljjzZ8VxY0j1ByqEQuJU9AP0h1DcwlZ8g3dw3zM7FKnZ3rq6lhuXkwD+oAtPAvbcB42ctSphTpxJLdSesXNfXcZbnyetStc+vKaYBt/g3GG+Z3Y4WrPtnAS29dXyKUN9oMmBAmaczecTZ05a/2t2vP8C1e4UNS88ti6jgO5KJAp+wbHwDK7nGVfMvwS/Y/Wfue1ra/m0Wf9wswP73PoiR3XS2ldMx3epdqaoU6qYMnZ3xYY1cCv8gmOda92N8DvGHyditdxo/0sHO/kLfOwT9tbHwVolY3zmudH1r1dtS1GnFfpLJWt9Zk76T+A2CylOwS34bQa941PDD+OD8JZVtRwWBXFgH58EcJaef7AhYz4wvSN+mWpPilqypsf63yH96MeBeOBNsQlOwes8u824Vl4cfsfqQLmta2r58QHfnQTQKM4mHDlj54oZ44NMnEm1QrchPV3a+H3pV3n0L989G+CwKFyCTzfQO3YHv2N10Ny2tbWcsyjYrEAd9nzizKz5tUPbfJzokAqs7MS0WevreC7AN4lEhT9wCB51Pt16afA7xpdgPWDHuhVbFMTZ10mcKY3CxJlUR2QnEp1c4USi4A2LecLf6eb1i9kb/I7xpUk5CYytf6lQJxe0Da4/r2xDf1iu9n/ExJlUJ7V389lnlTPGx/FyjY4+G6D4Am/gziv0jpcHP4wCwFva/5AQzrK4B2tvVMia353eGX+Lag+K6rimR/t/q+IkEm3nswGKKfshHZeLeW68fPgd2wWSf0fW1PITrV0URFDnE2dmzAOllPEeFf/QSaY3g7WxwTPUj77Q3k19Z6Jc6sfQqZoxrm7LC2gdhoQncGXz1QLoHbcOfseqcDn7ScHWrAfgXuuRXYO1ctr6/MzY6piKeah0aGdfbylrfTmXMp6ckWGn+rUvlHvnWWdLuZ4qZ80voZzq16HSzLboKrkwfQH9tCXPBoAbezGvbwFXrXTr4XeMwi5jURBnTydxZiljPlwYj1+h4hw6VVLWu8oZ61e1q9+IWD7+zGjsN9R/+UJIGjmbMp9C+UpSzkp64Cr1X6FTJR2/opQ2/x791lMiUXDSgsU8N24f/I5R+OSqJS0KOvfs5yatfCVtXI/EjCq2odLUePzCatb8NuaUWOuA82njsZZnaF2msEkqlzL3Yt7rrMlUs9a9U93wqmkP2if9Vfrth5eUSFRxUV/MU/P6Zjy10O2HH0ZFxDkXi4J64sywziORJr2aMf9T5aRXmwcBfpTTuRtTxt2YtHmz38rbKc2OxjeeNpGoYgFcgA+HlaYctdidgd+xU7GRNbXchLNzsD400l52+bjMnUKbOLM0Ed9azVh77FicdB85KPDDepvKKOCRXCq+Rf156FRIGSmJweN4y878ZiGn7wsH9cW8zkHvuLPwO1aVxI4j6TS1Oek0MkQ6WkoZn0TCRRWzUCk/tmadDPH/BleIUz1BFiT4daM+yJojo5mv5kfWrlUfC5Wkzc5DYlj0c/R39PvFdtx1wisDv2OpdHnLqlppbN3B2XEjlIkzsQehOGG+v5I1T/tq86DCj/q8tPfCnC6nzN9DPj318VAJiUQr0t/R71cKescrC7/4ha2x2vRQ5CcqNqGSDPl+V672/xdQuNk1FlT4HaN+zq5LGQb/KLfTvEgdIlSaHer9Kfp9Mx466RWH/6gEYWY48pCKSyj0mAwBKxnzzypZ6wTSpDcDpZmDDr9u5NAvS/0rafNPwzTVq4lnhyMPo98346GTJvwdVmFiIF2dtJ7A1X6pmWK6CX7U28m0JPPgx2dTRkodrqtF+DWHBX7kDpzLmPcgRxwW9ZYCveNugt8x4uDc3pXR0D2zqfhGddiuFOHX3O3wA9RCeuBGmeOWcZUruHng4xTuRvgdIy6IT3XSLEm8bnjIZ3VslQi/5m6Gf3Zi/WYZ0tqPerYiL3w3ww8jPtp7FR6eHVu/SX1F14jwa+5G+PFq5HLG/GKrX23e7fDrRtywSaYkcczt7F+jvirwIvyauw3+UnrgfXLVeg5Xr5Zu7xSHCX7Ebf5dipPWgVJq4F+rrwu0CL/mboFfhqi/Xc2a/9tO7LCrPYkdwgS/Y8QR8bQ3C2Ws7xfH429TXxtIEX7NQYd/38jac0pp84/l6nSk3Smdwgi/4/nUbRLnUsr8xJNj685WXx8oEX7NQYa/PGGNVTuYzDHM8MOIr7ZZ6JcyFRhVRQiMCL/mIMI/vcOwyhnzv9lpnNvc4XWHHX7d2CyE+JfT5p3TY5apiuJ7EX7NQYL/wQt7Xl7JmB+Sq878Cxyadcx2mfAvtLNZSNojV0kbv4/2UUXyrQi/5qDAXxg33ilX+/9nrz57Sc/UAhP+RqMdnHRv5Yz103LKfIcqli9F+DX7Hf79W9dEK1nzs/ZLG5ewCacdJvyLG5ukqpPWCbyc9entayKqeL4S4dfsZ/grafNflbPms7iqtPqevRcT/sWN9nGeDShlzGdlivZuVUTfiPBr9iP8eBmInjhzpaF3TPjdGe2FWKlnA76FRKiqqCsuwq/ZT/Dnd7/pHJnXNyTO9IsJ/9LsPBsg7TlXTps3o31VkVdMhF+zn+DP7ej/cO09F9SqbXpCb7km/Es32hHtiXadGe2/XhV5xUT4NfsJ/unNvTfXRvtquZ2dffGoWxP+JVq1H3Lh10bX1KR9b1JFXjERfs1+gn92KPrR2og0ylCkltu6ppYbb+07B5drwu/Sqs3Qfrmt9Vz4aFfpZx9RRV4xEX7NvoN/26p62VRmVbwrLZdy3jHQpKN10IT/NEb7APoJo5YbWfiOO7Qr4V9owq9pAfyOnZPAdm/vHGylCf8iRrvgHXfSTnq7OSb8jSb8mprC7xidCS8eHe1/qbOd3AHbbMLfxKod0C6LvdiS8Dea8GtaFH4YHQvesqq+KKiGmQ0dsk0m/JoRd4k/2gHtMd82zdpNTPgbTfg1nRZ+x05H27pa5pedWxQk/GIVa8Qd8T8d9I4Jf6MJvybX8DtWnQ6LgnlnUbBZh22RQw8/4itxRrz1+Lsx4W804de0ZPgdOyeBHe1dFAwt/IgnFvMkvnq8l2LC32jCr8kz/I7RKZOxWm6sPYuCoYNfxQ/xRFy9QO+Y8Dea8GtaNvwwOii8ZXUttzNe78AtOgmEBn4VM8QPcZyPabN4uzThbzTh19QS+B2rDpsbWWM/dNKKk0DXw+9APzFgx60V0Dsm/I0m/JpaCr9j1Xlz29Yue1Gwq+FHXOzFvLUL4tYqE/5GE35NbYHfsXMSsBcFVWdvBsEi7kr4EQeJx3IW89yY8Dea8GtqK/yO0bmTq+ydZvOd/2QgTuGugl/VG3FAPNoFvWPC32jCr6kj8MPo6DAWBcfdLwp2Bfyqrqh3qxbz3JjwN5rwa+oY/I5Vx8fiVn5+UbAJMMqBht95FNrecdfaxTw3JvyNJvyaOg6/YwVBbjsWBU/9kFBg4Ud9pF6on17fTprwN5rwa1ox+B0DikV2DgYOflX+3Oi6RXfcdcKEv9GEX9OKww8PwQIJFgXxkJAGUWDgd6DHQzrOYh7q1ay+HTLhbzTh1+QL+B3bwIixc1AtCh69cqO/4Uf5pJz2Yt4Sdtx1woS/0YRfk6/gd+wAtK2vdnQSw2nznwCbKrIvdOjSc1+dy1iPH8ladjn9BL1jwt9owq/Jl/A7FpiObJWpwLa+R2pjg2eoIvtCezf1nZnbtmbPYSmf36B3TPgbTfg1+Rp+sR2rod4HVXF9pdmh3of80KFPZcLfaMKvKRDw+yRWuvzUoU9lwt9owq+J8HsT4Xcvwq+Z8Ls34fduwt9owq+J8HsT4Xcvwq+Z8Ls34fduwt9owq+J8HsT4Xcvwq+Z8Ls34fduwt9owq+J8HsT4Xcvwq+Z8Ls34fduwt9owq+J8HsT4Xcvwq+Z8Ls34fduwt9owq+J8HsT4Xcvwq+Z8Ls34fduwt9owq+J8HsT4Xcvwq+Z8Ls34fduwt9owq+J8HsT4Xcvwq+Z8Ls34fduwt9owq+J8HsT4Xcvwq+Z8Ls34fduwt9owq+J8HsT4Xcvwq+Z8Ls34fduwt9owq+J8HsT4Xcvwq+Z8Ls34fduwt9owq+J8HsT4Xcvwq+Z8Ls34fduwt9owq+J8HsT4Xcvwq+Z8Ls34fduwt9owq+J8HsT4Xcvwq+Z8Ls34fduwt9owq+J8HsT4Xcvwq+Z8Ls34fduwt9owq+J8HsT4Xcvwq+Z8Ls34fduwt9owq+J8HsT4Xcvwq+Z8Ls34fduwt9owq+J8HsT4Xcvwq+Z8Ls34fduwt9owq+J8HsT4Xcvwq+Z8Ls34fduwt9owq+J8HsT4Xcvwq+Z8Ls34fduwt9owq+J8HsT4Xcvwq+Z8Ls34fduwt9owq+J8HsT4Xcvwq+Z8Ls34fduwt9owq+J8HsT4Xcvwq+Z8Ls34fduwt9owq+J8HsT4Xcvwq+Z8Ls34fduwt9owq+J8HsT4Xcvwq+Z8Ls34fduwt9owq+J8HsT4Xcvwq+Z8Ls34fduwt9owq+J8HsT4Xcvwq+Z8Ls34fduwt9owq+J8HsT4Xcvwq+Z8Ls34fduwt9owq+J8HsT4Xcvwq+Z8Ls34fduwt9owq+J8HsT4Xcvwq+Z8Ls34fduwt9owq+J8HsT4Xcvwq+Z8Ls34fduwt9owq+J8HsT4Xcvwq+Z8Ls34fduwt9owq+J8HsT4Xcvwq+Z8Ls34fduwt9owq+J8HsT4Xcvwq+Z8Ls34fduwt9owq+J8HsT4Xcvwq+Z8Ls34fduwt9owq+J8HsT4Xcvwq+Z8Ls34fduwt9owq+J8HsT4Xcvwq+Z8Ls34fduwt9owq+J8HsT4Xcvwq+Z8Ls34fduwt9owq+J8HsT4Xcvwq+Z8Ls34fduwt/onmNbV9UKiagEpvkftNuE370Jv3cT/rrBOXgH9z0zQ9HPVZKx2gsrVBjC796E37sJf93gHLyDe7tAUphNpUT04drIqloZ/9HkQ+0y4Xdvwu/dYYYfPINr8F1KxB4C7/XSKO27ePUrSsnYHxYT0fKL8kd59aFmB2ulCb97E37vDiP84DcvQ3zwLBf3Umk48uEfCOf1kjTRbKL3AjlL3I3hwZEt7S8g4Xdvwu/dYYQf/ILjciJ218Et52+sl8CFColoRkYB+xA0+dc+izT7guWa8Ls34ffusMAPTsGr4vap2WQ0Xf/mJepAMnKeTAU+XU5GXzjepsISfvcm/N4dBvhz4uMjsVpJeBVuP/WY8Fv/1mUol4xcVEzEfoS5Q7XFC4KE370Jv3d3M/zgsSpDfPBZTER+ODsUeXv921qkG3p6XiYjgGtl/jCFQGIhoRUnAcLv3oTfu7sRfvAHDlE34fJQcTh6LTitf1MbdHBL77ricOyrWExoRQUIv3sTfu/uRvidRflSInp7ftMb1ta/oQMqJqJbxHvss84ypgKE370Jv3d3C/zgzL5nL/UR/h4pJqPJ+pE7rL2bzz6rOBS9uZKIVU7IfAMLDs0KvJgJv3sTfu/uBvjBF+b1MsSvCPgf23Ppua+uH3UFld8ce3N+OHLfiZFY7bAMQ5YyCiD87k34vTvo8IMrrOTLHP9bU8Jb/Wg+Un4odlUlGd2vhiSuTgKE370Jv3cHEX7wA45Q9nIiuj+XiO2uH8Wnmtq0JiKjgNuweQA7h5pVSjfhd2/C791BhB/8gKPcUPRzh4bO660fIQDKD/W+o5yM/gRBt3cRNakcTPjdm/B7d1DgByfgxd5kl4j+OD98/mX1TwZMezf1nSlzlA9JJXIIfrNnAwi/exN+7w4C/M49e5k6z+aHYx/cMzZ4Rv1TAdb0pqgpo4A7m20WIvzuTfi92+/wH1XPzQgnd0wNRY36X3eR8oneUTmrPYaGKCXrowDC796E37v9CD/6f0nds5fR8aP5ZGRH/a+6VPsTfeeWEtE/LiaiR/FsAEYDhN+dCb93+w1+9Hv0f2HhSCER/cTPhYv6X4RAM1esfmslEf1+bXR1TaD7hfr1iorwexPhX5okVo+i30v//9705t5/rn4dLqHTlJOR908PR+6rXX3hy+u/XTkRfm8i/O5VGxs8Q2J1nwzzf0/9Ktya3nLOa2oX9/y6+nHFRPi9ifC7F/o5+rv6kfKLCL83EX4q8CL83kT4qcCL8HsT4acCL8LvTYSfCrwIvzcRfirwIvzeRPipwIvwexPhpwIvwu9NhJ8KvAi/NxF+KvAi/N5E+KnAi/B7E+GnAi/C702Enwq8CL83EX4q8CL83kT4qcDL7/Aj+8v0cPQnqri+kpTvpyjfyWX2iwk/taj8Dj/e0VZIRA9ODUcuVkX2hXKbey8pSrlQvmbl9oMJP7Wo/A6/k9O9lIgeKyVif3rgksh5qugrouc29b5eyvFnUp7ji72bwQ8m/NSi8jv8MABzXuEk0D1ZHI5kVfE7qmIyMinf/xTK4fbVbCtpwk8tqunN0Zv8Dr9uvAPhmMyzy8noPQcv771AVaOten7oDW8sJ6L/89hI4zsY/Gy7XYd6P6aqQVELJVf+j9XGVmNe7fsrmWPntc1yFS6XEpEb9l28+hWqOi3VA2+L/UZpOHKjjDQq+D4vr2NfCaMd0Z5oV175qVNqZnh1TK6iX6jKFc3PK9cnGx0ci232VCAZ/fupzb1Dqkot0dRw77Ac9x9wfHxPUE6MMNqxIu1ZGI58YXrrmqiqEkU11+xwZFM5EXsoqJ29KmWWofkXf7U5ukpVyZOevSK6Wo7zpSCfDNGOU0ORzapKFHV64SWjpUTshqIMpwM5zMUoIBE9WBiOvU9VaUnKDcWukc8/j+MEaRqUF6O90G6lZOQP0Y6qShS1NB1M9F5QTMTuxsLa4QAtcAHWOSmvWg/4/kxi9VtVlRbVzPCqf1FJRr+PV0nh80Ea9WABEiMUmaLcM7vp/I2qShS1POGWmlxNAnNryzFGLOqdcEcLw9E/2XNF7LWqSgv0jPy+JP+Pv8PfB2mkg/ZQ7fJkLhHNqCpRVOt0IBk5T4aSn5J58LHjI8EaBeCtyACkkog+NpvoHVNVslUY7t0pV/t/sqcK6u3JzY7jN+MEhXYoJ6MvlJKxTz/7zujrVJUoqj2Sq8vvlhORH9bkConFtSAOjWeGIt+Y2Ry5fHYoegd+DtI9e8QbcX9RwC9KO+SSkYtU01BU+1W7oedlxWT02nIiNhXERTEAjxVx/Iufg1B2lNFZzETci0PRf3+DtINqEorqrA5tfv364nDsq1gM9POW1mbOC0jNfu9X40SFOMsJ4PbnE2/oV01AUSsrmQpsKSVij9TnzcGaCvjZiKNzz76SjP08NxzZqkJOUf4RXr0sV9Oby4loNUgr5n414odblTLEr8jU5KZHf4evtqZ8rvzm2JuLyci3j0vHxTCVo4ClGfFC3BC/4nDk24inCi1FBUP5odhVMgrYjyFrkJ4NWCkjPs49e7naP50f7r1ShZKigqepTa+L5Iain0Oyi2NyJWvW6em6ER+Z35/IS7yelripEFJUsJXfsuod5WT0x3g2wO9ZbzppxAHxQFxklPRAfvj8y1TIKKp7hE0m+eHof5ROnsNCVtButbXaqL+9oJeMzs4moh/aMzZ4hgoVRXWnnt0UNYvJ6J32k3UBezagVcYzEbDM8e+YGooaKjQUFQ4VEr1jlWT00fqzAd2/IIj6OXsM5Gr/aH5z76gKBUWFT/sTfeeWEtFPBG033VKNejm7C4vJ2H9GvVUIKCrcOhjgffSLGfVAfVCvaiL6Pbd5BSgqdJpN9P4bGRo/h6FxkDYLnWyU29mEI/U5UBqOvFdVkaKoUwm592R4/EVcMbEo2AwuvxvlRvmREPWZ4dUxVTWKotxodiiyWU4CD9v3wAPwbADK52zCKSViDyERqqoKRVFLFfLwF4ajNxZ9nEgU0DuJM+VkVSokIjf8oE3vD6Co0Gk20XuBXFXvxiOwfsu6g/LYj+YmYncd3MLEmRTVFqlEovswtF7JzUL4XmcTjvz7VIGJMymq/aonEo19WubVx4+vwIIgph524sxE7JiU41OPSXlU0SiK6oRyw5GLi4nYjzDX7kQiURy/njgTV/vID0tMnElRK6cbenpeVtYSiWKzTKtPAjgejovj43uKw9Fr8b2qCBRFraScRKJYfGt1IlF7A5Ict5SI3l5g4kyK8qeKiegW8R77Kr2MqQA+59yzl+M9UkxGk+orKIryq/ZuPvus4lD05koiVvGyWcjZhCND/IoM8W96dAsTZ1JUoITEl/nhyH0nRur57t2MAuqJM/Eij8i3p5g4k6KCLSQSrSRPnUgUP+P39QW96P5iIrZbfZSiqKBratOaiFzNb7MTiW5dmEgUP+P3SDR6aOi8XvURiqK6SfmhXiQS/Qnu1QN4/CtX+x8zcSZFhUC4R19I9l5THYk9kRuOvE/9mgqVenr+P+OvTjWo+kMRAAAAAElFTkSuQmCC
+ image/png
+ magento-logo.png
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/ProductAttributeData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/ProductAttributeData.xml
new file mode 100644
index 0000000000000..d5f29230f0308
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/Data/ProductAttributeData.xml
@@ -0,0 +1,77 @@
+
+
+
+
+
+ ProductAttributeWithTwoOptions
+ attribute
+ textarea
+ global
+ false
+ false
+ true
+ true
+ text
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+ ProductAttributeFrontendLabel
+
+
+ testattribute
+ select
+ global
+ false
+ false
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+ ProductAttributeFrontendLabel
+
+
+ attribute
+ multiselect
+ global
+ false
+ false
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+ ProductAttributeFrontendLabel
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/ProductAttributeMediaGalleryEntryData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/ProductAttributeMediaGalleryEntryData.xml
new file mode 100644
index 0000000000000..aa7c10122d930
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/Data/ProductAttributeMediaGalleryEntryData.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
+ image
+ Magento Logo
+ 1
+
+ - image
+ - small_image
+ - thumbnail
+
+ false
+ MagentoLogoImageContent
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/ProductAttributeOptionData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/ProductAttributeOptionData.xml
new file mode 100644
index 0000000000000..25512c0f266e9
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/Data/ProductAttributeOptionData.xml
@@ -0,0 +1,62 @@
+
+
+
+
+
+
+ option1
+ false
+ 0
+ Option1Store0
+ Option1Store1
+
+
+
+ option2
+ true
+ 1
+ Option2Store0
+ Option2Store1
+
+
+
+
+
+
+ option3
+ false
+ 2
+ Option3Store0
+ Option3Store1
+
+
+
+ option4
+ false
+ 3
+ Option4Store0
+ Option4Store1
+
+
+
+ option5
+ false
+ 4
+ Option5Store0
+ Option5Store1
+
+
+
+ option6
+ false
+ 5
+ Option6Store0
+ Option6Store1
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/ProductAttributeSetData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/ProductAttributeSetData.xml
new file mode 100644
index 0000000000000..68c0a54ff88fc
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/Data/ProductAttributeSetData.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+ 4
+ 7
+ 0
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/ProductConfigurableAttributeData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/ProductConfigurableAttributeData.xml
new file mode 100644
index 0000000000000..0c9a2f7041902
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/Data/ProductConfigurableAttributeData.xml
@@ -0,0 +1,27 @@
+
+
+
+
+
+ Color
+ 1
+
+
+ White
+ 1.00
+
+
+ Red
+ 2.00
+
+
+ Blue
+ 3.00
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/ProductData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/ProductData.xml
new file mode 100644
index 0000000000000..35e6d198c7f54
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/Data/ProductData.xml
@@ -0,0 +1,252 @@
+
+
+
+
+
+ testSku
+ simple
+ 4
+ 4
+ testProductName
+ 123.00
+ testurlkey
+ 1
+ 100
+ EavStockItem
+ CustomAttributeCategoryIds
+
+
+ api-simple-product
+ simple
+ 4
+ 4
+ Api Simple Product
+ 123.00
+ api-simple-product
+ 1
+ 100
+ EavStockItem
+ CustomAttributeCategoryIds
+
+
+ api-simple-product
+ simple
+ 4
+ 4
+ Api Simple Product
+ 123.00
+ api-simple-product
+ 1
+ 100
+ EavStockItem
+ CustomAttributeProductAttribute
+
+
+ api-simple-product-two
+ simple
+ 4
+ 4
+ Api Simple Product Two
+ 234.00
+ api-simple-product-two
+ 1
+ 100
+ EavStockItem
+ CustomAttributeProductAttribute
+
+
+ ApiProductDescription
+ ApiProductShortDescription
+
+
+ Updated Api Simple Product
+ api-simple-product
+
+
+ SimpleProduct
+ simple
+ 4
+ SimpleProduct
+ 200.00
+ 4
+ 1
+ EavStockItem
+ CustomAttributeCategoryIds
+
+
+ SimpleProduct2
+ simple
+ 4
+ SimpleProduct2
+ 300.00
+ 4
+ 1
+ EavStockItem
+ CustomAttributeCategoryIds
+
+
+ SimpleProduct
+ simple
+ 4
+ SimpleProduct
+ 123.00
+ 4
+ 1
+ 1000
+ EavStockItem
+
+
+ SimpleOne
+ simple
+ 4
+ SimpleProduct
+ 1.23
+ 4
+ 1
+ EavStockItem
+ CustomAttributeProductAttribute
+
+
+ SimpleOne
+ simple
+ 4
+ SimpleProductOption
+ 10.00
+ 4
+ 1
+ EavStockItem
+ CustomAttributeProductAttribute
+
+
+ Image1
+ 1.00
+ Upload File
+ Yes
+ magento-logo.png
+ magento-logo
+
+
+ MagentoLogo
+ 1.00
+ Upload File
+ Yes
+ magento-logo.png
+ magento-logo
+ png
+
+
+ magento-again
+ 1.00
+ Upload File
+ Yes
+ magento-again.jpg
+ magento-again
+ jpg
+
+
+ testProductWithDescriptionSku
+ simple
+ 4
+ 4
+ testProductWithDescriptionName
+ 123.00
+ testproductwithdescriptionurlkey
+ 1
+ 100
+ EavStockItem
+ CustomAttributeCategoryIds
+ ApiProductDescription
+ ApiProductShortDescription
+
+
+
+ magento.jpg
+ ProductOptionField
+ ProductOptionArea
+ ProductOptionFile
+ ProductOptionDropDown
+ ProductOptionRadiobutton
+ ProductOptionCheckbox
+ ProductOptionMultiSelect
+ ProductOptionDate
+ ProductOptionDateTime
+ ProductOptionTime
+
+
+ virtualproduct
+ virtual
+ 4
+ VirtualProduct
+ 99.99
+ 250
+ 0
+ 1
+ EavStockItem
+
+
+ SimpleProduct
+ simple
+ 4
+ SimpleProduct
+ 125.00
+ 4
+ 1
+ 1000
+ simpleproduct
+ 1
+ EavStockItem
+ ApiProductNewsFromDate
+
+
+ SimpleProduct
+ simple
+ 4
+ SimpleProduct
+ 125.00
+ 4
+ 1
+ 1000
+ simple-product-with-image
+ 1
+ EavStockItem
+ ProductAttributeMediaGalleryEntryMagentoLogo
+ CustomAttributeCategoryIds
+
+
+ api-simple-product
+ simple
+ 4
+ 4
+ Api Simple Product
+ 123.00
+ api-simple-product
+ 1
+ 100
+ EavStockItem
+ ApiProductDescription
+ ApiProductShortDescription
+
+
+ api-simple-product
+ simple
+ 4
+ 1
+ Api Simple Product
+ 123.00
+ api-simple-product
+ 1
+ 100
+ EavStockItem
+ CustomAttributeProductAttribute
+
+
+
+ ProductOptionDropDownWithLongValuesTitle
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/ProductExtensionAttributeData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/ProductExtensionAttributeData.xml
new file mode 100644
index 0000000000000..0f6b383c3b743
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/Data/ProductExtensionAttributeData.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+ Qty_1000
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/ProductOptionData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/ProductOptionData.xml
new file mode 100644
index 0000000000000..ae8bcf0893ed0
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/Data/ProductOptionData.xml
@@ -0,0 +1,113 @@
+
+
+
+
+
+ OptionField
+ field
+ true
+ 1
+ 10
+ fixed
+ 0
+
+
+
+ OptionArea
+ area
+ true
+ 2
+ 10
+ percent
+ 0
+
+
+
+ OptionFile
+ file
+ true
+ 3
+ 9.99
+ fixed
+ png, jpg, gif
+ 0
+ 0
+
+
+
+ OptionDropDown
+ drop_down
+ 4
+ true
+ ProductOptionValueDropdown1
+ ProductOptionValueDropdown2
+
+
+
+ OptionDropDownWithLongTitles
+ drop_down
+ 4
+ true
+ ProductOptionValueDropdownLongTitle1
+ ProductOptionValueDropdownLongTitle2
+
+
+
+ OptionRadioButtons
+ radio
+ 5
+ true
+ ProductOptionValueRadioButtons1
+ ProductOptionValueRadioButtons2
+
+
+
+ OptionCheckbox
+ checkbox
+ 6
+ true
+ ProductOptionValueCheckbox
+
+
+
+ OptionMultiSelect
+ multiple
+ 7
+ true
+ ProductOptionValueMultiSelect1
+ ProductOptionValueMultiSelect2
+
+
+
+ OptionDate
+ date
+ 8
+ true
+ 1234
+ fixed
+
+
+
+ OptionDateTime
+ date_time
+ 9
+ true
+ 0.00
+ fixed
+
+
+
+ OptionTime
+ time
+ 10
+ true
+ 0.00
+ percent
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/ProductOptionValueData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/ProductOptionValueData.xml
new file mode 100644
index 0000000000000..82063a771d2a3
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/Data/ProductOptionValueData.xml
@@ -0,0 +1,64 @@
+
+
+
+
+ OptionValueDropDown1
+ 1
+ 0.01
+ fixed
+
+
+ OptionValueDropDown2
+ 2
+ 0.01
+ percent
+
+
+ OptionValueRadioButtons1
+ 1
+ 99.99
+ fixed
+
+
+ OptionValueRadioButtons2
+ 2
+ 99.99
+ percent
+
+
+ OptionValueCheckbox
+ 1
+ 123
+ percent
+
+
+ OptionValueMultiSelect1
+ 1
+ 1
+ fixed
+
+
+ OptionValueMultiSelect2
+ 2
+ 2
+ fixed
+
+
+ Optisfvdklvfnkljvnfdklpvnfdjklfdvnjkvfdkjnvfdjkfvndj11111Optisfvdklvfnkljvnfdklpvnfdjklfdvnjkvfdkjnvfdjkfvndj11111
+ 1
+ 10
+ fixed
+
+
+ Optisfvdklvfnkljvnfdklpvnfdjklfdvnjkvfdkjnvfdjkfvndj22222Optisfvdklvfnkljvnfdklpvnfdjklfdvnjkvfdkjnvfdjkfvndj22222
+ 2
+ 20
+ percent
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/StockItemData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/StockItemData.xml
new file mode 100644
index 0000000000000..46a3fa3657f2c
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/Data/StockItemData.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
+ 1000
+ true
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/StoreLabelData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/StoreLabelData.xml
new file mode 100644
index 0000000000000..a703e56beda01
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/Data/StoreLabelData.xml
@@ -0,0 +1,59 @@
+
+
+
+
+
+ 0
+ option1
+
+
+ 1
+ option1
+
+
+ 0
+ option2
+
+
+ 1
+ option2
+
+
+ 0
+ option3
+
+
+ 1
+ option3
+
+
+ 0
+ option4
+
+
+ 1
+ option4
+
+
+ 0
+ option5
+
+
+ 1
+ option5
+
+
+ 0
+ option6
+
+
+ 1
+ option6
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/TierPriceData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/TierPriceData.xml
new file mode 100644
index 0000000000000..ae1b5afe4008a
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/Data/TierPriceData.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
+ $676.50
+ $615.00
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/LICENSE.txt b/app/code/Magento/Catalog/Test/Mftf/LICENSE.txt
new file mode 100644
index 0000000000000..49525fd99da9c
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/LICENSE.txt
@@ -0,0 +1,48 @@
+
+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/Catalog/Test/Mftf/LICENSE_AFL.txt b/app/code/Magento/Catalog/Test/Mftf/LICENSE_AFL.txt
new file mode 100644
index 0000000000000..f39d641b18a19
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/LICENSE_AFL.txt
@@ -0,0 +1,48 @@
+
+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/Catalog/Test/Mftf/Metadata/catalog_price-meta.xml b/app/code/Magento/Catalog/Test/Mftf/Metadata/catalog_price-meta.xml
new file mode 100644
index 0000000000000..7c57827356242
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/Metadata/catalog_price-meta.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+ string
+
+
+ string
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Metadata/category-meta.xml b/app/code/Magento/Catalog/Test/Mftf/Metadata/category-meta.xml
new file mode 100644
index 0000000000000..c22743f6a0642
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/Metadata/category-meta.xml
@@ -0,0 +1,64 @@
+
+
+
+
+ application/json
+
+ integer
+ string
+ boolean
+ integer
+ integer
+ string
+ string
+ string
+ string
+ boolean
+
+ string
+
+ empty_extension_attribute
+
+ custom_attribute
+
+
+
+
+
+ application/json
+
+ integer
+ integer
+ string
+ boolean
+ integer
+ integer
+ string
+ string
+ string
+ string
+
+ string
+
+ boolean
+ empty_extension_attribute
+
+ custom_attribute
+
+
+
+
+
+ application/json
+
+
+
+ application/json
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Metadata/custom_attribute-meta.xml b/app/code/Magento/Catalog/Test/Mftf/Metadata/custom_attribute-meta.xml
new file mode 100644
index 0000000000000..aed9b7a979836
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/Metadata/custom_attribute-meta.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+ string
+ string
+
+
+ string
+
+ string
+
+
+
+ string
+ string
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Metadata/empty_extension_attribute-meta.xml b/app/code/Magento/Catalog/Test/Mftf/Metadata/empty_extension_attribute-meta.xml
new file mode 100644
index 0000000000000..d8410593cb5b4
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/Metadata/empty_extension_attribute-meta.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Metadata/frontend_label-meta.xml b/app/code/Magento/Catalog/Test/Mftf/Metadata/frontend_label-meta.xml
new file mode 100644
index 0000000000000..d0bcbd3e5db97
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/Metadata/frontend_label-meta.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
+ integer
+ string
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Metadata/product-meta.xml b/app/code/Magento/Catalog/Test/Mftf/Metadata/product-meta.xml
new file mode 100644
index 0000000000000..3ac0645fa13d2
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/Metadata/product-meta.xml
@@ -0,0 +1,124 @@
+
+
+
+
+
+ application/json
+
+ string
+ string
+ integer
+ number
+ integer
+ integer
+ string
+ string
+ string
+ integer
+ product_extension_attribute
+
+ product_link
+
+
+ custom_attribute_array
+
+
+ product_option
+
+
+ media_gallery_entries
+
+
+
+
+ application/json
+
+ integer
+ string
+ string
+ integer
+ number
+ integer
+ integer
+ string
+ string
+ string
+ integer
+ product_extension_attribute
+
+ product_link
+
+
+ custom_attribute_array
+
+
+ product_option
+
+
+ boolean
+
+
+ application/json
+
+
+ application/json
+
+ string
+ string
+ integer
+ number
+ integer
+ integer
+ string
+ string
+ string
+ integer
+ product_extension_attribute
+
+ product_link
+
+
+ custom_attribute
+
+
+ product_option
+
+
+
+
+ application/json
+
+ integer
+ string
+ string
+ integer
+ number
+ integer
+ integer
+ string
+ string
+ string
+ integer
+ product_extension_attribute
+
+ product_link
+
+
+ custom_attribute
+
+
+ product_option
+
+
+ boolean
+
+
+ application/json
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Metadata/product_attribute-meta.xml b/app/code/Magento/Catalog/Test/Mftf/Metadata/product_attribute-meta.xml
new file mode 100644
index 0000000000000..93396352ba506
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/Metadata/product_attribute-meta.xml
@@ -0,0 +1,115 @@
+
+
+
+
+
+ application/json
+
+ string
+ string
+ string
+ string
+ string
+ string
+ string
+ string
+ string
+ string
+ string
+ boolean
+ boolean
+ boolean
+ boolean
+ boolean
+ boolean
+ boolean
+ boolean
+ boolean
+ boolean
+ boolean
+ boolean
+ boolean
+ boolean
+ boolean
+ boolean
+ boolean
+ boolean
+ empty_extension_attribute-meta
+
+ string
+
+
+ ProductAttributeOption
+
+
+ custom_attribute_array
+
+
+ validation_rule
+
+
+ FrontendLabel
+
+
+
+
+ application/json
+
+ string
+ string
+ string
+ string
+ string
+ string
+ string
+ string
+ string
+ string
+ string
+ string
+ boolean
+ boolean
+ boolean
+ boolean
+ boolean
+ boolean
+ boolean
+ boolean
+ boolean
+ boolean
+ boolean
+ boolean
+ boolean
+ boolean
+ boolean
+ boolean
+ boolean
+ boolean
+ empty_extension_attribute-meta
+
+ string
+
+
+ ProductAttributeOption
+
+
+ custom_attribute_array
+
+
+ validation_rule
+
+
+ FrontendLabel
+
+
+
+
+ application/json
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Metadata/product_attribute_media_gallery_entry-meta.xml b/app/code/Magento/Catalog/Test/Mftf/Metadata/product_attribute_media_gallery_entry-meta.xml
new file mode 100644
index 0000000000000..50690655cf919
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/Metadata/product_attribute_media_gallery_entry-meta.xml
@@ -0,0 +1,29 @@
+
+
+
+
+
+ integer
+ string
+ string
+ integer
+ boolean
+
+ string
+
+ string
+ image_content
+ empty_extension_attribute
+
+
+ string
+ string
+ string
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Metadata/product_attribute_option-meta.xml b/app/code/Magento/Catalog/Test/Mftf/Metadata/product_attribute_option-meta.xml
new file mode 100644
index 0000000000000..176afa8d58d7c
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/Metadata/product_attribute_option-meta.xml
@@ -0,0 +1,29 @@
+
+
+
+
+
+ application/json
+
+ string
+ string
+ integer
+ boolean
+
+ StoreLabel
+
+
+
+
+ application/json
+
+
+ application/json
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Metadata/product_attribute_set-meta.xml b/app/code/Magento/Catalog/Test/Mftf/Metadata/product_attribute_set-meta.xml
new file mode 100644
index 0000000000000..eef82b07aaf4f
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/Metadata/product_attribute_set-meta.xml
@@ -0,0 +1,27 @@
+
+
+
+
+
+ application/json
+ integer
+ integer
+ string
+ integer
+
+
+ application/json
+
+
+ application/json
+
+
+ application/json
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Metadata/product_extension_attribute-meta.xml b/app/code/Magento/Catalog/Test/Mftf/Metadata/product_extension_attribute-meta.xml
new file mode 100644
index 0000000000000..8d0d1e66c81e3
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/Metadata/product_extension_attribute-meta.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+ stock_item
+
+
+ stock_item
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Metadata/product_link-meta.xml b/app/code/Magento/Catalog/Test/Mftf/Metadata/product_link-meta.xml
new file mode 100644
index 0000000000000..3c90f32990b7d
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/Metadata/product_link-meta.xml
@@ -0,0 +1,31 @@
+
+
+
+
+
+ string
+ string
+ string
+ string
+ integer
+
+ product_link_extension_attribute
+
+
+
+ string
+ string
+ string
+ string
+ integer
+
+ product_link_extension_attribute
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Metadata/product_link_extension_attribute-meta.xml b/app/code/Magento/Catalog/Test/Mftf/Metadata/product_link_extension_attribute-meta.xml
new file mode 100644
index 0000000000000..07ea02f5b7aee
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/Metadata/product_link_extension_attribute-meta.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+ application/json
+ integer
+
+
+ application/json
+ integer
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Metadata/product_option-meta.xml b/app/code/Magento/Catalog/Test/Mftf/Metadata/product_option-meta.xml
new file mode 100644
index 0000000000000..adc5a33507af6
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/Metadata/product_option-meta.xml
@@ -0,0 +1,47 @@
+
+
+
+
+
+ string
+ integer
+ string
+ string
+ integer
+ boolean
+ number
+ string
+ string
+ string
+ integer
+ integer
+ integer
+
+ product_option_value
+
+
+
+ string
+ integer
+ string
+ string
+ integer
+ boolean
+ number
+ string
+ string
+ string
+ integer
+ integer
+ integer
+
+ product_option_value
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Metadata/product_option_value-meta.xml b/app/code/Magento/Catalog/Test/Mftf/Metadata/product_option_value-meta.xml
new file mode 100644
index 0000000000000..f4273f5796830
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/Metadata/product_option_value-meta.xml
@@ -0,0 +1,27 @@
+
+
+
+
+
+ string
+ integer
+ number
+ string
+ string
+ integer
+
+
+ string
+ integer
+ number
+ string
+ string
+ integer
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Metadata/stock_item-meta.xml b/app/code/Magento/Catalog/Test/Mftf/Metadata/stock_item-meta.xml
new file mode 100644
index 0000000000000..e7e79d69055c6
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/Metadata/stock_item-meta.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+ integer
+ boolean
+
+
+ integer
+ boolean
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Metadata/store_label-meta.xml b/app/code/Magento/Catalog/Test/Mftf/Metadata/store_label-meta.xml
new file mode 100644
index 0000000000000..abb9b003dc59e
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/Metadata/store_label-meta.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
+ integer
+ string
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Metadata/validation_rule-meta.xml b/app/code/Magento/Catalog/Test/Mftf/Metadata/validation_rule-meta.xml
new file mode 100644
index 0000000000000..c568e52b2ab3c
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/Metadata/validation_rule-meta.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+ string
+ string
+
+
+ string
+ string
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Page/AdminCatalogProductPage.xml b/app/code/Magento/Catalog/Test/Mftf/Page/AdminCatalogProductPage.xml
new file mode 100644
index 0000000000000..e5d3704bafcd1
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/Page/AdminCatalogProductPage.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Page/AdminCategoryEditPage.xml b/app/code/Magento/Catalog/Test/Mftf/Page/AdminCategoryEditPage.xml
new file mode 100644
index 0000000000000..68c432a136ac9
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/Page/AdminCategoryEditPage.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Page/AdminCategoryPage.xml b/app/code/Magento/Catalog/Test/Mftf/Page/AdminCategoryPage.xml
new file mode 100644
index 0000000000000..a11d05140c638
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/Page/AdminCategoryPage.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Page/AdminCategoryProductAttributeEditPage.xml b/app/code/Magento/Catalog/Test/Mftf/Page/AdminCategoryProductAttributeEditPage.xml
new file mode 100644
index 0000000000000..035b6739c5aaf
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/Page/AdminCategoryProductAttributeEditPage.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Page/AdminProductAttributeEditPage.xml b/app/code/Magento/Catalog/Test/Mftf/Page/AdminProductAttributeEditPage.xml
new file mode 100644
index 0000000000000..3d7e79343155b
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/Page/AdminProductAttributeEditPage.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Page/AdminProductAttributeFormPage.xml b/app/code/Magento/Catalog/Test/Mftf/Page/AdminProductAttributeFormPage.xml
new file mode 100644
index 0000000000000..8213ec6ab5fbe
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/Page/AdminProductAttributeFormPage.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Page/AdminProductAttributeGridPage.xml b/app/code/Magento/Catalog/Test/Mftf/Page/AdminProductAttributeGridPage.xml
new file mode 100644
index 0000000000000..a5de7453d9c23
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/Page/AdminProductAttributeGridPage.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Page/AdminProductAttributeSetEditPage.xml b/app/code/Magento/Catalog/Test/Mftf/Page/AdminProductAttributeSetEditPage.xml
new file mode 100644
index 0000000000000..1e5ef870ba8fd
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/Page/AdminProductAttributeSetEditPage.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Page/AdminProductAttributeSetGridPage.xml b/app/code/Magento/Catalog/Test/Mftf/Page/AdminProductAttributeSetGridPage.xml
new file mode 100644
index 0000000000000..ace73a45c69a2
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/Page/AdminProductAttributeSetGridPage.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Page/AdminProductCreatePage.xml b/app/code/Magento/Catalog/Test/Mftf/Page/AdminProductCreatePage.xml
new file mode 100644
index 0000000000000..8c8ae0ede2eb8
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/Page/AdminProductCreatePage.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Page/AdminProductEditPage.xml b/app/code/Magento/Catalog/Test/Mftf/Page/AdminProductEditPage.xml
new file mode 100644
index 0000000000000..4b240c4dc9421
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/Page/AdminProductEditPage.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Page/AdminProductIndexPage.xml b/app/code/Magento/Catalog/Test/Mftf/Page/AdminProductIndexPage.xml
new file mode 100644
index 0000000000000..d77a041baf662
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/Page/AdminProductIndexPage.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Page/AdminProductPage.xml b/app/code/Magento/Catalog/Test/Mftf/Page/AdminProductPage.xml
new file mode 100644
index 0000000000000..ee60c809aec10
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/Page/AdminProductPage.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Page/StorefrontCategoryPage.xml b/app/code/Magento/Catalog/Test/Mftf/Page/StorefrontCategoryPage.xml
new file mode 100644
index 0000000000000..b32c3ded033f9
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/Page/StorefrontCategoryPage.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Page/StorefrontProductComparePage.xml b/app/code/Magento/Catalog/Test/Mftf/Page/StorefrontProductComparePage.xml
new file mode 100644
index 0000000000000..f0599a021d4c4
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/Page/StorefrontProductComparePage.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Page/StorefrontProductPage.xml b/app/code/Magento/Catalog/Test/Mftf/Page/StorefrontProductPage.xml
new file mode 100644
index 0000000000000..9fcbcc199176b
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/Page/StorefrontProductPage.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/README.md b/app/code/Magento/Catalog/Test/Mftf/README.md
new file mode 100644
index 0000000000000..e7a95609c394b
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/README.md
@@ -0,0 +1,3 @@
+# Catalog Functional Tests
+
+The Functional Test Module for **Magento Catalog** module.
diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminAddProductsToOptionPanelSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminAddProductsToOptionPanelSection.xml
new file mode 100644
index 0000000000000..75fda66ac5cb1
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminAddProductsToOptionPanelSection.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryBasicFieldSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryBasicFieldSection.xml
new file mode 100644
index 0000000000000..555b3c2a6e00e
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryBasicFieldSection.xml
@@ -0,0 +1,57 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryMainActionsSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryMainActionsSection.xml
new file mode 100644
index 0000000000000..14489d155bcc3
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryMainActionsSection.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryMessagesSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryMessagesSection.xml
new file mode 100644
index 0000000000000..1214cfd2eb224
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryMessagesSection.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryModalSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryModalSection.xml
new file mode 100644
index 0000000000000..03b9d76778555
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryModalSection.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryProductsSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryProductsSection.xml
new file mode 100644
index 0000000000000..bf6f3c692b6c3
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryProductsSection.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategorySEOSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategorySEOSection.xml
new file mode 100644
index 0000000000000..93ba3581ff81f
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategorySEOSection.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategorySidebarActionSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategorySidebarActionSection.xml
new file mode 100644
index 0000000000000..e53a9989d661c
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategorySidebarActionSection.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategorySidebarTreeSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategorySidebarTreeSection.xml
new file mode 100644
index 0000000000000..31174589d7e6b
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategorySidebarTreeSection.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryWarningMessagesPopupSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryWarningMessagesPopupSection.xml
new file mode 100644
index 0000000000000..7dd78bcddb5f5
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryWarningMessagesPopupSection.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminCreateProductAttributeSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCreateProductAttributeSection.xml
new file mode 100644
index 0000000000000..e5e37b59af359
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCreateProductAttributeSection.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductAttributeEditSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductAttributeEditSection.xml
new file mode 100644
index 0000000000000..798832f707bf0
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductAttributeEditSection.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductAttributeGridSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductAttributeGridSection.xml
new file mode 100644
index 0000000000000..bb944270a10a9
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductAttributeGridSection.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductAttributeGroupSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductAttributeGroupSection.xml
new file mode 100644
index 0000000000000..ad035503bff9f
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductAttributeGroupSection.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductAttributeSetActionSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductAttributeSetActionSection.xml
new file mode 100644
index 0000000000000..bf06119b75e4e
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductAttributeSetActionSection.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductAttributeSetEditSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductAttributeSetEditSection.xml
new file mode 100644
index 0000000000000..3334cd2dc1805
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductAttributeSetEditSection.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductAttributeSetGridSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductAttributeSetGridSection.xml
new file mode 100644
index 0000000000000..bc179ff95841e
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductAttributeSetGridSection.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductAttributeSetSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductAttributeSetSection.xml
new file mode 100644
index 0000000000000..963c4db8d7bfd
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductAttributeSetSection.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductAttributeUnassignedSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductAttributeUnassignedSection.xml
new file mode 100644
index 0000000000000..102a21cdca9c0
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductAttributeUnassignedSection.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductCustomizableOptionsSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductCustomizableOptionsSection.xml
new file mode 100644
index 0000000000000..81c6a80e7e3a7
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductCustomizableOptionsSection.xml
@@ -0,0 +1,33 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFiltersSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFiltersSection.xml
new file mode 100644
index 0000000000000..6844006e4e399
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFiltersSection.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormActionSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormActionSection.xml
new file mode 100644
index 0000000000000..797e603bace30
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormActionSection.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormAdvancedPricingSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormAdvancedPricingSection.xml
new file mode 100644
index 0000000000000..0f1fe8abeb3b5
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormAdvancedPricingSection.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormChangeStoreSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormChangeStoreSection.xml
new file mode 100644
index 0000000000000..594a1e4171a4e
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormChangeStoreSection.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormSection.xml
new file mode 100644
index 0000000000000..0cbb0cb519751
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormSection.xml
@@ -0,0 +1,184 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductGridActionSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductGridActionSection.xml
new file mode 100644
index 0000000000000..9f51c6b8e21b4
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductGridActionSection.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductGridConfirmActionSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductGridConfirmActionSection.xml
new file mode 100644
index 0000000000000..d8567df81b6b3
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductGridConfirmActionSection.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductGridFilterSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductGridFilterSection.xml
new file mode 100644
index 0000000000000..a02da2df51702
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductGridFilterSection.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductGridSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductGridSection.xml
new file mode 100644
index 0000000000000..569b20a9c1479
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductGridSection.xml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductImagesSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductImagesSection.xml
new file mode 100644
index 0000000000000..904003412b9c1
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductImagesSection.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductMessagesSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductMessagesSection.xml
new file mode 100644
index 0000000000000..49486d6e4e0f9
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductMessagesSection.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductSEOSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductSEOSection.xml
new file mode 100644
index 0000000000000..1d49d05363612
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductSEOSection.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AttributePropertiesSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AttributePropertiesSection.xml
new file mode 100644
index 0000000000000..55a1892389416
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/Section/AttributePropertiesSection.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategoryMainSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategoryMainSection.xml
new file mode 100644
index 0000000000000..f793cd14441f7
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategoryMainSection.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategoryProductSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategoryProductSection.xml
new file mode 100644
index 0000000000000..ccb5ae60db59b
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategoryProductSection.xml
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategorySidebarSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategorySidebarSection.xml
new file mode 100644
index 0000000000000..ef4c475730174
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategorySidebarSection.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontComparisonSidebarSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontComparisonSidebarSection.xml
new file mode 100644
index 0000000000000..a54d46c490f91
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontComparisonSidebarSection.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontFooterSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontFooterSection.xml
new file mode 100644
index 0000000000000..9db7de6ccc063
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontFooterSection.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontHeaderSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontHeaderSection.xml
new file mode 100644
index 0000000000000..6b0130eefc39b
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontHeaderSection.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontMessagesSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontMessagesSection.xml
new file mode 100644
index 0000000000000..f1456ac8ee387
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontMessagesSection.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontMiniCartSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontMiniCartSection.xml
new file mode 100644
index 0000000000000..ff2e5f2f36015
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontMiniCartSection.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontNavigationSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontNavigationSection.xml
new file mode 100644
index 0000000000000..bf411a9cc48b2
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontNavigationSection.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductActionSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductActionSection.xml
new file mode 100644
index 0000000000000..65d6b7c5f61cb
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductActionSection.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductAdditionalInformationSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductAdditionalInformationSection.xml
new file mode 100644
index 0000000000000..fcec0af892580
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductAdditionalInformationSection.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductCompareMainSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductCompareMainSection.xml
new file mode 100644
index 0000000000000..1f7d259fef0f5
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductCompareMainSection.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductInfoDetailsSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductInfoDetailsSection.xml
new file mode 100644
index 0000000000000..40f49fc2cc77b
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductInfoDetailsSection.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductInfoMainSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductInfoMainSection.xml
new file mode 100644
index 0000000000000..65fe9b06be66c
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductInfoMainSection.xml
@@ -0,0 +1,59 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductMediaSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductMediaSection.xml
new file mode 100644
index 0000000000000..c738e32b3e0d7
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductMediaSection.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductPageSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductPageSection.xml
new file mode 100644
index 0000000000000..c960f8432e64d
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductPageSection.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminApplyTierPriceToProductTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminApplyTierPriceToProductTest.xml
new file mode 100644
index 0000000000000..5742f44e1fedf
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminApplyTierPriceToProductTest.xml
@@ -0,0 +1,272 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 1
+
+
+
+
+ 100
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ $1,000.00
+ grabTextFromSubtotalField1
+
+
+
+
+
+
+ $1,350.00
+ grabTextFromSubtotalField2
+
+
+
+
+
+
+ $1,640.00
+ grabTextFromSubtotalField3
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ $1,500.00
+ grabTextFromSubtotalField4
+
+
+
+ $1,500.00
+ grabTextFromCheckoutCartSummarySectionSubtotal1
+
+
+
+
+
+ $1,500.00
+ grabTextFromMiniCartSubtotalField
+
+
+
+
+
+
+
+
+
+
+
+
+
+ $1,500.00
+ grabTextFromSubtotalField5
+
+
+
+
+
+
+
+
+
+
+
+
+ $1,500.00
+ grabTextFromSubtotalField6
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ $4,000.00
+ grabTextFromSubtotalField7
+
+
+
+ $4,000.00
+ grabTextFromCheckoutCartSummarySectionSubtotal2
+
+
+
+
+
+ $4,000.00
+ grabTextFromMiniCartSubtotalField2
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminAssignProductAttributeToAttributeSetTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminAssignProductAttributeToAttributeSetTest.xml
new file mode 100644
index 0000000000000..4b75e0db0c9ab
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminAssignProductAttributeToAttributeSetTest.xml
@@ -0,0 +1,57 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryTest.xml
new file mode 100644
index 0000000000000..6f477a5325e64
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryTest.xml
@@ -0,0 +1,40 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductCustomAttributeSet.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductCustomAttributeSet.xml
new file mode 100644
index 0000000000000..294766794b92c
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductCustomAttributeSet.xml
@@ -0,0 +1,79 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateRootCategoryAndSubcategoriesTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateRootCategoryAndSubcategoriesTest.xml
new file mode 100644
index 0000000000000..0fb4f2fd784e3
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateRootCategoryAndSubcategoriesTest.xml
@@ -0,0 +1,83 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductTest.xml
new file mode 100644
index 0000000000000..740daefdff313
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductTest.xml
@@ -0,0 +1,62 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveAnchoredCategoryTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveAnchoredCategoryTest.xml
new file mode 100644
index 0000000000000..0909856168fe2
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveAnchoredCategoryTest.xml
@@ -0,0 +1,122 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMultipleWebsitesUseDefaultValuesTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMultipleWebsitesUseDefaultValuesTest.xml
new file mode 100644
index 0000000000000..2a761f192f29f
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMultipleWebsitesUseDefaultValuesTest.xml
@@ -0,0 +1,74 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductGridFilteringByDateAttributeTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductGridFilteringByDateAttributeTest.xml
new file mode 100644
index 0000000000000..75d37d8baa931
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductGridFilteringByDateAttributeTest.xml
@@ -0,0 +1,58 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductStatusAttributeDisabledByDefaultTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductStatusAttributeDisabledByDefaultTest.xml
new file mode 100644
index 0000000000000..9ec47e5a36464
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductStatusAttributeDisabledByDefaultTest.xml
@@ -0,0 +1,55 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminSimpleProductImagesTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminSimpleProductImagesTest.xml
new file mode 100644
index 0000000000000..19b03b4bdc06d
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminSimpleProductImagesTest.xml
@@ -0,0 +1,168 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUnassignProductAttributeFromAttributeSetTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUnassignProductAttributeFromAttributeSetTest.xml
new file mode 100644
index 0000000000000..bfa22e94ccbed
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUnassignProductAttributeFromAttributeSetTest.xml
@@ -0,0 +1,80 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryStoreUrlKeyTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryStoreUrlKeyTest.xml
new file mode 100644
index 0000000000000..2e82f89b6fb87
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryStoreUrlKeyTest.xml
@@ -0,0 +1,78 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/CheckTierPricingOfProductsTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/CheckTierPricingOfProductsTest.xml
new file mode 100644
index 0000000000000..4058112a2d76c
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/CheckTierPricingOfProductsTest.xml
@@ -0,0 +1,341 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 123.00
+
+
+
+ 123.00
+
+
+
+ 123.00
+
+
+
+ 123.00
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{TestDataTierPrice.goldenPrice1}}
+ $checkProductPrice1
+
+
+
+
+ {{TestDataTierPrice.goldenPrice1}}
+ $checkProductPrice2
+
+
+
+
+ {{TestDataTierPrice.goldenPrice1}}
+ $checkProductPrice3
+
+
+
+
+
+
+
+
+
+
+
+ {{TestDataTierPrice.goldenPrice2}}
+ $checkProductPrice4
+
+
+
+
+ {{TestDataTierPrice.goldenPrice1}}
+ $checkProductPrice5
+
+
+
+ {{TestDataTierPrice.goldenPrice1}}
+ $checkProductPrice3
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{TestDataTierPrice.goldenPrice1}}
+ $checkProductPrice7
+
+
+
+
+ {{TestDataTierPrice.goldenPrice1}}
+ $checkProductPrice8
+
+
+
+
+ {{TestDataTierPrice.goldenPrice1}}
+ $checkProductPrice9
+
+
+
+
+
+
+
+
+
+
+
+
+ {{TestDataTierPrice.goldenPrice1}}
+ $checkProductPrice10
+
+
+
+
+ {{TestDataTierPrice.goldenPrice1}}
+ $checkProductPrice11
+
+
+
+
+ {{TestDataTierPrice.goldenPrice1}}
+ $checkProductPrice12
+
+
+
+
+ {{TestDataTierPrice.goldenPrice1}}
+ $checkProductPrice13
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{TestDataTierPrice.goldenPrice1}}
+ $checkProductPrice14
+
+
+ {{TestDataTierPrice.goldenPrice1}}
+ $checkProductPrice15
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/DeleteCategoriesTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/DeleteCategoriesTest.xml
new file mode 100644
index 0000000000000..e4ea511efe46c
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/DeleteCategoriesTest.xml
@@ -0,0 +1,163 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/SaveProductWithCustomOptionsSecondWebsiteTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/SaveProductWithCustomOptionsSecondWebsiteTest.xml
new file mode 100644
index 0000000000000..9575d7fea56fc
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/SaveProductWithCustomOptionsSecondWebsiteTest.xml
@@ -0,0 +1,92 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontProductNameWithDoubleQuoteTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontProductNameWithDoubleQuoteTest.xml
new file mode 100644
index 0000000000000..325a6028e4464
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontProductNameWithDoubleQuoteTest.xml
@@ -0,0 +1,47 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Double Quote"
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontProductsCompareWithEmptyAttributeTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontProductsCompareWithEmptyAttributeTest.xml
new file mode 100644
index 0000000000000..c2b502fd43f34
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontProductsCompareWithEmptyAttributeTest.xml
@@ -0,0 +1,85 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontPurchaseProductCustomOptionsDifferentStoreViewsTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontPurchaseProductCustomOptionsDifferentStoreViewsTest.xml
new file mode 100644
index 0000000000000..d60130c545f10
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontPurchaseProductCustomOptionsDifferentStoreViewsTest.xml
@@ -0,0 +1,303 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 100
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontPurchaseProductWithCustomOptionsTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontPurchaseProductWithCustomOptionsTest.xml
new file mode 100644
index 0000000000000..e92c47d2a3f48
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontPurchaseProductWithCustomOptionsTest.xml
@@ -0,0 +1,179 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 17
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontPurchaseProductWithCustomOptionsWithLongValuesTitle.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontPurchaseProductWithCustomOptionsWithLongValuesTitle.xml
new file mode 100644
index 0000000000000..714ea833d5b0b
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontPurchaseProductWithCustomOptionsWithLongValuesTitle.xml
@@ -0,0 +1,87 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 17
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ productOptionValueText
+ Optisfvdklvfnkljvnfdklpvnfdjklfdvnjkvfdkjnvfdjkfvndj111 ...
+
+
+
+
+ productOptionValueTruncatedText
+ 11Optisfvdklvfnkljvnfdklpvnfdjklfdvnjkvfdkjnvfdjkfvndj11111
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/VerifyChildCategoriesShouldNotIncludeInMenuTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/VerifyChildCategoriesShouldNotIncludeInMenuTest.xml
new file mode 100644
index 0000000000000..dfac039d6ab43
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/VerifyChildCategoriesShouldNotIncludeInMenuTest.xml
@@ -0,0 +1,87 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/code/Magento/Catalog/Test/Unit/Block/Adminhtml/Product/Edit/Tab/AlertsTest.php b/app/code/Magento/Catalog/Test/Unit/Block/Adminhtml/Product/Edit/Tab/AlertsTest.php
index b45df0380dcc6..5d8db5d5ba589 100644
--- a/app/code/Magento/Catalog/Test/Unit/Block/Adminhtml/Product/Edit/Tab/AlertsTest.php
+++ b/app/code/Magento/Catalog/Test/Unit/Block/Adminhtml/Product/Edit/Tab/AlertsTest.php
@@ -55,6 +55,9 @@ public function testCanShowTab($priceAllow, $stockAllow, $canShowTab)
$this->assertEquals($canShowTab, $this->alerts->canShowTab());
}
+ /**
+ * @return array
+ */
public function canShowTabDataProvider()
{
return [
diff --git a/app/code/Magento/Catalog/Test/Unit/Block/Adminhtml/Product/Helper/Form/CategoryTest.php b/app/code/Magento/Catalog/Test/Unit/Block/Adminhtml/Product/Helper/Form/CategoryTest.php
index 5e899263519da..1fc105686011f 100644
--- a/app/code/Magento/Catalog/Test/Unit/Block/Adminhtml/Product/Helper/Form/CategoryTest.php
+++ b/app/code/Magento/Catalog/Test/Unit/Block/Adminhtml/Product/Helper/Form/CategoryTest.php
@@ -50,6 +50,9 @@ public function testIsAllowed($isAllowed)
}
}
+ /**
+ * @return array
+ */
public function isAllowedDataProvider()
{
return [
diff --git a/app/code/Magento/Catalog/Test/Unit/Block/Adminhtml/Product/Helper/Form/Gallery/ContentTest.php b/app/code/Magento/Catalog/Test/Unit/Block/Adminhtml/Product/Helper/Form/Gallery/ContentTest.php
index 804eef25ebdd9..249c32ff276c3 100644
--- a/app/code/Magento/Catalog/Test/Unit/Block/Adminhtml/Product/Helper/Form/Gallery/ContentTest.php
+++ b/app/code/Magento/Catalog/Test/Unit/Block/Adminhtml/Product/Helper/Form/Gallery/ContentTest.php
@@ -6,9 +6,13 @@
namespace Magento\Catalog\Test\Unit\Block\Adminhtml\Product\Helper\Form\Gallery;
use Magento\Catalog\Block\Adminhtml\Product\Helper\Form\Gallery\Content;
-use Magento\Framework\Filesystem;
+use Magento\Catalog\Model\Entity\Attribute;
+use Magento\Catalog\Model\Product;
use Magento\Framework\Phrase;
+/**
+ * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
+ */
class ContentTest extends \PHPUnit\Framework\TestCase
{
/**
@@ -219,4 +223,146 @@ public function testGetImagesJsonWithException()
$this->assertSame(json_encode($imagesResult), $this->content->getImagesJson());
}
+
+ /**
+ * Test GetImageTypes() will return value for given attribute from data persistor.
+ *
+ * @return void
+ */
+ public function testGetImageTypesFromDataPersistor()
+ {
+ $attributeCode = 'thumbnail';
+ $value = 'testImageValue';
+ $scopeLabel = 'testScopeLabel';
+ $label = 'testLabel';
+ $name = 'testName';
+ $expectedTypes = [
+ $attributeCode => [
+ 'code' => $attributeCode,
+ 'value' => $value,
+ 'label' => $label,
+ 'name' => $name,
+ ],
+ ];
+ $product = $this->getMockBuilder(Product::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $product->expects($this->once())
+ ->method('getData')
+ ->with($this->identicalTo($attributeCode))
+ ->willReturn(null);
+ $mediaAttribute = $this->getMediaAttribute($label, $attributeCode);
+ $product->expects($this->once())
+ ->method('getMediaAttributes')
+ ->willReturn([$mediaAttribute]);
+ $this->galleryMock->expects($this->exactly(2))
+ ->method('getDataObject')
+ ->willReturn($product);
+ $this->galleryMock->expects($this->once())
+ ->method('getImageValue')
+ ->with($this->identicalTo($attributeCode))
+ ->willReturn($value);
+ $this->galleryMock->expects($this->once())
+ ->method('getScopeLabel')
+ ->with($this->identicalTo($mediaAttribute))
+ ->willReturn($scopeLabel);
+ $this->galleryMock->expects($this->once())
+ ->method('getAttributeFieldName')
+ ->with($this->identicalTo($mediaAttribute))
+ ->willReturn($name);
+ $this->getImageTypesAssertions($attributeCode, $scopeLabel, $expectedTypes);
+ }
+
+ /**
+ * Test GetImageTypes() will return value for given attribute from product.
+ *
+ * @return void
+ */
+ public function testGetImageTypesFromProduct()
+ {
+ $attributeCode = 'thumbnail';
+ $value = 'testImageValue';
+ $scopeLabel = 'testScopeLabel';
+ $label = 'testLabel';
+ $name = 'testName';
+ $expectedTypes = [
+ $attributeCode => [
+ 'code' => $attributeCode,
+ 'value' => $value,
+ 'label' => $label,
+ 'name' => $name,
+ ],
+ ];
+ $product = $this->getMockBuilder(Product::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $product->expects($this->once())
+ ->method('getData')
+ ->with($this->identicalTo($attributeCode))
+ ->willReturn($value);
+ $mediaAttribute = $this->getMediaAttribute($label, $attributeCode);
+ $product->expects($this->once())
+ ->method('getMediaAttributes')
+ ->willReturn([$mediaAttribute]);
+ $this->galleryMock->expects($this->exactly(2))
+ ->method('getDataObject')
+ ->willReturn($product);
+ $this->galleryMock->expects($this->never())
+ ->method('getImageValue');
+ $this->galleryMock->expects($this->once())
+ ->method('getScopeLabel')
+ ->with($this->identicalTo($mediaAttribute))
+ ->willReturn($scopeLabel);
+ $this->galleryMock->expects($this->once())
+ ->method('getAttributeFieldName')
+ ->with($this->identicalTo($mediaAttribute))
+ ->willReturn($name);
+ $this->getImageTypesAssertions($attributeCode, $scopeLabel, $expectedTypes);
+ }
+
+ /**
+ * Perform assertions.
+ *
+ * @param string $attributeCode
+ * @param string $scopeLabel
+ * @param array $expectedTypes
+ * @return void
+ */
+ private function getImageTypesAssertions(string $attributeCode, string $scopeLabel, array $expectedTypes)
+ {
+ $this->content->setElement($this->galleryMock);
+ $result = $this->content->getImageTypes();
+ $scope = $result[$attributeCode]['scope'];
+ $this->assertSame($scopeLabel, $scope->getText());
+ unset($result[$attributeCode]['scope']);
+ $this->assertSame($expectedTypes, $result);
+ }
+
+ /**
+ * Get media attribute mock.
+ *
+ * @param string $label
+ * @param string $attributeCode
+ * @return \PHPUnit_Framework_MockObject_MockObject
+ */
+ private function getMediaAttribute(string $label, string $attributeCode)
+ {
+ $frontend = $this->getMockBuilder(Product\Attribute\Frontend\Image::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $frontend->expects($this->once())
+ ->method('getLabel')
+ ->willReturn($label);
+ $mediaAttribute = $this->getMockBuilder(Attribute::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $mediaAttribute->expects($this->any())
+ ->method('getAttributeCode')
+ ->willReturn($attributeCode);
+ $mediaAttribute->expects($this->once())
+ ->method('getFrontend')
+ ->willReturn($frontend);
+
+ return $mediaAttribute;
+ }
}
diff --git a/app/code/Magento/Catalog/Test/Unit/Block/Adminhtml/Product/Helper/Form/GalleryTest.php b/app/code/Magento/Catalog/Test/Unit/Block/Adminhtml/Product/Helper/Form/GalleryTest.php
index 06e2368f3080e..1e04680676eb2 100644
--- a/app/code/Magento/Catalog/Test/Unit/Block/Adminhtml/Product/Helper/Form/GalleryTest.php
+++ b/app/code/Magento/Catalog/Test/Unit/Block/Adminhtml/Product/Helper/Form/GalleryTest.php
@@ -5,6 +5,8 @@
*/
namespace Magento\Catalog\Test\Unit\Block\Adminhtml\Product\Helper\Form;
+use Magento\Framework\App\Request\DataPersistorInterface;
+
class GalleryTest extends \PHPUnit\Framework\TestCase
{
/**
@@ -32,18 +34,27 @@ class GalleryTest extends \PHPUnit\Framework\TestCase
*/
protected $objectManager;
+ /**
+ * @var DataPersistorInterface|\PHPUnit_Framework_MockObject_MockObject
+ */
+ private $dataPersistorMock;
+
public function setUp()
{
$this->registryMock = $this->createMock(\Magento\Framework\Registry::class);
$this->productMock = $this->createPartialMock(\Magento\Catalog\Model\Product::class, ['getData']);
$this->formMock = $this->createMock(\Magento\Framework\Data\Form::class);
-
+ $this->dataPersistorMock = $this->getMockBuilder(DataPersistorInterface::class)
+ ->disableOriginalConstructor()
+ ->setMethods(['get'])
+ ->getMockForAbstractClass();
$this->objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this);
$this->gallery = $this->objectManager->getObject(
\Magento\Catalog\Block\Adminhtml\Product\Helper\Form\Gallery::class,
[
'registry' => $this->registryMock,
- 'form' => $this->formMock
+ 'form' => $this->formMock,
+ 'dataPersistor' => $this->dataPersistorMock
]
);
}
@@ -70,6 +81,68 @@ public function testGetImages()
$this->assertSame($mediaGallery, $this->gallery->getImages());
}
+ /**
+ * Test getImages() will try get data from data persistor, if it's absent in registry.
+ *
+ * @return void
+ */
+ public function testGetImagesWithDataPersistor()
+ {
+ $product = [
+ 'product' => [
+ 'media_gallery' => [
+ 'images' => [
+ [
+ 'value_id' => '1',
+ 'file' => 'image_1.jpg',
+ 'media_type' => 'image',
+ ],
+ [
+ 'value_id' => '2',
+ 'file' => 'image_2.jpg',
+ 'media_type' => 'image',
+ ],
+ ],
+ ],
+ ],
+ ];
+ $this->registryMock->expects($this->once())->method('registry')->willReturn($this->productMock);
+ $this->productMock->expects($this->once())->method('getData')->willReturn(null);
+ $this->dataPersistorMock->expects($this->once())
+ ->method('get')
+ ->with($this->identicalTo('catalog_product'))
+ ->willReturn($product);
+
+ $this->assertSame($product['product']['media_gallery'], $this->gallery->getImages());
+ }
+
+ /**
+ * Test get image value from data persistor in case it's absent in product from registry.
+ *
+ * @return void
+ */
+ public function testGetImageValue()
+ {
+ $product = [
+ 'product' => [
+ 'media_gallery' => [
+ 'images' => [
+ 'value_id' => '1',
+ 'file' => 'image_1.jpg',
+ 'media_type' => 'image',
+ ],
+ ],
+ 'small' => 'testSmallImage',
+ 'thumbnail' => 'testThumbnail'
+ ]
+ ];
+ $this->dataPersistorMock->expects($this->once())
+ ->method('get')
+ ->with($this->identicalTo('catalog_product'))
+ ->willReturn($product);
+ $this->assertSame($product['product']['small'], $this->gallery->getImageValue('small'));
+ }
+
public function testGetDataObject()
{
$this->registryMock->expects($this->once())->method('registry')->willReturn($this->productMock);
diff --git a/app/code/Magento/Catalog/Test/Unit/Block/Category/Plugin/PriceBoxTagsTest.php b/app/code/Magento/Catalog/Test/Unit/Block/Category/Plugin/PriceBoxTagsTest.php
index 57e94b59b330f..3f388d00eaf9f 100644
--- a/app/code/Magento/Catalog/Test/Unit/Block/Category/Plugin/PriceBoxTagsTest.php
+++ b/app/code/Magento/Catalog/Test/Unit/Block/Category/Plugin/PriceBoxTagsTest.php
@@ -16,6 +16,11 @@ class PriceBoxTagsTest extends \PHPUnit\Framework\TestCase
*/
private $priceCurrencyInterface;
+ /**
+ * @var \Magento\Directory\Model\Currency | \PHPUnit_Framework_MockObject_MockObject
+ */
+ private $currency;
+
/**
* @var \Magento\Framework\Stdlib\DateTime\TimezoneInterface | \PHPUnit_Framework_MockObject_MockObject
*/
@@ -46,6 +51,9 @@ protected function setUp()
$this->priceCurrencyInterface = $this->getMockBuilder(
\Magento\Framework\Pricing\PriceCurrencyInterface::class
)->getMock();
+ $this->currency = $this->getMockBuilder(\Magento\Directory\Model\Currency::class)
+ ->disableOriginalConstructor()
+ ->getMock();
$this->timezoneInterface = $this->getMockBuilder(
\Magento\Framework\Stdlib\DateTime\TimezoneInterface::class
)->getMock();
@@ -82,7 +90,7 @@ protected function setUp()
public function testAfterGetCacheKey()
{
$date = date('Ymd');
- $currencySymbol = '$';
+ $currencyCode = 'USD';
$result = 'result_string';
$billingAddress = ['billing_address'];
$shippingAddress = ['shipping_address'];
@@ -95,7 +103,7 @@ public function testAfterGetCacheKey()
'-',
[
$result,
- $currencySymbol,
+ $currencyCode,
$date,
$scopeId,
$customerGroupId,
@@ -104,7 +112,8 @@ public function testAfterGetCacheKey()
);
$priceBox = $this->getMockBuilder(\Magento\Framework\Pricing\Render\PriceBox::class)
->disableOriginalConstructor()->getMock();
- $this->priceCurrencyInterface->expects($this->once())->method('getCurrencySymbol')->willReturn($currencySymbol);
+ $this->priceCurrencyInterface->expects($this->once())->method('getCurrency')->willReturn($this->currency);
+ $this->currency->expects($this->once())->method('getCode')->willReturn($currencyCode);
$scope = $this->getMockBuilder(\Magento\Framework\App\ScopeInterface::class)->getMock();
$this->scopeResolverInterface->expects($this->any())->method('getScope')->willReturn($scope);
$scope->expects($this->any())->method('getId')->willReturn($scopeId);
@@ -119,8 +128,8 @@ public function testAfterGetCacheKey()
$this->session->expects($this->once())->method('getCustomerId')->willReturn($customerId);
$rateRequest = $this->getMockBuilder(\Magento\Framework\DataObject::class)->getMock();
$this->taxCalculation->expects($this->once())->method('getRateRequest')->with(
- new \Magento\Framework\DataObject($billingAddress),
new \Magento\Framework\DataObject($shippingAddress),
+ new \Magento\Framework\DataObject($billingAddress),
$customerTaxClassId,
$scopeId,
$customerId
diff --git a/app/code/Magento/Catalog/Test/Unit/Block/Category/Rss/LinkTest.php b/app/code/Magento/Catalog/Test/Unit/Block/Category/Rss/LinkTest.php
index 8932d77a81247..0cff8b2d0f207 100644
--- a/app/code/Magento/Catalog/Test/Unit/Block/Category/Rss/LinkTest.php
+++ b/app/code/Magento/Catalog/Test/Unit/Block/Category/Rss/LinkTest.php
@@ -72,6 +72,9 @@ public function testIsRssAllowed($isAllowed)
$this->assertEquals($isAllowed, $this->link->isRssAllowed());
}
+ /**
+ * @return array
+ */
public function isRssAllowedDataProvider()
{
return [
@@ -98,6 +101,9 @@ public function testIsTopCategory($isTop, $categoryLevel)
$this->assertEquals($isTop, $this->link->isTopCategory());
}
+ /**
+ * @return array
+ */
public function isTopCategoryDataProvider()
{
return [
diff --git a/app/code/Magento/Catalog/Test/Unit/Block/Product/ImageBuilderTest.php b/app/code/Magento/Catalog/Test/Unit/Block/Product/ImageBuilderTest.php
index e0b5d6ef3992a..8612858960c8c 100644
--- a/app/code/Magento/Catalog/Test/Unit/Block/Product/ImageBuilderTest.php
+++ b/app/code/Magento/Catalog/Test/Unit/Block/Product/ImageBuilderTest.php
@@ -5,49 +5,43 @@
*/
namespace Magento\Catalog\Test\Unit\Block\Product;
+use Magento\Catalog\Block\Product\ImageBuilder;
+use Magento\Catalog\Block\Product\ImageFactory;
+use Magento\Catalog\Helper\Image;
+use Magento\Catalog\Model\Product;
+
class ImageBuilderTest extends \PHPUnit\Framework\TestCase
{
/**
- * @var \Magento\Catalog\Block\Product\ImageBuilder
+ * @var ImageBuilder
*/
- protected $model;
+ private $model;
/**
* @var \Magento\Catalog\Helper\ImageFactory|\PHPUnit_Framework_MockObject_MockObject
*/
- protected $helperFactory;
+ private $helperFactory;
/**
- * @var \Magento\Catalog\Block\Product\ImageFactory|\PHPUnit_Framework_MockObject_MockObject
+ * @var ImageFactory|\PHPUnit_Framework_MockObject_MockObject
*/
- protected $imageFactory;
+ private $imageFactory;
protected function setUp()
{
- $this->helperFactory = $this->getMockBuilder(\Magento\Catalog\Helper\ImageFactory::class)
- ->disableOriginalConstructor()
- ->setMethods(['create'])
- ->getMock();
-
- $this->imageFactory = $this->getMockBuilder(\Magento\Catalog\Block\Product\ImageFactory::class)
- ->disableOriginalConstructor()
- ->setMethods(['create'])
- ->getMock();
-
- $this->model = new \Magento\Catalog\Block\Product\ImageBuilder(
- $this->helperFactory,
- $this->imageFactory
- );
+ $this->helperFactory = $this->createPartialMock(\Magento\Catalog\Helper\ImageFactory::class, ['create']);
+
+ $this->imageFactory = $this->createPartialMock(ImageFactory::class, ['create']);
+
+ $this->model = new ImageBuilder($this->helperFactory, $this->imageFactory);
}
public function testSetProduct()
{
- $productMock = $this->getMockBuilder(\Magento\Catalog\Model\Product::class)
- ->disableOriginalConstructor()
- ->getMock();
+ $productMock = $this->createMock(Product::class);
$this->assertInstanceOf(
- \Magento\Catalog\Block\Product\ImageBuilder::class,
+ ImageBuilder::class,
$this->model->setProduct($productMock)
);
}
@@ -57,7 +51,7 @@ public function testSetImageId()
$imageId = 'test_image_id';
$this->assertInstanceOf(
- \Magento\Catalog\Block\Product\ImageBuilder::class,
+ ImageBuilder::class,
$this->model->setImageId($imageId)
);
}
@@ -68,7 +62,7 @@ public function testSetAttributes()
'name' => 'value',
];
$this->assertInstanceOf(
- \Magento\Catalog\Block\Product\ImageBuilder::class,
+ ImageBuilder::class,
$this->model->setAttributes($attributes)
);
}
@@ -81,13 +75,9 @@ public function testCreate($data, $expected)
{
$imageId = 'test_image_id';
- $productMock = $this->getMockBuilder(\Magento\Catalog\Model\Product::class)
- ->disableOriginalConstructor()
- ->getMock();
+ $productMock = $this->createMock(Product::class);
- $helperMock = $this->getMockBuilder(\Magento\Catalog\Helper\Image::class)
- ->disableOriginalConstructor()
- ->getMock();
+ $helperMock = $this->createMock(Image::class);
$helperMock->expects($this->once())
->method('init')
->with($productMock, $imageId)
@@ -116,9 +106,7 @@ public function testCreate($data, $expected)
->method('create')
->willReturn($helperMock);
- $imageMock = $this->getMockBuilder(\Magento\Catalog\Block\Product\Image::class)
- ->disableOriginalConstructor()
- ->getMock();
+ $imageMock = $this->createMock(\Magento\Catalog\Block\Product\Image::class);
$this->imageFactory->expects($this->once())
->method('create')
@@ -131,63 +119,231 @@ public function testCreate($data, $expected)
$this->assertInstanceOf(\Magento\Catalog\Block\Product\Image::class, $this->model->create());
}
+ /**
+ * Check if custom attributes will be overridden when builder used few times
+ * @param array $data
+ * @dataProvider createMultipleCallsDataProvider
+ */
+ public function testCreateMultipleCalls($data)
+ {
+ list ($firstCall, $secondCall) = array_values($data);
+
+ $imageId = 'test_image_id';
+
+ $productMock = $this->createMock(Product::class);
+
+ $helperMock = $this->createMock(Image::class);
+ $helperMock->expects($this->exactly(2))
+ ->method('init')
+ ->with($productMock, $imageId)
+ ->willReturnSelf();
+
+ $helperMock->expects($this->exactly(2))
+ ->method('getFrame')
+ ->willReturnOnConsecutiveCalls($firstCall['data']['frame'], $secondCall['data']['frame']);
+ $helperMock->expects($this->exactly(2))
+ ->method('getUrl')
+ ->willReturnOnConsecutiveCalls($firstCall['data']['url'], $secondCall['data']['url']);
+ $helperMock->expects($this->exactly(4))
+ ->method('getWidth')
+ ->willReturnOnConsecutiveCalls(
+ $firstCall['data']['width'],
+ $firstCall['data']['width'],
+ $secondCall['data']['width'],
+ $secondCall['data']['width']
+ );
+ $helperMock->expects($this->exactly(4))
+ ->method('getHeight')
+ ->willReturnOnConsecutiveCalls(
+ $firstCall['data']['height'],
+ $firstCall['data']['height'],
+ $secondCall['data']['height'],
+ $secondCall['data']['height']
+ );
+ $helperMock->expects($this->exactly(2))
+ ->method('getLabel')
+ ->willReturnOnConsecutiveCalls($firstCall['data']['label'], $secondCall['data']['label']);
+ $helperMock->expects($this->exactly(2))
+ ->method('getResizedImageInfo')
+ ->willReturnOnConsecutiveCalls($firstCall['data']['imagesize'], $secondCall['data']['imagesize']);
+ $this->helperFactory->expects($this->exactly(2))
+ ->method('create')
+ ->willReturn($helperMock);
+
+ $imageMock = $this->createMock(\Magento\Catalog\Block\Product\Image::class);
+
+ $this->imageFactory->expects($this->at(0))
+ ->method('create')
+ ->with($firstCall['expected'])
+ ->willReturn($imageMock);
+
+ $this->imageFactory->expects($this->at(1))
+ ->method('create')
+ ->with($secondCall['expected'])
+ ->willReturn($imageMock);
+
+ $this->model->setProduct($productMock);
+ $this->model->setImageId($imageId);
+ $this->model->setAttributes($firstCall['data']['custom_attributes']);
+
+ $this->assertInstanceOf(\Magento\Catalog\Block\Product\Image::class, $this->model->create());
+
+ $this->model->setProduct($productMock);
+ $this->model->setImageId($imageId);
+ $this->model->setAttributes($secondCall['data']['custom_attributes']);
+ $this->assertInstanceOf(\Magento\Catalog\Block\Product\Image::class, $this->model->create());
+ }
+
+ /**
+ * @return array
+ */
+ public function createDataProvider(): array
+ {
+ return [
+ $this->getTestDataWithoutAttributes(),
+ $this->getTestDataWithAttributes(),
+ ];
+ }
+
/**
* @return array
*/
- public function createDataProvider()
+ public function createMultipleCallsDataProvider(): array
{
return [
[
+ [
+ 'without_attributes' => $this->getTestDataWithoutAttributes(),
+ 'with_attributes' => $this->getTestDataWithAttributes(),
+ ],
+ ],
+ [
+ [
+ 'with_attributes' => $this->getTestDataWithAttributes(),
+ 'without_attributes' => $this->getTestDataWithoutAttributes(),
+ ],
+ ],
+ ];
+ }
+
+ /**
+ * @return array
+ */
+ private function getTestDataWithoutAttributes(): array
+ {
+ return [
+ 'data' => [
+ 'frame' => 0,
+ 'url' => 'test_url_1',
+ 'width' => 100,
+ 'height' => 100,
+ 'label' => 'test_label',
+ 'custom_attributes' => [],
+ 'imagesize' => [100, 100],
+ ],
+ 'expected' => [
'data' => [
- 'frame' => 0,
- 'url' => 'test_url_1',
+ 'template' => 'Magento_Catalog::product/image_with_borders.phtml',
+ 'image_url' => 'test_url_1',
'width' => 100,
'height' => 100,
'label' => 'test_label',
- 'custom_attributes' => [],
- 'imagesize' => [100, 100],
+ 'ratio' => 1,
+ 'custom_attributes' => '',
+ 'resized_image_width' => 100,
+ 'resized_image_height' => 100,
+ 'product_id' => null
],
- 'expected' => [
- 'data' => [
- 'template' => 'Magento_Catalog::product/image_with_borders.phtml',
- 'image_url' => 'test_url_1',
- 'width' => 100,
- 'height' => 100,
- 'label' => 'test_label',
- 'ratio' => 1,
- 'custom_attributes' => '',
- 'resized_image_width' => 100,
- 'resized_image_height' => 100,
- ],
+ ],
+ ];
+ }
+
+ /**
+ * @return array
+ */
+ private function getTestDataWithAttributes(): array
+ {
+ return [
+ 'data' => [
+ 'frame' => 1,
+ 'url' => 'test_url_2',
+ 'width' => 100,
+ 'height' => 50,
+ 'label' => 'test_label_2',
+ 'custom_attributes' => [
+ 'name_1' => 'value_1',
+ 'name_2' => 'value_2',
],
+ 'imagesize' => [120, 70],
],
- [
+ 'expected' => [
'data' => [
- 'frame' => 1,
- 'url' => 'test_url_2',
+ 'template' => 'Magento_Catalog::product/image.phtml',
+ 'image_url' => 'test_url_2',
'width' => 100,
'height' => 50,
'label' => 'test_label_2',
- 'custom_attributes' => [
- 'name_1' => 'value_1',
- 'name_2' => 'value_2',
- ],
- 'imagesize' => [120, 70],
- ],
- 'expected' => [
- 'data' => [
- 'template' => 'Magento_Catalog::product/image.phtml',
- 'image_url' => 'test_url_2',
- 'width' => 100,
- 'height' => 50,
- 'label' => 'test_label_2',
- 'ratio' => 0.5,
- 'custom_attributes' => 'name_1="value_1" name_2="value_2"',
- 'resized_image_width' => 120,
- 'resized_image_height' => 70,
- ],
+ 'ratio' => 0.5,
+ 'custom_attributes' => 'name_1="value_1" name_2="value_2"',
+ 'resized_image_width' => 120,
+ 'resized_image_height' => 70,
+ 'product_id' => null
],
],
];
}
+
+ /**
+ * @param array $data
+ * @param array $expected
+ * @dataProvider createDataProvider
+ */
+ public function testCreateWithSimpleProduct($data, $expected)
+ {
+ $imageId = 'test_image_id';
+
+ $productMock = $this->createMock(\Magento\Catalog\Model\Product::class);
+ $simpleProductMock = $this->createMock(\Magento\Catalog\Model\Product::class);
+
+ $helperMock = $this->createMock(\Magento\Catalog\Helper\Image::class);
+ $helperMock->expects($this->once())
+ ->method('init')
+ ->with($simpleProductMock, $imageId)
+ ->willReturnSelf();
+ $helperMock->expects($this->once())
+ ->method('getFrame')
+ ->willReturn($data['frame']);
+ $helperMock->expects($this->once())
+ ->method('getUrl')
+ ->willReturn($data['url']);
+ $helperMock->expects($this->exactly(2))
+ ->method('getWidth')
+ ->willReturn($data['width']);
+ $helperMock->expects($this->exactly(2))
+ ->method('getHeight')
+ ->willReturn($data['height']);
+ $helperMock->expects($this->once())
+ ->method('getLabel')
+ ->willReturn($data['label']);
+ $helperMock->expects($this->once())
+ ->method('getResizedImageInfo')
+ ->willReturn($data['imagesize']);
+
+ $this->helperFactory->expects($this->once())
+ ->method('create')
+ ->willReturn($helperMock);
+
+ $imageMock = $this->createMock(\Magento\Catalog\Block\Product\Image::class);
+
+ $this->imageFactory->expects($this->once())
+ ->method('create')
+ ->with($expected)
+ ->willReturn($imageMock);
+
+ $this->model->setProduct($productMock);
+ $this->model->setImageId($imageId);
+ $this->model->setAttributes($data['custom_attributes']);
+
+ $this->assertInstanceOf(\Magento\Catalog\Block\Product\Image::class, $this->model->create());
+ }
}
diff --git a/app/code/Magento/Catalog/Test/Unit/Block/Product/ListProductTest.php b/app/code/Magento/Catalog/Test/Unit/Block/Product/ListProductTest.php
index b42357db89041..fe07f69e8046f 100644
--- a/app/code/Magento/Catalog/Test/Unit/Block/Product/ListProductTest.php
+++ b/app/code/Magento/Catalog/Test/Unit/Block/Product/ListProductTest.php
@@ -192,7 +192,7 @@ public function testGetIdentities()
->will($this->returnValue($this->toolbarMock));
$this->assertEquals(
- [$productTag, $categoryTag],
+ [$categoryTag, $productTag],
$this->block->getIdentities()
);
$this->assertEquals(
diff --git a/app/code/Magento/Catalog/Test/Unit/Block/Product/ProductList/RelatedTest.php b/app/code/Magento/Catalog/Test/Unit/Block/Product/ProductList/RelatedTest.php
index 1d927a6e04ef5..deb84b7b2d3c4 100644
--- a/app/code/Magento/Catalog/Test/Unit/Block/Product/ProductList/RelatedTest.php
+++ b/app/code/Magento/Catalog/Test/Unit/Block/Product/ProductList/RelatedTest.php
@@ -72,6 +72,9 @@ public function testCanItemsAddToCart($isComposite, $isSaleable, $hasRequiredOpt
);
}
+ /**
+ * @return array
+ */
public function canItemsAddToCartDataProvider()
{
return [
diff --git a/app/code/Magento/Catalog/Test/Unit/Block/Product/ProductList/ToolbarTest.php b/app/code/Magento/Catalog/Test/Unit/Block/Product/ProductList/ToolbarTest.php
index dce0be8e62df3..ac963326dbfa1 100644
--- a/app/code/Magento/Catalog/Test/Unit/Block/Product/ProductList/ToolbarTest.php
+++ b/app/code/Magento/Catalog/Test/Unit/Block/Product/ProductList/ToolbarTest.php
@@ -216,6 +216,9 @@ public function testSetModes($mode, $expected)
$this->assertEquals($expected, $block->getModes());
}
+ /**
+ * @return array
+ */
public function setModesDataProvider()
{
return [
diff --git a/app/code/Magento/Catalog/Test/Unit/Block/Product/View/AttributesTest.php b/app/code/Magento/Catalog/Test/Unit/Block/Product/View/AttributesTest.php
new file mode 100644
index 0000000000000..4602a0d99f6f1
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Unit/Block/Product/View/AttributesTest.php
@@ -0,0 +1,156 @@
+attribute = $this
+ ->getMockBuilder(AbstractAttribute::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->attribute
+ ->expects($this->any())
+ ->method('getIsVisibleOnFront')
+ ->willReturn(true);
+ $this->attribute
+ ->expects($this->any())
+ ->method('getAttributeCode')
+ ->willReturn('phrase');
+ $this->frontendAttribute = $this
+ ->getMockBuilder(AbstractFrontend::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->attribute
+ ->expects($this->any())
+ ->method('getFrontendInput')
+ ->willReturn('phrase');
+ $this->attribute
+ ->expects($this->any())
+ ->method('getFrontend')
+ ->willReturn($this->frontendAttribute);
+ $this->product = $this
+ ->getMockBuilder(Product::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->product
+ ->expects($this->any())
+ ->method('getAttributes')
+ ->willReturn([$this->attribute]);
+ $this->product
+ ->expects($this->any())
+ ->method('hasData')
+ ->willReturn(true);
+ $this->context = $this
+ ->getMockBuilder(Context::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->registry = $this
+ ->getMockBuilder(Registry::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->registry
+ ->expects($this->any())
+ ->method('registry')
+ ->willReturn($this->product);
+ $this->priceCurrencyInterface = $this
+ ->getMockBuilder(PriceCurrencyInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->attributesBlock = new AttributesBlock(
+ $this->context,
+ $this->registry,
+ $this->priceCurrencyInterface
+ );
+ }
+
+ /**
+ * @return void
+ */
+ public function testGetAttributeNoValue()
+ {
+ $this->phrase = '';
+ $this->frontendAttribute
+ ->expects($this->any())
+ ->method('getValue')
+ ->willReturn($this->phrase);
+ $attributes = $this->attributesBlock->getAdditionalData();
+ $this->assertTrue(empty($attributes['phrase']));
+ }
+
+ /**
+ * @return void
+ */
+ public function testGetAttributeHasValue()
+ {
+ $this->phrase = __('Yes');
+ $this->frontendAttribute
+ ->expects($this->any())
+ ->method('getValue')
+ ->willReturn($this->phrase);
+ $attributes = $this->attributesBlock->getAdditionalData();
+ $this->assertNotTrue(empty($attributes['phrase']));
+ $this->assertNotTrue(empty($attributes['phrase']['value']));
+ $this->assertEquals('Yes', $attributes['phrase']['value']);
+ }
+}
diff --git a/app/code/Magento/Catalog/Test/Unit/Block/Product/View/GalleryTest.php b/app/code/Magento/Catalog/Test/Unit/Block/Product/View/GalleryTest.php
index ec7779fcbb781..ae5176e78df7b 100644
--- a/app/code/Magento/Catalog/Test/Unit/Block/Product/View/GalleryTest.php
+++ b/app/code/Magento/Catalog/Test/Unit/Block/Product/View/GalleryTest.php
@@ -77,6 +77,88 @@ protected function mockContext()
->willReturn($this->registry);
}
+ public function testGetGalleryImagesJsonWithLabel()
+ {
+ $this->prepareGetGalleryImagesJsonMocks();
+ $json = $this->model->getGalleryImagesJson();
+ $decodedJson = json_decode($json, true);
+ $this->assertEquals('product_page_image_small_url', $decodedJson[0]['thumb']);
+ $this->assertEquals('product_page_image_medium_url', $decodedJson[0]['img']);
+ $this->assertEquals('product_page_image_large_url', $decodedJson[0]['full']);
+ $this->assertEquals('test_label', $decodedJson[0]['caption']);
+ $this->assertEquals('2', $decodedJson[0]['position']);
+ $this->assertEquals(false, $decodedJson[0]['isMain']);
+ $this->assertEquals('test_media_type', $decodedJson[0]['type']);
+ $this->assertEquals('test_video_url', $decodedJson[0]['videoUrl']);
+ }
+
+ public function testGetGalleryImagesJsonWithoutLabel()
+ {
+ $this->prepareGetGalleryImagesJsonMocks(false);
+ $json = $this->model->getGalleryImagesJson();
+ $decodedJson = json_decode($json, true);
+ $this->assertEquals('test_product_name', $decodedJson[0]['caption']);
+ }
+
+ /**
+ * @param bool $hasLabel
+ */
+ private function prepareGetGalleryImagesJsonMocks($hasLabel = true)
+ {
+ $storeMock = $this->getMockBuilder(\Magento\Store\Model\Store::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $productMock = $this->getMockBuilder(\Magento\Catalog\Model\Product::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $productTypeMock = $this->getMockBuilder(\Magento\Catalog\Model\Product\Type\AbstractType::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $productTypeMock->expects($this->any())
+ ->method('getStoreFilter')
+ ->with($productMock)
+ ->willReturn($storeMock);
+
+ $productMock->expects($this->any())
+ ->method('getTypeInstance')
+ ->willReturn($productTypeMock);
+ $productMock->expects($this->any())
+ ->method('getMediaGalleryImages')
+ ->willReturn($this->getImagesCollectionWithPopulatedDataObject($hasLabel));
+ $productMock->expects($this->any())
+ ->method('getName')
+ ->willReturn('test_product_name');
+
+ $this->registry->expects($this->any())
+ ->method('registry')
+ ->with('product')
+ ->willReturn($productMock);
+
+ $this->imageHelper->expects($this->any())
+ ->method('init')
+ ->willReturnMap([
+ [$productMock, 'product_page_image_small', [], $this->imageHelper],
+ [$productMock, 'product_page_image_medium_no_frame', [], $this->imageHelper],
+ [$productMock, 'product_page_image_large_no_frame', [], $this->imageHelper],
+ ])
+ ->willReturnSelf();
+ $this->imageHelper->expects($this->any())
+ ->method('setImageFile')
+ ->with('test_file')
+ ->willReturnSelf();
+ $this->imageHelper->expects($this->at(2))
+ ->method('getUrl')
+ ->willReturn('product_page_image_small_url');
+ $this->imageHelper->expects($this->at(5))
+ ->method('getUrl')
+ ->willReturn('product_page_image_medium_url');
+ $this->imageHelper->expects($this->at(8))
+ ->method('getUrl')
+ ->willReturn('product_page_image_large_url');
+ }
+
public function testGetGalleryImages()
{
$storeMock = $this->getMockBuilder(\Magento\Store\Model\Store::class)
@@ -154,4 +236,30 @@ private function getImagesCollection()
return $collectionMock;
}
+
+ /**
+ * @return \Magento\Framework\Data\Collection
+ */
+ private function getImagesCollectionWithPopulatedDataObject($hasLabel)
+ {
+ $collectionMock = $this->getMockBuilder(\Magento\Framework\Data\Collection::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $items = [
+ new \Magento\Framework\DataObject([
+ 'file' => 'test_file',
+ 'label' => ($hasLabel ? 'test_label' : ''),
+ 'position' => '2',
+ 'media_type' => 'external-test_media_type',
+ "video_url" => 'test_video_url'
+ ]),
+ ];
+
+ $collectionMock->expects($this->any())
+ ->method('getIterator')
+ ->willReturn(new \ArrayIterator($items));
+
+ return $collectionMock;
+ }
}
diff --git a/app/code/Magento/Catalog/Test/Unit/Block/Rss/Product/NewProductsTest.php b/app/code/Magento/Catalog/Test/Unit/Block/Rss/Product/NewProductsTest.php
index e2e0fa2f27667..129dea37b185e 100644
--- a/app/code/Magento/Catalog/Test/Unit/Block/Rss/Product/NewProductsTest.php
+++ b/app/code/Magento/Catalog/Test/Unit/Block/Rss/Product/NewProductsTest.php
@@ -91,6 +91,9 @@ protected function setUp()
);
}
+ /**
+ * @return array
+ */
public function isAllowedDataProvider()
{
return [
@@ -108,6 +111,9 @@ public function testIsAllowed($configValue, $expectedResult)
$this->assertEquals($expectedResult, $this->block->isAllowed());
}
+ /**
+ * @return \PHPUnit_Framework_MockObject_MockObject
+ */
protected function getItemMock()
{
$methods = [
diff --git a/app/code/Magento/Catalog/Test/Unit/Block/Rss/Product/SpecialTest.php b/app/code/Magento/Catalog/Test/Unit/Block/Rss/Product/SpecialTest.php
index 6509aa138802e..3c9f19d61d16a 100644
--- a/app/code/Magento/Catalog/Test/Unit/Block/Rss/Product/SpecialTest.php
+++ b/app/code/Magento/Catalog/Test/Unit/Block/Rss/Product/SpecialTest.php
@@ -167,6 +167,9 @@ public function testGetRssData()
);
}
+ /**
+ * @return \PHPUnit_Framework_MockObject_MockObject
+ */
protected function getItemMock()
{
$item = $this->getMockBuilder(\Magento\Catalog\Model\Product::class)
diff --git a/app/code/Magento/Catalog/Test/Unit/Console/Command/ImagesResizeCommandTest.php b/app/code/Magento/Catalog/Test/Unit/Console/Command/ImagesResizeCommandTest.php
deleted file mode 100644
index 457ba7b94529b..0000000000000
--- a/app/code/Magento/Catalog/Test/Unit/Console/Command/ImagesResizeCommandTest.php
+++ /dev/null
@@ -1,211 +0,0 @@
-appState = $this->getMockBuilder(\Magento\Framework\App\State::class)
- ->disableOriginalConstructor()
- ->getMock();
-
- $this->productRepository = $this->getMockBuilder(\Magento\Catalog\Api\ProductRepositoryInterface::class)
- ->getMockForAbstractClass();
-
- $this->prepareProductCollection();
- $this->prepareImageCache();
-
- $this->command = new ImagesResizeCommand(
- $this->appState,
- $this->productCollectionFactory,
- $this->productRepository,
- $this->imageCacheFactory
- );
- }
-
- public function testExecuteNoProducts()
- {
- $this->appState->expects($this->once())
- ->method('setAreaCode')
- ->with(Area::AREA_GLOBAL)
- ->willReturnSelf();
-
- $this->productCollection->expects($this->once())
- ->method('getAllIds')
- ->willReturn([]);
-
- $commandTester = new CommandTester($this->command);
- $commandTester->execute([]);
-
- $this->assertContains(
- 'No product images to resize',
- $commandTester->getDisplay()
- );
- }
-
- public function testExecute()
- {
- $productsIds = [1, 2];
-
- $this->appState->expects($this->once())
- ->method('setAreaCode')
- ->with(Area::AREA_GLOBAL)
- ->willReturnSelf();
-
- $this->productCollection->expects($this->once())
- ->method('getAllIds')
- ->willReturn($productsIds);
-
- $productMock = $this->getMockBuilder(\Magento\Catalog\Model\Product::class)
- ->disableOriginalConstructor()
- ->getMock();
-
- $this->productRepository->expects($this->at(0))
- ->method('getById')
- ->with($productsIds[0])
- ->willReturn($productMock);
- $this->productRepository->expects($this->at(1))
- ->method('getById')
- ->with($productsIds[1])
- ->willThrowException(new NoSuchEntityException());
-
- $this->imageCache->expects($this->exactly(count($productsIds) - 1))
- ->method('generate')
- ->with($productMock)
- ->willReturnSelf();
-
- $commandTester = new CommandTester($this->command);
- $commandTester->execute([]);
-
- $this->assertContains(
- 'Product images resized successfully',
- $commandTester->getDisplay()
- );
- }
-
- public function testExecuteWithException()
- {
- $productsIds = [1];
- $exceptionMessage = 'Test exception text';
-
- $this->appState->expects($this->once())
- ->method('setAreaCode')
- ->with(Area::AREA_GLOBAL)
- ->willReturnSelf();
-
- $this->productCollection->expects($this->once())
- ->method('getAllIds')
- ->willReturn($productsIds);
-
- $productMock = $this->getMockBuilder(\Magento\Catalog\Model\Product::class)
- ->disableOriginalConstructor()
- ->getMock();
-
- $this->productRepository->expects($this->exactly(count($productsIds)))
- ->method('getById')
- ->with($productsIds[0])
- ->willReturn($productMock);
-
- $this->imageCache->expects($this->once())
- ->method('generate')
- ->with($productMock)
- ->willThrowException(new \Exception($exceptionMessage));
-
- $commandTester = new CommandTester($this->command);
- $commandTester->execute([]);
-
- $this->assertContains(
- $exceptionMessage,
- $commandTester->getDisplay()
- );
- }
-
- protected function prepareProductCollection()
- {
- $this->productCollectionFactory = $this->getMockBuilder(
- \Magento\Catalog\Model\ResourceModel\Product\CollectionFactory::class
- )
- ->disableOriginalConstructor()
- ->setMethods(['create'])
- ->getMock();
-
- $this->productCollection = $this->getMockBuilder(
- \Magento\Catalog\Model\ResourceModel\Product\Collection::class
- )
- ->disableOriginalConstructor()
- ->getMock();
-
- $this->productCollectionFactory->expects($this->any())
- ->method('create')
- ->willReturn($this->productCollection);
- }
-
- protected function prepareImageCache()
- {
- $this->imageCacheFactory = $this->getMockBuilder(\Magento\Catalog\Model\Product\Image\CacheFactory::class)
- ->disableOriginalConstructor()
- ->setMethods(['create'])
- ->getMock();
-
- $this->imageCache = $this->getMockBuilder(\Magento\Catalog\Model\Product\Image\Cache::class)
- ->disableOriginalConstructor()
- ->getMock();
-
- $this->imageCacheFactory->expects($this->any())
- ->method('create')
- ->willReturn($this->imageCache);
- }
-}
diff --git a/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Category/DeleteTest.php b/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Category/DeleteTest.php
index af1ded6987196..196b4df5b47c0 100644
--- a/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Category/DeleteTest.php
+++ b/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Category/DeleteTest.php
@@ -71,7 +71,7 @@ protected function setUp()
false,
true,
true,
- ['addSuccess']
+ ['addSuccessMessage']
);
$this->categoryRepository = $this->createMock(\Magento\Catalog\Api\CategoryRepositoryInterface::class);
$context->expects($this->any())
diff --git a/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Category/Image/UploadTest.php b/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Category/Image/UploadTest.php
index 07dacae7298cf..e2cd01fd1c23a 100644
--- a/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Category/Image/UploadTest.php
+++ b/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Category/Image/UploadTest.php
@@ -23,6 +23,9 @@ protected function setUp()
$this->objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this);
}
+ /**
+ * @return array
+ */
public function executeDataProvider()
{
return [
diff --git a/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Category/RefreshPathTest.php b/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Category/RefreshPathTest.php
new file mode 100644
index 0000000000000..4077ecb11f355
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Category/RefreshPathTest.php
@@ -0,0 +1,147 @@
+resultJsonFactoryMock = $this->getMockBuilder(JsonFactory::class)
+ ->disableOriginalConstructor()
+ ->setMethods(['create', 'setData'])
+ ->getMock();
+
+ $this->contextMock = $this->getMockBuilder(Context::class)
+ ->disableOriginalConstructor()
+ ->setMethods(['getRequest'])
+ ->getMock();
+ }
+
+ /**
+ * Sets object non-public property.
+ *
+ * @param mixed $object
+ * @param string $propertyName
+ * @param mixed $value
+ *
+ * @return void
+ */
+ private function setObjectProperty($object, string $propertyName, $value)
+ {
+ $reflectionClass = new \ReflectionClass($object);
+ $reflectionProperty = $reflectionClass->getProperty($propertyName);
+ $reflectionProperty->setAccessible(true);
+ $reflectionProperty->setValue($object, $value);
+ }
+
+ /**
+ * @return void
+ */
+ public function testExecute()
+ {
+ $value = ['id' => 3, 'path' => '1/2/3', 'parentId' => 2];
+ $result = '{"id":3,"path":"1/2/3","parentId":"2"}';
+
+ $requestMock = $this->getMockForAbstractClass(\Magento\Framework\App\RequestInterface::class);
+
+ $refreshPath = $this->getMockBuilder(RefreshPath::class)
+ ->setMethods(['getRequest', 'create'])
+ ->setConstructorArgs([
+ $this->contextMock,
+ $this->resultJsonFactoryMock,
+ ])->getMock();
+
+ $refreshPath->expects($this->any())->method('getRequest')->willReturn($requestMock);
+ $requestMock->expects($this->any())->method('getParam')->with('id')->willReturn($value['id']);
+
+ $categoryMock = $this->getMockBuilder(\Magento\Catalog\Model\Category::class)
+ ->disableOriginalConstructor()
+ ->setMethods(['getPath', 'getParentId', 'getResource'])
+ ->getMock();
+
+ $categoryMock->expects($this->any())->method('getPath')->willReturn($value['path']);
+ $categoryMock->expects($this->any())->method('getParentId')->willReturn($value['parentId']);
+
+ $categoryResource = $this->createMock(\Magento\Catalog\Model\ResourceModel\Category::class);
+
+ $objectManagerMock = $this->getMockBuilder(ObjectManager::class)
+ ->disableOriginalConstructor()
+ ->setMethods(['create'])
+ ->getMock();
+
+ $this->setObjectProperty($refreshPath, '_objectManager', $objectManagerMock);
+ $this->setObjectProperty($categoryMock, '_resource', $categoryResource);
+
+ $objectManagerMock->expects($this->once())
+ ->method('create')
+ ->with(\Magento\Catalog\Model\Category::class)
+ ->willReturn($categoryMock);
+
+ $this->resultJsonFactoryMock->expects($this->any())->method('create')->willReturnSelf();
+ $this->resultJsonFactoryMock->expects($this->any())
+ ->method('setData')
+ ->with($value)
+ ->willReturn($result);
+
+ $this->assertEquals($result, $refreshPath->execute());
+ }
+
+ /**
+ * @return void
+ */
+ public function testExecuteWithoutCategoryId()
+ {
+ $requestMock = $this->getMockForAbstractClass(\Magento\Framework\App\RequestInterface::class);
+
+ $refreshPath = $this->getMockBuilder(RefreshPath::class)
+ ->setMethods(['getRequest', 'create'])
+ ->setConstructorArgs([
+ $this->contextMock,
+ $this->resultJsonFactoryMock,
+ ])->getMock();
+
+ $refreshPath->expects($this->any())->method('getRequest')->willReturn($requestMock);
+ $requestMock->expects($this->any())->method('getParam')->with('id')->willReturn(null);
+
+ $objectManagerMock = $this->getMockBuilder(ObjectManager::class)
+ ->disableOriginalConstructor()
+ ->setMethods(['create'])
+ ->getMock();
+
+ $this->setObjectProperty($refreshPath, '_objectManager', $objectManagerMock);
+
+ $objectManagerMock->expects($this->never())
+ ->method('create')
+ ->with(\Magento\Catalog\Model\Category::class)
+ ->willReturnSelf();
+
+ $refreshPath->execute();
+ }
+}
diff --git a/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Category/SaveTest.php b/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Category/SaveTest.php
index f6a586950afdc..74173dc926d97 100644
--- a/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Category/SaveTest.php
+++ b/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Category/SaveTest.php
@@ -5,8 +5,6 @@
*/
namespace Magento\Catalog\Test\Unit\Controller\Adminhtml\Category;
-use Magento\Catalog\Controller\Adminhtml\Category\Save as Model;
-
/**
* Class SaveTest
* @SuppressWarnings(PHPMD.CouplingBetweenObjects)
@@ -104,7 +102,7 @@ protected function setUp()
false,
true,
true,
- ['addSuccess', 'getMessages']
+ ['addSuccessMessage', 'getMessages']
);
$this->save = $this->objectManager->getObject(
@@ -394,7 +392,7 @@ public function testExecute($categoryId, $storeId, $parentId)
$categoryMock->expects($this->once())
->method('save');
$this->messageManagerMock->expects($this->once())
- ->method('addSuccess')
+ ->method('addSuccessMessage')
->with(__('You saved the category.'));
$categoryMock->expects($this->at(1))
->method('getId')
@@ -463,9 +461,25 @@ public function dataProviderExecute()
*/
public function imagePreprocessingDataProvider()
{
+ $dataWithImage = [
+ 'image' => 'path.jpg',
+ 'name' => 'category',
+ 'description' => '',
+ 'parent' => 0
+ ];
+ $expectedSameAsDataWithImage = $dataWithImage;
+
+ $dataWithoutImage = [
+ 'name' => 'category',
+ 'description' => '',
+ 'parent' => 0
+ ];
+ $expectedIfDataWithoutImage = $dataWithoutImage;
+ $expectedIfDataWithoutImage['image'] = '';
+
return [
- [['attribute1' => null, 'attribute2' => 123]],
- [['attribute2' => 123]]
+ 'categoryPostData contains image' => [$dataWithImage, $expectedSameAsDataWithImage],
+ 'categoryPostData doesn\'t contain image' => [$dataWithoutImage, $expectedIfDataWithoutImage],
];
}
@@ -473,8 +487,9 @@ public function imagePreprocessingDataProvider()
* @dataProvider imagePreprocessingDataProvider
*
* @param array $data
+ * @param array $expected
*/
- public function testImagePreprocessingWithoutValue($data)
+ public function testImagePreprocessing($data, $expected)
{
$eavConfig = $this->createPartialMock(\Magento\Eav\Model\Config::class, ['getEntityType']);
@@ -484,49 +499,17 @@ public function testImagePreprocessingWithoutValue($data)
$collection = new \Magento\Framework\DataObject(['attribute_collection' => [
new \Magento\Framework\DataObject([
- 'attribute_code' => 'attribute1',
+ 'attribute_code' => 'image',
'backend' => $imageBackendModel
]),
new \Magento\Framework\DataObject([
- 'attribute_code' => 'attribute2',
+ 'attribute_code' => 'name',
'backend' => new \Magento\Framework\DataObject()
- ])
- ]]);
-
- $eavConfig->expects($this->once())
- ->method('getEntityType')
- ->with(\Magento\Catalog\Api\Data\CategoryAttributeInterface::ENTITY_TYPE_CODE)
- ->will($this->returnValue($collection));
-
- $model = $this->objectManager->getObject(\Magento\Catalog\Controller\Adminhtml\Category\Save::class, [
- 'eavConfig' => $eavConfig
- ]);
-
- $result = $model->imagePreprocessing($data);
-
- $this->assertEquals([
- 'attribute1' => false,
- 'attribute2' => 123
- ], $result);
- }
-
- public function testImagePreprocessingWithValue()
- {
- $eavConfig = $this->createPartialMock(\Magento\Eav\Model\Config::class, ['getEntityType']);
-
- $imageBackendModel = $this->objectManager->getObject(
- \Magento\Catalog\Model\Category\Attribute\Backend\Image::class
- );
-
- $collection = new \Magento\Framework\DataObject(['attribute_collection' => [
- new \Magento\Framework\DataObject([
- 'attribute_code' => 'attribute1',
- 'backend' => $imageBackendModel
]),
new \Magento\Framework\DataObject([
- 'attribute_code' => 'attribute2',
+ 'attribute_code' => 'level',
'backend' => new \Magento\Framework\DataObject()
- ])
+ ]),
]]);
$eavConfig->expects($this->once())
@@ -534,18 +517,12 @@ public function testImagePreprocessingWithValue()
->with(\Magento\Catalog\Api\Data\CategoryAttributeInterface::ENTITY_TYPE_CODE)
->will($this->returnValue($collection));
- $model = $this->objectManager->getObject(Model::class, [
+ $model = $this->objectManager->getObject(\Magento\Catalog\Controller\Adminhtml\Category\Save::class, [
'eavConfig' => $eavConfig
]);
- $result = $model->imagePreprocessing([
- 'attribute1' => 'somevalue',
- 'attribute2' => null
- ]);
+ $result = $model->imagePreprocessing($data);
- $this->assertEquals([
- 'attribute1' => 'somevalue',
- 'attribute2' => null
- ], $result);
+ $this->assertEquals($expected, $result);
}
}
diff --git a/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Action/Attribute/EditTest.php b/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Action/Attribute/EditTest.php
index 5a977b7934670..0ddd89afeac22 100644
--- a/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Action/Attribute/EditTest.php
+++ b/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Action/Attribute/EditTest.php
@@ -94,7 +94,7 @@ private function prepareContext()
$messageManager = $this->getMockBuilder(\Magento\Framework\Message\ManagerInterface::class)
->setMethods([])
->disableOriginalConstructor()->getMock();
- $messageManager->expects($this->any())->method('addError')->willReturn(true);
+ $messageManager->expects($this->any())->method('addErrorMessage')->willReturn(true);
$this->context = $this->getMockBuilder(\Magento\Backend\App\Action\Context::class)
->setMethods(['getRequest', 'getObjectManager', 'getMessageManager', 'getResultRedirectFactory'])
->disableOriginalConstructor()->getMock();
diff --git a/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Action/Attribute/SaveTest.php b/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Action/Attribute/SaveTest.php
index c88a008efb19b..de44af7f58afc 100644
--- a/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Action/Attribute/SaveTest.php
+++ b/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Action/Attribute/SaveTest.php
@@ -250,8 +250,8 @@ public function testExecuteThatProductIdsAreObtainedFromAttributeHelper()
['inventory', [], [7]],
]));
- $this->messageManager->expects($this->never())->method('addError');
- $this->messageManager->expects($this->never())->method('addException');
+ $this->messageManager->expects($this->never())->method('addErrorMessage');
+ $this->messageManager->expects($this->never())->method('addExceptionMessage');
$this->object->execute();
}
diff --git a/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Attribute/ValidateTest.php b/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Attribute/ValidateTest.php
index 6373712066695..9c747393cc72a 100644
--- a/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Attribute/ValidateTest.php
+++ b/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Attribute/ValidateTest.php
@@ -195,6 +195,9 @@ public function testUniqueValidation(array $options, $isError)
$this->assertInstanceOf(ResultJson::class, $this->getModel()->execute());
}
+ /**
+ * @return array
+ */
public function provideUniqueData()
{
return [
@@ -249,6 +252,20 @@ public function provideUniqueData()
]
], false
],
+ 'empty and deleted' => [
+ [
+ 'value' => [
+ "option_0" => [1, 0],
+ "option_1" => [2, 0],
+ "option_2" => ["", ""],
+ ],
+ 'delete' => [
+ "option_0" => "",
+ "option_1" => "",
+ "option_2" => "1",
+ ]
+ ], false
+ ],
];
}
@@ -321,7 +338,34 @@ public function provideEmptyOption()
(object) [
'error' => false,
]
- ]
+ ],
+ 'empty admin scope options and deleted' => [
+ [
+ 'value' => [
+ "option_0" => [''],
+ ],
+ 'delete' => [
+ 'option_0' => '1',
+ ],
+ ],
+ (object) [
+ 'error' => false,
+ ],
+ ],
+ 'empty admin scope options and not deleted' => [
+ [
+ 'value' => [
+ "option_0" => [''],
+ ],
+ 'delete' => [
+ 'option_0' => '0',
+ ],
+ ],
+ (object) [
+ 'error' => true,
+ 'message' => 'The value of Admin scope can\'t be empty.',
+ ],
+ ],
];
}
}
diff --git a/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/BuilderTest.php b/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/BuilderTest.php
index 4113ce636d66b..c71fa90fb02dd 100644
--- a/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/BuilderTest.php
+++ b/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/BuilderTest.php
@@ -14,6 +14,9 @@
use Magento\Framework\Registry;
use Magento\Cms\Model\Wysiwyg\Config as WysiwygConfig;
use Magento\Framework\App\Request\Http;
+use Magento\Catalog\Api\ProductRepositoryInterface;
+use Magento\Framework\Exception\NoSuchEntityException;
+use Magento\Catalog\Model\Product\Type as ProductTypes;
/**
* Class BuilderTest
@@ -67,6 +70,11 @@ class BuilderTest extends \PHPUnit\Framework\TestCase
*/
protected $storeFactoryMock;
+ /**
+ * @var ProductRepositoryInterface|\PHPUnit_Framework_MockObject_MockObject
+ */
+ protected $productRepositoryMock;
+
/**
* @var StoreInterface|\PHPUnit_Framework_MockObject_MockObject
*/
@@ -90,9 +98,10 @@ protected function setUp()
->setMethods(['load'])
->getMockForAbstractClass();
- $this->storeFactoryMock->expects($this->any())
- ->method('create')
- ->willReturn($this->storeMock);
+ $this->productRepositoryMock = $this->getMockBuilder(ProductRepositoryInterface::class)
+ ->setMethods(['getById'])
+ ->disableOriginalConstructor()
+ ->getMockForAbstractClass();
$this->builder = $this->objectManager->getObject(
Builder::class,
@@ -102,140 +111,198 @@ protected function setUp()
'registry' => $this->registryMock,
'wysiwygConfig' => $this->wysiwygConfigMock,
'storeFactory' => $this->storeFactoryMock,
+ 'productRepository' => $this->productRepositoryMock
]
);
}
public function testBuildWhenProductExistAndPossibleToLoadProduct()
{
+ $productId = 2;
+ $productType = 'type_id';
+ $productStore = 'store';
+ $productSet = 3;
+
$valueMap = [
- ['id', null, 2],
- ['store', 0, 'some_store'],
- ['type', null, 'type_id'],
- ['set', null, 3],
- ['store', null, 'store'],
+ ['id', null, $productId],
+ ['type', null, $productType],
+ ['set', null, $productSet],
+ ['store', 0, $productStore],
];
+
$this->requestMock->expects($this->any())
->method('getParam')
->willReturnMap($valueMap);
- $this->productFactoryMock->expects($this->once())
+
+ $this->productRepositoryMock->expects($this->any())
+ ->method('getById')
+ ->with($productId, true, $productStore)
+ ->willReturn($this->productMock);
+
+ $this->storeFactoryMock->expects($this->any())
->method('create')
- ->will($this->returnValue($this->productMock));
- $this->productMock->expects($this->once())
- ->method('setStoreId')
- ->with('some_store')
- ->willReturnSelf();
- $this->productMock->expects($this->never())
- ->method('setTypeId');
- $this->productMock->expects($this->once())
+ ->willReturn($this->storeMock);
+
+ $this->storeMock->expects($this->any())
->method('load')
- ->with(2)
- ->will($this->returnSelf());
- $this->productMock->expects($this->once())
- ->method('setAttributeSetId')
- ->with(3)
- ->will($this->returnSelf());
+ ->with($productStore)
+ ->willReturnSelf();
+
$registryValueMap = [
['product', $this->productMock, $this->registryMock],
['current_product', $this->productMock, $this->registryMock],
+ ['current_store', $this->registryMock, $this->storeMock],
];
+
$this->registryMock->expects($this->any())
->method('register')
->willReturn($registryValueMap);
+
$this->wysiwygConfigMock->expects($this->once())
->method('setStoreId')
- ->with('store');
+ ->with($productStore);
+
$this->assertEquals($this->productMock, $this->builder->build($this->requestMock));
}
public function testBuildWhenImpossibleLoadProduct()
{
+ $productId = 2;
+ $productType = 'type_id';
+ $productStore = 'store';
+ $productSet = 3;
+
$valueMap = [
- ['id', null, 15],
- ['store', 0, 'some_store'],
- ['type', null, 'type_id'],
- ['set', null, 3],
- ['store', null, 'store'],
+ ['id', null, $productId],
+ ['type', null, $productType],
+ ['set', null, $productSet],
+ ['store', 0, $productStore],
];
+
$this->requestMock->expects($this->any())
->method('getParam')
- ->will($this->returnValueMap($valueMap));
+ ->willReturnMap($valueMap);
+
+ $this->productRepositoryMock->expects($this->any())
+ ->method('getById')
+ ->with($productId, true, $productStore)
+ ->willThrowException(new NoSuchEntityException());
+
$this->productFactoryMock->expects($this->once())
->method('create')
- ->willReturn($this->productMock);
- $this->productMock->expects($this->once())
- ->method('setStoreId')
- ->with('some_store')
- ->willReturnSelf();
- $this->productMock->expects($this->once())
+ ->will($this->returnValue($this->productMock));
+
+ $this->productMock->expects($this->any())
+ ->method('setData')
+ ->with('_edit_mode', true);
+
+ $this->productMock->expects($this->any())
->method('setTypeId')
- ->with(\Magento\Catalog\Model\Product\Type::DEFAULT_TYPE)
- ->willReturnSelf();
- $this->productMock->expects($this->once())
- ->method('load')
- ->with(15)
- ->willThrowException(new \Exception());
+ ->with(ProductTypes::DEFAULT_TYPE);
+
+ $this->productMock->expects($this->any())
+ ->method('setStoreId')
+ ->with($productStore);
+
+ $this->productMock->expects($this->any())
+ ->method('setAttributeSetId')
+ ->with($productSet);
+
$this->loggerMock->expects($this->once())
->method('critical');
- $this->productMock->expects($this->once())
- ->method('setAttributeSetId')
- ->with(3)
- ->will($this->returnSelf());
+
+ $this->storeFactoryMock->expects($this->any())
+ ->method('create')
+ ->willReturn($this->storeMock);
+
+ $this->storeMock->expects($this->any())
+ ->method('load')
+ ->with($productStore)
+ ->willReturnSelf();
+
$registryValueMap = [
['product', $this->productMock, $this->registryMock],
['current_product', $this->productMock, $this->registryMock],
+ ['current_store', $this->registryMock, $this->storeMock],
];
+
$this->registryMock->expects($this->any())
->method('register')
- ->will($this->returnValueMap($registryValueMap));
+ ->willReturn($registryValueMap);
+
$this->wysiwygConfigMock->expects($this->once())
->method('setStoreId')
- ->with('store');
+ ->with($productStore);
+
$this->assertEquals($this->productMock, $this->builder->build($this->requestMock));
}
public function testBuildWhenProductNotExist()
{
+ $productId = 0;
+ $productType = 'type_id';
+ $productStore = 'store';
+ $productSet = 3;
+
$valueMap = [
- ['id', null, null],
- ['store', 0, 'some_store'],
- ['type', null, 'type_id'],
- ['set', null, 3],
- ['store', null, 'store'],
+ ['id', null, $productId],
+ ['type', null, $productType],
+ ['set', null, $productSet],
+ ['store', 0, $productStore],
];
+
$this->requestMock->expects($this->any())
->method('getParam')
- ->will($this->returnValueMap($valueMap));
+ ->willReturnMap($valueMap);
+
+ $this->productRepositoryMock->expects($this->any())
+ ->method('getById')
+ ->with($productId, true, $productStore)
+ ->willThrowException(new NoSuchEntityException());
+
$this->productFactoryMock->expects($this->once())
->method('create')
- ->willReturn($this->productMock);
- $this->productMock->expects($this->once())
- ->method('setStoreId')
- ->with('some_store')
- ->willReturnSelf();
- $productValueMap = [
- ['type_id', $this->productMock],
- [\Magento\Catalog\Model\Product\Type::DEFAULT_TYPE, $this->productMock],
- ];
+ ->will($this->returnValue($this->productMock));
+
+ $this->productMock->expects($this->any())
+ ->method('setData')
+ ->with('_edit_mode', true);
+
$this->productMock->expects($this->any())
->method('setTypeId')
- ->willReturnMap($productValueMap);
- $this->productMock->expects($this->never())
- ->method('load');
- $this->productMock->expects($this->once())
+ ->with($productType);
+
+ $this->productMock->expects($this->any())
+ ->method('setStoreId')
+ ->with($productStore);
+
+ $this->productMock->expects($this->any())
->method('setAttributeSetId')
- ->with(3)
- ->will($this->returnSelf());
+ ->with($productSet);
+
+ $this->storeFactoryMock->expects($this->any())
+ ->method('create')
+ ->willReturn($this->storeMock);
+
+ $this->storeMock->expects($this->any())
+ ->method('load')
+ ->with($productStore)
+ ->willReturnSelf();
+
$registryValueMap = [
['product', $this->productMock, $this->registryMock],
['current_product', $this->productMock, $this->registryMock],
+ ['current_store', $this->registryMock, $this->storeMock],
];
+
$this->registryMock->expects($this->any())
->method('register')
- ->will($this->returnValueMap($registryValueMap));
+ ->willReturn($registryValueMap);
+
$this->wysiwygConfigMock->expects($this->once())
->method('setStoreId')
- ->with('store');
+ ->with($productStore);
+
$this->assertEquals($this->productMock, $this->builder->build($this->requestMock));
}
}
diff --git a/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Initialization/Helper/AttributeFilterTest.php b/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Initialization/Helper/AttributeFilterTest.php
index 28617addc6d27..137eadb07d529 100644
--- a/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Initialization/Helper/AttributeFilterTest.php
+++ b/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Initialization/Helper/AttributeFilterTest.php
@@ -3,10 +3,13 @@
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
+
namespace Magento\Catalog\Test\Unit\Controller\Adminhtml\Product\Initialization\Helper;
use Magento\Catalog\Controller\Adminhtml\Product\Initialization\Helper\AttributeFilter;
use Magento\Catalog\Model\Product;
+use Magento\Catalog\Model\ResourceModel\Eav\Attribute;
+use PHPUnit_Framework_MockObject_MockObject;
class AttributeFilterTest extends \PHPUnit\Framework\TestCase
{
@@ -16,12 +19,12 @@ class AttributeFilterTest extends \PHPUnit\Framework\TestCase
protected $model;
/**
- * @var \PHPUnit_Framework_MockObject_MockObject
+ * @var PHPUnit_Framework_MockObject_MockObject
*/
protected $objectManagerMock;
/**
- * @var Product|\PHPUnit_Framework_MockObject_MockObject
+ * @var Product|PHPUnit_Framework_MockObject_MockObject
*/
protected $productMock;
@@ -44,15 +47,25 @@ public function testPrepareProductAttributes(
$expectedProductData,
$initialProductData
) {
+ /** @var PHPUnit_Framework_MockObject_MockObject | Product $productMockMap */
$productMockMap = $this->getMockBuilder(Product::class)
->disableOriginalConstructor()
- ->setMethods(['getData'])
+ ->setMethods(['getData', 'getAttributes'])
->getMock();
if (!empty($initialProductData)) {
$productMockMap->expects($this->any())->method('getData')->willReturnMap($initialProductData);
}
+ if ($useDefaults) {
+ $productMockMap
+ ->expects($this->once())
+ ->method('getAttributes')
+ ->willReturn(
+ $this->getProductAttributesMock($useDefaults)
+ );
+ }
+
$actualProductData = $this->model->prepareProductAttributes($productMockMap, $requestProductData, $useDefaults);
$this->assertEquals($expectedProductData, $actualProductData);
}
@@ -169,7 +182,7 @@ public function setupInputDataProvider()
'description' => 'descr modified'
],
'initialProductData' => [
- ['name', null,'testName2'],
+ ['name', null, 'testName2'],
['sku', null, 'testSku2'],
['price', null, '101'],
['description', null, 'descr text']
@@ -180,7 +193,8 @@ public function setupInputDataProvider()
'name' => 'testName3',
'sku' => 'testSku3',
'price' => '103',
- 'special_price' => '100'
+ 'special_price' => '100',
+ 'description' => 'descr modified',
],
'useDefaults' => [
'description' => '1'
@@ -190,9 +204,10 @@ public function setupInputDataProvider()
'sku' => 'testSku3',
'price' => '103',
'special_price' => '100',
+ 'description' => false
],
'initialProductData' => [
- ['name', null,'testName2'],
+ ['name', null, 'testName2'],
['sku', null, 'testSku2'],
['price', null, '101'],
['description', null, 'descr text']
@@ -200,4 +215,27 @@ public function setupInputDataProvider()
],
];
}
+
+ /**
+ * @param array $useDefaults
+ * @return array
+ */
+ private function getProductAttributesMock(array $useDefaults): array
+ {
+ $returnArray = [];
+ foreach ($useDefaults as $attributecode => $isDefault) {
+ if ($isDefault === '1') {
+ /** @var Attribute | PHPUnit_Framework_MockObject_MockObject $attribute */
+ $attribute = $this->getMockBuilder(Attribute::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $attribute->expects($this->any())
+ ->method('getBackendType')
+ ->willReturn('varchar');
+
+ $returnArray[$attributecode] = $attribute;
+ }
+ }
+ return $returnArray;
+ }
}
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 dce3f5886d1a8..aed87f918ebb8 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
@@ -198,10 +198,12 @@ public function testInitialize(
'option2' => ['is_delete' => false, 'name' => 'name1', 'price' => 'price1', 'option_id' => '13'],
'option3' => ['is_delete' => false, 'name' => 'name1', 'price' => 'price1', 'option_id' => '14']
];
+ $specialFromDate = '2018-03-03 19:30:00';
$productData = [
'stock_data' => ['stock_data'],
'options' => $optionsData,
- 'website_ids' => $websiteIds
+ 'website_ids' => $websiteIds,
+ 'special_from_date' => $specialFromDate,
];
if (!empty($tierPrice)) {
$productData = array_merge($productData, ['tier_price' => $tierPrice]);
@@ -306,6 +308,7 @@ public function testInitialize(
}
$this->assertEquals($expectedLinks, $resultLinks);
+ $this->assertEquals($specialFromDate, $productData['special_from_date']);
}
/**
diff --git a/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/ShowUpdateResultTest.php b/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/ShowUpdateResultTest.php
index ba716fdb53c89..47a60a1916142 100644
--- a/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/ShowUpdateResultTest.php
+++ b/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/ShowUpdateResultTest.php
@@ -58,7 +58,7 @@ protected function getContext()
$objectManagerMock = $this->getMockForAbstractClass(\Magento\Framework\ObjectManagerInterface::class);
$objectManagerMock->expects($this->any())
->method('get')
- ->willreturn($productActionMock);
+ ->willReturn($productActionMock);
$eventManager = $this->getMockBuilder(\Magento\Framework\Event\Manager::class)
->setMethods(['dispatch'])
diff --git a/app/code/Magento/Catalog/Test/Unit/Cron/DeleteAbandonedStoreFlatTablesTest.php b/app/code/Magento/Catalog/Test/Unit/Cron/DeleteAbandonedStoreFlatTablesTest.php
new file mode 100644
index 0000000000000..1a9d7959dda9c
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Unit/Cron/DeleteAbandonedStoreFlatTablesTest.php
@@ -0,0 +1,51 @@
+indexerMock = $this->createMock(Indexer::class);
+ $this->deleteAbandonedStoreFlatTables = new DeleteAbandonedStoreFlatTables($this->indexerMock);
+ }
+
+ /**
+ * Test execute method
+ *
+ * @return void
+ */
+ public function testExecute()
+ {
+ $this->indexerMock->expects($this->once())->method('deleteAbandonedStoreFlatTables');
+ $this->deleteAbandonedStoreFlatTables->execute();
+ }
+}
diff --git a/app/code/Magento/Catalog/Test/Unit/Cron/DeleteOutdatedPriceValuesTest.php b/app/code/Magento/Catalog/Test/Unit/Cron/DeleteOutdatedPriceValuesTest.php
new file mode 100644
index 0000000000000..c59d86aa3d5f1
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Unit/Cron/DeleteOutdatedPriceValuesTest.php
@@ -0,0 +1,127 @@
+resourceConnectionMock = $this->createMock(ResourceConnection::class);
+ $this->attributeRepositoryMock = $this->createMock(AttributeRepository::class);
+ $this->attributeMock = $this->createMock(Attribute::class);
+ $this->scopeConfigMock = $this->createMock(ScopeConfig::class);
+ $this->dbAdapterMock = $this->createMock(AdapterInterface::class);
+ $this->attributeBackendMock = $this->createMock(BackendInterface::class);
+ $this->deleteOutdatedPriceValues = new DeleteOutdatedPriceValues(
+ $this->resourceConnectionMock,
+ $this->attributeRepositoryMock,
+ $this->scopeConfigMock
+ );
+ }
+
+ /**
+ * Test execute method
+ *
+ * @return void
+ */
+ public function testExecute()
+ {
+ $table = 'catalog_product_entity_decimal';
+ $attributeId = 15;
+ $conditions = ['first', 'second'];
+
+ $this->scopeConfigMock->expects($this->once())->method('getValue')->with(Store::XML_PATH_PRICE_SCOPE)
+ ->willReturn(Store::XML_PATH_PRICE_SCOPE);
+ $this->attributeRepositoryMock->expects($this->once())->method('get')
+ ->with(ProductAttributeInterface::ENTITY_TYPE_CODE, ProductAttributeInterface::CODE_PRICE)
+ ->willReturn($this->attributeMock);
+ $this->attributeMock->expects($this->once())->method('getId')->willReturn($attributeId);
+ $this->attributeMock->expects($this->once())->method('getBackend')->willReturn($this->attributeBackendMock);
+ $this->attributeBackendMock->expects($this->once())->method('getTable')->willReturn($table);
+ $this->resourceConnectionMock->expects($this->once())
+ ->method('getConnection')
+ ->willReturn($this->dbAdapterMock);
+ $this->dbAdapterMock->expects($this->exactly(2))->method('quoteInto')->willReturnMap([
+ ['attribute_id = ?', $attributeId, null, null, $conditions[0]],
+ ['store_id != ?', Store::DEFAULT_STORE_ID, null, null, $conditions[1]],
+ ]);
+ $this->dbAdapterMock->expects($this->once())->method('delete')->with($table, $conditions);
+ $this->deleteOutdatedPriceValues->execute();
+ }
+
+ /**
+ * Test execute method
+ * The price scope config option is not equal to global value
+ *
+ * @return void
+ */
+ public function testExecutePriceConfigIsNotSetToGlobal()
+ {
+ $this->scopeConfigMock->expects($this->once())->method('getValue')->with(Store::XML_PATH_PRICE_SCOPE)
+ ->willReturn(null);
+ $this->attributeRepositoryMock->expects($this->never())->method('get');
+ $this->dbAdapterMock->expects($this->never())->method('delete');
+
+ $this->deleteOutdatedPriceValues->execute();
+ }
+}
diff --git a/app/code/Magento/Catalog/Test/Unit/CustomerData/CompareProductsTest.php b/app/code/Magento/Catalog/Test/Unit/CustomerData/CompareProductsTest.php
new file mode 100644
index 0000000000000..e30ddda0b70b9
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Unit/CustomerData/CompareProductsTest.php
@@ -0,0 +1,286 @@
+ 'getId',
+ ProductInterface::NAME => 'getName'
+ ];
+
+ protected function setUp()
+ {
+ parent::setUp();
+
+ $this->helperMock = $this->getMockBuilder(Compare::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->productUrlMock = $this->getMockBuilder(Url::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->outputHelperMock = $this->getMockBuilder(Output::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->objectManagerHelper = new ObjectManagerHelper($this);
+
+ $this->model = $this->objectManagerHelper->getObject(
+ CompareProducts::class,
+ [
+ 'helper' => $this->helperMock,
+ 'productUrl' => $this->productUrlMock,
+ 'outputHelper' => $this->outputHelperMock
+ ]
+ );
+ }
+
+ /**
+ * Prepare compare items collection.
+ *
+ * @param array $items
+ * @return \PHPUnit_Framework_MockObject_MockObject
+ */
+ private function getItemCollectionMock(array $items) : \PHPUnit_Framework_MockObject_MockObject
+ {
+ $itemCollectionMock = $this->getMockBuilder(Collection::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $itemCollectionMock->expects($this->any())
+ ->method('getIterator')
+ ->willReturn(new \ArrayIterator($items));
+
+ return $itemCollectionMock;
+ }
+
+ /**
+ * Prepare product mocks objects and add corresponding method mocks for helpers.
+ *
+ * @param array $dataSet
+ * @return array
+ */
+ private function prepareProductsWithCorrespondingMocks(array $dataSet) : array
+ {
+ $items = [];
+ $urlMap = [];
+ $outputMap = [];
+ $helperMap = [];
+
+ $count = count($dataSet);
+
+ foreach ($dataSet as $data) {
+ $item = $this->getProductMock($data);
+ $items[] = $item;
+
+ $outputMap[] = [$item, $data['name'], 'name', 'productName#' . $data['id']];
+ $helperMap[] = [$item, 'http://remove.url/' . $data['id']];
+ $urlMap[] = [$item, [], 'http://product.url/' . $data['id']];
+ }
+
+ $this->productUrlMock->expects($this->exactly($count))
+ ->method('getUrl')
+ ->will($this->returnValueMap($urlMap));
+
+ $this->outputHelperMock->expects($this->exactly($count))
+ ->method('productAttribute')
+ ->will($this->returnValueMap($outputMap));
+
+ $this->helperMock->expects($this->exactly($count))
+ ->method('getPostDataRemove')
+ ->will($this->returnValueMap($helperMap));
+
+ return $items;
+ }
+
+ /**
+ * Prepare mock of product object.
+ *
+ * @param array $data
+ * @return \PHPUnit_Framework_MockObject_MockObject
+ */
+ private function getProductMock(array $data) : \PHPUnit_Framework_MockObject_MockObject
+ {
+ $product = $this->getMockBuilder(Product::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ foreach ($data as $index => $value) {
+ $product->expects($this->once())
+ ->method($this->productValueMap[$index])
+ ->willReturn($value);
+ }
+
+ return $product;
+ }
+
+ public function testGetSectionData()
+ {
+ $dataSet = [
+ ['id' => 1, 'name' => 'product#1'],
+ ['id' => 2, 'name' => 'product#2'],
+ ['id' => 3, 'name' => 'product#3']
+ ];
+
+ $count = count($dataSet);
+
+ $this->helperMock->expects($this->once())
+ ->method('getItemCount')
+ ->willReturn($count);
+
+ $items = $this->prepareProductsWithCorrespondingMocks($dataSet);
+
+ $itemCollectionMock = $this->getItemCollectionMock($items);
+
+ $this->helperMock->expects($this->once())
+ ->method('getItemCollection')
+ ->willReturn($itemCollectionMock);
+
+ $this->helperMock->expects($this->once())
+ ->method('getListUrl')
+ ->willReturn('http://list.url');
+
+ $this->assertEquals(
+ [
+ 'count' => $count,
+ 'countCaption' => __('%1 items', $count),
+ 'listUrl' => 'http://list.url',
+ 'items' => [
+ [
+ 'id' => 1,
+ 'product_url' => 'http://product.url/1',
+ 'name' => 'productName#1',
+ 'remove_url' => 'http://remove.url/1'
+ ],
+ [
+ 'id' => 2,
+ 'product_url' => 'http://product.url/2',
+ 'name' => 'productName#2',
+ 'remove_url' => 'http://remove.url/2'
+ ],
+ [
+ 'id' => 3,
+ 'product_url' => 'http://product.url/3',
+ 'name' => 'productName#3',
+ 'remove_url' => 'http://remove.url/3'
+ ]
+ ]
+ ],
+ $this->model->getSectionData()
+ );
+ }
+
+ public function testGetSectionDataNoItems()
+ {
+ $count = 0;
+
+ $this->helperMock->expects($this->once())
+ ->method('getItemCount')
+ ->willReturn($count);
+
+ $this->helperMock->expects($this->never())
+ ->method('getItemCollection');
+
+ $this->helperMock->expects($this->once())
+ ->method('getListUrl')
+ ->willReturn('http://list.url');
+
+ $this->assertEquals(
+ [
+ 'count' => $count,
+ 'countCaption' => __('%1 items', $count),
+ 'listUrl' => 'http://list.url',
+ 'items' => []
+ ],
+ $this->model->getSectionData()
+ );
+ }
+
+ public function testGetSectionDataSingleItem()
+ {
+ $count = 1;
+
+ $this->helperMock->expects($this->once())
+ ->method('getItemCount')
+ ->willReturn($count);
+
+ $items = $this->prepareProductsWithCorrespondingMocks(
+ [
+ [
+ 'id' => 12345,
+ 'name' => 'SingleProduct'
+ ]
+ ]
+ );
+
+ $itemCollectionMock = $this->getItemCollectionMock($items);
+
+ $this->helperMock->expects($this->once())
+ ->method('getItemCollection')
+ ->willReturn($itemCollectionMock);
+
+ $this->helperMock->expects($this->once())
+ ->method('getListUrl')
+ ->willReturn('http://list.url');
+
+ $this->assertEquals(
+ [
+ 'count' => 1,
+ 'countCaption' => __('1 item'),
+ 'listUrl' => 'http://list.url',
+ 'items' => [
+ [
+ 'id' => 12345,
+ 'product_url' => 'http://product.url/12345',
+ 'name' => 'productName#12345',
+ 'remove_url' => 'http://remove.url/12345'
+ ]
+ ]
+ ],
+ $this->model->getSectionData()
+ );
+ }
+}
diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Api/SearchCriteria/CollectionProcessor/ConditionProcessor/ConditionBuilder/FactoryTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Api/SearchCriteria/CollectionProcessor/ConditionProcessor/ConditionBuilder/FactoryTest.php
new file mode 100644
index 0000000000000..70b75e7ff2790
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Unit/Model/Api/SearchCriteria/CollectionProcessor/ConditionProcessor/ConditionBuilder/FactoryTest.php
@@ -0,0 +1,139 @@
+productResourceMock = $this->getMockBuilder(ProductResource::class)
+ ->disableOriginalConstructor()
+ ->setMethods(['getEntityTable'])
+ ->getMock();
+
+ $this->eavConfigMock = $this->getMockBuilder(EavConfig::class)
+ ->disableOriginalConstructor()
+ ->setMethods(['getAttribute'])
+ ->getMock();
+
+ $this->eavAttrConditionBuilderMock = $this->getMockBuilder(CustomConditionInterface::class)
+ ->disableOriginalConstructor()
+ ->getMockForAbstractClass();
+
+ $this->nativeAttrConditionBuilderMock = $this->getMockBuilder(CustomConditionInterface::class)
+ ->disableOriginalConstructor()
+ ->getMockForAbstractClass();
+
+ $objectManagerHelper = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this);
+
+ $this->conditionBuilderFactory = $objectManagerHelper->getObject(
+ Factory::class,
+ [
+ 'eavConfig' => $this->eavConfigMock,
+ 'productResource' => $this->productResourceMock,
+ 'eavAttributeConditionBuilder' => $this->eavAttrConditionBuilderMock,
+ 'nativeAttributeConditionBuilder' => $this->nativeAttrConditionBuilderMock,
+ ]
+ );
+ }
+
+ public function testNativeAttrConditionBuilder()
+ {
+ $fieldName = 'super_field';
+ $attributeTable = 'my-table';
+ $productResourceTable = 'my-table';
+
+ $filterMock = $this->getMockBuilder(Filter::class)
+ ->disableOriginalConstructor()
+ ->setMethods(['getField'])
+ ->getMock();
+
+ $filterMock
+ ->method('getField')
+ ->willReturn($fieldName);
+
+ $attributeMock = $this->getMockBuilder(Attribute::class)
+ ->disableOriginalConstructor()
+ ->setMethods(['getBackendTable'])
+ ->getMock();
+
+ $this->eavConfigMock
+ ->method('getAttribute')
+ ->with(Product::ENTITY, $fieldName)
+ ->willReturn($attributeMock);
+
+ $attributeMock
+ ->method('getBackendTable')
+ ->willReturn($attributeTable);
+
+ $this->productResourceMock
+ ->method('getEntityTable')
+ ->willReturn($productResourceTable);
+
+ $this->assertEquals(
+ $this->nativeAttrConditionBuilderMock,
+ $this->conditionBuilderFactory->createByFilter($filterMock)
+ );
+ }
+
+ public function testEavAttrConditionBuilder()
+ {
+ $fieldName = 'super_field';
+ $attributeTable = 'my-table';
+ $productResourceTable = 'not-my-table';
+
+ $filterMock = $this->getMockBuilder(Filter::class)
+ ->disableOriginalConstructor()
+ ->setMethods(['getField'])
+ ->getMock();
+
+ $filterMock
+ ->method('getField')
+ ->willReturn($fieldName);
+
+ $attributeMock = $this->getMockBuilder(Attribute::class)
+ ->disableOriginalConstructor()
+ ->setMethods(['getBackendTable'])
+ ->getMock();
+
+ $this->eavConfigMock
+ ->method('getAttribute')
+ ->with(Product::ENTITY, $fieldName)
+ ->willReturn($attributeMock);
+
+ $attributeMock
+ ->method('getBackendTable')
+ ->willReturn($attributeTable);
+
+ $this->productResourceMock
+ ->method('getEntityTable')
+ ->willReturn($productResourceTable);
+
+ $this->assertEquals(
+ $this->eavAttrConditionBuilderMock,
+ $this->conditionBuilderFactory->createByFilter($filterMock)
+ );
+ }
+}
diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Api/SearchCriteria/CollectionProcessor/FilterProcessor/ProductCategoryFilterTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Api/SearchCriteria/CollectionProcessor/FilterProcessor/ProductCategoryFilterTest.php
index f667638cc4da8..157c72fcedf10 100644
--- a/app/code/Magento/Catalog/Test/Unit/Model/Api/SearchCriteria/CollectionProcessor/FilterProcessor/ProductCategoryFilterTest.php
+++ b/app/code/Magento/Catalog/Test/Unit/Model/Api/SearchCriteria/CollectionProcessor/FilterProcessor/ProductCategoryFilterTest.php
@@ -31,7 +31,7 @@ public function testApply()
->disableOriginalConstructor()
->getMock();
- $filterMock->expects($this->exactly(2))
+ $filterMock->expects($this->exactly(1))
->method('getConditionType')
->willReturn('condition');
$filterMock->expects($this->once())
@@ -66,7 +66,7 @@ public function testApplyWithoutCondition()
$collectionMock->expects($this->once())
->method('addCategoriesFilter')
- ->with(['eq' => ['value']]);
+ ->with(['in' => ['value']]);
$this->assertTrue($this->model->apply($filterMock, $collectionMock));
}
diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Attribute/Backend/TierPrice/SaveHandlerTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Attribute/Backend/TierPrice/SaveHandlerTest.php
new file mode 100644
index 0000000000000..027eb5ad505ed
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Unit/Model/Attribute/Backend/TierPrice/SaveHandlerTest.php
@@ -0,0 +1,176 @@
+objectManager = new ObjectManager($this);
+ $this->storeManager = $this->getMockBuilder(StoreManagerInterface::class)
+ ->disableOriginalConstructor()
+ ->setMethods(['getStore'])
+ ->getMockForAbstractClass();
+ $this->attributeRepository = $this->getMockBuilder(ProductAttributeRepositoryInterface::class)
+ ->disableOriginalConstructor()
+ ->setMethods(['get'])
+ ->getMockForAbstractClass();
+ $this->groupManagement = $this->getMockBuilder(GroupManagementInterface::class)
+ ->disableOriginalConstructor()
+ ->setMethods(['getAllCustomersGroup'])
+ ->getMockForAbstractClass();
+ $this->metadataPoll = $this->getMockBuilder(MetadataPool::class)
+ ->disableOriginalConstructor()
+ ->setMethods(['getMetadata'])
+ ->getMock();
+ $this->tierPriceResource = $this->getMockBuilder(Tierprice::class)
+ ->disableOriginalConstructor()
+ ->setMethods([])
+ ->getMock();
+
+ $this->saveHandler = $this->objectManager->getObject(
+ \Magento\Catalog\Model\Product\Attribute\Backend\TierPrice\SaveHandler::class,
+ [
+ 'storeManager' => $this->storeManager,
+ 'attributeRepository' => $this->attributeRepository,
+ 'groupManagement' => $this->groupManagement,
+ 'metadataPoll' => $this->metadataPoll,
+ 'tierPriceResource' => $this->tierPriceResource
+ ]
+ );
+ }
+
+ public function testExecute()
+ {
+ $tierPrices = [
+ ['website_id' => 0, 'price_qty' => 2, 'cust_group' => 0, 'price' => 10],
+ ['website_id' => 0, 'price_qty' => 3, 'cust_group' => 3200, 'price' => null, 'percentage_value' => 20]
+ ];
+ $linkField = 'entity_id';
+ $productId = 10;
+
+ /** @var \PHPUnit_Framework_MockObject_MockObject $product */
+ $product = $this->getMockBuilder(\Magento\Catalog\Api\Data\ProductInterface::class)
+ ->disableOriginalConstructor()
+ ->setMethods(['getData','setData', 'getStoreId'])
+ ->getMockForAbstractClass();
+ $product->expects($this->atLeastOnce())->method('getData')->willReturnMap(
+ [
+ ['tier_price', $tierPrices],
+ ['entity_id', $productId]
+ ]
+ );
+ $product->expects($this->atLeastOnce())->method('getStoreId')->willReturn(0);
+ $product->expects($this->atLeastOnce())->method('setData')->with('tier_price_changed', 1);
+ $store = $this->getMockBuilder(\Magento\Store\Api\Data\StoreInterface::class)
+ ->disableOriginalConstructor()
+ ->setMethods(['getWebsiteId'])
+ ->getMockForAbstractClass();
+ $store->expects($this->atLeastOnce())->method('getWebsiteId')->willReturn(0);
+ $this->storeManager->expects($this->atLeastOnce())->method('getStore')->willReturn($store);
+ /** @var \PHPUnit_Framework_MockObject_MockObject $attribute */
+ $attribute = $this->getMockBuilder(\Magento\Catalog\Api\Data\ProductAttributeInterface::class)
+ ->disableOriginalConstructor()
+ ->setMethods(['getName', 'isScopeGlobal'])
+ ->getMockForAbstractClass();
+ $attribute->expects($this->atLeastOnce())->method('getName')->willReturn('tier_price');
+ $attribute->expects($this->atLeastOnce())->method('isScopeGlobal')->willReturn(true);
+ $this->attributeRepository->expects($this->atLeastOnce())->method('get')->with('tier_price')
+ ->willReturn($attribute);
+ $productMetadata = $this->getMockBuilder(\Magento\Framework\EntityManager\EntityMetadataInterface::class)
+ ->disableOriginalConstructor()
+ ->setMethods(['getLinkField'])
+ ->getMockForAbstractClass();
+ $productMetadata->expects($this->atLeastOnce())->method('getLinkField')->willReturn($linkField);
+ $this->metadataPoll->expects($this->atLeastOnce())->method('getMetadata')
+ ->with(\Magento\Catalog\Api\Data\ProductInterface::class)
+ ->willReturn($productMetadata);
+ $customerGroup = $this->getMockBuilder(\Magento\Customer\Api\Data\GroupInterface::class)
+ ->disableOriginalConstructor()
+ ->setMethods(['getId'])
+ ->getMockForAbstractClass();
+ $customerGroup->expects($this->atLeastOnce())->method('getId')->willReturn(3200);
+ $this->groupManagement->expects($this->atLeastOnce())->method('getAllCustomersGroup')
+ ->willReturn($customerGroup);
+ $this->tierPriceResource->expects($this->atLeastOnce())->method('savePriceData')->willReturnSelf();
+
+ $this->assertEquals($product, $this->saveHandler->execute($product));
+ }
+
+ /**
+ * @expectedException \Magento\Framework\Exception\InputException
+ * @expectedExceptionMessage Tier prices data should be array, but actually other type is received
+ */
+ public function testExecuteWithException()
+ {
+ /** @var \PHPUnit_Framework_MockObject_MockObject $attribute */
+ $attribute = $this->getMockBuilder(\Magento\Catalog\Api\Data\ProductAttributeInterface::class)
+ ->disableOriginalConstructor()
+ ->setMethods(['getName', 'isScopeGlobal'])
+ ->getMockForAbstractClass();
+ $attribute->expects($this->atLeastOnce())->method('getName')->willReturn('tier_price');
+ $this->attributeRepository->expects($this->atLeastOnce())->method('get')->with('tier_price')
+ ->willReturn($attribute);
+ /** @var \PHPUnit_Framework_MockObject_MockObject $product */
+ $product = $this->getMockBuilder(\Magento\Catalog\Api\Data\ProductInterface::class)
+ ->disableOriginalConstructor()
+ ->setMethods(['getData','setData', 'getStoreId', 'getOrigData'])
+ ->getMockForAbstractClass();
+ $product->expects($this->atLeastOnce())->method('getData')->with('tier_price')->willReturn(1);
+
+ $this->saveHandler->execute($product);
+ }
+}
diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Attribute/Backend/TierPrice/UpdateHandlerTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Attribute/Backend/TierPrice/UpdateHandlerTest.php
new file mode 100644
index 0000000000000..be2f0c5185fff
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Unit/Model/Attribute/Backend/TierPrice/UpdateHandlerTest.php
@@ -0,0 +1,185 @@
+objectManager = new ObjectManager($this);
+ $this->storeManager = $this->getMockBuilder(StoreManagerInterface::class)
+ ->disableOriginalConstructor()
+ ->setMethods(['getStore'])
+ ->getMockForAbstractClass();
+ $this->attributeRepository = $this->getMockBuilder(ProductAttributeRepositoryInterface::class)
+ ->disableOriginalConstructor()
+ ->setMethods(['get'])
+ ->getMockForAbstractClass();
+ $this->groupManagement = $this->getMockBuilder(GroupManagementInterface::class)
+ ->disableOriginalConstructor()
+ ->setMethods(['getAllCustomersGroup'])
+ ->getMockForAbstractClass();
+ $this->metadataPoll = $this->getMockBuilder(MetadataPool::class)
+ ->disableOriginalConstructor()
+ ->setMethods(['getMetadata'])
+ ->getMock();
+ $this->tierPriceResource = $this->getMockBuilder(Tierprice::class)
+ ->disableOriginalConstructor()
+ ->setMethods([])
+ ->getMock();
+
+ $this->updateHandler = $this->objectManager->getObject(
+ \Magento\Catalog\Model\Product\Attribute\Backend\TierPrice\UpdateHandler::class,
+ [
+ 'storeManager' => $this->storeManager,
+ 'attributeRepository' => $this->attributeRepository,
+ 'groupManagement' => $this->groupManagement,
+ 'metadataPoll' => $this->metadataPoll,
+ 'tierPriceResource' => $this->tierPriceResource
+ ]
+ );
+ }
+
+ public function testExecute()
+ {
+ $newTierPrices = [
+ ['website_id' => 0, 'price_qty' => 2, 'cust_group' => 0, 'price' => 15],
+ ['website_id' => 0, 'price_qty' => 3, 'cust_group' => 3200, 'price' => null, 'percentage_value' => 20]
+ ];
+ $priceIdToDelete = 2;
+ $originalTierPrices = [
+ ['price_id' => 1, 'website_id' => 0, 'price_qty' => 2, 'cust_group' => 0, 'price' => 10],
+ ['price_id' => $priceIdToDelete, 'website_id' => 0, 'price_qty' => 4, 'cust_group' => 0, 'price' => 20],
+ ];
+ $linkField = 'entity_id';
+ $productId = 10;
+
+ /** @var \PHPUnit_Framework_MockObject_MockObject $product */
+ $product = $this->getMockBuilder(\Magento\Catalog\Api\Data\ProductInterface::class)
+ ->disableOriginalConstructor()
+ ->setMethods(['getData','setData', 'getStoreId', 'getOrigData'])
+ ->getMockForAbstractClass();
+ $product->expects($this->atLeastOnce())->method('getData')->willReturnMap(
+ [
+ ['tier_price', $newTierPrices],
+ ['entity_id', $productId]
+ ]
+ );
+ $product->expects($this->atLeastOnce())->method('getOrigData')->with('tier_price')
+ ->willReturn($originalTierPrices);
+ $product->expects($this->atLeastOnce())->method('getStoreId')->willReturn(0);
+ $product->expects($this->atLeastOnce())->method('setData')->with('tier_price_changed', 1);
+ $store = $this->getMockBuilder(\Magento\Store\Api\Data\StoreInterface::class)
+ ->disableOriginalConstructor()
+ ->setMethods(['getWebsiteId'])
+ ->getMockForAbstractClass();
+ $store->expects($this->atLeastOnce())->method('getWebsiteId')->willReturn(0);
+ $this->storeManager->expects($this->atLeastOnce())->method('getStore')->willReturn($store);
+ /** @var \PHPUnit_Framework_MockObject_MockObject $attribute */
+ $attribute = $this->getMockBuilder(\Magento\Catalog\Api\Data\ProductAttributeInterface::class)
+ ->disableOriginalConstructor()
+ ->setMethods(['getName', 'isScopeGlobal'])
+ ->getMockForAbstractClass();
+ $attribute->expects($this->atLeastOnce())->method('getName')->willReturn('tier_price');
+ $attribute->expects($this->atLeastOnce())->method('isScopeGlobal')->willReturn(true);
+ $this->attributeRepository->expects($this->atLeastOnce())->method('get')->with('tier_price')
+ ->willReturn($attribute);
+ $productMetadata = $this->getMockBuilder(\Magento\Framework\EntityManager\EntityMetadataInterface::class)
+ ->disableOriginalConstructor()
+ ->setMethods(['getLinkField'])
+ ->getMockForAbstractClass();
+ $productMetadata->expects($this->atLeastOnce())->method('getLinkField')->willReturn($linkField);
+ $this->metadataPoll->expects($this->atLeastOnce())->method('getMetadata')
+ ->with(\Magento\Catalog\Api\Data\ProductInterface::class)
+ ->willReturn($productMetadata);
+ $customerGroup = $this->getMockBuilder(\Magento\Customer\Api\Data\GroupInterface::class)
+ ->disableOriginalConstructor()
+ ->setMethods(['getId'])
+ ->getMockForAbstractClass();
+ $customerGroup->expects($this->atLeastOnce())->method('getId')->willReturn(3200);
+ $this->groupManagement->expects($this->atLeastOnce())->method('getAllCustomersGroup')
+ ->willReturn($customerGroup);
+ $this->tierPriceResource->expects($this->exactly(2))->method('savePriceData')->willReturnSelf();
+ $this->tierPriceResource->expects($this->once())->method('deletePriceData')
+ ->with($productId, null, $priceIdToDelete);
+
+ $this->assertEquals($product, $this->updateHandler->execute($product));
+ }
+
+ /**
+ * @expectedException \Magento\Framework\Exception\InputException
+ * @expectedExceptionMessage Tier prices data should be array, but actually other type is received
+ */
+ public function testExecuteWithException()
+ {
+ /** @var \PHPUnit_Framework_MockObject_MockObject $attribute */
+ $attribute = $this->getMockBuilder(\Magento\Catalog\Api\Data\ProductAttributeInterface::class)
+ ->disableOriginalConstructor()
+ ->setMethods(['getName', 'isScopeGlobal'])
+ ->getMockForAbstractClass();
+ $attribute->expects($this->atLeastOnce())->method('getName')->willReturn('tier_price');
+ $this->attributeRepository->expects($this->atLeastOnce())->method('get')->with('tier_price')
+ ->willReturn($attribute);
+ /** @var \PHPUnit_Framework_MockObject_MockObject $product */
+ $product = $this->getMockBuilder(\Magento\Catalog\Api\Data\ProductInterface::class)
+ ->disableOriginalConstructor()
+ ->setMethods(['getData','setData', 'getStoreId', 'getOrigData'])
+ ->getMockForAbstractClass();
+ $product->expects($this->atLeastOnce())->method('getData')->with('tier_price')->willReturn(1);
+
+ $this->updateHandler->execute($product);
+ }
+}
diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Attribute/Config/XsdTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Attribute/Config/XsdTest.php
index a0a563e1e070e..779630b9559c6 100644
--- a/app/code/Magento/Catalog/Test/Unit/Model/Attribute/Config/XsdTest.php
+++ b/app/code/Magento/Catalog/Test/Unit/Model/Attribute/Config/XsdTest.php
@@ -39,6 +39,9 @@ public function testExemplarXml($fixtureXml, array $expectedErrors)
$this->assertEquals($expectedErrors, $actualErrors);
}
+ /**
+ * @return array
+ */
public function exemplarXmlDataProvider()
{
return [
diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Category/Attribute/Backend/ImageTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Category/Attribute/Backend/ImageTest.php
index d4e09714d0522..a8dc5b328b199 100644
--- a/app/code/Magento/Catalog/Test/Unit/Model/Category/Attribute/Backend/ImageTest.php
+++ b/app/code/Magento/Catalog/Test/Unit/Model/Category/Attribute/Backend/ImageTest.php
@@ -88,7 +88,7 @@ public function testBeforeSaveValueDeletion($value)
$model->beforeSave($object);
- $this->assertEquals('', $object->getTestAttribute());
+ $this->assertEquals(null, $object->getTestAttribute());
}
/**
@@ -205,6 +205,9 @@ private function setUpModelForAfterSave()
return $model->setAttribute($this->attribute);
}
+ /**
+ * @return array
+ */
public function attributeValueDataProvider()
{
return [
diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Category/Attribute/Backend/SortbyTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Category/Attribute/Backend/SortbyTest.php
index 3b99f2697f6b8..073a07818b52e 100644
--- a/app/code/Magento/Catalog/Test/Unit/Model/Category/Attribute/Backend/SortbyTest.php
+++ b/app/code/Magento/Catalog/Test/Unit/Model/Category/Attribute/Backend/SortbyTest.php
@@ -70,6 +70,9 @@ public function testBeforeSave($attributeCode, $data, $expected)
$this->assertSame($expected, $object->getData($attributeCode));
}
+ /**
+ * @return array
+ */
public function beforeSaveDataProvider()
{
return [
@@ -116,6 +119,9 @@ public function testAfterLoad($attributeCode, $data, $expected)
$this->assertSame($expected, $object->getData($attributeCode));
}
+ /**
+ * @return array
+ */
public function afterLoadDataProvider()
{
return [
@@ -158,6 +164,9 @@ public function testValidate($attributeData, $data, $expected)
$this->assertSame($expected, $this->_model->validate($object));
}
+ /**
+ * @return array
+ */
public function validateDataProvider()
{
return [
@@ -250,6 +259,9 @@ public function testValidateDefaultSort($attributeCode, $data)
$this->assertTrue($this->_model->validate($object));
}
+ /**
+ * @return array
+ */
public function validateDefaultSortDataProvider()
{
return [
@@ -293,6 +305,9 @@ public function testValidateDefaultSortException($attributeCode, $data)
$this->_model->validate($object);
}
+ /**
+ * @return array
+ */
public function validateDefaultSortException()
{
return [
diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Category/Link/SaveHandlerTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Category/Link/SaveHandlerTest.php
index 78db12be56b42..0b85ef38387fa 100644
--- a/app/code/Magento/Catalog/Test/Unit/Model/Category/Link/SaveHandlerTest.php
+++ b/app/code/Magento/Catalog/Test/Unit/Model/Category/Link/SaveHandlerTest.php
@@ -197,6 +197,21 @@ public function getCategoryDataProvider()
],
[], //affected category_ids
],
+ [
+ [3], //model category_ids
+ [
+ ['category_id' => 3, 'position' => 20],
+ ['category_id' => 4, 'position' => 30],
+ ], // dto category links
+ [
+ ['category_id' => 3, 'position' => 10],
+ ],
+ [
+ ['category_id' => 3, 'position' => 20],
+ ['category_id' => 4, 'position' => 30],
+ ],
+ [3, 4], //affected category_ids
+ ],
];
}
diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Category/Product/PositionResolverTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Category/Product/PositionResolverTest.php
new file mode 100644
index 0000000000000..9545e5eb4b37d
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Unit/Model/Category/Product/PositionResolverTest.php
@@ -0,0 +1,120 @@
+ 100,
+ '2' => 101,
+ '1' => 102
+ ];
+
+ /**
+ * @var array
+ */
+ private $flippedPositions = [
+ '100' => 3,
+ '101' => 2,
+ '102' => 1
+ ];
+
+ /**
+ * @var int
+ */
+ private $categoryId = 1;
+
+ protected function setUp()
+ {
+ $this->context = $this->getMockBuilder(Context::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->resources = $this->getMockBuilder(ResourceConnection::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->connection = $this->getMockBuilder(AdapterInterface::class)
+ ->disableOriginalConstructor()
+ ->getMockForAbstractClass();
+
+ $this->select = $this->getMockBuilder(Select::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->model = (new ObjectManager($this))->getObject(
+ PositionResolver::class,
+ [
+ 'context' => $this->context,
+ null,
+ '_resources' => $this->resources
+ ]
+ );
+ }
+
+ public function testGetPositions()
+ {
+ $this->resources->expects($this->once())
+ ->method('getConnection')
+ ->willReturn($this->connection);
+
+ $this->connection->expects($this->once())
+ ->method('select')
+ ->willReturn($this->select);
+ $this->select->expects($this->once())
+ ->method('from')
+ ->willReturnSelf();
+ $this->select->expects($this->once())
+ ->method('where')
+ ->willReturnSelf();
+ $this->select->expects($this->once())
+ ->method('order')
+ ->willReturnSelf();
+ $this->select->expects($this->once())
+ ->method('joinLeft')
+ ->willReturnSelf();
+ $this->connection->expects($this->once())
+ ->method('fetchCol')
+ ->willReturn($this->positions);
+
+ $this->assertEquals($this->flippedPositions, $this->model->getPositions($this->categoryId));
+ }
+}
diff --git a/app/code/Magento/Catalog/Test/Unit/Model/CategoryRepositoryTest.php b/app/code/Magento/Catalog/Test/Unit/Model/CategoryRepositoryTest.php
index 1bc5e450ae153..7d448302666cc 100644
--- a/app/code/Magento/Catalog/Test/Unit/Model/CategoryRepositoryTest.php
+++ b/app/code/Magento/Catalog/Test/Unit/Model/CategoryRepositoryTest.php
@@ -255,7 +255,8 @@ public function testSaveWithException()
*/
public function testSaveWithValidateCategoryException($error, $expectedException, $expectedExceptionMessage)
{
- $this->expectException($expectedException, $expectedExceptionMessage);
+ $this->expectException($expectedException);
+ $this->expectExceptionMessage($expectedExceptionMessage);
$categoryId = 5;
$categoryMock = $this->createMock(\Magento\Catalog\Model\Category::class);
$this->extensibleDataObjectConverterMock
@@ -279,6 +280,9 @@ public function testSaveWithValidateCategoryException($error, $expectedException
$this->model->save($categoryMock);
}
+ /**
+ * @return array
+ */
public function saveWithValidateCategoryExceptionDataProvider()
{
return [
diff --git a/app/code/Magento/Catalog/Test/Unit/Model/CategoryTest.php b/app/code/Magento/Catalog/Test/Unit/Model/CategoryTest.php
index 4b3facf79fbee..a53b87dcf1567 100644
--- a/app/code/Magento/Catalog/Test/Unit/Model/CategoryTest.php
+++ b/app/code/Magento/Catalog/Test/Unit/Model/CategoryTest.php
@@ -259,6 +259,9 @@ public function testGetUseFlatResourceTrue()
$this->assertEquals(true, $category->getUseFlatResource());
}
+ /**
+ * @return object
+ */
protected function getCategoryModel()
{
return $this->objectManager->getObject(
@@ -287,6 +290,9 @@ protected function getCategoryModel()
);
}
+ /**
+ * @return array
+ */
public function reindexFlatEnabledTestDataProvider()
{
return [
@@ -318,7 +324,7 @@ public function testReindexFlatEnabled($flatScheduled, $productScheduled, $expec
->will($this->returnValue(true));
$this->flatIndexer->expects($this->exactly(1))->method('isScheduled')->will($this->returnValue($flatScheduled));
- $this->flatIndexer->expects($this->exactly($expectedFlatReindexCalls))->method('reindexRow')->with('123');
+ $this->flatIndexer->expects($this->exactly($expectedFlatReindexCalls))->method('reindexList')->with(['123']);
$this->productIndexer->expects($this->exactly(1))->method('isScheduled')->will($this->returnValue($productScheduled));
$this->productIndexer->expects($this->exactly($expectedProductReindexCall))->method('reindexList')->with($pathIds);
@@ -336,6 +342,9 @@ public function testReindexFlatEnabled($flatScheduled, $productScheduled, $expec
$this->category->reindex();
}
+ /**
+ * @return array
+ */
public function reindexFlatDisabledTestDataProvider()
{
return [
@@ -426,7 +435,7 @@ public function testGetCustomAttributes()
$this->assertEquals("description", $this->category->getCustomAttribute($descriptionAttributeCode)->getValue());
//Change the attribute value, should reflect in getCustomAttribute
- $this->category->setData($descriptionAttributeCode, "new description");
+ $this->category->setCustomAttribute($descriptionAttributeCode, "new description");
$this->assertEquals(1, count($this->category->getCustomAttributes()));
$this->assertNotNull($this->category->getCustomAttribute($descriptionAttributeCode));
$this->assertEquals(
@@ -506,4 +515,33 @@ public function testGetImageWithoutAttributeCode()
$this->assertEquals('http://www.example.com/catalog/category/myimage', $result);
}
+
+ /**
+ * @return void
+ */
+ public function testGetIdentities()
+ {
+ $category = $this->getCategoryModel();
+
+ //Without an ID no identities can be given.
+ $this->assertEmpty($category->getIdentities());
+
+ //Now because ID is set we can get some
+ $category->setId(42);
+
+ $this->assertNotEmpty($category->getIdentities());
+ }
+
+ /**
+ * @return void
+ */
+ public function testGetIdentitiesWithAffectedCategories()
+ {
+ $category = $this->getCategoryModel();
+ $expectedIdentities = ['cat_c_1', 'cat_c_2', 'cat_c_3', 'cat_c_p_1'];
+ $category->setId(1);
+ $category->setAffectedCategoryIds([1,2,3]);
+
+ $this->assertEquals($expectedIdentities, $category->getIdentities());
+ }
}
diff --git a/app/code/Magento/Catalog/Test/Unit/Model/CollectionProviderTest.php b/app/code/Magento/Catalog/Test/Unit/Model/CollectionProviderTest.php
index 9df0a6bc1eac0..b9e8abe18f4ac 100644
--- a/app/code/Magento/Catalog/Test/Unit/Model/CollectionProviderTest.php
+++ b/app/code/Magento/Catalog/Test/Unit/Model/CollectionProviderTest.php
@@ -57,10 +57,14 @@ public function testGetCollection()
$linkedProductOneMock = $this->createMock(Product::class);
$linkedProductTwoMock = $this->createMock(Product::class);
$linkedProductThreeMock = $this->createMock(Product::class);
+ $linkedProductFourMock = $this->createMock(Product::class);
+ $linkedProductFiveMock = $this->createMock(Product::class);
$linkedProductOneMock->expects($this->once())->method('getId')->willReturn(1);
$linkedProductTwoMock->expects($this->once())->method('getId')->willReturn(2);
$linkedProductThreeMock->expects($this->once())->method('getId')->willReturn(3);
+ $linkedProductFourMock->expects($this->once())->method('getId')->willReturn(4);
+ $linkedProductFiveMock->expects($this->once())->method('getId')->willReturn(5);
$this->converterPoolMock->expects($this->once())
->method('getConverter')
@@ -71,9 +75,11 @@ public function testGetCollection()
[$linkedProductOneMock, ['name' => 'Product One', 'position' => 10]],
[$linkedProductTwoMock, ['name' => 'Product Two', 'position' => 2]],
[$linkedProductThreeMock, ['name' => 'Product Three', 'position' => 2]],
+ [$linkedProductFourMock, ['name' => 'Product Four', 'position' => null]],
+ [$linkedProductFiveMock, ['name' => 'Product Five']],
];
- $this->converterMock->expects($this->exactly(3))->method('convert')->willReturnMap($map);
+ $this->converterMock->expects($this->exactly(5))->method('convert')->willReturnMap($map);
$this->providerMock->expects($this->once())
->method('getLinkedProducts')
@@ -82,14 +88,18 @@ public function testGetCollection()
[
$linkedProductOneMock,
$linkedProductTwoMock,
- $linkedProductThreeMock
+ $linkedProductThreeMock,
+ $linkedProductFourMock,
+ $linkedProductFiveMock,
]
);
$expectedResult = [
- 2 => ['name' => 'Product Two', 'position' => 2],
- 3 => ['name' => 'Product Three', 'position' => 2],
- 10 => ['name' => 'Product One', 'position' => 10],
+ 0 => ['name' => 'Product Four', 'position' => 0],
+ 1 => ['name' => 'Product Five', 'position' => 0],
+ 2 => ['name' => 'Product Three', 'position' => 2],
+ 3 => ['name' => 'Product Two', 'position' => 2],
+ 4 => ['name' => 'Product One', 'position' => 10],
];
$actualResult = $this->model->getCollection($this->productMock, 'crosssell');
diff --git a/app/code/Magento/Catalog/Test/Unit/Model/ImageExtractorTest.php b/app/code/Magento/Catalog/Test/Unit/Model/ImageExtractorTest.php
index 05ed780fc1e3d..3b0262aa99e4f 100644
--- a/app/code/Magento/Catalog/Test/Unit/Model/ImageExtractorTest.php
+++ b/app/code/Magento/Catalog/Test/Unit/Model/ImageExtractorTest.php
@@ -23,7 +23,7 @@ protected function setUp()
public function testProcess()
{
$expectedArray = include(__DIR__ . '/_files/converted_view.php');
- $this->assertEquals($expectedArray, $this->model->process($this->getDomElement(), 'media'));
+ $this->assertSame($expectedArray, $this->model->process($this->getDomElement(), 'media'));
}
/**
diff --git a/app/code/Magento/Catalog/Test/Unit/Model/ImageUploaderTest.php b/app/code/Magento/Catalog/Test/Unit/Model/ImageUploaderTest.php
index b8196fcd8bea3..bd8871c21de9e 100644
--- a/app/code/Magento/Catalog/Test/Unit/Model/ImageUploaderTest.php
+++ b/app/code/Magento/Catalog/Test/Unit/Model/ImageUploaderTest.php
@@ -114,6 +114,12 @@ protected function setUp()
public function testSaveFileToTmpDir()
{
$fileId = 'file.jpg';
+ $allowedMimeTypes = [
+ 'image/jpg',
+ 'image/jpeg',
+ 'image/gif',
+ 'image/png'
+ ];
/** @var \Magento\MediaStorage\Model\File\Uploader|\PHPUnit_Framework_MockObject_MockObject $uploader */
$uploader = $this->createMock(\Magento\MediaStorage\Model\File\Uploader::class);
$this->uploaderFactoryMock->expects($this->once())->method('create')->willReturn($uploader);
@@ -123,6 +129,7 @@ public function testSaveFileToTmpDir()
->willReturn($this->basePath);
$uploader->expects($this->once())->method('save')->with($this->basePath)
->willReturn(['tmp_name' => $this->baseTmpPath, 'file' => $fileId, 'path' => $this->basePath]);
+ $uploader->expects($this->atLeastOnce())->method('checkMimeType')->with($allowedMimeTypes)->willReturn(true);
$storeMock = $this->createPartialMock(
\Magento\Store\Model\Store::class,
['getBaseUrl']
diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Category/Flat/Plugin/IndexerConfigDataTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Category/Flat/Plugin/IndexerConfigDataTest.php
index 56bd04594018c..f69cbeb91631f 100644
--- a/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Category/Flat/Plugin/IndexerConfigDataTest.php
+++ b/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Category/Flat/Plugin/IndexerConfigDataTest.php
@@ -48,6 +48,9 @@ public function testAroundGet($isFlat, $path, $default, $inputData, $outputData)
$this->assertEquals($outputData, $this->model->afterGet($this->subjectMock, $inputData, $path, $default));
}
+ /**
+ * @return array
+ */
public function aroundGetDataProvider()
{
$flatIndexerData = [
diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Category/Flat/StateTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Category/Flat/StateTest.php
index 3beb9a3ffb773..6916cef2dfa61 100644
--- a/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Category/Flat/StateTest.php
+++ b/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Category/Flat/StateTest.php
@@ -102,6 +102,9 @@ public function testIsAvailable($isAvailable, $isFlatEnabled, $isValid, $result)
$this->assertEquals($result, $this->model->isAvailable());
}
+ /**
+ * @return array
+ */
public function isAvailableDataProvider()
{
return [
diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Category/Flat/System/Config/ModeTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Category/Flat/System/Config/ModeTest.php
index 3b3941d116fde..fb02b80a60175 100644
--- a/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Category/Flat/System/Config/ModeTest.php
+++ b/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Category/Flat/System/Config/ModeTest.php
@@ -57,6 +57,9 @@ protected function setUp()
);
}
+ /**
+ * @return array
+ */
public function dataProviderProcessValueEqual()
{
return [['0', '0'], ['', '0'], ['0', ''], ['1', '1']];
@@ -92,6 +95,9 @@ public function testProcessValueEqual($oldValue, $value)
$this->model->processValue();
}
+ /**
+ * @return array
+ */
public function dataProviderProcessValueOn()
{
return [['0', '1'], ['', '1']];
@@ -143,6 +149,9 @@ public function testProcessValueOn($oldValue, $value)
$this->model->processValue();
}
+ /**
+ * @return array
+ */
public function dataProviderProcessValueOff()
{
return [['1', '0'], ['1', '']];
diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Category/Product/Plugin/StoreGroupTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Category/Product/Plugin/StoreGroupTest.php
index 8310f3692d966..e134407d547ac 100644
--- a/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Category/Product/Plugin/StoreGroupTest.php
+++ b/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Category/Product/Plugin/StoreGroupTest.php
@@ -89,6 +89,9 @@ public function testBeforeAndAfterSaveNotNew($valueMap)
$this->assertSame($this->subject, $this->model->afterSave($this->subject, $this->subject, $this->groupMock));
}
+ /**
+ * @return array
+ */
public function changedDataProvider()
{
return [
diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Category/Product/Plugin/StoreViewTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Category/Product/Plugin/StoreViewTest.php
index 6e3cd6ed30b52..4da831f5257d0 100644
--- a/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Category/Product/Plugin/StoreViewTest.php
+++ b/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Category/Product/Plugin/StoreViewTest.php
@@ -33,6 +33,11 @@ class StoreViewTest extends \PHPUnit\Framework\TestCase
*/
protected $indexerRegistryMock;
+ /**
+ * @var \Magento\Catalog\Model\Indexer\Category\Product\TableMaintainer|\PHPUnit_Framework_MockObject_MockObject
+ */
+ protected $tableMaintainer;
+
/**
* @var \PHPUnit_Framework_MockObject_MockObject
*/
@@ -51,15 +56,30 @@ protected function setUp()
);
$this->subject = $this->createMock(Group::class);
$this->indexerRegistryMock = $this->createPartialMock(IndexerRegistry::class, ['get']);
- $this->storeMock = $this->createPartialMock(Store::class, ['isObjectNew', 'dataHasChangedFor', '__wakeup']);
+ $this->storeMock = $this->createPartialMock(
+ Store::class,
+ [
+ 'isObjectNew',
+ 'getId',
+ 'dataHasChangedFor',
+ '__wakeup'
+ ]
+ );
+ $this->tableMaintainer = $this->createPartialMock(
+ \Magento\Catalog\Model\Indexer\Category\Product\TableMaintainer::class,
+ [
+ 'createTablesForStore'
+ ]
+ );
- $this->model = new StoreView($this->indexerRegistryMock);
+ $this->model = new StoreView($this->indexerRegistryMock, $this->tableMaintainer);
}
public function testAroundSaveNewObject()
{
$this->mockIndexerMethods();
- $this->storeMock->expects($this->once())->method('isObjectNew')->willReturn(true);
+ $this->storeMock->expects($this->atLeastOnce())->method('isObjectNew')->willReturn(true);
+ $this->storeMock->expects($this->atLeastOnce())->method('getId')->willReturn(1);
$this->model->beforeSave($this->subject, $this->storeMock);
$this->assertSame($this->subject, $this->model->afterSave($this->subject, $this->subject, $this->storeMock));
}
diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Product/Eav/AbstractActionTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Product/Eav/AbstractActionTest.php
index 58654136ab5a8..3f86b577d0213 100644
--- a/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Product/Eav/AbstractActionTest.php
+++ b/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Product/Eav/AbstractActionTest.php
@@ -22,6 +22,11 @@ class AbstractActionTest extends \PHPUnit\Framework\TestCase
*/
protected $_eavSourceFactoryMock;
+ /**
+ * @var \Magento\Framework\App\Config\ScopeConfigInterface|\PHPUnit_Framework_MockObject_MockObject
+ */
+ private $scopeConfig;
+
protected function setUp()
{
$this->_eavDecimalFactoryMock = $this->createPartialMock(
@@ -32,9 +37,16 @@ protected function setUp()
\Magento\Catalog\Model\ResourceModel\Product\Indexer\Eav\SourceFactory::class,
['create']
);
+ $this->scopeConfig = $this->getMockBuilder(\Magento\Framework\App\Config\ScopeConfigInterface::class)
+ ->disableOriginalConstructor()
+ ->getMockForAbstractClass();
$this->_model = $this->getMockForAbstractClass(
\Magento\Catalog\Model\Indexer\Product\Eav\AbstractAction::class,
- [$this->_eavDecimalFactoryMock, $this->_eavSourceFactoryMock, []]
+ [
+ $this->_eavDecimalFactoryMock,
+ $this->_eavSourceFactoryMock,
+ $this->scopeConfig
+ ]
);
}
@@ -110,14 +122,27 @@ public function testReindexWithoutArgumentsExecutesReindexAll()
->method('create')
->will($this->returnValue($eavDecimal));
+ $this->scopeConfig->expects($this->once())
+ ->method('getValue')
+ ->willReturn(1);
+
$this->_model->reindex();
}
- public function testReindexWithNotNullArgumentExecutesReindexEntities()
- {
- $childIds = [1, 2, 3];
- $parentIds = [4];
- $reindexIds = array_merge($childIds, $parentIds);
+ /**
+ * @param array $ids
+ * @param array $parentIds
+ * @param array $childIds
+ * @throws \Exception
+ * @dataProvider reindexEntitiesDataProvider
+ */
+ public function testReindexWithNotNullArgumentExecutesReindexEntities(
+ array $ids,
+ array $parentIds,
+ array $childIds
+ ) {
+ $reindexIds = array_unique(array_merge($ids, $parentIds, $childIds));
+
$connectionMock = $this->getMockBuilder(\Magento\Framework\DB\Adapter\AdapterInterface::class)
->getMockForAbstractClass();
@@ -129,11 +154,23 @@ public function testReindexWithNotNullArgumentExecutesReindexEntities()
->disableOriginalConstructor()
->getMock();
- $eavSource->expects($this->once())->method('getRelationsByChild')->with($childIds)->willReturn($childIds);
- $eavSource->expects($this->once())->method('getRelationsByParent')->with($childIds)->willReturn($parentIds);
+ $eavSource->expects($this->once())
+ ->method('getRelationsByChild')
+ ->with($ids)
+ ->willReturn($parentIds);
+ $eavSource->expects($this->once())
+ ->method('getRelationsByParent')
+ ->with(array_unique(array_merge($parentIds, $ids)))
+ ->willReturn($childIds);
- $eavDecimal->expects($this->once())->method('getRelationsByChild')->with($reindexIds)->willReturn($reindexIds);
- $eavDecimal->expects($this->once())->method('getRelationsByParent')->with($reindexIds)->willReturn([]);
+ $eavDecimal->expects($this->once())
+ ->method('getRelationsByChild')
+ ->with($reindexIds)
+ ->willReturn($parentIds);
+ $eavDecimal->expects($this->once())
+ ->method('getRelationsByParent')
+ ->with(array_unique(array_merge($parentIds, $reindexIds)))
+ ->willReturn($childIds);
$eavSource->expects($this->once())->method('getConnection')->willReturn($connectionMock);
$eavDecimal->expects($this->once())->method('getConnection')->willReturn($connectionMock);
@@ -153,6 +190,34 @@ public function testReindexWithNotNullArgumentExecutesReindexEntities()
->method('create')
->will($this->returnValue($eavDecimal));
- $this->_model->reindex($childIds);
+ $this->scopeConfig->expects($this->once())
+ ->method('getValue')
+ ->willReturn(1);
+
+ $this->_model->reindex($ids);
+ }
+
+ public function testReindexWithDisabledEavIndexer()
+ {
+ $this->scopeConfig->expects($this->once())
+ ->method('getValue')
+ ->willReturn(0);
+
+ $this->_eavSourceFactoryMock->expects($this->never())->method('create');
+ $this->_eavDecimalFactoryMock->expects($this->never())->method('create');
+
+ $this->_model->reindex();
+ }
+
+ /**
+ * @return array
+ */
+ public function reindexEntitiesDataProvider()
+ {
+ return [
+ [[4], [], [1, 2, 3]],
+ [[3], [4], []],
+ [[5], [], []]
+ ];
}
}
diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Product/Eav/Action/FullTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Product/Eav/Action/FullTest.php
index eb5fdabe53303..cf9e83ed39650 100644
--- a/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Product/Eav/Action/FullTest.php
+++ b/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Product/Eav/Action/FullTest.php
@@ -5,52 +5,84 @@
*/
namespace Magento\Catalog\Test\Unit\Model\Indexer\Product\Eav\Action;
+use Magento\Catalog\Model\ResourceModel\Indexer\ActiveTableSwitcher;
+use Magento\Framework\TestFramework\Unit\Helper\ObjectManager;
+use Magento\Catalog\Model\ResourceModel\Product\Indexer\Eav\DecimalFactory;
+use Magento\Catalog\Model\ResourceModel\Product\Indexer\Eav\SourceFactory;
+use Magento\Framework\EntityManager\MetadataPool;
+use Magento\Framework\Indexer\BatchProviderInterface;
+use Magento\Catalog\Model\ResourceModel\Product\Indexer\Eav\BatchSizeCalculator;
+
/**
* @SuppressWarnings(PHPMD.CouplingBetweenObjects)
*/
class FullTest extends \PHPUnit\Framework\TestCase
{
- public function testExecuteWithAdapterErrorThrowsException()
- {
- $eavDecimalFactory = $this->createPartialMock(
- \Magento\Catalog\Model\ResourceModel\Product\Indexer\Eav\DecimalFactory::class,
- ['create']
- );
- $eavSourceFactory = $this->createPartialMock(
- \Magento\Catalog\Model\ResourceModel\Product\Indexer\Eav\SourceFactory::class,
- ['create']
- );
+ /**
+ * @var \Magento\Catalog\Model\Indexer\Product\Eav\Action\Full|\PHPUnit_Framework_MockObject_MockObject
+ */
+ private $model;
- $exceptionMessage = 'exception message';
- $exception = new \Exception($exceptionMessage);
+ /**
+ * @var DecimalFactory|\PHPUnit_Framework_MockObject_MockObject
+ */
+ private $eavDecimalFactory;
- $eavDecimalFactory->expects($this->once())
- ->method('create')
- ->will($this->throwException($exception));
+ /**
+ * @var SourceFactory|\PHPUnit_Framework_MockObject_MockObject
+ */
+ private $eavSourceFactory;
- $metadataMock = $this->createMock(\Magento\Framework\EntityManager\MetadataPool::class);
- $batchProviderMock = $this->createMock(\Magento\Framework\Indexer\BatchProviderInterface::class);
+ /**
+ * @var MetadataPool|\PHPUnit_Framework_MockObject_MockObject
+ */
+ private $metadataPool;
- $batchManagementMock = $this->createMock(
- \Magento\Catalog\Model\ResourceModel\Product\Indexer\Eav\BatchSizeCalculator::class
- );
+ /**
+ * @var BatchProviderInterface|\PHPUnit_Framework_MockObject_MockObject
+ */
+ private $batchProvider;
- $tableSwitcherMock = $this->getMockBuilder(
- \Magento\Catalog\Model\ResourceModel\Indexer\ActiveTableSwitcher::class
- )->disableOriginalConstructor()->getMock();
-
- $model = new \Magento\Catalog\Model\Indexer\Product\Eav\Action\Full(
- $eavDecimalFactory,
- $eavSourceFactory,
- $metadataMock,
- $batchProviderMock,
- $batchManagementMock,
- $tableSwitcherMock
- );
+ /**
+ * @var BatchSizeCalculator|\PHPUnit_Framework_MockObject_MockObject
+ */
+ private $batchSizeCalculator;
- $this->expectException(\Magento\Framework\Exception\LocalizedException::class, $exceptionMessage);
+ /**
+ * @var ActiveTableSwitcher|\PHPUnit_Framework_MockObject_MockObject
+ */
+ private $activeTableSwitcher;
- $model->execute();
+ /**
+ * @var \Magento\Framework\App\Config\ScopeConfigInterface|\PHPUnit_Framework_MockObject_MockObject
+ */
+ private $scopeConfig;
+
+ protected function setUp()
+ {
+ $this->eavDecimalFactory = $this->createPartialMock(DecimalFactory::class, ['create']);
+ $this->eavSourceFactory = $this->createPartialMock(SourceFactory::class, ['create']);
+ $this->metadataPool = $this->createMock(MetadataPool::class);
+ $this->batchProvider = $this->getMockForAbstractClass(BatchProviderInterface::class);
+ $this->batchSizeCalculator = $this->createMock(BatchSizeCalculator::class);
+ $this->activeTableSwitcher = $this->createMock(ActiveTableSwitcher::class);
+ $this->scopeConfig = $this->getMockBuilder(\Magento\Framework\App\Config\ScopeConfigInterface::class)
+ ->disableOriginalConstructor()
+ ->getMockForAbstractClass();
+
+ $objectManager = new ObjectManager($this);
+ $this->model = $objectManager->getObject(
+ \Magento\Catalog\Model\Indexer\Product\Eav\Action\Full::class,
+ [
+ 'eavDecimalFactory' => $this->eavDecimalFactory,
+ 'eavSourceFactory' => $this->eavSourceFactory,
+ 'metadataPool' => $this->metadataPool,
+ 'batchProvider' => $this->batchProvider,
+ 'batchSizeCalculator' => $this->batchSizeCalculator,
+ 'activeTableSwitcher' => $this->activeTableSwitcher,
+ 'scopeConfig' => $this->scopeConfig
+ ]
+ );
}
/**
@@ -58,14 +90,7 @@ public function testExecuteWithAdapterErrorThrowsException()
*/
public function testExecute()
{
- $eavDecimalFactory = $this->createPartialMock(
- \Magento\Catalog\Model\ResourceModel\Product\Indexer\Eav\DecimalFactory::class,
- ['create']
- );
- $eavSourceFactory = $this->createPartialMock(
- \Magento\Catalog\Model\ResourceModel\Product\Indexer\Eav\SourceFactory::class,
- ['create']
- );
+ $this->scopeConfig->expects($this->once())->method('getValue')->willReturn(1);
$ids = [1, 2, 3];
$connectionMock = $this->getMockBuilder(\Magento\Framework\DB\Adapter\AdapterInterface::class)
@@ -89,42 +114,29 @@ public function testExecute()
$eavSource->expects($this->atLeastOnce())->method('getConnection')->willReturn($connectionMock);
$eavDecimal->expects($this->atLeastOnce())->method('getConnection')->willReturn($connectionMock);
- $eavDecimal->expects($this->once())
- ->method('reindexEntities')
- ->with($ids);
+ $eavDecimal->expects($this->once())->method('reindexEntities')->with($ids);
- $eavSource->expects($this->once())
- ->method('reindexEntities')
- ->with($ids);
+ $eavSource->expects($this->once())->method('reindexEntities')->with($ids);
- $eavDecimalFactory->expects($this->once())
- ->method('create')
- ->will($this->returnValue($eavSource));
+ $this->eavDecimalFactory->expects($this->once())->method('create')->will($this->returnValue($eavSource));
- $eavSourceFactory->expects($this->once())
- ->method('create')
- ->will($this->returnValue($eavDecimal));
+ $this->eavSourceFactory->expects($this->once())->method('create')->will($this->returnValue($eavDecimal));
- $metadataMock = $this->createMock(\Magento\Framework\EntityManager\MetadataPool::class);
$entityMetadataMock = $this->getMockBuilder(\Magento\Framework\EntityManager\EntityMetadataInterface::class)
->getMockForAbstractClass();
- $metadataMock->expects($this->atLeastOnce())
+ $this->metadataPool->expects($this->atLeastOnce())
->method('getMetadata')
->with(\Magento\Catalog\Api\Data\ProductInterface::class)
->willReturn($entityMetadataMock);
- $batchProviderMock = $this->createMock(\Magento\Framework\Indexer\BatchProviderInterface::class);
- $batchProviderMock->expects($this->atLeastOnce())
+ $this->batchProvider->expects($this->atLeastOnce())
->method('getBatches')
->willReturn([['from' => 10, 'to' => 100]]);
- $batchProviderMock->expects($this->atLeastOnce())
+ $this->batchProvider->expects($this->atLeastOnce())
->method('getBatchIds')
->willReturn($ids);
- $batchManagementMock = $this->createMock(
- \Magento\Catalog\Model\ResourceModel\Product\Indexer\Eav\BatchSizeCalculator::class
- );
$selectMock = $this->getMockBuilder(\Magento\Framework\DB\Select::class)
->disableOriginalConstructor()
->getMock();
@@ -133,19 +145,13 @@ public function testExecute()
$selectMock->expects($this->atLeastOnce())->method('distinct')->willReturnSelf();
$selectMock->expects($this->atLeastOnce())->method('from')->willReturnSelf();
- $tableSwitcherMock = $this->getMockBuilder(
- \Magento\Catalog\Model\ResourceModel\Indexer\ActiveTableSwitcher::class
- )->disableOriginalConstructor()->getMock();
-
- $model = new \Magento\Catalog\Model\Indexer\Product\Eav\Action\Full(
- $eavDecimalFactory,
- $eavSourceFactory,
- $metadataMock,
- $batchProviderMock,
- $batchManagementMock,
- $tableSwitcherMock
- );
+ $this->model->execute();
+ }
- $model->execute();
+ public function testExecuteWithDisabledEavIndexer()
+ {
+ $this->scopeConfig->expects($this->once())->method('getValue')->willReturn(0);
+ $this->metadataPool->expects($this->never())->method('getMetadata');
+ $this->model->execute();
}
}
diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Product/Flat/Action/RowTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Product/Flat/Action/RowTest.php
index 41b3d36227431..f095867ed5c39 100644
--- a/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Product/Flat/Action/RowTest.php
+++ b/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Product/Flat/Action/RowTest.php
@@ -8,8 +8,13 @@
namespace Magento\Catalog\Test\Unit\Model\Indexer\Product\Flat\Action;
+use Magento\Catalog\Api\Data\ProductInterface;
use Magento\Framework\TestFramework\Unit\Helper\ObjectManager;
+use PHPUnit_Framework_MockObject_MockObject as MockObject;
+/**
+ * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
+ */
class RowTest extends \PHPUnit\Framework\TestCase
{
/**
@@ -18,72 +23,112 @@ class RowTest extends \PHPUnit\Framework\TestCase
protected $model;
/**
- * @var \PHPUnit_Framework_MockObject_MockObject
+ * @var MockObject
*/
protected $storeManager;
/**
- * @var \PHPUnit_Framework_MockObject_MockObject
+ * @var MockObject
*/
protected $store;
/**
- * @var \PHPUnit_Framework_MockObject_MockObject
+ * @var MockObject
*/
protected $productIndexerHelper;
/**
- * @var \PHPUnit_Framework_MockObject_MockObject
+ * @var MockObject
*/
protected $resource;
/**
- * @var \PHPUnit_Framework_MockObject_MockObject
+ * @var MockObject
*/
protected $connection;
/**
- * @var \PHPUnit_Framework_MockObject_MockObject
+ * @var MockObject
*/
protected $flatItemWriter;
/**
- * @var \PHPUnit_Framework_MockObject_MockObject
+ * @var MockObject
*/
protected $flatItemEraser;
/**
- * @var \PHPUnit_Framework_MockObject_MockObject
+ * @var MockObject
*/
protected $flatTableBuilder;
+ /**
+ * @inheritdoc
+ */
protected function setUp()
{
$objectManager = new ObjectManager($this);
+ $attributeTable = 'catalog_product_entity_int';
+ $statusId = 22;
$this->connection = $this->createMock(\Magento\Framework\DB\Adapter\AdapterInterface::class);
$this->resource = $this->createMock(\Magento\Framework\App\ResourceConnection::class);
$this->resource->expects($this->any())->method('getConnection')
->with('default')
- ->will($this->returnValue($this->connection));
+ ->willReturn($this->connection);
$this->storeManager = $this->createMock(\Magento\Store\Model\StoreManagerInterface::class);
$this->store = $this->createMock(\Magento\Store\Model\Store::class);
- $this->store->expects($this->any())->method('getId')->will($this->returnValue('store_id_1'));
- $this->storeManager->expects($this->any())->method('getStores')->will($this->returnValue([$this->store]));
- $this->productIndexerHelper = $this->createMock(\Magento\Catalog\Helper\Product\Flat\Indexer::class);
+ $this->store->expects($this->any())->method('getId')->willReturn('store_id_1');
+ $this->storeManager->expects($this->any())->method('getStores')->willReturn([$this->store]);
$this->flatItemEraser = $this->createMock(\Magento\Catalog\Model\Indexer\Product\Flat\Action\Eraser::class);
$this->flatItemWriter = $this->createMock(\Magento\Catalog\Model\Indexer\Product\Flat\Action\Indexer::class);
$this->flatTableBuilder = $this->createMock(\Magento\Catalog\Model\Indexer\Product\Flat\FlatTableBuilder::class);
+ $this->productIndexerHelper = $this->createMock(\Magento\Catalog\Helper\Product\Flat\Indexer::class);
+ $statusAttributeMock = $this->getMockBuilder(\Magento\Eav\Model\Entity\Attribute::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->productIndexerHelper->expects($this->any())->method('getAttribute')
+ ->with('status')
+ ->willReturn($statusAttributeMock);
+ $backendMock = $this->getMockBuilder(\Magento\Eav\Model\Entity\Attribute\Backend\AbstractBackend::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $backendMock->expects($this->any())->method('getTable')->willReturn($attributeTable);
+ $statusAttributeMock->expects($this->any())->method('getBackend')->willReturn($backendMock);
+ $statusAttributeMock->expects($this->any())->method('getId')->willReturn($statusId);
+ $selectMock = $this->getMockBuilder(\Magento\Framework\DB\Select::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->connection->expects($this->any())->method('select')->willReturn($selectMock);
+ $selectMock->expects($this->any())->method('from')->with(
+ $attributeTable,
+ ['value']
+ )->willReturnSelf();
+ $selectMock->expects($this->any())->method('where')->willReturnSelf();
+ $selectMock->expects($this->any())->method('order')->willReturnSelf();
+ $selectMock->expects($this->any())->method('limit')->willReturnSelf();
+ $pdoMock = $this->createMock(\Zend_Db_Statement_Pdo::class);
+ $this->connection->expects($this->any())->method('query')->with($selectMock)->willReturn($pdoMock);
+ $pdoMock->expects($this->any())->method('fetchColumn')->willReturn('1');
+
+ $metadataPool = $this->createMock(\Magento\Framework\EntityManager\MetadataPool::class);
+ $productMetadata = $this->getMockBuilder(\Magento\Framework\EntityManager\EntityMetadataInterface::class)
+ ->getMockForAbstractClass();
+ $metadataPool->expects($this->any())->method('getMetadata')->with(ProductInterface::class)
+ ->willReturn($productMetadata);
+ $productMetadata->expects($this->any())->method('getLinkField')->willReturn('entity_id');
$this->model = $objectManager->getObject(
\Magento\Catalog\Model\Indexer\Product\Flat\Action\Row::class, [
- 'resource' => $this->resource,
- 'storeManager' => $this->storeManager,
- 'productHelper' => $this->productIndexerHelper,
- 'flatItemEraser' => $this->flatItemEraser,
- 'flatItemWriter' => $this->flatItemWriter,
- 'flatTableBuilder' => $this->flatTableBuilder
+ 'resource' => $this->resource,
+ 'storeManager' => $this->storeManager,
+ 'productHelper' => $this->productIndexerHelper,
+ 'flatItemEraser' => $this->flatItemEraser,
+ 'flatItemWriter' => $this->flatItemWriter,
+ 'flatTableBuilder' => $this->flatTableBuilder,
]);
+
+ $objectManager->setBackwardCompatibleProperty($this->model, 'metadataPool', $metadataPool);
}
/**
@@ -98,9 +143,9 @@ public function testExecuteWithEmptyId()
public function testExecuteWithNonExistingFlatTablesCreatesTables()
{
$this->productIndexerHelper->expects($this->any())->method('getFlatTableName')
- ->will($this->returnValue('store_flat_table'));
+ ->willReturn('store_flat_table');
$this->connection->expects($this->any())->method('isTableExists')->with('store_flat_table')
- ->will($this->returnValue(false));
+ ->willReturn(false);
$this->flatItemEraser->expects($this->never())->method('removeDeletedProducts');
$this->flatTableBuilder->expects($this->once())->method('build')->with('store_id_1', ['product_id_1']);
$this->flatItemWriter->expects($this->once())->method('write')->with('store_id_1', 'product_id_1');
@@ -110,12 +155,11 @@ public function testExecuteWithNonExistingFlatTablesCreatesTables()
public function testExecuteWithExistingFlatTablesCreatesTables()
{
$this->productIndexerHelper->expects($this->any())->method('getFlatTableName')
- ->will($this->returnValue('store_flat_table'));
+ ->willReturn('store_flat_table');
$this->connection->expects($this->any())->method('isTableExists')->with('store_flat_table')
- ->will($this->returnValue(true));
+ ->willReturn(true);
$this->flatItemEraser->expects($this->once())->method('removeDeletedProducts');
$this->flatTableBuilder->expects($this->never())->method('build')->with('store_id_1', ['product_id_1']);
$this->model->execute('product_id_1');
}
}
-
diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Product/Flat/ProcessorTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Product/Flat/ProcessorTest.php
index 8c63d77b74f53..d30a8da0e77a2 100644
--- a/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Product/Flat/ProcessorTest.php
+++ b/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Product/Flat/ProcessorTest.php
@@ -129,6 +129,9 @@ public function testReindexRow(
$this->_model->reindexRow(1, $forceReindex);
}
+ /**
+ * @return array
+ */
public function dataProviderReindexRow()
{
return [
@@ -198,6 +201,9 @@ public function testReindexList(
$this->_model->reindexList([1], $forceReindex);
}
+ /**
+ * @return array
+ */
public function dataProviderReindexList()
{
return [
diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Product/Flat/System/Config/ModeTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Product/Flat/System/Config/ModeTest.php
index ca67185203738..34cc5c70418b9 100644
--- a/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Product/Flat/System/Config/ModeTest.php
+++ b/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Product/Flat/System/Config/ModeTest.php
@@ -50,6 +50,9 @@ protected function setUp()
);
}
+ /**
+ * @return array
+ */
public function dataProviderProcessValueEqual()
{
return [['0', '0'], ['', '0'], ['0', ''], ['1', '1']];
@@ -84,6 +87,9 @@ public function testProcessValueEqual($oldValue, $value)
$this->model->processValue();
}
+ /**
+ * @return array
+ */
public function dataProviderProcessValueOn()
{
return [['0', '1'], ['', '1']];
@@ -134,6 +140,9 @@ public function testProcessValueOn($oldValue, $value)
$this->model->processValue();
}
+ /**
+ * @return array
+ */
public function dataProviderProcessValueOff()
{
return [['1', '0'], ['1', '']];
diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Product/Price/Plugin/WebsiteTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Product/Price/Plugin/WebsiteTest.php
index d551822d975ea..1a5fea5e12769 100644
--- a/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Product/Price/Plugin/WebsiteTest.php
+++ b/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Product/Price/Plugin/WebsiteTest.php
@@ -5,43 +5,179 @@
*/
namespace Magento\Catalog\Test\Unit\Model\Indexer\Product\Price\Plugin;
+use Magento\Catalog\Model\Indexer\Product\Price\DimensionModeConfiguration;
+
class WebsiteTest extends \PHPUnit\Framework\TestCase
{
/**
* @var \Magento\Framework\TestFramework\Unit\Helper\ObjectManager
*/
- protected $_objectManager;
+ protected $objectManager;
/**
* @var \Magento\Catalog\Model\Indexer\Product\Price\Plugin\Website
*/
- protected $_model;
+ protected $model;
+
+ /**
+ * @var \Magento\Framework\Indexer\DimensionFactory|\PHPUnit_Framework_MockObject_MockObject
+ */
+ protected $dimensionFactory;
+
+ /**
+ * @var \Magento\Catalog\Model\Indexer\Product\Price\TableMaintainer|\PHPUnit_Framework_MockObject_MockObject
+ */
+ protected $tableMaintainer;
/**
- * @var \Magento\Catalog\Model\Indexer\Product\Price\Processor|\PHPUnit_Framework_MockObject_MockObject
+ * @var DimensionModeConfiguration|\PHPUnit_Framework_MockObject_MockObject
*/
- protected $_priceProcessorMock;
+ protected $dimensionModeConfiguration;
protected function setUp()
{
- $this->_objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this);
+ $this->objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this);
- $this->_priceProcessorMock = $this->createPartialMock(
- \Magento\Catalog\Model\Indexer\Product\Price\Processor::class,
- ['markIndexerAsInvalid']
+ $this->dimensionFactory = $this->createPartialMock(
+ \Magento\Framework\Indexer\DimensionFactory::class,
+ ['create']
);
- $this->_model = $this->_objectManager->getObject(
+ $this->tableMaintainer = $this->createPartialMock(
+ \Magento\Catalog\Model\Indexer\Product\Price\TableMaintainer::class,
+ ['dropTablesForDimensions', 'createTablesForDimensions']
+ );
+
+ $this->dimensionModeConfiguration = $this->createPartialMock(
+ DimensionModeConfiguration::class,
+ ['getDimensionConfiguration']
+ );
+
+ $this->model = $this->objectManager->getObject(
\Magento\Catalog\Model\Indexer\Product\Price\Plugin\Website::class,
- ['processor' => $this->_priceProcessorMock]
+ [
+ 'dimensionFactory' => $this->dimensionFactory,
+ 'tableMaintainer' => $this->tableMaintainer,
+ 'dimensionModeConfiguration' => $this->dimensionModeConfiguration,
+ ]
);
}
public function testAfterDelete()
{
- $this->_priceProcessorMock->expects($this->once())->method('markIndexerAsInvalid');
+ $dimensionMock = $this->createMock(\Magento\Framework\Indexer\Dimension::class);
+
+ $this->dimensionFactory->expects($this->once())->method('create')->willReturn(
+ $dimensionMock
+ );
+ $this->tableMaintainer->expects($this->once())->method('dropTablesForDimensions')->with(
+ [$dimensionMock]
+ );
+
+ $this->dimensionModeConfiguration->expects($this->once())->method('getDimensionConfiguration')->willReturn(
+ [\Magento\Store\Model\Indexer\WebsiteDimensionProvider::DIMENSION_NAME]
+ );
+
+ $subjectMock = $this->createMock(\Magento\Framework\Model\ResourceModel\Db\AbstractDb::class);
+ $objectResourceMock = $this->createMock(\Magento\Framework\Model\ResourceModel\Db\AbstractDb::class);
+ $websiteMock = $this->createMock(\Magento\Framework\Model\AbstractModel::class);
+ $websiteMock->expects($this->once())
+ ->method('getId')
+ ->willReturn(1);
+
+ $this->assertEquals(
+ $objectResourceMock,
+ $this->model->afterDelete($subjectMock, $objectResourceMock, $websiteMock)
+ );
+ }
+
+ public function testAfterDeleteOnModeWithoutWebsiteDimension()
+ {
+ $dimensionMock = $this->createMock(\Magento\Framework\Indexer\Dimension::class);
+
+ $this->dimensionFactory->expects($this->never())->method('create')->willReturn(
+ $dimensionMock
+ );
+ $this->tableMaintainer->expects($this->never())->method('dropTablesForDimensions')->with(
+ [$dimensionMock]
+ );
+
+ $this->dimensionModeConfiguration->expects($this->once())->method('getDimensionConfiguration')->willReturn(
+ []
+ );
+
+ $subjectMock = $this->createMock(\Magento\Framework\Model\ResourceModel\Db\AbstractDb::class);
+ $objectResourceMock = $this->createMock(\Magento\Framework\Model\ResourceModel\Db\AbstractDb::class);
+ $websiteMock = $this->createMock(\Magento\Framework\Model\AbstractModel::class);
+ $websiteMock->expects($this->once())
+ ->method('getId')
+ ->willReturn(1);
+
+ $this->assertEquals(
+ $objectResourceMock,
+ $this->model->afterDelete($subjectMock, $objectResourceMock, $websiteMock)
+ );
+ }
+
+ public function testAfterSave()
+ {
+ $dimensionMock = $this->createMock(\Magento\Framework\Indexer\Dimension::class);
+
+ $this->dimensionFactory->expects($this->once())->method('create')->willReturn(
+ $dimensionMock
+ );
+ $this->tableMaintainer->expects($this->once())->method('createTablesForDimensions')->with(
+ [$dimensionMock]
+ );
+
+ $this->dimensionModeConfiguration->expects($this->once())->method('getDimensionConfiguration')->willReturn(
+ [\Magento\Store\Model\Indexer\WebsiteDimensionProvider::DIMENSION_NAME]
+ );
+
+ $subjectMock = $this->createMock(\Magento\Framework\Model\ResourceModel\Db\AbstractDb::class);
+ $objectResourceMock = $this->createMock(\Magento\Framework\Model\ResourceModel\Db\AbstractDb::class);
+ $websiteMock = $this->createMock(\Magento\Framework\Model\AbstractModel::class);
+ $websiteMock->expects($this->once())
+ ->method('getId')
+ ->willReturn(1);
+ $websiteMock->expects($this->once())
+ ->method('isObjectNew')
+ ->willReturn(true);
- $websiteMock = $this->createMock(\Magento\Store\Model\ResourceModel\Website::class);
- $this->assertEquals('return_value', $this->_model->afterDelete($websiteMock, 'return_value'));
+ $this->assertEquals(
+ $objectResourceMock,
+ $this->model->afterSave($subjectMock, $objectResourceMock, $websiteMock)
+ );
+ }
+
+ public function testAfterSaveOnModeWithoutWebsiteDimension()
+ {
+ $dimensionMock = $this->createMock(\Magento\Framework\Indexer\Dimension::class);
+
+ $this->dimensionFactory->expects($this->never())->method('create')->willReturn(
+ $dimensionMock
+ );
+ $this->tableMaintainer->expects($this->never())->method('createTablesForDimensions')->with(
+ [$dimensionMock]
+ );
+
+ $this->dimensionModeConfiguration->expects($this->once())->method('getDimensionConfiguration')->willReturn(
+ []
+ );
+
+ $subjectMock = $this->createMock(\Magento\Framework\Model\ResourceModel\Db\AbstractDb::class);
+ $objectResourceMock = $this->createMock(\Magento\Framework\Model\ResourceModel\Db\AbstractDb::class);
+ $websiteMock = $this->createMock(\Magento\Framework\Model\AbstractModel::class);
+ $websiteMock->expects($this->once())
+ ->method('getId')
+ ->willReturn(1);
+ $websiteMock->expects($this->once())
+ ->method('isObjectNew')
+ ->willReturn(true);
+
+ $this->assertEquals(
+ $objectResourceMock,
+ $this->model->afterSave($subjectMock, $objectResourceMock, $websiteMock)
+ );
}
}
diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Layer/Filter/DataProvider/CategoryTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Layer/Filter/DataProvider/CategoryTest.php
index 3e5daf1a98a9c..257a84e50248d 100644
--- a/app/code/Magento/Catalog/Test/Unit/Model/Layer/Filter/DataProvider/CategoryTest.php
+++ b/app/code/Magento/Catalog/Test/Unit/Model/Layer/Filter/DataProvider/CategoryTest.php
@@ -77,6 +77,9 @@ protected function setUp()
);
}
+ /**
+ * @return \Magento\Catalog\Model\Layer\Filter\DataProvider\Category
+ */
public function testGetCategoryWithAppliedId()
{
$storeId = 1234;
diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Layer/Filter/DataProvider/PriceTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Layer/Filter/DataProvider/PriceTest.php
index 3a23ebcdf4518..8ca23df31cdee 100644
--- a/app/code/Magento/Catalog/Test/Unit/Model/Layer/Filter/DataProvider/PriceTest.php
+++ b/app/code/Magento/Catalog/Test/Unit/Model/Layer/Filter/DataProvider/PriceTest.php
@@ -152,7 +152,7 @@ public function testGetMaxPrice()
$this->productCollection->expects($this->once())
->method('getMaxPrice')
->will($this->returnValue($maxPrice));
- $this->assertSame(floatval($maxPrice), $this->target->getMaxPrice());
+ $this->assertSame((float)$maxPrice, $this->target->getMaxPrice());
}
/**
@@ -165,6 +165,9 @@ public function testValidateFilter($filter, $expectedResult)
$this->assertSame($expectedResult, $this->target->validateFilter($filter));
}
+ /**
+ * @return array
+ */
public function validateFilterDataProvider()
{
return [
diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Product/ActionTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Product/ActionTest.php
index 8cf075f4d8504..a97e4650b49bd 100644
--- a/app/code/Magento/Catalog/Test/Unit/Model/Product/ActionTest.php
+++ b/app/code/Magento/Catalog/Test/Unit/Model/Product/ActionTest.php
@@ -164,6 +164,9 @@ public function testUpdateWebsites($type, $methodName)
$this->assertEquals($this->model->getDataByKey('action_type'), $type);
}
+ /**
+ * @return array
+ */
public function updateWebsitesDataProvider()
{
return [
diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Product/Attribute/Backend/GroupPrice/AbstractTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Product/Attribute/Backend/GroupPrice/AbstractTest.php
index 5963d8b161633..3003c2f8085e4 100644
--- a/app/code/Magento/Catalog/Test/Unit/Model/Product/Attribute/Backend/GroupPrice/AbstractTest.php
+++ b/app/code/Magento/Catalog/Test/Unit/Model/Product/Attribute/Backend/GroupPrice/AbstractTest.php
@@ -47,7 +47,7 @@ protected function setUp()
'scopeOverriddenValue' => $scopeOverriddenValue
]
);
- $resource = $this->createPartialMock(\StdClass::class, ['getMainTable']);
+ $resource = $this->createPartialMock(\stdClass::class, ['getMainTable']);
$resource->expects($this->any())->method('getMainTable')->will($this->returnValue('table'));
$this->_model->expects($this->any())->method('_getResource')->will($this->returnValue($resource));
diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Product/Attribute/Backend/TierpriceTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Product/Attribute/Backend/TierpriceTest.php
index ebcf76b739162..db103c3017e04 100644
--- a/app/code/Magento/Catalog/Test/Unit/Model/Product/Attribute/Backend/TierpriceTest.php
+++ b/app/code/Magento/Catalog/Test/Unit/Model/Product/Attribute/Backend/TierpriceTest.php
@@ -141,6 +141,10 @@ public function testSetPriceData()
{
$attributeName = 'tier_price';
$tierPrices = [
+ [
+ 'price' => 0,
+ 'all_groups' => 1,
+ ],
[
'price' => 10,
'all_groups' => 1,
@@ -153,6 +157,12 @@ public function testSetPriceData()
$productPrice = 20;
$allCustomersGroupId = 32000;
$finalTierPrices = [
+ [
+ 'price' => 0,
+ 'all_groups' => 1,
+ 'website_price' => 0,
+ 'cust_group' => 32000,
+ ],
[
'price' => 10,
'all_groups' => 1,
@@ -170,8 +180,11 @@ public function testSetPriceData()
->disableOriginalConstructor()->getMock();
$allCustomersGroup = $this->getMockBuilder(\Magento\Customer\Api\Data\GroupInterface::class)
->disableOriginalConstructor()->getMock();
- $this->groupManagement->expects($this->once())->method('getAllCustomersGroup')->willReturn($allCustomersGroup);
- $allCustomersGroup->expects($this->once())->method('getId')->willReturn($allCustomersGroupId);
+ $this->groupManagement
+ ->expects($this->exactly(2))
+ ->method('getAllCustomersGroup')
+ ->willReturn($allCustomersGroup);
+ $allCustomersGroup->expects($this->exactly(2))->method('getId')->willReturn($allCustomersGroupId);
$object->expects($this->once())->method('getPrice')->willReturn($productPrice);
$this->attribute->expects($this->atLeastOnce())->method('isScopeGlobal')->willReturn(true);
$object->expects($this->once())->method('getStoreId')->willReturn(null);
diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Product/Attribute/Frontend/ImageTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Product/Attribute/Frontend/ImageTest.php
index 115a333a38b5b..3ceedddc2b713 100644
--- a/app/code/Magento/Catalog/Test/Unit/Model/Product/Attribute/Frontend/ImageTest.php
+++ b/app/code/Magento/Catalog/Test/Unit/Model/Product/Attribute/Frontend/ImageTest.php
@@ -3,45 +3,71 @@
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
+
namespace Magento\Catalog\Test\Unit\Model\Product\Attribute\Frontend;
+use Magento\Catalog\Model\Product;
+use Magento\Catalog\Model\Product\Attribute\Frontend\Image;
+use Magento\Eav\Model\Entity\Attribute\AbstractAttribute;
use Magento\Framework\TestFramework\Unit\Helper\ObjectManager;
+use Magento\Store\Model\Store;
+use Magento\Store\Model\StoreManagerInterface;
+use PHPUnit\Framework\TestCase;
-class ImageTest extends \PHPUnit\Framework\TestCase
+class ImageTest extends TestCase
{
/**
- * @var \Magento\Catalog\Model\Product\Attribute\Frontend\Image
+ * @var Image
*/
private $model;
- public function testGetUrl()
+ /**
+ * @dataProvider getUrlDataProvider
+ * @param string $expectedImage
+ * @param string $productImage
+ */
+ public function testGetUrl(string $expectedImage, string $productImage)
+ {
+ $this->assertEquals($expectedImage, $this->model->getUrl($this->getMockedProduct($productImage)));
+ }
+
+ /**
+ * Data provider for testGetUrl
+ *
+ * @return array
+ */
+ public function getUrlDataProvider(): array
{
- $this->assertEquals('catalog/product/img.jpg', $this->model->getUrl($this->getMockedProduct()));
+ return [
+ ['catalog/product/img.jpg', 'img.jpg'],
+ ['catalog/product/img.jpg', '/img.jpg'],
+ ];
}
protected function setUp()
{
$helper = new ObjectManager($this);
$this->model = $helper->getObject(
- \Magento\Catalog\Model\Product\Attribute\Frontend\Image::class,
+ Image::class,
['storeManager' => $this->getMockedStoreManager()]
);
$this->model->setAttribute($this->getMockedAttribute());
}
/**
- * @return \Magento\Catalog\Model\Product
+ * @param string $productImage
+ * @return Product
*/
- private function getMockedProduct()
+ private function getMockedProduct(string $productImage): Product
{
- $mockBuilder = $this->getMockBuilder(\Magento\Catalog\Model\Product::class);
+ $mockBuilder = $this->getMockBuilder(Product::class);
$mock = $mockBuilder->setMethods(['getData', 'getStore', '__wakeup'])
->disableOriginalConstructor()
->getMock();
$mock->expects($this->any())
->method('getData')
- ->will($this->returnValue('img.jpg'));
+ ->will($this->returnValue($productImage));
$mock->expects($this->any())
->method('getStore');
@@ -50,13 +76,13 @@ private function getMockedProduct()
}
/**
- * @return \Magento\Store\Model\StoreManagerInterface
+ * @return StoreManagerInterface
*/
- private function getMockedStoreManager()
+ private function getMockedStoreManager(): StoreManagerInterface
{
$mockedStore = $this->getMockedStore();
- $mockBuilder = $this->getMockBuilder(\Magento\Store\Model\StoreManagerInterface::class);
+ $mockBuilder = $this->getMockBuilder(StoreManagerInterface::class);
$mock = $mockBuilder->setMethods(['getStore'])
->disableOriginalConstructor()
->getMockForAbstractClass();
@@ -69,11 +95,11 @@ private function getMockedStoreManager()
}
/**
- * @return \Magento\Store\Model\Store
+ * @return Store
*/
- private function getMockedStore()
+ private function getMockedStore(): Store
{
- $mockBuilder = $this->getMockBuilder(\Magento\Store\Model\Store::class);
+ $mockBuilder = $this->getMockBuilder(Store::class);
$mock = $mockBuilder->setMethods(['getBaseUrl', '__wakeup'])
->disableOriginalConstructor()
->getMockForAbstractClass();
@@ -86,11 +112,11 @@ private function getMockedStore()
}
/**
- * @return \Magento\Eav\Model\Entity\Attribute\AbstractAttribute
+ * @return AbstractAttribute
*/
- private function getMockedAttribute()
+ private function getMockedAttribute(): AbstractAttribute
{
- $mockBuilder = $this->getMockBuilder(\Magento\Eav\Model\Entity\Attribute\AbstractAttribute::class);
+ $mockBuilder = $this->getMockBuilder(AbstractAttribute::class);
$mockBuilder->setMethods(['getAttributeCode', '__wakeup']);
$mockBuilder->disableOriginalConstructor();
$mock = $mockBuilder->getMockForAbstractClass();
diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Product/Attribute/RepositoryTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Product/Attribute/RepositoryTest.php
index ca63ba4a30761..5efc60bebac40 100644
--- a/app/code/Magento/Catalog/Test/Unit/Model/Product/Attribute/RepositoryTest.php
+++ b/app/code/Magento/Catalog/Test/Unit/Model/Product/Attribute/RepositoryTest.php
@@ -12,7 +12,6 @@
use Magento\Catalog\Api\Data\ProductAttributeInterface;
use Magento\Catalog\Model\Product\Attribute\Repository;
use Magento\Catalog\Model\ResourceModel\Eav\Attribute;
-use Magento\Eav\Api\Data\AttributeFrontendLabelInterface;
/**
* @SuppressWarnings(PHPMD.CouplingBetweenObjects)
@@ -87,7 +86,10 @@ protected function setUp()
$this->eavConfigMock = $this->createMock(\Magento\Eav\Model\Config::class);
$this->eavConfigMock->expects($this->any())->method('getEntityType')
->willReturn(new \Magento\Framework\DataObject(['default_attribute_set_id' => 4]));
- $this->validatorFactoryMock = $this->createPartialMock(\Magento\Eav\Model\Adminhtml\System\Config\Source\Inputtype\ValidatorFactory::class, ['create']);
+ $this->validatorFactoryMock = $this->createPartialMock(
+ \Magento\Eav\Model\Adminhtml\System\Config\Source\Inputtype\ValidatorFactory::class,
+ ['create']
+ );
$this->searchCriteriaBuilderMock =
$this->createMock(\Magento\Framework\Api\SearchCriteriaBuilder::class);
$this->searchResultMock =
@@ -210,7 +212,10 @@ public function testSaveNoSuchEntityException()
*/
public function testSaveInputExceptionRequiredField()
{
- $attributeMock = $this->createPartialMock(\Magento\Catalog\Model\ResourceModel\Eav\Attribute::class, ['getFrontendLabels', 'getDefaultFrontendLabel', '__wakeup', 'getAttributeId', 'setAttributeId']);
+ $attributeMock = $this->createPartialMock(
+ \Magento\Catalog\Model\ResourceModel\Eav\Attribute::class,
+ ['getFrontendLabels', 'getDefaultFrontendLabel', '__wakeup', 'getAttributeId', 'setAttributeId']
+ );
$attributeMock->expects($this->once())->method('getAttributeId')->willReturn(null);
$attributeMock->expects($this->once())->method('setAttributeId')->with(null)->willReturnSelf();
$attributeMock->expects($this->once())->method('getFrontendLabels')->willReturn(null);
@@ -225,12 +230,15 @@ public function testSaveInputExceptionRequiredField()
*/
public function testSaveInputExceptionInvalidFieldValue()
{
- $attributeMock = $this->createPartialMock(\Magento\Catalog\Model\ResourceModel\Eav\Attribute::class, ['getFrontendLabels', 'getDefaultFrontendLabel', 'getAttributeId', '__wakeup', 'setAttributeId']);
+ $attributeMock = $this->createPartialMock(
+ \Magento\Catalog\Model\ResourceModel\Eav\Attribute::class,
+ ['getFrontendLabels', 'getDefaultFrontendLabel', 'getAttributeId', '__wakeup', 'setAttributeId']
+ );
$attributeMock->expects($this->once())->method('getAttributeId')->willReturn(null);
$attributeMock->expects($this->once())->method('setAttributeId')->with(null)->willReturnSelf();
- $labelMock = $this->createMock(\Magento\Eav\Api\Data\AttributeFrontendLabelInterface::class);
- $attributeMock->expects($this->exactly(4))->method('getFrontendLabels')->willReturn([$labelMock]);
- $attributeMock->expects($this->exactly(2))->method('getDefaultFrontendLabel')->willReturn('test');
+ $labelMock = $this->createMock(\Magento\Eav\Model\Entity\Attribute\FrontendLabel::class);
+ $attributeMock->expects($this->any())->method('getFrontendLabels')->willReturn([$labelMock]);
+ $attributeMock->expects($this->any())->method('getDefaultFrontendLabel')->willReturn(null);
$labelMock->expects($this->once())->method('getStoreId')->willReturn(0);
$labelMock->expects($this->once())->method('getLabel')->willReturn(null);
@@ -253,7 +261,7 @@ public function testSaveDoesNotSaveAttributeOptionsIfOptionsAreAbsentInPayload()
->method('get')
->with(ProductAttributeInterface::ENTITY_TYPE_CODE, $attributeCode)
->willReturn($existingModelMock);
-
+ $existingModelMock->expects($this->once())->method('getDefaultFrontendLabel')->willReturn('default_label');
// Attribute code must not be changed after attribute creation
$attributeMock->expects($this->once())->method('setAttributeCode')->with($attributeCode);
$this->attributeResourceMock->expects($this->once())->method('save')->with($attributeMock);
@@ -264,7 +272,7 @@ public function testSaveDoesNotSaveAttributeOptionsIfOptionsAreAbsentInPayload()
public function testSaveSavesDefaultFrontendLabelIfItIsPresentInPayload()
{
- $labelMock = $this->createMock(AttributeFrontendLabelInterface::class);
+ $labelMock = $this->createMock(\Magento\Eav\Api\Data\AttributeFrontendLabelInterface::class);
$labelMock->expects($this->any())->method('getStoreId')->willReturn(1);
$labelMock->expects($this->any())->method('getLabel')->willReturn('Store Scope Label');
@@ -273,11 +281,12 @@ public function testSaveSavesDefaultFrontendLabelIfItIsPresentInPayload()
$attributeMock = $this->createMock(Attribute::class);
$attributeMock->expects($this->any())->method('getAttributeCode')->willReturn($attributeCode);
$attributeMock->expects($this->any())->method('getAttributeId')->willReturn($attributeId);
- $attributeMock->expects($this->any())->method('getDefaultFrontendLabel')->willReturn('Default Label');
+ $attributeMock->expects($this->any())->method('getDefaultFrontendLabel')->willReturn(null);
$attributeMock->expects($this->any())->method('getFrontendLabels')->willReturn([$labelMock]);
$attributeMock->expects($this->any())->method('getOptions')->willReturn([]);
$existingModelMock = $this->createMock(Attribute::class);
+ $existingModelMock->expects($this->any())->method('getDefaultFrontendLabel')->willReturn('Default Label');
$existingModelMock->expects($this->any())->method('getAttributeId')->willReturn($attributeId);
$existingModelMock->expects($this->any())->method('getAttributeCode')->willReturn($attributeCode);
@@ -288,12 +297,7 @@ public function testSaveSavesDefaultFrontendLabelIfItIsPresentInPayload()
$attributeMock->expects($this->once())
->method('setDefaultFrontendLabel')
- ->with(
- [
- 0 => 'Default Label',
- 1 => 'Store Scope Label'
- ]
- );
+ ->with('Default Label');
$this->attributeResourceMock->expects($this->once())->method('save')->with($attributeMock);
$this->model->save($attributeMock);
diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Product/CartConfigurationTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Product/CartConfigurationTest.php
index 6f1f5e120b100..2144cf34c2a09 100644
--- a/app/code/Magento/Catalog/Test/Unit/Model/Product/CartConfigurationTest.php
+++ b/app/code/Magento/Catalog/Test/Unit/Model/Product/CartConfigurationTest.php
@@ -21,6 +21,9 @@ public function testIsProductConfigured($productType, $config, $expected)
$this->assertEquals($expected, $cartConfiguration->isProductConfigured($productMock, $config));
}
+ /**
+ * @return array
+ */
public function isProductConfiguredDataProvider()
{
return [
diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Product/Gallery/ProcessorTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Product/Gallery/ProcessorTest.php
index d52aad50f05f3..15f003282dc04 100644
--- a/app/code/Magento/Catalog/Test/Unit/Model/Product/Gallery/ProcessorTest.php
+++ b/app/code/Magento/Catalog/Test/Unit/Model/Product/Gallery/ProcessorTest.php
@@ -151,6 +151,9 @@ public function testValidate($value)
$this->assertEquals(!$value, $this->model->validate($this->dataObject));
}
+ /**
+ * @return array
+ */
public function validateDataProvider()
{
return [
diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Product/Image/CacheTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Product/Image/CacheTest.php
index 3ff3601da8ccc..428ef432888f0 100644
--- a/app/code/Magento/Catalog/Test/Unit/Model/Product/Image/CacheTest.php
+++ b/app/code/Magento/Catalog/Test/Unit/Model/Product/Image/CacheTest.php
@@ -189,6 +189,58 @@ public function testGenerate()
$this->model->generate($this->product);
}
+ /**
+ * @expectedException \Magento\Framework\Exception\RuntimeException
+ */
+ public function testGenerateWithException()
+ {
+ $imageFile = 'image.jpg';
+ $imageItem = $this->objectManager->getObject(
+ \Magento\Framework\DataObject::class,
+ [
+ 'data' => ['file' => $imageFile]
+ ]
+ );
+ $this->mediaGalleryCollection->expects($this->once())
+ ->method('getIterator')
+ ->willReturn(new \ArrayIterator([$imageItem]));
+
+ $this->product->expects($this->atLeastOnce())
+ ->method('getMediaGalleryImages')
+ ->willReturn($this->mediaGalleryCollection);
+
+ $data = $this->getTestData();
+ $this->config->expects($this->once())
+ ->method('getMediaEntities')
+ ->with('Magento_Catalog')
+ ->willReturn($data);
+
+ $themeMock = $this->getMockBuilder(\Magento\Theme\Model\Theme::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $themeMock->expects($this->exactly(3))
+ ->method('getCode')
+ ->willReturn('Magento\theme');
+
+ $this->themeCollection->expects($this->once())
+ ->method('loadRegisteredThemes')
+ ->willReturn([$themeMock]);
+
+ $this->viewConfig->expects($this->once())
+ ->method('getViewConfig')
+ ->with([
+ 'area' => Area::AREA_FRONTEND,
+ 'themeModel' => $themeMock,
+ ])
+ ->willReturn($this->config);
+
+ $this->imageHelper->expects($this->exactly(3))
+ ->method('init')
+ ->willThrowException(new \Exception('Some text '));
+
+ $this->model->generate($this->product);
+ }
+
/**
* @return array
*/
diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Product/ImageTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Product/ImageTest.php
index f918692cb2753..627aa1848506e 100644
--- a/app/code/Magento/Catalog/Test/Unit/Model/Product/ImageTest.php
+++ b/app/code/Magento/Catalog/Test/Unit/Model/Product/ImageTest.php
@@ -5,12 +5,10 @@
*/
namespace Magento\Catalog\Test\Unit\Model\Product;
-use Magento\Catalog\Model\View\Asset\Image\ContextFactory;
use Magento\Catalog\Model\View\Asset\ImageFactory;
use Magento\Catalog\Model\View\Asset\PlaceholderFactory;
use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper;
use Magento\Framework\App\Filesystem\DirectoryList;
-use Magento\Framework\View\Asset\ContextInterface;
/**
* @SuppressWarnings(PHPMD.CouplingBetweenObjects)
@@ -73,10 +71,24 @@ class ImageTest extends \PHPUnit\Framework\TestCase
*/
private $viewAssetPlaceholderFactory;
+ /**
+ * @var \Magento\Framework\Serialize\SerializerInterface|\PHPUnit_Framework_MockObject_MockObject
+ */
+ private $serializer;
+
+ /**
+ * @var \Magento\Framework\App\CacheInterface|\PHPUnit_Framework_MockObject_MockObject
+ */
+ private $cacheManager;
+
protected function setUp()
{
$objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this);
$this->context = $this->createMock(\Magento\Framework\Model\Context::class);
+ $this->cacheManager = $this->getMockBuilder(\Magento\Framework\App\CacheInterface::class)
+ ->disableOriginalConstructor()
+ ->getMockForAbstractClass();
+ $this->context->expects($this->any())->method('getCacheManager')->will($this->returnValue($this->cacheManager));
$this->storeManager = $this->getMockBuilder(\Magento\Store\Model\StoreManager::class)
->disableOriginalConstructor()
@@ -112,17 +124,36 @@ protected function setUp()
->disableOriginalConstructor()
->setMethods(['create'])
->getMock();
+ $this->serializer = $this->getMockBuilder(
+ \Magento\Framework\Serialize\SerializerInterface::class
+ )->getMockForAbstractClass();
+ $this->serializer->expects($this->any())
+ ->method('serialize')
+ ->willReturnCallback(
+ function ($value) {
+ return json_encode($value);
+ }
+ );
+ $this->serializer->expects($this->any())
+ ->method('unserialize')
+ ->willReturnCallback(
+ function ($value) {
+ return json_decode($value, true);
+ }
+ );
$this->image = $objectManager->getObject(
\Magento\Catalog\Model\Product\Image::class,
[
+ 'context' => $this->context,
'storeManager' => $this->storeManager,
'catalogProductMediaConfig' => $this->config,
'coreFileStorageDatabase' => $this->coreFileHelper,
'filesystem' => $this->filesystem,
'imageFactory' => $this->factory,
'viewAssetImageFactory' => $this->viewAssetImageFactory,
- 'viewAssetPlaceholderFactory' => $this->viewAssetPlaceholderFactory
+ 'viewAssetPlaceholderFactory' => $this->viewAssetPlaceholderFactory,
+ 'serializer' => $this->serializer
]
);
@@ -354,12 +385,16 @@ public function testIsCached()
$this->testSetGetBaseFile();
$absolutePath = dirname(dirname(__DIR__)) . '/_files/catalog/product/watermark/somefile.png';
$this->imageAsset->expects($this->any())->method('getPath')->willReturn($absolutePath);
+ $this->cacheManager->expects($this->once())->method('load')->willReturn(
+ json_encode(['size' => ['image data']])
+ );
$this->assertTrue($this->image->isCached());
}
public function testClearCache()
{
$this->coreFileHelper->expects($this->once())->method('deleteFolder')->will($this->returnValue(true));
+ $this->cacheManager->expects($this->once())->method('clean');
$this->image->clearCache();
}
@@ -383,4 +418,24 @@ public function testIsBaseFilePlaceholder()
{
$this->assertFalse($this->image->isBaseFilePlaceholder());
}
+
+ public function testGetResizedImageInfoWithCache()
+ {
+ $absolutePath = dirname(dirname(__DIR__)) . '/_files/catalog/product/watermark/somefile.png';
+ $this->imageAsset->expects($this->any())->method('getPath')->willReturn($absolutePath);
+ $this->cacheManager->expects($this->once())->method('load')->willReturn(
+ json_encode(['size' => ['image data']])
+ );
+ $this->cacheManager->expects($this->never())->method('save');
+ $this->assertEquals(['image data'], $this->image->getResizedImageInfo());
+ }
+
+ public function testGetResizedImageInfoEmptyCache()
+ {
+ $absolutePath = dirname(dirname(__DIR__)) . '/_files/catalog/product/watermark/somefile.png';
+ $this->imageAsset->expects($this->any())->method('getPath')->willReturn($absolutePath);
+ $this->cacheManager->expects($this->once())->method('load')->willReturn(false);
+ $this->cacheManager->expects($this->once())->method('save');
+ $this->assertTrue(is_array($this->image->getResizedImageInfo()));
+ }
}
diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Product/LinkTypeProviderTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Product/LinkTypeProviderTest.php
index 0e29aeab697af..c1e5559dbfd66 100644
--- a/app/code/Magento/Catalog/Test/Unit/Model/Product/LinkTypeProviderTest.php
+++ b/app/code/Magento/Catalog/Test/Unit/Model/Product/LinkTypeProviderTest.php
@@ -111,6 +111,9 @@ public function testGetItemAttributes($type, $typeId)
$this->assertEquals($expectedResult, $this->model->getItemAttributes($type));
}
+ /**
+ * @return array
+ */
public function getItemAttributesDataProvider()
{
return [
diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Product/Option/Validator/DefaultValidatorTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Product/Option/Validator/DefaultValidatorTest.php
index 1eb5f1a2dacd2..5a8dba5c8c2b2 100644
--- a/app/code/Magento/Catalog/Test/Unit/Model/Product/Option/Validator/DefaultValidatorTest.php
+++ b/app/code/Magento/Catalog/Test/Unit/Model/Product/Option/Validator/DefaultValidatorTest.php
@@ -60,10 +60,10 @@ public function isValidTitleDataProvider()
{
$mess = ['option required fields' => 'Missed values for option required fields'];
return [
- ['option_title', 'name 1.1', 'fixed', 10, new \Magento\Framework\DataObject(['store_id' => 1]), [], true],
- ['option_title', 'name 1.1', 'fixed', 10, new \Magento\Framework\DataObject(['store_id' => 0]), [], true],
- [null, 'name 1.1', 'fixed', 10, new \Magento\Framework\DataObject(['store_id' => 1]), [], true],
- [null, 'name 1.1', 'fixed', 10, new \Magento\Framework\DataObject(['store_id' => 0]), $mess, false],
+ ['option_title', 'name 1.1', 'fixed', new \Magento\Framework\DataObject(['store_id' => 1]), [], true],
+ ['option_title', 'name 1.1', 'fixed', new \Magento\Framework\DataObject(['store_id' => 0]), [], true],
+ [null, 'name 1.1', 'fixed', new \Magento\Framework\DataObject(['store_id' => 1]), [], true],
+ [null, 'name 1.1', 'fixed', new \Magento\Framework\DataObject(['store_id' => 0]), $mess, false],
];
}
@@ -71,20 +71,19 @@ public function isValidTitleDataProvider()
* @param $title
* @param $type
* @param $priceType
- * @param $price
* @param $product
* @param $messages
* @param $result
* @dataProvider isValidTitleDataProvider
*/
- public function testIsValidTitle($title, $type, $priceType, $price, $product, $messages, $result)
+ public function testIsValidTitle($title, $type, $priceType, $product, $messages, $result)
{
- $methods = ['getTitle', 'getType', 'getPriceType', 'getPrice', '__wakeup', 'getProduct'];
+ $methods = ['getTitle', 'getType', 'getPriceType', '__wakeup', 'getProduct'];
$valueMock = $this->createPartialMock(\Magento\Catalog\Model\Product\Option::class, $methods);
$valueMock->expects($this->once())->method('getTitle')->will($this->returnValue($title));
$valueMock->expects($this->any())->method('getType')->will($this->returnValue($type));
$valueMock->expects($this->once())->method('getPriceType')->will($this->returnValue($priceType));
- $valueMock->expects($this->once())->method('getPrice')->will($this->returnValue($price));
+ // $valueMock->expects($this->once())->method('getPrice')->will($this->returnValue($price));
$valueMock->expects($this->once())->method('getProduct')->will($this->returnValue($product));
$this->assertEquals($result, $this->validator->isValid($valueMock));
$this->assertEquals($messages, $this->validator->getMessages());
@@ -124,41 +123,4 @@ public function testIsValidFail($product)
$this->assertFalse($this->validator->isValid($valueMock));
$this->assertEquals($messages, $this->validator->getMessages());
}
-
- /**
- * Data provider for testValidationNegativePrice
- * @return array
- */
- public function validationNegativePriceDataProvider()
- {
- return [
- ['option_title', 'name 1.1', 'fixed', -12, new \Magento\Framework\DataObject(['store_id' => 1])],
- ['option_title', 'name 1.1', 'fixed', -12, new \Magento\Framework\DataObject(['store_id' => 0])],
- ];
- }
-
- /**
- * @param $title
- * @param $type
- * @param $priceType
- * @param $price
- * @param $product
- * @dataProvider validationNegativePriceDataProvider
- */
- public function testValidationNegativePrice($title, $type, $priceType, $price, $product)
- {
- $methods = ['getTitle', 'getType', 'getPriceType', 'getPrice', '__wakeup', 'getProduct'];
- $valueMock = $this->createPartialMock(\Magento\Catalog\Model\Product\Option::class, $methods);
- $valueMock->expects($this->once())->method('getTitle')->will($this->returnValue($title));
- $valueMock->expects($this->exactly(2))->method('getType')->will($this->returnValue($type));
- $valueMock->expects($this->once())->method('getPriceType')->will($this->returnValue($priceType));
- $valueMock->expects($this->once())->method('getPrice')->will($this->returnValue($price));
- $valueMock->expects($this->once())->method('getProduct')->will($this->returnValue($product));
-
- $messages = [
- 'option values' => 'Invalid option value',
- ];
- $this->assertFalse($this->validator->isValid($valueMock));
- $this->assertEquals($messages, $this->validator->getMessages());
- }
}
diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Product/Option/Validator/FileTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Product/Option/Validator/FileTest.php
index 3c06db0e7ce5f..d8b48d0cc984e 100644
--- a/app/code/Magento/Catalog/Test/Unit/Model/Product/Option/Validator/FileTest.php
+++ b/app/code/Magento/Catalog/Test/Unit/Model/Product/Option/Validator/FileTest.php
@@ -58,8 +58,10 @@ public function testIsValidSuccess()
{
$this->valueMock->expects($this->once())->method('getTitle')->will($this->returnValue('option_title'));
$this->valueMock->expects($this->exactly(2))->method('getType')->will($this->returnValue('name 1.1'));
- $this->valueMock->expects($this->once())->method('getPriceType')->will($this->returnValue('fixed'));
- $this->valueMock->expects($this->once())->method('getPrice')->will($this->returnValue(10));
+ $this->valueMock->method('getPriceType')
+ ->willReturn('fixed');
+ $this->valueMock->method('getPrice')
+ ->willReturn(10);
$this->valueMock->expects($this->once())->method('getImageSizeX')->will($this->returnValue(10));
$this->valueMock->expects($this->once())->method('getImageSizeY')->will($this->returnValue(15));
$this->assertEmpty($this->validator->getMessages());
@@ -70,8 +72,10 @@ public function testIsValidWithNegativeImageSize()
{
$this->valueMock->expects($this->once())->method('getTitle')->will($this->returnValue('option_title'));
$this->valueMock->expects($this->exactly(2))->method('getType')->will($this->returnValue('name 1.1'));
- $this->valueMock->expects($this->once())->method('getPriceType')->will($this->returnValue('fixed'));
- $this->valueMock->expects($this->once())->method('getPrice')->will($this->returnValue(10));
+ $this->valueMock->method('getPriceType')
+ ->willReturn('fixed');
+ $this->valueMock->method('getPrice')
+ ->willReturn(10);
$this->valueMock->expects($this->once())->method('getImageSizeX')->will($this->returnValue(-10));
$this->valueMock->expects($this->never())->method('getImageSizeY');
$messages = [
@@ -85,8 +89,10 @@ public function testIsValidWithNegativeImageSizeY()
{
$this->valueMock->expects($this->once())->method('getTitle')->will($this->returnValue('option_title'));
$this->valueMock->expects($this->exactly(2))->method('getType')->will($this->returnValue('name 1.1'));
- $this->valueMock->expects($this->once())->method('getPriceType')->will($this->returnValue('fixed'));
- $this->valueMock->expects($this->once())->method('getPrice')->will($this->returnValue(10));
+ $this->valueMock->method('getPriceType')
+ ->willReturn('fixed');
+ $this->valueMock->method('getPrice')
+ ->willReturn(10);
$this->valueMock->expects($this->once())->method('getImageSizeX')->will($this->returnValue(10));
$this->valueMock->expects($this->once())->method('getImageSizeY')->will($this->returnValue(-10));
$messages = [
diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Product/Option/Validator/SelectTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Product/Option/Validator/SelectTest.php
index 046ee703c850e..675821fcda111 100644
--- a/app/code/Magento/Catalog/Test/Unit/Model/Product/Option/Validator/SelectTest.php
+++ b/app/code/Magento/Catalog/Test/Unit/Model/Product/Option/Validator/SelectTest.php
@@ -69,6 +69,9 @@ public function testIsValidSuccess($expectedResult, array $value)
$this->assertEquals($expectedResult, $this->validator->isValid($this->valueMock));
}
+ /**
+ * @return array
+ */
public function isValidSuccessDataProvider()
{
return [
@@ -87,7 +90,7 @@ public function isValidSuccessDataProvider()
]
],
[
- false,
+ true,
[
'title' => 'Some Title',
'price_type' => 'fixed',
@@ -154,10 +157,12 @@ public function testIsValidateWithInvalidData($priceType, $price, $title)
$this->assertEquals($messages, $this->validator->getMessages());
}
+ /**
+ * @return array
+ */
public function isValidateWithInvalidDataDataProvider()
{
return [
- 'invalid_price' => ['fixed', -10, 'Title'],
'invalid_price_type' => ['some_value', '10', 'Title'],
'empty_title' => ['fixed', 10, null]
];
diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Product/Option/Validator/TextTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Product/Option/Validator/TextTest.php
index cf31d67817684..ffd858c3d433e 100644
--- a/app/code/Magento/Catalog/Test/Unit/Model/Product/Option/Validator/TextTest.php
+++ b/app/code/Magento/Catalog/Test/Unit/Model/Product/Option/Validator/TextTest.php
@@ -58,8 +58,10 @@ public function testIsValidSuccess()
{
$this->valueMock->expects($this->once())->method('getTitle')->will($this->returnValue('option_title'));
$this->valueMock->expects($this->exactly(2))->method('getType')->will($this->returnValue('name 1.1'));
- $this->valueMock->expects($this->once())->method('getPriceType')->will($this->returnValue('fixed'));
- $this->valueMock->expects($this->once())->method('getPrice')->will($this->returnValue(10));
+ $this->valueMock->method('getPriceType')
+ ->willReturn('fixed');
+ $this->valueMock->method('getPrice')
+ ->willReturn(10);
$this->valueMock->expects($this->once())->method('getMaxCharacters')->will($this->returnValue(10));
$this->assertTrue($this->validator->isValid($this->valueMock));
$this->assertEmpty($this->validator->getMessages());
@@ -69,8 +71,10 @@ public function testIsValidWithNegativeMaxCharacters()
{
$this->valueMock->expects($this->once())->method('getTitle')->will($this->returnValue('option_title'));
$this->valueMock->expects($this->exactly(2))->method('getType')->will($this->returnValue('name 1.1'));
- $this->valueMock->expects($this->once())->method('getPriceType')->will($this->returnValue('fixed'));
- $this->valueMock->expects($this->once())->method('getPrice')->will($this->returnValue(10));
+ $this->valueMock->method('getPriceType')
+ ->willReturn('fixed');
+ $this->valueMock->method('getPrice')
+ ->willReturn(10);
$this->valueMock->expects($this->once())->method('getMaxCharacters')->will($this->returnValue(-10));
$messages = [
'option values' => 'Invalid option value',
diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Product/Option/ValueTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Product/Option/ValueTest.php
index a2d31f377e925..a3769fa13beca 100644
--- a/app/code/Magento/Catalog/Test/Unit/Model/Product/Option/ValueTest.php
+++ b/app/code/Magento/Catalog/Test/Unit/Model/Product/Option/ValueTest.php
@@ -19,6 +19,32 @@ class ValueTest extends \PHPUnit\Framework\TestCase
*/
private $model;
+ /**
+ * @var \Magento\Catalog\Pricing\Price\CustomOptionPriceCalculator
+ */
+ private $customOptionPriceCalculatorMock;
+
+ protected function setUp()
+ {
+ $mockedResource = $this->getMockedResource();
+ $mockedCollectionFactory = $this->getMockedValueCollectionFactory();
+
+ $this->customOptionPriceCalculatorMock = $this->createMock(
+ \Magento\Catalog\Pricing\Price\CustomOptionPriceCalculator::class
+ );
+
+ $helper = new ObjectManager($this);
+ $this->model = $helper->getObject(
+ \Magento\Catalog\Model\Product\Option\Value::class,
+ [
+ 'resource' => $mockedResource,
+ 'valueCollectionFactory' => $mockedCollectionFactory,
+ 'customOptionPriceCalculator' => $this->customOptionPriceCalculatorMock,
+ ]
+ );
+ $this->model->setOption($this->getMockedOption());
+ }
+
public function testSaveProduct()
{
$this->model->setValues([100])
@@ -35,11 +61,16 @@ public function testSaveProduct()
public function testGetPrice()
{
- $this->model->setPrice(1000);
+ $price = 1000;
+ $this->model->setPrice($price);
$this->model->setPriceType(Value::TYPE_PERCENT);
- $this->assertEquals(1000, $this->model->getPrice(false));
+ $this->assertEquals($price, $this->model->getPrice(false));
- $this->assertEquals(100, $this->model->getPrice(true));
+ $percentPice = 100;
+ $this->customOptionPriceCalculatorMock->expects($this->atLeastOnce())
+ ->method('getOptionPriceByPriceCode')
+ ->willReturn($percentPice);
+ $this->assertEquals($percentPice, $this->model->getPrice(true));
}
public function testGetValuesCollection()
@@ -78,23 +109,6 @@ public function testDeleteValue()
$this->assertInstanceOf(\Magento\Catalog\Model\Product\Option\Value::class, $this->model->deleteValue(1));
}
- protected function setUp()
- {
- $mockedResource = $this->getMockedResource();
- $mockedCollectionFactory = $this->getMockedValueCollectionFactory();
- $mockedContext = $this->getMockedContext();
- $helper = new ObjectManager($this);
- $this->model = $helper->getObject(
- \Magento\Catalog\Model\Product\Option\Value::class,
- [
- 'resource' => $mockedResource,
- 'valueCollectionFactory' => $mockedCollectionFactory,
- 'context' => $mockedContext
- ]
- );
- $this->model->setOption($this->getMockedOption());
- }
-
/**
* @return \Magento\Catalog\Model\ResourceModel\Product\Option\Value\CollectionFactory
*/
@@ -164,13 +178,27 @@ private function getMockedOption()
private function getMockedProduct()
{
$mockBuilder = $this->getMockBuilder(\Magento\Catalog\Model\Product::class)
- ->setMethods(['getFinalPrice', '__wakeup'])
+ ->setMethods(['getPriceInfo', '__wakeup'])
->disableOriginalConstructor();
$mock = $mockBuilder->getMock();
- $mock->expects($this->any())
- ->method('getFinalPrice')
- ->will($this->returnValue(10));
+ $priceInfoMock = $this->getMockForAbstractClass(
+ \Magento\Framework\Pricing\PriceInfoInterface::class,
+ [],
+ '',
+ false,
+ false,
+ true,
+ ['getPrice']
+ );
+
+ $priceMock = $this->getMockForAbstractClass(\Magento\Framework\Pricing\Price\PriceInterface::class);
+
+ $priceInfoMock->expects($this->any())->method('getPrice')->willReturn($priceMock);
+
+ $mock->expects($this->any())->method('getPriceInfo')->willReturn($priceInfoMock);
+
+ $priceMock->expects($this->any())->method('getValue')->willReturn(10);
return $mock;
}
@@ -229,61 +257,4 @@ private function getMockedResource()
return $mock;
}
-
- /**
- * @return \Magento\Framework\Model\Context
- */
- private function getMockedContext()
- {
- $mockedRemoveAction = $this->getMockedRemoveAction();
- $mockEventManager = $this->getMockedEventManager();
-
- $mockBuilder = $this->getMockBuilder(\Magento\Framework\Model\Context::class)
- ->setMethods(['getActionValidator', 'getEventDispatcher'])
- ->disableOriginalConstructor();
- $mock = $mockBuilder->getMock();
-
- $mock->expects($this->any())
- ->method('getActionValidator')
- ->will($this->returnValue($mockedRemoveAction));
-
- $mock->expects($this->any())
- ->method('getEventDispatcher')
- ->will($this->returnValue($mockEventManager));
-
- return $mock;
- }
-
- /**
- * @return RemoveAction
- */
- private function getMockedRemoveAction()
- {
- $mockBuilder = $this->getMockBuilder(\Magento\Framework\Model\Context::class)
- ->setMethods(['isAllowed'])
- ->disableOriginalConstructor();
- $mock = $mockBuilder->getMock();
-
- $mock->expects($this->any())
- ->method('isAllowed')
- ->will($this->returnValue(true));
-
- return $mock;
- }
-
- /**
- * @return \Magento\Framework\Event\ManagerInterface
- */
- private function getMockedEventManager()
- {
- $mockBuilder = $this->getMockBuilder(\Magento\Framework\Event\ManagerInterface::class)
- ->setMethods(['dispatch'])
- ->disableOriginalConstructor();
- $mock = $mockBuilder->getMockForAbstractClass();
-
- $mock->expects($this->any())
- ->method('dispatch');
-
- return $mock;
- }
}
diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Product/OptionTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Product/OptionTest.php
index cd0af47180974..83c69ba14a290 100644
--- a/app/code/Magento/Catalog/Test/Unit/Model/Product/OptionTest.php
+++ b/app/code/Magento/Catalog/Test/Unit/Model/Product/OptionTest.php
@@ -8,6 +8,9 @@
use \Magento\Catalog\Model\Product\Option;
use Magento\Framework\TestFramework\Unit\Helper\ObjectManager;
+/**
+ * Tests \Magento\Catalog\Model\Product\Option.
+ */
class OptionTest extends \PHPUnit\Framework\TestCase
{
/**
@@ -68,4 +71,41 @@ public function testGetRegularPrice()
$this->model->setPriceType(null);
$this->assertEquals(50, $this->model->getRegularPrice());
}
+
+ /**
+ * Tests removing ineligible characters from file_extension.
+ *
+ * @param string $rawExtensions
+ * @param string $expectedExtensions
+ * @dataProvider beforeSaveFileOptionDataProvider
+ */
+ public function testBeforeSaveFileOption($rawExtensions, $expectedExtensions)
+ {
+ $this->model->setType(Option::OPTION_GROUP_FILE);
+ $this->model->setFileExtension($rawExtensions);
+ $this->model->beforeSave();
+ $actualExtensions = $this->model->getFileExtension();
+ $this->assertEquals(
+ $expectedExtensions,
+ $actualExtensions
+ );
+ }
+
+ /**
+ * Data provider for testBeforeSaveFileOption.
+ *
+ * @return array
+ */
+ public function beforeSaveFileOptionDataProvider()
+ {
+ return [
+ ['JPG, PNG, GIF', 'jpg, png, gif'],
+ ['jpg, jpg, jpg', 'jpg'],
+ ['jpg, png, gif', 'jpg, png, gif'],
+ ['jpg png gif', 'jpg, png, gif'],
+ ['!jpg@png#gif%', 'jpg, png, gif'],
+ ['jpg, png, 123', 'jpg, png, 123'],
+ ['', ''],
+ ];
+ }
}
diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Product/ProductList/ToolbarTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Product/ProductList/ToolbarTest.php
index 84a9e9ded094b..3789ba4ee126d 100644
--- a/app/code/Magento/Catalog/Test/Unit/Model/Product/ProductList/ToolbarTest.php
+++ b/app/code/Magento/Catalog/Test/Unit/Model/Product/ProductList/ToolbarTest.php
@@ -112,6 +112,9 @@ public function testGetCurrentPageNoParam()
$this->assertEquals(1, $this->toolbarModel->getCurrentPage());
}
+ /**
+ * @return array
+ */
public function stringParamProvider()
{
return [
@@ -119,6 +122,9 @@ public function stringParamProvider()
];
}
+ /**
+ * @return array
+ */
public function intParamProvider()
{
return [
diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Product/ReservedAttributeListTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Product/ReservedAttributeListTest.php
index 7506b4adc1d3a..5080e64f46e27 100644
--- a/app/code/Magento/Catalog/Test/Unit/Model/Product/ReservedAttributeListTest.php
+++ b/app/code/Magento/Catalog/Test/Unit/Model/Product/ReservedAttributeListTest.php
@@ -40,6 +40,9 @@ public function testIsReservedAttribute($isUserDefined, $attributeCode, $expecte
$this->assertEquals($expected, $this->model->isReservedAttribute($attribute));
}
+ /**
+ * @return array
+ */
public function dataProvider()
{
return [
diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Product/TierPriceManagementTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Product/TierPriceManagementTest.php
index 6d42a33d1fc73..bbd0886442fd3 100644
--- a/app/code/Magento/Catalog/Test/Unit/Model/Product/TierPriceManagementTest.php
+++ b/app/code/Magento/Catalog/Test/Unit/Model/Product/TierPriceManagementTest.php
@@ -147,6 +147,9 @@ public function testGetList($configValue, $customerGroupId, $groupData, $expecte
}
}
+ /**
+ * @return array
+ */
public function getListDataProvider()
{
return [
@@ -399,6 +402,9 @@ public function testAddWithInvalidData($price, $qty)
$this->service->add('product_sku', 1, $price, $qty);
}
+ /**
+ * @return array
+ */
public function addDataProvider()
{
return [
diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Product/Type/AbstractTypeTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Product/Type/AbstractTypeTest.php
index dcddab60fb0b9..b34375256a959 100644
--- a/app/code/Magento/Catalog/Test/Unit/Model/Product/Type/AbstractTypeTest.php
+++ b/app/code/Magento/Catalog/Test/Unit/Model/Product/Type/AbstractTypeTest.php
@@ -96,6 +96,9 @@ public function testAttributesCompare($attr1, $attr2, $expectedResult)
$this->assertEquals($expectedResult, $this->model->attributesCompare($attribute, $attribute2));
}
+ /**
+ * @return array
+ */
public function attributeCompareProvider()
{
return [
diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Product/UrlTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Product/UrlTest.php
index 9fa820d64bae1..ef7aad2cbb802 100644
--- a/app/code/Magento/Catalog/Test/Unit/Model/Product/UrlTest.php
+++ b/app/code/Magento/Catalog/Test/Unit/Model/Product/UrlTest.php
@@ -169,6 +169,9 @@ public function testGetUrl(
}
}
+ /**
+ * @return array
+ */
public function getUrlDataProvider()
{
return [
diff --git a/app/code/Magento/Catalog/Test/Unit/Model/ProductRepository/MediaGalleryProcessorTest.php b/app/code/Magento/Catalog/Test/Unit/Model/ProductRepository/MediaGalleryProcessorTest.php
new file mode 100644
index 0000000000000..02773b2fb3d70
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Unit/Model/ProductRepository/MediaGalleryProcessorTest.php
@@ -0,0 +1,227 @@
+product = $this->createPartialMock(
+ \Magento\Catalog\Model\Product::class,
+ [
+ 'hasGalleryAttribute',
+ 'getMediaConfig',
+ 'getMediaAttributes',
+ 'getMediaGalleryEntries',
+ ]
+ );
+ $this->product->expects($this->any())
+ ->method('hasGalleryAttribute')
+ ->willReturn(true);
+ $this->processor = $this->getMockBuilder(Processor::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->contentFactory = $this->getMockBuilder(ImageContentInterfaceFactory::class)
+ ->setMethods(['create'])
+ ->disableOriginalConstructor()
+ ->getMockForAbstractClass();
+ $this->imageProcessor = $this->getMockBuilder(ImageProcessorInterface::class)
+ ->disableOriginalConstructor()
+ ->getMockForAbstractClass();
+ $objectManager = new ObjectManager($this);
+ $this->model = $objectManager->getObject(
+ MediaGalleryProcessor::class,
+ [
+ 'processor' => $this->processor,
+ 'contentFactory' => $this->contentFactory,
+ 'imageProcessor' => $this->imageProcessor,
+ ]
+ );
+ }
+
+ /**
+ * Test add image.
+ *
+ * @return void
+ */
+ public function testProcessWithNewMediaEntry()
+ {
+ $mediaGalleryEntries = [
+ [
+ 'value_id' => null,
+ 'label' => 'label_text',
+ 'position' => 10,
+ 'disabled' => false,
+ 'types' => ['image', 'small_image'],
+ 'content' => [
+ ImageContentInterface::NAME => 'filename',
+ ImageContentInterface::TYPE => 'image/jpeg',
+ ImageContentInterface::BASE64_ENCODED_DATA => 'encoded_content',
+ ],
+ 'media_type' => 'media_type',
+ ],
+ ];
+
+ //setup media attribute backend.
+ $mediaTmpPath = '/tmp';
+ $absolutePath = '/a/b/filename.jpg';
+ $mediaConfigMock = $this->getMockBuilder(\Magento\Catalog\Model\Product\Media\Config::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $mediaConfigMock->expects($this->once())
+ ->method('getTmpMediaShortUrl')
+ ->with($absolutePath)
+ ->willReturn($mediaTmpPath . $absolutePath);
+ $this->product->setData('media_gallery', ['images' => $mediaGalleryEntries]);
+ $this->product->expects($this->any())
+ ->method('getMediaAttributes')
+ ->willReturn(['image' => 'imageAttribute', 'small_image' => 'small_image_attribute']);
+ $this->product->expects($this->once())
+ ->method('getMediaConfig')
+ ->willReturn($mediaConfigMock);
+ $this->processor->expects($this->once())->method('clearMediaAttribute')
+ ->with($this->product, ['image', 'small_image']);
+
+ //verify new entries.
+ $contentDataObject = $this->getMockBuilder(\Magento\Framework\Api\ImageContent::class)
+ ->disableOriginalConstructor()
+ ->setMethods(null)
+ ->getMock();
+ $this->contentFactory->expects($this->once())
+ ->method('create')
+ ->willReturn($contentDataObject);
+
+ $this->imageProcessor->expects($this->once())
+ ->method('processImageContent')
+ ->willReturn($absolutePath);
+
+ $imageFileUri = 'imageFileUri';
+ $this->processor->expects($this->once())->method('addImage')
+ ->with($this->product, $mediaTmpPath . $absolutePath, ['image', 'small_image'], true, false)
+ ->willReturn($imageFileUri);
+ $this->processor->expects($this->once())->method('updateImage')
+ ->with(
+ $this->product,
+ $imageFileUri,
+ [
+ 'label' => 'label_text',
+ 'position' => 10,
+ 'disabled' => false,
+ 'media_type' => 'media_type',
+ ]
+ );
+
+ $this->model->processMediaGallery($this->product, $mediaGalleryEntries);
+ }
+
+ /**
+ * Test update(delete) images.
+ */
+ public function testProcessExistingWithMediaGalleryEntries()
+ {
+ //update one entry, delete one entry.
+ $newEntries = [
+ [
+ 'id' => 5,
+ 'label' => 'new_label_text',
+ 'file' => 'filename1',
+ 'position' => 10,
+ 'disabled' => false,
+ 'types' => ['image', 'small_image'],
+ ],
+ ];
+
+ $existingMediaGallery = [
+ 'images' => [
+ [
+ 'value_id' => 5,
+ 'label' => 'label_text',
+ 'file' => 'filename1',
+ 'position' => 10,
+ 'disabled' => true,
+ ],
+ [
+ 'value_id' => 6, //will be deleted.
+ 'file' => 'filename2',
+ ],
+ ],
+ ];
+
+ $expectedResult = [
+ [
+ 'value_id' => 5,
+ 'id' => 5,
+ 'label' => 'new_label_text',
+ 'file' => 'filename1',
+ 'position' => 10,
+ 'disabled' => false,
+ 'types' => ['image', 'small_image'],
+ ],
+ [
+ 'value_id' => 6, //will be deleted.
+ 'file' => 'filename2',
+ 'removed' => true,
+ ],
+ ];
+
+ $this->product->setData('media_gallery', $existingMediaGallery);
+ $this->product->expects($this->any())
+ ->method('getMediaAttributes')
+ ->willReturn(['image' => 'filename1', 'small_image' => 'filename2']);
+
+ $this->processor->expects($this->once())->method('clearMediaAttribute')
+ ->with($this->product, ['image', 'small_image']);
+ $this->processor->expects($this->once())
+ ->method('setMediaAttribute')
+ ->with($this->product, ['image', 'small_image'], 'filename1');
+ $this->model->processMediaGallery($this->product, $newEntries);
+ $this->assertEquals($expectedResult, $this->product->getMediaGallery('images'));
+ }
+}
diff --git a/app/code/Magento/Catalog/Test/Unit/Model/ProductRepositoryTest.php b/app/code/Magento/Catalog/Test/Unit/Model/ProductRepositoryTest.php
index c98705b4eda63..ff680916bd15f 100644
--- a/app/code/Magento/Catalog/Test/Unit/Model/ProductRepositoryTest.php
+++ b/app/code/Magento/Catalog/Test/Unit/Model/ProductRepositoryTest.php
@@ -5,21 +5,39 @@
* See COPYING.txt for license details.
*/
-// @codingStandardsIgnoreFile
+declare(strict_types=1);
namespace Magento\Catalog\Test\Unit\Model;
-use Magento\Catalog\Api\Data\ProductAttributeInterface;
+use Magento\Catalog\Api\Data\ProductExtensionInterface;
+use Magento\Catalog\Api\Data\ProductSearchResultsInterfaceFactory;
+use Magento\Catalog\Api\ProductAttributeRepositoryInterface;
+use Magento\Catalog\Controller\Adminhtml\Product\Initialization\Helper;
+use Magento\Catalog\Model\Product;
+use Magento\Catalog\Model\Product\Gallery\MimeTypeExtensionMap;
+use Magento\Catalog\Model\Product\LinkTypeProvider;
+use Magento\Catalog\Model\ProductFactory;
+use Magento\Catalog\Model\ProductRepository;
+use Magento\Catalog\Model\ProductRepository\MediaGalleryProcessor;
+use Magento\Catalog\Model\ResourceModel\Product\CollectionFactory;
use Magento\Framework\Api\Data\ImageContentInterface;
+use Magento\Framework\Api\Data\ImageContentInterfaceFactory;
+use Magento\Framework\Api\ExtensibleDataObjectConverter;
+use Magento\Framework\Api\FilterBuilder;
+use Magento\Framework\Api\ImageContentValidator;
+use Magento\Framework\Api\ImageContentValidatorInterface;
+use Magento\Framework\Api\ImageProcessorInterface;
use Magento\Framework\Api\SearchCriteria\CollectionProcessorInterface;
+use Magento\Framework\Api\SearchCriteriaBuilder;
use Magento\Framework\DB\Adapter\ConnectionException;
+use Magento\Framework\Filesystem;
use Magento\Framework\Serialize\Serializer\Json;
use Magento\Framework\TestFramework\Unit\Helper\ObjectManager;
use Magento\Store\Api\Data\StoreInterface;
+use Magento\Store\Model\StoreManagerInterface;
+use PHPUnit_Framework_MockObject_MockObject as MockObject;
/**
- * Class ProductRepositoryTest
- * @package Magento\Catalog\Test\Unit\Model
* @SuppressWarnings(PHPMD.TooManyFields)
* @SuppressWarnings(PHPMD.CouplingBetweenObjects)
* @SuppressWarnings(PHPMD.ExcessiveMethodLength)
@@ -27,132 +45,127 @@
class ProductRepositoryTest extends \PHPUnit\Framework\TestCase
{
/**
- * @var \PHPUnit_Framework_MockObject_MockObject
+ * @var Product|MockObject
*/
- protected $productMock;
+ private $product;
/**
- * @var \PHPUnit_Framework_MockObject_MockObject
+ * @var Product|MockObject
*/
- protected $initializedProductMock;
+ private $initializedProduct;
/**
- * @var \Magento\Catalog\Model\ProductRepository
+ * @var ProductRepository
*/
- protected $model;
+ private $model;
/**
- * @var \PHPUnit_Framework_MockObject_MockObject
+ * @var Helper|MockObject
*/
- protected $initializationHelperMock;
+ private $initializationHelper;
/**
- * @var \PHPUnit_Framework_MockObject_MockObject
+ * @var Product|MockObject
*/
- protected $resourceModelMock;
+ private $resourceModel;
/**
- * @var \PHPUnit_Framework_MockObject_MockObject
+ * @var ProductFactory|MockObject
*/
- protected $productFactoryMock;
+ private $productFactory;
/**
- * @var \PHPUnit_Framework_MockObject_MockObject
+ * @var CollectionFactory|MockObject
*/
- protected $collectionFactoryMock;
+ private $collectionFactory;
/**
- * @var \PHPUnit_Framework_MockObject_MockObject
+ * @var SearchCriteriaBuilder|MockObject
*/
- protected $searchCriteriaBuilderMock;
+ private $searchCriteriaBuilder;
/**
- * @var \PHPUnit_Framework_MockObject_MockObject
+ * @var FilterBuilder|MockObject
*/
- protected $filterBuilderMock;
+ private $filterBuilder;
/**
- * @var \PHPUnit_Framework_MockObject_MockObject
+ * @var ProductAttributeRepositoryInterface|MockObject
*/
- protected $metadataServiceMock;
+ private $metadataService;
/**
- * @var \PHPUnit_Framework_MockObject_MockObject
+ * @var ProductSearchResultsInterfaceFactory|MockObject
*/
- protected $searchResultsFactoryMock;
+ private $searchResultsFactory;
/**
- * @var \Magento\Eav\Model\Config|\PHPUnit_Framework_MockObject_MockObject
+ * @var ExtensibleDataObjectConverter|MockObject
*/
- protected $eavConfigMock;
-
- /**
- * @var \PHPUnit_Framework_MockObject_MockObject
- */
- protected $extensibleDataObjectConverterMock;
+ private $extensibleDataObjectConverter;
/**
* @var array data to create product
*/
- protected $productData = [
+ private $productData = [
'sku' => 'exisiting',
'name' => 'existing product',
];
/**
- * @var \PHPUnit_Framework_MockObject_MockObject|\Magento\Framework\Filesystem
+ * @var Filesystem|MockObject
*/
- protected $fileSystemMock;
+ private $fileSystem;
/**
- * @var \PHPUnit_Framework_MockObject_MockObject|\Magento\Catalog\Model\Product\Gallery\MimeTypeExtensionMap
+ * @var MimeTypeExtensionMap|MockObject
*/
- protected $mimeTypeExtensionMapMock;
+ private $mimeTypeExtensionMap;
/**
- * @var \PHPUnit_Framework_MockObject_MockObject
+ * @var ImageContentInterfaceFactory|MockObject
*/
- protected $contentFactoryMock;
+ private $contentFactory;
/**
- * @var \PHPUnit_Framework_MockObject_MockObject|\Magento\Framework\Api\ImageContentValidator
+ * @var ImageContentValidator|MockObject
*/
- protected $contentValidatorMock;
+ private $contentValidator;
/**
- * @var \PHPUnit_Framework_MockObject_MockObject
+ * @var LinkTypeProvider|MockObject
*/
- protected $linkTypeProviderMock;
+ private $linkTypeProvider;
/**
- * @var \PHPUnit_Framework_MockObject_MockObject|\Magento\Framework\Api\ImageProcessorInterface
+ * @var ImageProcessorInterface|MockObject
*/
- protected $imageProcessorMock;
+ private $imageProcessor;
/**
- * @var \Magento\Framework\TestFramework\Unit\Helper\ObjectManager
+ * @var ObjectManager
*/
- protected $objectManager;
+ private $objectManager;
/**
- * @var \Magento\Store\Model\StoreManagerInterface|\PHPUnit_Framework_MockObject_MockObject
+ * @var StoreManagerInterface|MockObject
*/
- protected $storeManagerMock;
+ private $storeManager;
/**
- * @var \Magento\Catalog\Model\Product\Gallery\Processor|\PHPUnit_Framework_MockObject_MockObject
+ * @var MediaGalleryProcessor|MockObject
*/
- protected $mediaGalleryProcessor;
+ private $mediaGalleryProcessor;
/**
- * @var CollectionProcessorInterface|\PHPUnit_Framework_MockObject_MockObject
+ * @var CollectionProcessorInterface|MockObject
*/
- private $collectionProcessorMock;
+ private $collectionProcessor;
/**
- * @var Json|\PHPUnit_Framework_MockObject_MockObject
+ * @var ProductExtensionInterface|MockObject
*/
- private $serializerMock;
+ private $productExtension;
/**
* Product repository cache limit.
@@ -166,9 +179,11 @@ class ProductRepositoryTest extends \PHPUnit\Framework\TestCase
*/
protected function setUp()
{
- $this->productFactoryMock = $this->createPartialMock(\Magento\Catalog\Model\ProductFactory::class, ['create', 'setData']);
+ $this->productFactory = $this->createPartialMock(ProductFactory::class, ['create', 'setData']);
- $this->productMock = $this->createPartialMock(\Magento\Catalog\Model\Product::class, [
+ $this->product = $this->createPartialMock(
+ Product::class,
+ [
'getId',
'getSku',
'setWebsiteIds',
@@ -176,15 +191,20 @@ protected function setUp()
'load',
'setData',
'getStoreId',
- 'getMediaGalleryEntries'
- ]);
+ 'getMediaGalleryEntries',
+ 'getExtensionAttributes'
+ ]
+ );
- $this->initializedProductMock = $this->createPartialMock(\Magento\Catalog\Model\Product::class, [
+ $this->initializedProduct = $this->createPartialMock(
+ \Magento\Catalog\Model\Product::class,
+ [
'getWebsiteIds',
'setProductOptions',
'load',
'getOptions',
'getSku',
+ 'getId',
'hasGalleryAttribute',
'getMediaConfig',
'getMediaAttributes',
@@ -193,86 +213,88 @@ protected function setUp()
'validate',
'save',
'getMediaGalleryEntries',
+ 'getExtensionAttributes'
]
);
- $this->initializedProductMock->expects($this->any())
+ $this->initializedProduct->expects($this->any())
->method('hasGalleryAttribute')
->willReturn(true);
- $this->filterBuilderMock = $this->createMock(\Magento\Framework\Api\FilterBuilder::class);
- $this->initializationHelperMock = $this->createMock(\Magento\Catalog\Controller\Adminhtml\Product\Initialization\Helper::class);
- $this->collectionFactoryMock = $this->createPartialMock(\Magento\Catalog\Model\ResourceModel\Product\CollectionFactory::class, ['create']);
- $this->searchCriteriaBuilderMock = $this->createMock(\Magento\Framework\Api\SearchCriteriaBuilder::class);
- $this->metadataServiceMock = $this->createMock(\Magento\Catalog\Api\ProductAttributeRepositoryInterface::class);
- $this->searchResultsFactoryMock = $this->createPartialMock(\Magento\Catalog\Api\Data\ProductSearchResultsInterfaceFactory::class, ['create']);
- $this->resourceModelMock = $this->createMock(\Magento\Catalog\Model\ResourceModel\Product::class);
+ $this->filterBuilder = $this->createMock(FilterBuilder::class);
+ $this->initializationHelper = $this->createMock(Helper::class);
+ $this->collectionFactory = $this->createPartialMock(CollectionFactory::class, ['create']);
+ $this->searchCriteriaBuilder = $this->createMock(SearchCriteriaBuilder::class);
+ $this->metadataService = $this->createMock(ProductAttributeRepositoryInterface::class);
+ $this->searchResultsFactory = $this->createPartialMock(ProductSearchResultsInterfaceFactory::class, ['create']);
+ $this->resourceModel = $this->createMock(\Magento\Catalog\Model\ResourceModel\Product::class);
$this->objectManager = new ObjectManager($this);
- $this->extensibleDataObjectConverterMock = $this
- ->getMockBuilder(\Magento\Framework\Api\ExtensibleDataObjectConverter::class)
+ $this->extensibleDataObjectConverter = $this
+ ->getMockBuilder(ExtensibleDataObjectConverter::class)
->setMethods(['toNestedArray'])
->disableOriginalConstructor()
->getMock();
- $this->fileSystemMock = $this->getMockBuilder(\Magento\Framework\Filesystem::class)
+ $this->fileSystem = $this->getMockBuilder(Filesystem::class)
->disableOriginalConstructor()->getMock();
- $this->mimeTypeExtensionMapMock =
- $this->getMockBuilder(\Magento\Catalog\Model\Product\Gallery\MimeTypeExtensionMap::class)->getMock();
- $this->contentFactoryMock = $this->createPartialMock(\Magento\Framework\Api\Data\ImageContentInterfaceFactory::class, ['create']);
- $this->contentValidatorMock = $this->getMockBuilder(
- \Magento\Framework\Api\ImageContentValidatorInterface::class)
+ $this->mimeTypeExtensionMap = $this->getMockBuilder(MimeTypeExtensionMap::class)
+ ->getMock();
+ $this->contentFactory = $this->createPartialMock(ImageContentInterfaceFactory::class, ['create']);
+ $this->contentValidator = $this->getMockBuilder(ImageContentValidatorInterface::class)
->disableOriginalConstructor()
->getMock();
- $this->linkTypeProviderMock = $this->createPartialMock(\Magento\Catalog\Model\Product\LinkTypeProvider::class, ['getLinkTypes']);
- $this->imageProcessorMock = $this->createMock(\Magento\Framework\Api\ImageProcessorInterface::class);
+ $this->linkTypeProvider = $this->createPartialMock(LinkTypeProvider::class, ['getLinkTypes']);
+ $this->imageProcessor = $this->createMock(ImageProcessorInterface::class);
- $this->storeManagerMock = $this->getMockBuilder(\Magento\Store\Model\StoreManagerInterface::class)
+ $this->storeManager = $this->getMockBuilder(StoreManagerInterface::class)
->disableOriginalConstructor()
->setMethods([])
->getMockForAbstractClass();
- $storeMock = $this->getMockBuilder(\Magento\Store\Api\Data\StoreInterface::class)
+ $this->productExtension = $this->getMockBuilder(ProductExtensionInterface::class)
+ ->setMethods(['__toArray'])
+ ->disableOriginalConstructor()
+ ->getMockForAbstractClass();
+ $this->productExtension
+ ->method('__toArray')
+ ->willReturn([]);
+ $this->product
+ ->method('getExtensionAttributes')
+ ->willReturn($this->productExtension);
+ $this->initializedProduct
+ ->method('getExtensionAttributes')
+ ->willReturn($this->productExtension);
+ $storeMock = $this->getMockBuilder(StoreInterface::class)
->disableOriginalConstructor()
->setMethods([])
->getMockForAbstractClass();
$storeMock->expects($this->any())->method('getWebsiteId')->willReturn('1');
$storeMock->expects($this->any())->method('getCode')->willReturn(\Magento\Store\Model\Store::ADMIN_CODE);
- $this->storeManagerMock->expects($this->any())->method('getStore')->willReturn($storeMock);
+ $this->storeManager->expects($this->any())->method('getStore')->willReturn($storeMock);
- $this->mediaGalleryProcessor = $this->createMock(\Magento\Catalog\Model\Product\Gallery\Processor::class);
+ $this->mediaGalleryProcessor = $this->createMock(MediaGalleryProcessor::class);
- $this->collectionProcessorMock = $this->getMockBuilder(CollectionProcessorInterface::class)
+ $this->collectionProcessor = $this->getMockBuilder(CollectionProcessorInterface::class)
->getMock();
- $this->serializerMock = $this->getMockBuilder(Json::class)->getMock();
- $this->serializerMock->expects($this->any())
- ->method('unserialize')
- ->will(
- $this->returnCallback(
- function ($value) {
- return json_decode($value, true);
- }
- )
- );
-
$this->model = $this->objectManager->getObject(
- \Magento\Catalog\Model\ProductRepository::class,
+ ProductRepository::class,
[
- 'productFactory' => $this->productFactoryMock,
- 'initializationHelper' => $this->initializationHelperMock,
- 'resourceModel' => $this->resourceModelMock,
- 'filterBuilder' => $this->filterBuilderMock,
- 'collectionFactory' => $this->collectionFactoryMock,
- 'searchCriteriaBuilder' => $this->searchCriteriaBuilderMock,
- 'metadataServiceInterface' => $this->metadataServiceMock,
- 'searchResultsFactory' => $this->searchResultsFactoryMock,
- 'extensibleDataObjectConverter' => $this->extensibleDataObjectConverterMock,
- 'contentValidator' => $this->contentValidatorMock,
- 'fileSystem' => $this->fileSystemMock,
- 'contentFactory' => $this->contentFactoryMock,
- 'mimeTypeExtensionMap' => $this->mimeTypeExtensionMapMock,
- 'linkTypeProvider' => $this->linkTypeProviderMock,
- 'imageProcessor' => $this->imageProcessorMock,
- 'storeManager' => $this->storeManagerMock,
+ 'productFactory' => $this->productFactory,
+ 'initializationHelper' => $this->initializationHelper,
+ 'resourceModel' => $this->resourceModel,
+ 'filterBuilder' => $this->filterBuilder,
+ 'collectionFactory' => $this->collectionFactory,
+ 'searchCriteriaBuilder' => $this->searchCriteriaBuilder,
+ 'metadataServiceInterface' => $this->metadataService,
+ 'searchResultsFactory' => $this->searchResultsFactory,
+ 'extensibleDataObjectConverter' => $this->extensibleDataObjectConverter,
+ 'contentValidator' => $this->contentValidator,
+ 'fileSystem' => $this->fileSystem,
+ 'contentFactory' => $this->contentFactory,
+ 'mimeTypeExtensionMap' => $this->mimeTypeExtensionMap,
+ 'linkTypeProvider' => $this->linkTypeProvider,
+ 'imageProcessor' => $this->imageProcessor,
+ 'storeManager' => $this->storeManager,
'mediaGalleryProcessor' => $this->mediaGalleryProcessor,
- 'collectionProcessor' => $this->collectionProcessorMock,
- 'serializer' => $this->serializerMock,
+ 'collectionProcessor' => $this->collectionProcessor,
+ 'serializer' => new Json(),
'cacheLimit' => $this->cacheLimit
]
);
@@ -284,50 +306,53 @@ function ($value) {
*/
public function testGetAbsentProduct()
{
- $this->productFactoryMock->expects($this->once())->method('create')
- ->will($this->returnValue($this->productMock));
- $this->resourceModelMock->expects($this->once())->method('getIdBySku')->with('test_sku')
+ $this->productFactory->expects($this->never())->method('create')
+ ->will($this->returnValue($this->product));
+ $this->resourceModel->expects($this->once())->method('getIdBySku')->with('test_sku')
->will($this->returnValue(null));
- $this->productFactoryMock->expects($this->never())->method('setData');
+ $this->productFactory->expects($this->never())->method('setData');
$this->model->get('test_sku');
}
public function testCreateCreatesProduct()
{
$sku = 'test_sku';
- $this->productFactoryMock->expects($this->once())->method('create')
- ->will($this->returnValue($this->productMock));
- $this->resourceModelMock->expects($this->once())->method('getIdBySku')->with($sku)
+ $this->resourceModel->expects($this->once())->method('getIdBySku')->with($sku)
->will($this->returnValue('test_id'));
- $this->productMock->expects($this->once())->method('load')->with('test_id');
- $this->productMock->expects($this->once())->method('getSku')->willReturn($sku);
- $this->assertEquals($this->productMock, $this->model->get($sku));
+ $this->productFactory->expects($this->once())->method('create')
+ ->will($this->returnValue($this->product));
+ $this->product->expects($this->once())->method('load')->with('test_id');
+ $this->product->expects($this->any())->method('getId')->willReturn('test_id');
+ $this->product->expects($this->any())->method('getSku')->willReturn($sku);
+ $this->assertEquals($this->product, $this->model->get($sku));
}
public function testGetProductInEditMode()
{
$sku = 'test_sku';
- $this->productFactoryMock->expects($this->once())->method('create')
- ->will($this->returnValue($this->productMock));
- $this->resourceModelMock->expects($this->once())->method('getIdBySku')->with($sku)
+ $this->resourceModel->expects($this->once())->method('getIdBySku')->with($sku)
->will($this->returnValue('test_id'));
- $this->productMock->expects($this->once())->method('setData')->with('_edit_mode', true);
- $this->productMock->expects($this->once())->method('load')->with('test_id');
- $this->productMock->expects($this->once())->method('getSku')->willReturn($sku);
- $this->assertEquals($this->productMock, $this->model->get($sku, true));
+ $this->productFactory->expects($this->once())->method('create')
+ ->will($this->returnValue($this->product));
+ $this->product->expects($this->once())->method('setData')->with('_edit_mode', true);
+ $this->product->expects($this->once())->method('load')->with('test_id');
+ $this->product->expects($this->any())->method('getId')->willReturn('test_id');
+ $this->product->expects($this->any())->method('getSku')->willReturn($sku);
+ $this->assertEquals($this->product, $this->model->get($sku, true));
}
public function testGetBySkuWithSpace()
{
$trimmedSku = 'test_sku';
$sku = 'test_sku ';
- $this->productFactoryMock->expects($this->once())->method('create')
- ->will($this->returnValue($this->productMock));
- $this->resourceModelMock->expects($this->once())->method('getIdBySku')->with($sku)
+ $this->resourceModel->expects($this->once())->method('getIdBySku')->with($sku)
->will($this->returnValue('test_id'));
- $this->productMock->expects($this->once())->method('load')->with('test_id');
- $this->productMock->expects($this->once())->method('getSku')->willReturn($trimmedSku);
- $this->assertEquals($this->productMock, $this->model->get($sku));
+ $this->productFactory->expects($this->once())->method('create')
+ ->will($this->returnValue($this->product));
+ $this->product->expects($this->once())->method('load')->with('test_id');
+ $this->product->expects($this->any())->method('getId')->willReturn('test_id');
+ $this->product->expects($this->any())->method('getSku')->willReturn($trimmedSku);
+ $this->assertEquals($this->product, $this->model->get($sku));
}
public function testGetWithSetStoreId()
@@ -335,13 +360,13 @@ public function testGetWithSetStoreId()
$productId = 123;
$sku = 'test-sku';
$storeId = 7;
- $this->productFactoryMock->expects($this->once())->method('create')->willReturn($this->productMock);
- $this->resourceModelMock->expects($this->once())->method('getIdBySku')->with($sku)->willReturn($productId);
- $this->productMock->expects($this->once())->method('setData')->with('store_id', $storeId);
- $this->productMock->expects($this->once())->method('load')->with($productId);
- $this->productMock->expects($this->once())->method('getId')->willReturn($productId);
- $this->productMock->expects($this->once())->method('getSku')->willReturn($sku);
- $this->assertSame($this->productMock, $this->model->get($sku, false, $storeId));
+ $this->resourceModel->expects($this->once())->method('getIdBySku')->with($sku)->willReturn($productId);
+ $this->productFactory->expects($this->once())->method('create')->willReturn($this->product);
+ $this->product->expects($this->once())->method('setData')->with('store_id', $storeId);
+ $this->product->expects($this->once())->method('load')->with($productId);
+ $this->product->expects($this->any())->method('getId')->willReturn($productId);
+ $this->product->expects($this->any())->method('getSku')->willReturn($sku);
+ $this->assertSame($this->product, $this->model->get($sku, false, $storeId));
}
/**
@@ -350,22 +375,28 @@ public function testGetWithSetStoreId()
*/
public function testGetByIdAbsentProduct()
{
- $this->productFactoryMock->expects($this->once())->method('create')
- ->will($this->returnValue($this->productMock));
- $this->productMock->expects($this->once())->method('load')->with('product_id');
- $this->productMock->expects($this->once())->method('getId')->willReturn(null);
+ $this->productFactory->expects($this->once())->method('create')
+ ->will($this->returnValue($this->product));
+ $this->product->expects($this->once())->method('load')->with('product_id');
+ $this->product->expects($this->once())->method('getId')->willReturn(null);
$this->model->getById('product_id');
}
public function testGetByIdProductInEditMode()
{
$productId = 123;
- $this->productFactoryMock->expects($this->once())->method('create')
- ->will($this->returnValue($this->productMock));
- $this->productMock->expects($this->once())->method('setData')->with('_edit_mode', true);
- $this->productMock->expects($this->once())->method('load')->with($productId);
- $this->productMock->expects($this->atLeastOnce())->method('getId')->willReturn($productId);
- $this->assertEquals($this->productMock, $this->model->getById($productId, true));
+ $this->productFactory->method('create')
+ ->willReturn($this->product);
+ $this->product->method('setData')
+ ->with('_edit_mode', true);
+ $this->product->method('load')
+ ->with($productId);
+ $this->product->expects($this->atLeastOnce())
+ ->method('getId')
+ ->willReturn($productId);
+ $this->product->method('getSku')
+ ->willReturn('simple');
+ $this->assertEquals($this->product, $this->model->getById($productId, true));
}
/**
@@ -379,20 +410,24 @@ public function testGetByIdProductInEditMode()
public function testGetByIdForCacheKeyGenerate($identifier, $editMode, $storeId)
{
$callIndex = 0;
- $this->productFactoryMock->expects($this->once())->method('create')
- ->will($this->returnValue($this->productMock));
+ $this->productFactory->expects($this->once())->method('create')
+ ->will($this->returnValue($this->product));
if ($editMode) {
- $this->productMock->expects($this->at($callIndex))->method('setData')->with('_edit_mode', $editMode);
+ $this->product->expects($this->at($callIndex))->method('setData')->with('_edit_mode', $editMode);
++$callIndex;
}
if ($storeId !== null) {
- $this->productMock->expects($this->at($callIndex))->method('setData')->with('store_id', $storeId);
+ $this->product->expects($this->at($callIndex))->method('setData')->with('store_id', $storeId);
}
- $this->productMock->expects($this->once())->method('load')->with($identifier);
- $this->productMock->expects($this->atLeastOnce())->method('getId')->willReturn($identifier);
- $this->assertEquals($this->productMock, $this->model->getById($identifier, $editMode, $storeId));
+ $this->product->method('load')->with($identifier);
+ $this->product->expects($this->atLeastOnce())
+ ->method('getId')
+ ->willReturn($identifier);
+ $this->product->method('getSku')
+ ->willReturn('simple');
+ $this->assertEquals($this->product, $this->model->getById($identifier, $editMode, $storeId));
//Second invocation should just return from cache
- $this->assertEquals($this->productMock, $this->model->getById($identifier, $editMode, $storeId));
+ $this->assertEquals($this->product, $this->model->getById($identifier, $editMode, $storeId));
}
/**
@@ -406,17 +441,22 @@ public function testGetByIdForcedReload()
$editMode = false;
$storeId = 0;
- $this->productFactoryMock->expects($this->exactly(2))->method('create')
- ->will($this->returnValue($this->productMock));
- $this->productMock->expects($this->exactly(2))->method('load');
- $this->serializerMock->expects($this->exactly(3))->method('serialize');
+ $this->productFactory->expects($this->exactly(2))->method('create')
+ ->willReturn($this->product);
+ $this->product->expects($this->exactly(2))
+ ->method('load');
- $this->productMock->expects($this->exactly(4))->method('getId')->willReturn($identifier);
- $this->assertEquals($this->productMock, $this->model->getById($identifier, $editMode, $storeId));
+ $this->product->expects($this->exactly(4))
+ ->method('getId')
+ ->willReturn($identifier);
+ $this->product->method('getSku')
+ ->willReturn('simple');
+
+ $this->assertEquals($this->product, $this->model->getById($identifier, $editMode, $storeId));
//second invocation should just return from cache
- $this->assertEquals($this->productMock, $this->model->getById($identifier, $editMode, $storeId));
+ $this->assertEquals($this->product, $this->model->getById($identifier, $editMode, $storeId));
//force reload
- $this->assertEquals($this->productMock, $this->model->getById($identifier, $editMode, $storeId, true));
+ $this->assertEquals($this->product, $this->model->getById($identifier, $editMode, $storeId, true));
}
/**
@@ -431,10 +471,9 @@ public function testGetByIdWhenCacheReduced()
$productsCount = $this->cacheLimit * 2;
$productMocks = $this->getProductMocksForReducedCache($productsCount);
- $productFactoryInvMock = $this->productFactoryMock->expects($this->exactly($productsCount))
+ $productFactoryInvMock = $this->productFactory->expects($this->exactly($productsCount))
->method('create');
call_user_func_array([$productFactoryInvMock, 'willReturnOnConsecutiveCalls'], $productMocks);
- $this->serializerMock->expects($this->atLeastOnce())->method('serialize');
for ($i = 1; $i <= $productsCount; $i++) {
$product = $this->model->getById($i, false, 0);
@@ -486,87 +525,101 @@ public function testGetForcedReload()
$editMode = false;
$storeId = 0;
- $this->productFactoryMock->expects($this->exactly(2))->method('create')
- ->will($this->returnValue($this->productMock));
- $this->productMock->expects($this->exactly(2))->method('load');
- $this->productMock->expects($this->exactly(2))->method('getId')->willReturn($sku);
- $this->resourceModelMock->expects($this->exactly(2))->method('getIdBySku')
+ $this->resourceModel->expects($this->exactly(2))->method('getIdBySku')
->with($sku)->willReturn($id);
- $this->productMock->expects($this->exactly(2))->method('getSku')->willReturn($sku);
- $this->serializerMock->expects($this->exactly(3))->method('serialize');
+ $this->productFactory->expects($this->exactly(2))->method('create')
+ ->will($this->returnValue($this->product));
+ $this->product->expects($this->exactly(2))->method('load');
+ $this->product->expects($this->any())->method('getId')->willReturn($id);
+ $this->product->expects($this->any())->method('getSku')->willReturn($sku);
- $this->assertEquals($this->productMock, $this->model->get($sku, $editMode, $storeId));
+ $this->assertEquals($this->product, $this->model->get($sku, $editMode, $storeId));
//second invocation should just return from cache
- $this->assertEquals($this->productMock, $this->model->get($sku, $editMode, $storeId));
+ $this->assertEquals($this->product, $this->model->get($sku, $editMode, $storeId));
//force reload
- $this->assertEquals($this->productMock, $this->model->get($sku, $editMode, $storeId, true));
+ $this->assertEquals($this->product, $this->model->get($sku, $editMode, $storeId, true));
}
public function testGetByIdWithSetStoreId()
{
$productId = 123;
$storeId = 1;
- $this->productFactoryMock->expects($this->atLeastOnce())->method('create')
- ->will($this->returnValue($this->productMock));
- $this->productMock->expects($this->once())->method('setData')->with('store_id', $storeId);
- $this->productMock->expects($this->once())->method('load')->with($productId);
- $this->productMock->expects($this->atLeastOnce())->method('getId')->willReturn($productId);
- $this->assertEquals($this->productMock, $this->model->getById($productId, false, $storeId));
+ $this->productFactory->method('create')
+ ->willReturn($this->product);
+ $this->product->method('setData')
+ ->with('store_id', $storeId);
+ $this->product->method('load')
+ ->with($productId);
+ $this->product->expects($this->atLeastOnce())
+ ->method('getId')
+ ->willReturn($productId);
+ $this->product->method('getSku')
+ ->willReturn('simple');
+ $this->assertEquals($this->product, $this->model->getById($productId, false, $storeId));
}
public function testGetBySkuFromCacheInitializedInGetById()
{
$productId = 123;
$productSku = 'product_123';
- $this->productFactoryMock->expects($this->once())->method('create')
- ->will($this->returnValue($this->productMock));
- $this->productMock->expects($this->once())->method('load')->with($productId);
- $this->productMock->expects($this->atLeastOnce())->method('getId')->willReturn($productId);
- $this->productMock->expects($this->once())->method('getSku')->willReturn($productSku);
- $this->assertEquals($this->productMock, $this->model->getById($productId));
- $this->assertEquals($this->productMock, $this->model->get($productSku));
+ $this->productFactory->method('create')
+ ->willReturn($this->product);
+ $this->product->method('load')
+ ->with($productId);
+ $this->product->expects($this->atLeastOnce())
+ ->method('getId')
+ ->willReturn($productId);
+ $this->product->method('getSku')
+ ->willReturn($productSku);
+ $this->assertEquals($this->product, $this->model->getById($productId));
+ $this->assertEquals($this->product, $this->model->get($productSku));
}
public function testSaveExisting()
{
- $this->storeManagerMock->expects($this->any())->method('getWebsites')->willReturn([1 => 'default']);
- $this->resourceModelMock->expects($this->any())->method('getIdBySku')->will($this->returnValue(100));
- $this->productFactoryMock->expects($this->any())
+ $id = 100;
+ $this->storeManager->expects($this->any())->method('getWebsites')->willReturn([1 => 'default']);
+ $this->resourceModel->expects($this->any())->method('getIdBySku')->willReturn($id);
+ $this->productFactory->expects($this->any())
->method('create')
- ->will($this->returnValue($this->productMock));
- $this->initializationHelperMock->expects($this->never())->method('initialize');
- $this->resourceModelMock->expects($this->once())->method('validate')->with($this->productMock)
+ ->willReturn($this->product);
+ $this->initializationHelper->expects($this->never())->method('initialize');
+ $this->resourceModel->expects($this->once())->method('validate')->with($this->product)
->willReturn(true);
- $this->resourceModelMock->expects($this->once())->method('save')->with($this->productMock)->willReturn(true);
- $this->extensibleDataObjectConverterMock
+ $this->resourceModel->expects($this->once())->method('save')->with($this->product)->willReturn(true);
+ $this->extensibleDataObjectConverter
->expects($this->once())
->method('toNestedArray')
- ->will($this->returnValue($this->productData));
- $this->productMock->expects($this->once())->method('getWebsiteIds')->willReturn([]);
- $this->productMock->expects($this->atLeastOnce())->method('getSku')->willReturn($this->productData['sku']);
+ ->willReturn($this->productData);
+ $this->product->expects($this->atLeastOnce())->method('getSku')->willReturn($this->productData['sku']);
+ $this->product->expects($this->at(0))->method('getId')->willReturn(null);
+ $this->product->expects($this->any())->method('getId')->willReturn($id);
- $this->assertEquals($this->productMock, $this->model->save($this->productMock));
+ $this->assertEquals($this->product, $this->model->save($this->product));
}
public function testSaveNew()
{
- $this->storeManagerMock->expects($this->any())->method('getWebsites')->willReturn([1 => 'default']);
- $this->resourceModelMock->expects($this->at(0))->method('getIdBySku')->will($this->returnValue(null));
- $this->resourceModelMock->expects($this->at(3))->method('getIdBySku')->will($this->returnValue(100));
- $this->productFactoryMock->expects($this->any())
+ $id = 100;
+ $this->storeManager->expects($this->any())->method('getWebsites')->willReturn([1 => 'default']);
+ $this->resourceModel->expects($this->at(0))->method('getIdBySku')->willReturn(null);
+ $this->resourceModel->expects($this->at(3))->method('getIdBySku')->willReturn($id);
+ $this->product->expects($this->at(0))->method('getId')->willReturn(null);
+ $this->product->expects($this->any())->method('getId')->willReturn($id);
+ $this->productFactory->expects($this->any())
->method('create')
- ->will($this->returnValue($this->productMock));
- $this->initializationHelperMock->expects($this->never())->method('initialize');
- $this->resourceModelMock->expects($this->once())->method('validate')->with($this->productMock)
+ ->willReturn($this->product);
+ $this->initializationHelper->expects($this->never())->method('initialize');
+ $this->resourceModel->expects($this->once())->method('validate')->with($this->product)
->willReturn(true);
- $this->resourceModelMock->expects($this->once())->method('save')->with($this->productMock)->willReturn(true);
- $this->extensibleDataObjectConverterMock
+ $this->resourceModel->expects($this->once())->method('save')->with($this->product)->willReturn(true);
+ $this->extensibleDataObjectConverter
->expects($this->once())
->method('toNestedArray')
- ->will($this->returnValue($this->productData));
- $this->productMock->expects($this->once())->method('getWebsiteIds')->willReturn([]);
+ ->willReturn($this->productData);
+ $this->product->method('getSku')->willReturn('simple');
- $this->assertEquals($this->productMock, $this->model->save($this->productMock));
+ $this->assertEquals($this->product, $this->model->save($this->product));
}
/**
@@ -575,23 +628,25 @@ public function testSaveNew()
*/
public function testSaveUnableToSaveException()
{
- $this->storeManagerMock->expects($this->any())->method('getWebsites')->willReturn([1 => 'default']);
- $this->resourceModelMock->expects($this->exactly(1))->method('getIdBySku')->will($this->returnValue(null));
- $this->productFactoryMock->expects($this->exactly(2))
+ $this->storeManager->expects($this->any())->method('getWebsites')->willReturn([1 => 'default']);
+ $this->resourceModel->expects($this->exactly(1))
+ ->method('getIdBySku')
+ ->willReturn(null);
+ $this->productFactory->expects($this->exactly(1))
->method('create')
- ->will($this->returnValue($this->productMock));
- $this->initializationHelperMock->expects($this->never())->method('initialize');
- $this->resourceModelMock->expects($this->once())->method('validate')->with($this->productMock)
+ ->willReturn($this->product);
+ $this->initializationHelper->expects($this->never())->method('initialize');
+ $this->resourceModel->expects($this->once())->method('validate')->with($this->product)
->willReturn(true);
- $this->resourceModelMock->expects($this->once())->method('save')->with($this->productMock)
+ $this->resourceModel->expects($this->once())->method('save')->with($this->product)
->willThrowException(new \Exception());
- $this->extensibleDataObjectConverterMock
+ $this->extensibleDataObjectConverter
->expects($this->once())
->method('toNestedArray')
- ->will($this->returnValue($this->productData));
- $this->productMock->expects($this->once())->method('getWebsiteIds')->willReturn([]);
+ ->willReturn($this->productData);
+ $this->product->method('getSku')->willReturn('simple');
- $this->model->save($this->productMock);
+ $this->model->save($this->product);
}
/**
@@ -600,24 +655,26 @@ public function testSaveUnableToSaveException()
*/
public function testSaveException()
{
- $this->storeManagerMock->expects($this->any())->method('getWebsites')->willReturn([1 => 'default']);
- $this->resourceModelMock->expects($this->exactly(1))->method('getIdBySku')->will($this->returnValue(null));
- $this->productFactoryMock->expects($this->exactly(2))
+ $this->storeManager->expects($this->any())->method('getWebsites')->willReturn([1 => 'default']);
+ $this->resourceModel->expects($this->exactly(1))
+ ->method('getIdBySku')
+ ->willReturn(null);
+ $this->productFactory->expects($this->exactly(1))
->method('create')
- ->will($this->returnValue($this->productMock));
- $this->initializationHelperMock->expects($this->never())->method('initialize');
- $this->resourceModelMock->expects($this->once())->method('validate')->with($this->productMock)
+ ->willReturn($this->product);
+ $this->initializationHelper->expects($this->never())->method('initialize');
+ $this->resourceModel->expects($this->once())->method('validate')->with($this->product)
->willReturn(true);
- $this->resourceModelMock->expects($this->once())->method('save')->with($this->productMock)
+ $this->resourceModel->expects($this->once())->method('save')->with($this->product)
->willThrowException(new \Magento\Eav\Model\Entity\Attribute\Exception(__('123')));
- $this->productMock->expects($this->once())->method('getId')->willReturn(null);
- $this->extensibleDataObjectConverterMock
+ $this->product->expects($this->exactly(2))->method('getId')->willReturn(null);
+ $this->extensibleDataObjectConverter
->expects($this->once())
->method('toNestedArray')
- ->will($this->returnValue($this->productData));
- $this->productMock->expects($this->once())->method('getWebsiteIds')->willReturn([]);
+ ->willReturn($this->productData);
+ $this->product->method('getSku')->willReturn('simple');
- $this->model->save($this->productMock);
+ $this->model->save($this->product);
}
/**
@@ -626,22 +683,28 @@ public function testSaveException()
*/
public function testSaveInvalidProductException()
{
- $this->storeManagerMock->expects($this->any())->method('getWebsites')->willReturn([1 => 'default']);
- $this->resourceModelMock->expects($this->exactly(1))->method('getIdBySku')->will($this->returnValue(null));
- $this->productFactoryMock->expects($this->exactly(2))
+ $this->storeManager->expects($this->any())->method('getWebsites')->willReturn([1 => 'default']);
+ $this->resourceModel
+ ->expects($this->exactly(1))
+ ->method('getIdBySku')
+ ->willReturn(null);
+ $this->productFactory->expects($this->exactly(1))
->method('create')
- ->will($this->returnValue($this->productMock));
- $this->initializationHelperMock->expects($this->never())->method('initialize');
- $this->resourceModelMock->expects($this->once())->method('validate')->with($this->productMock)
+ ->willReturn($this->product);
+ $this->initializationHelper->expects($this->never())->method('initialize');
+ $this->resourceModel
+ ->expects($this->once())
+ ->method('validate')
+ ->with($this->product)
->willReturn(['error1', 'error2']);
- $this->productMock->expects($this->never())->method('getId');
- $this->extensibleDataObjectConverterMock
+ $this->product->expects($this->once())->method('getId')->willReturn(null);
+ $this->extensibleDataObjectConverter
->expects($this->once())
->method('toNestedArray')
- ->will($this->returnValue($this->productData));
- $this->productMock->expects($this->once())->method('getWebsiteIds')->willReturn([]);
+ ->willReturn($this->productData);
+ $this->product->method('getSku')->willReturn('simple');
- $this->model->save($this->productMock);
+ $this->model->save($this->product);
}
/**
@@ -650,38 +713,37 @@ public function testSaveInvalidProductException()
*/
public function testSaveThrowsTemporaryStateExceptionIfDatabaseConnectionErrorOccurred()
{
- $this->storeManagerMock->expects($this->any())->method('getWebsites')->willReturn([1 => 'default']);
- $this->productFactoryMock->expects($this->any())
+ $this->storeManager->expects($this->any())->method('getWebsites')->willReturn([1 => 'default']);
+ $this->productFactory->expects($this->any())
->method('create')
- ->will($this->returnValue($this->productMock));
- $this->initializationHelperMock->expects($this->never())
+ ->willReturn($this->product);
+ $this->initializationHelper->expects($this->never())
->method('initialize');
- $this->resourceModelMock->expects($this->once())
+ $this->resourceModel->expects($this->once())
->method('validate')
- ->with($this->productMock)
+ ->with($this->product)
->willReturn(true);
- $this->resourceModelMock->expects($this->once())
+ $this->resourceModel->expects($this->once())
->method('save')
- ->with($this->productMock)
+ ->with($this->product)
->willThrowException(new ConnectionException('Connection lost'));
- $this->extensibleDataObjectConverterMock
+ $this->extensibleDataObjectConverter
->expects($this->once())
->method('toNestedArray')
- ->will($this->returnValue($this->productData));
- $this->productMock->expects($this->once())
- ->method('getWebsiteIds')
- ->willReturn([]);
+ ->willReturn($this->productData);
+ $this->product->method('getSku')
+ ->willReturn('simple');
- $this->model->save($this->productMock);
+ $this->model->save($this->product);
}
public function testDelete()
{
- $this->productMock->expects($this->exactly(2))->method('getSku')->willReturn('product-42');
- $this->productMock->expects($this->exactly(2))->method('getId')->willReturn(42);
- $this->resourceModelMock->expects($this->once())->method('delete')->with($this->productMock)
+ $this->product->expects($this->exactly(2))->method('getSku')->willReturn('product-42');
+ $this->product->expects($this->exactly(2))->method('getId')->willReturn(42);
+ $this->resourceModel->expects($this->once())->method('delete')->with($this->product)
->willReturn(true);
- $this->assertTrue($this->model->delete($this->productMock));
+ $this->assertTrue($this->model->delete($this->product));
}
/**
@@ -690,22 +752,23 @@ public function testDelete()
*/
public function testDeleteException()
{
- $this->productMock->expects($this->exactly(2))->method('getSku')->willReturn('product-42');
- $this->productMock->expects($this->exactly(2))->method('getId')->willReturn(42);
- $this->resourceModelMock->expects($this->once())->method('delete')->with($this->productMock)
+ $this->product->expects($this->exactly(2))->method('getSku')->willReturn('product-42');
+ $this->product->expects($this->exactly(2))->method('getId')->willReturn(42);
+ $this->resourceModel->expects($this->once())->method('delete')->with($this->product)
->willThrowException(new \Exception());
- $this->model->delete($this->productMock);
+ $this->model->delete($this->product);
}
public function testDeleteById()
{
$sku = 'product-42';
- $this->productFactoryMock->expects($this->once())->method('create')
- ->will($this->returnValue($this->productMock));
- $this->resourceModelMock->expects($this->once())->method('getIdBySku')->with($sku)
+ $this->productFactory->expects($this->once())->method('create')
+ ->will($this->returnValue($this->product));
+ $this->resourceModel->expects($this->once())->method('getIdBySku')->with($sku)
->will($this->returnValue('42'));
- $this->productMock->expects($this->once())->method('load')->with('42');
- $this->productMock->expects($this->atLeastOnce())->method('getSku')->willReturn($sku);
+ $this->product->expects($this->once())->method('load')->with('42');
+ $this->product->expects($this->atLeastOnce())->method('getSku')->willReturn($sku);
+ $this->product->expects($this->atLeastOnce())->method('getId')->willReturn(42);
$this->assertTrue($this->model->deleteById($sku));
}
@@ -714,24 +777,27 @@ public function testGetList()
$searchCriteriaMock = $this->createMock(\Magento\Framework\Api\SearchCriteriaInterface::class);
$collectionMock = $this->createMock(\Magento\Catalog\Model\ResourceModel\Product\Collection::class);
- $this->collectionFactoryMock->expects($this->once())->method('create')->willReturn($collectionMock);
+ $this->collectionFactory->expects($this->once())->method('create')->willReturn($collectionMock);
+
+ $this->product->method('getSku')
+ ->willReturn('simple');
$collectionMock->expects($this->once())->method('addAttributeToSelect')->with('*');
$collectionMock->expects($this->exactly(2))->method('joinAttribute')->withConsecutive(
['status', 'catalog_product/status', 'entity_id', null, 'inner'],
['visibility', 'catalog_product/visibility', 'entity_id', null, 'inner']
);
- $this->collectionProcessorMock->expects($this->once())
+ $this->collectionProcessor->expects($this->once())
->method('process')
->with($searchCriteriaMock, $collectionMock);
$collectionMock->expects($this->once())->method('load');
$collectionMock->expects($this->once())->method('addCategoryIds');
- $collectionMock->expects($this->atLeastOnce())->method('getItems')->willReturn([$this->productMock]);
+ $collectionMock->expects($this->atLeastOnce())->method('getItems')->willReturn([$this->product]);
$collectionMock->expects($this->once())->method('getSize')->willReturn(128);
$searchResultsMock = $this->createMock(\Magento\Catalog\Api\Data\ProductSearchResultsInterface::class);
$searchResultsMock->expects($this->once())->method('setSearchCriteria')->with($searchCriteriaMock);
- $searchResultsMock->expects($this->once())->method('setItems')->with([$this->productMock]);
- $this->searchResultsFactoryMock->expects($this->once())->method('create')->willReturn($searchResultsMock);
+ $searchResultsMock->expects($this->once())->method('setItems')->with([$this->product]);
+ $this->searchResultsFactory->expects($this->once())->method('create')->willReturn($searchResultsMock);
$this->assertEquals($searchResultsMock, $this->model->getList($searchCriteriaMock));
}
@@ -797,33 +863,42 @@ public function cacheKeyDataProvider()
* @param array $existingOptions
* @param array $expectedData
* @dataProvider saveExistingWithOptionsDataProvider
+ * @return void
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
*/
public function testSaveExistingWithOptions(array $newOptions, array $existingOptions, array $expectedData)
{
- $this->storeManagerMock->expects($this->any())->method('getWebsites')->willReturn([1 => 'default']);
- $this->resourceModelMock->expects($this->any())->method('getIdBySku')->will($this->returnValue(100));
- $this->productFactoryMock->expects($this->any())
+ $id = 100;
+ $this->storeManager->expects($this->any())->method('getWebsites')->willReturn([1 => 'default']);
+ $this->resourceModel->expects($this->any())->method('getIdBySku')->will($this->returnValue($id));
+ $this->productFactory->expects($this->any())
->method('create')
- ->will($this->returnValue($this->initializedProductMock));
- $this->initializationHelperMock->expects($this->never())->method('initialize');
- $this->resourceModelMock->expects($this->once())->method('validate')->with($this->initializedProductMock)
+ ->willReturn($this->initializedProduct);
+ $this->initializationHelper->expects($this->never())->method('initialize');
+ $this->resourceModel
+ ->expects($this->once())->method('validate')
+ ->with($this->initializedProduct)
+ ->willReturn(true);
+ $this->resourceModel
+ ->expects($this->once())->method('save')
+ ->with($this->initializedProduct)
->willReturn(true);
- $this->resourceModelMock->expects($this->once())->method('save')
- ->with($this->initializedProductMock)->willReturn(true);
//option data
$this->productData['options'] = $newOptions;
- $this->extensibleDataObjectConverterMock
+ $this->extensibleDataObjectConverter
->expects($this->once())
->method('toNestedArray')
- ->will($this->returnValue($this->productData));
+ ->willReturn($this->productData);
- $this->initializedProductMock->expects($this->once())->method('getWebsiteIds')->willReturn([]);
- $this->initializedProductMock->expects($this->atLeastOnce())
- ->method('getSku')->willReturn($this->productData['sku']);
- $this->productMock->expects($this->atLeastOnce())->method('getSku')->willReturn($this->productData['sku']);
+ $this->initializedProduct
+ ->expects($this->atLeastOnce())
+ ->method('getSku')
+ ->willReturn($this->productData['sku']);
+ $this->product->expects($this->atLeastOnce())->method('getSku')->willReturn($this->productData['sku']);
+ $this->initializedProduct->expects($this->at(0))->method('getId')->willReturn(null);
+ $this->initializedProduct->expects($this->any())->method('getId')->willReturn($id);
- $this->assertEquals($this->initializedProductMock, $this->model->save($this->productMock));
+ $this->assertEquals($this->initializedProduct, $this->model->save($this->product));
}
/**
@@ -863,7 +938,7 @@ public function saveExistingWithOptionsDataProvider()
],
];
- /** @var \Magento\Catalog\Model\Product\Option|\PHPUnit_Framework_MockObject_MockObject $existingOption1 */
+ /** @var \Magento\Catalog\Model\Product\Option|MockObject $existingOption1 */
$existingOption1 = $this->getMockBuilder(\Magento\Catalog\Model\Product\Option::class)
->disableOriginalConstructor()
->setMethods(null)
@@ -968,32 +1043,40 @@ public function saveExistingWithOptionsDataProvider()
* @param array $existingLinks
* @param array $expectedData
* @dataProvider saveWithLinksDataProvider
+ * @return void
* @throws \Magento\Framework\Exception\CouldNotSaveException
* @throws \Magento\Framework\Exception\InputException
*/
public function testSaveWithLinks(array $newLinks, array $existingLinks, array $expectedData)
{
- $this->storeManagerMock->expects($this->any())->method('getWebsites')->willReturn([1 => 'default']);
- $this->resourceModelMock->expects($this->any())->method('getIdBySku')->will($this->returnValue(100));
- $this->productFactoryMock->expects($this->any())
+ $this->storeManager->expects($this->any())->method('getWebsites')->willReturn([1 => 'default']);
+ $this->resourceModel->expects($this->any())->method('getIdBySku')->willReturn(100);
+ $this->productFactory
+ ->expects($this->any())
->method('create')
- ->will($this->returnValue($this->initializedProductMock));
- $this->initializationHelperMock->expects($this->never())->method('initialize');
- $this->resourceModelMock->expects($this->once())->method('validate')->with($this->initializedProductMock)
+ ->will($this->returnValue($this->initializedProduct));
+ $this->initializedProduct->method('getId')->willReturn(100);
+ $this->initializationHelper->expects($this->never())->method('initialize');
+ $this->resourceModel
+ ->expects($this->once())->method('validate')
+ ->with($this->initializedProduct)
+ ->willReturn(true);
+ $this->resourceModel
+ ->expects($this->once())->method('save')
+ ->with($this->initializedProduct)
->willReturn(true);
- $this->resourceModelMock->expects($this->once())->method('save')
- ->with($this->initializedProductMock)->willReturn(true);
- $this->initializedProductMock->setData("product_links", $existingLinks);
+ $this->initializedProduct->setData("product_links", $existingLinks);
if (!empty($newLinks)) {
$linkTypes = ['related' => 1, 'upsell' => 4, 'crosssell' => 5, 'associated' => 3];
- $this->linkTypeProviderMock->expects($this->once())
+ $this->linkTypeProvider
+ ->expects($this->once())
->method('getLinkTypes')
->willReturn($linkTypes);
- $this->initializedProductMock->setData("ignore_links_flag", false);
- $this->resourceModelMock
+ $this->initializedProduct->setData("ignore_links_flag", false);
+ $this->resourceModel
->expects($this->any())->method('getProductsIdsBySkus')
->willReturn([$newLinks['linked_product_sku'] => $newLinks['linked_product_sku']]);
@@ -1010,32 +1093,34 @@ public function testSaveWithLinks(array $newLinks, array $existingLinks, array $
$this->productData['product_links'] = [$inputLink];
- $this->initializedProductMock->expects($this->any())
+ $this->initializedProduct
+ ->expects($this->any())
->method('getProductLinks')
->willReturn([$inputLink]);
} else {
- $this->resourceModelMock
+ $this->resourceModel
->expects($this->any())->method('getProductsIdsBySkus')
->willReturn([]);
$this->productData['product_links'] = [];
- $this->initializedProductMock->setData('ignore_links_flag', true);
- $this->initializedProductMock->expects($this->never())
+ $this->initializedProduct->setData('ignore_links_flag', true);
+ $this->initializedProduct
+ ->expects($this->never())
->method('getProductLinks')
->willReturn([]);
}
- $this->extensibleDataObjectConverterMock
+ $this->extensibleDataObjectConverter
->expects($this->at(0))
->method('toNestedArray')
- ->will($this->returnValue($this->productData));
+ ->willReturn($this->productData);
if (!empty($newLinks)) {
- $this->extensibleDataObjectConverterMock
+ $this->extensibleDataObjectConverter
->expects($this->at(1))
->method('toNestedArray')
- ->will($this->returnValue($newLinks));
+ ->willReturn($newLinks);
}
$outputLinks = [];
@@ -1054,23 +1139,29 @@ public function testSaveWithLinks(array $newLinks, array $existingLinks, array $
$outputLinks[] = $outputLink;
}
}
- $this->initializedProductMock->expects($this->once())->method('getWebsiteIds')->willReturn([]);
if (!empty($outputLinks)) {
- $this->initializedProductMock->expects($this->once())
+ $this->initializedProduct
+ ->expects($this->once())
->method('setProductLinks')
->with($outputLinks);
} else {
- $this->initializedProductMock->expects($this->never())
+ $this->initializedProduct
+ ->expects($this->never())
->method('setProductLinks');
}
- $this->initializedProductMock->expects($this->atLeastOnce())
- ->method('getSku')->willReturn($this->productData['sku']);
+ $this->initializedProduct
+ ->expects($this->atLeastOnce())
+ ->method('getSku')
+ ->willReturn($this->productData['sku']);
- $results = $this->model->save($this->initializedProductMock);
- $this->assertEquals($this->initializedProductMock, $results);
+ $results = $this->model->save($this->initializedProduct);
+ $this->assertEquals($this->initializedProduct, $results);
}
+ /**
+ * @return mixed
+ */
public function saveWithLinksDataProvider()
{
// Scenario 1
@@ -1142,20 +1233,21 @@ public function saveWithLinksDataProvider()
protected function setupProductMocksForSave()
{
- $this->resourceModelMock->expects($this->any())->method('getIdBySku')->will($this->returnValue(100));
- $this->productFactoryMock->expects($this->any())
+ $this->resourceModel->expects($this->any())->method('getIdBySku')->willReturn(100);
+ $this->productFactory
+ ->expects($this->any())
->method('create')
- ->will($this->returnValue($this->initializedProductMock));
- $this->initializationHelperMock->expects($this->never())->method('initialize');
- $this->resourceModelMock->expects($this->once())->method('validate')->with($this->initializedProductMock)
+ ->willReturn($this->initializedProduct);
+ $this->initializationHelper->expects($this->never())->method('initialize');
+ $this->resourceModel->expects($this->once())->method('validate')->with($this->initializedProduct)
->willReturn(true);
- $this->resourceModelMock->expects($this->once())->method('save')
- ->with($this->initializedProductMock)->willReturn(true);
+ $this->resourceModel->expects($this->once())->method('save')
+ ->with($this->initializedProduct)->willReturn(true);
}
public function testSaveExistingWithNewMediaGalleryEntries()
{
- $this->storeManagerMock->expects($this->any())->method('getWebsites')->willReturn([1 => 'default']);
+ $this->storeManager->expects($this->any())->method('getWebsites')->willReturn([1 => 'default']);
$newEntriesData = [
'images' => [
[
@@ -1175,74 +1267,62 @@ public function testSaveExistingWithNewMediaGalleryEntries()
]
]
];
-
+ $expectedEntriesData = [
+ [
+ 'id' => null,
+ 'label' => "label_text",
+ 'position' => 10,
+ 'disabled' => false,
+ 'types' => ['image', 'small_image'],
+ 'content' => [
+ ImageContentInterface::NAME => 'filename',
+ ImageContentInterface::TYPE => 'image/jpeg',
+ ImageContentInterface::BASE64_ENCODED_DATA => 'encoded_content',
+ ],
+ 'media_type' => 'media_type',
+ ],
+ ];
$this->setupProductMocksForSave();
//media gallery data
- $this->productData['media_gallery'] = $newEntriesData;
- $this->extensibleDataObjectConverterMock
+ $this->productData['media_gallery_entries'] = [
+ [
+ 'id' => null,
+ 'label' => "label_text",
+ 'position' => 10,
+ 'disabled' => false,
+ 'types' => ['image', 'small_image'],
+ 'content' => [
+ ImageContentInterface::NAME => 'filename',
+ ImageContentInterface::TYPE => 'image/jpeg',
+ ImageContentInterface::BASE64_ENCODED_DATA => 'encoded_content',
+ ],
+ 'media_type' => 'media_type',
+ ]
+ ];
+ $this->extensibleDataObjectConverter
->expects($this->once())
->method('toNestedArray')
- ->will($this->returnValue($this->productData));
-
- $this->initializedProductMock->setData('media_gallery', $newEntriesData);
- $this->initializedProductMock->expects($this->any())
- ->method('getMediaAttributes')
- ->willReturn(["image" => "imageAttribute", "small_image" => "small_image_attribute"]);
-
- //setup media attribute backend
- $mediaTmpPath = '/tmp';
- $absolutePath = '/a/b/filename.jpg';
-
- $this->mediaGalleryProcessor->expects($this->once())->method('clearMediaAttribute')
- ->with($this->initializedProductMock, ['image', 'small_image']);
-
- $mediaConfigMock = $this->getMockBuilder(\Magento\Catalog\Model\Product\Media\Config::class)
- ->disableOriginalConstructor()
- ->getMock();
- $mediaConfigMock->expects($this->once())
- ->method('getTmpMediaShortUrl')
- ->with($absolutePath)
- ->willReturn($mediaTmpPath . $absolutePath);
- $this->initializedProductMock->expects($this->once())
- ->method('getMediaConfig')
- ->willReturn($mediaConfigMock);
-
- //verify new entries
- $contentDataObject = $this->getMockBuilder(\Magento\Framework\Api\ImageContent::class)
- ->disableOriginalConstructor()
- ->setMethods(null)
- ->getMock();
- $this->contentFactoryMock->expects($this->once())
- ->method('create')
- ->willReturn($contentDataObject);
-
- $this->imageProcessorMock->expects($this->once())
- ->method('processImageContent')
- ->willReturn($absolutePath);
-
- $imageFileUri = "imageFileUri";
- $this->mediaGalleryProcessor->expects($this->once())->method('addImage')
- ->with($this->initializedProductMock, $mediaTmpPath . $absolutePath, ['image', 'small_image'], true, false)
- ->willReturn($imageFileUri);
- $this->mediaGalleryProcessor->expects($this->once())->method('updateImage')
- ->with(
- $this->initializedProductMock,
- $imageFileUri,
- [
- 'label' => 'label_text',
- 'position' => 10,
- 'disabled' => false,
- 'media_type' => 'media_type',
- ]
- );
- $this->initializedProductMock->expects($this->once())->method('getWebsiteIds')->willReturn([]);
- $this->initializedProductMock->expects($this->atLeastOnce())
- ->method('getSku')->willReturn($this->productData['sku']);
- $this->productMock->expects($this->atLeastOnce())->method('getSku')->willReturn($this->productData['sku']);
+ ->willReturn($this->productData);
- $this->model->save($this->productMock);
+ $this->initializedProduct->setData('media_gallery', $newEntriesData);
+ $this->mediaGalleryProcessor
+ ->expects($this->once())
+ ->method('processMediaGallery')
+ ->with($this->initializedProduct, $expectedEntriesData);
+ $this->initializedProduct
+ ->expects($this->atLeastOnce())
+ ->method('getSku')
+ ->willReturn($this->productData['sku']);
+ $this->product->expects($this->atLeastOnce())->method('getSku')->willReturn($this->productData['sku']);
+ $this->initializedProduct->expects($this->at(0))->method('getId')->willReturn(null);
+ $this->initializedProduct->expects($this->any())->method('getId')->willReturn(100);
+
+ $this->model->save($this->product);
}
+ /**
+ * @return array
+ */
public function websitesProvider()
{
return [
@@ -1253,42 +1333,47 @@ public function websitesProvider()
public function testSaveWithDifferentWebsites()
{
$storeMock = $this->createMock(StoreInterface::class);
- $this->resourceModelMock->expects($this->at(0))->method('getIdBySku')->will($this->returnValue(null));
- $this->resourceModelMock->expects($this->at(3))->method('getIdBySku')->will($this->returnValue(100));
- $this->productFactoryMock->expects($this->any())
+ $this->resourceModel->expects($this->at(0))->method('getIdBySku')->willReturn(null);
+ $this->resourceModel->expects($this->at(3))->method('getIdBySku')->willReturn(100);
+ $this->productFactory
+ ->expects($this->any())
->method('create')
- ->will($this->returnValue($this->productMock));
- $this->initializationHelperMock->expects($this->never())->method('initialize');
- $this->resourceModelMock->expects($this->once())->method('validate')->with($this->productMock)
- ->willReturn(true);
- $this->resourceModelMock->expects($this->once())->method('save')->with($this->productMock)->willReturn(true);
- $this->extensibleDataObjectConverterMock
+ ->willReturn($this->product);
+ $this->initializationHelper->expects($this->never())->method('initialize');
+ $this->resourceModel->expects($this->once())->method('validate')->with($this->product)->willReturn(true);
+ $this->resourceModel->expects($this->once())->method('save')->with($this->product)->willReturn(true);
+ $this->extensibleDataObjectConverter
->expects($this->once())
->method('toNestedArray')
- ->will($this->returnValue($this->productData));
- $this->storeManagerMock->expects($this->any())
+ ->willReturn($this->productData);
+ $this->storeManager
+ ->expects($this->any())
->method('getStore')
->willReturn($storeMock);
- $this->storeManagerMock->expects($this->once())
+ $this->storeManager
+ ->expects($this->once())
->method('getWebsites')
->willReturn([
1 => ['first'],
2 => ['second'],
3 => ['third']
]);
- $this->productMock->expects($this->once())->method('getWebsiteIds')->willReturn([1,2,3]);
- $this->productMock->expects($this->once())->method('setWebsiteIds')->willReturn([2,3]);
+ $this->product->method('setWebsiteIds')->willReturn([2,3]);
+ $this->product->method('getSku')
+ ->willReturn('simple');
+ $this->product->expects($this->at(0))->method('getId')->willReturn(null);
+ $this->product->expects($this->any())->method('getId')->willReturn(100);
- $this->assertEquals($this->productMock, $this->model->save($this->productMock));
+ $this->assertEquals($this->product, $this->model->save($this->product));
}
public function testSaveExistingWithMediaGalleryEntries()
{
- $this->storeManagerMock->expects($this->any())->method('getWebsites')->willReturn([1 => 'default']);
+ $this->storeManager->expects($this->any())->method('getWebsites')->willReturn([1 => 'default']);
//update one entry, delete one entry
$newEntries = [
[
- 'value_id' => 5,
+ 'id' => 5,
"label" => "new_label_text",
'file' => 'filename1',
'position' => 10,
@@ -1312,48 +1397,27 @@ public function testSaveExistingWithMediaGalleryEntries()
],
],
];
-
- $expectedResult = [
- [
- 'value_id' => 5,
- 'value_id' => 5,
- "label" => "new_label_text",
- 'file' => 'filename1',
- 'position' => 10,
- 'disabled' => false,
- 'types' => ['image', 'small_image'],
- ],
- [
- 'value_id' => 6, //will be deleted
- 'file' => 'filename2',
- 'removed' => true,
- ],
- ];
-
$this->setupProductMocksForSave();
//media gallery data
- $this->productData['media_gallery']['images'] = $newEntries;
- $this->extensibleDataObjectConverterMock
+ $this->productData['media_gallery_entries'] = $newEntries;
+ $this->extensibleDataObjectConverter
->expects($this->once())
->method('toNestedArray')
->will($this->returnValue($this->productData));
- $this->initializedProductMock->setData('media_gallery', $existingMediaGallery);
- $this->initializedProductMock->expects($this->any())
- ->method('getMediaAttributes')
- ->willReturn(["image" => "filename1", "small_image" => "filename2"]);
-
- $this->mediaGalleryProcessor->expects($this->once())->method('clearMediaAttribute')
- ->with($this->initializedProductMock, ['image', 'small_image']);
- $this->mediaGalleryProcessor->expects($this->once())
- ->method('setMediaAttribute')
- ->with($this->initializedProductMock, ['image', 'small_image'], 'filename1');
- $this->initializedProductMock->expects($this->once())->method('getWebsiteIds')->willReturn([]);
- $this->initializedProductMock->expects($this->atLeastOnce())
- ->method('getSku')->willReturn($this->productData['sku']);
- $this->productMock->expects($this->atLeastOnce())->method('getSku')->willReturn($this->productData['sku']);
- $this->productMock->expects($this->any())->method('getMediaGalleryEntries')->willReturn(null);
- $this->model->save($this->productMock);
- $this->assertEquals($expectedResult, $this->initializedProductMock->getMediaGallery('images'));
+ $this->initializedProduct->setData('media_gallery', $existingMediaGallery);
+
+ $this->mediaGalleryProcessor
+ ->expects($this->once())
+ ->method('processMediaGallery')
+ ->with($this->initializedProduct, $newEntries);
+ $this->initializedProduct
+ ->expects($this->atLeastOnce())
+ ->method('getSku')
+ ->willReturn($this->productData['sku']);
+ $this->product->expects($this->atLeastOnce())->method('getSku')->willReturn($this->productData['sku']);
+ $this->product->expects($this->any())->method('getMediaGalleryEntries')->willReturn(null);
+ $this->initializedProduct->expects($this->any())->method('getId')->willReturn(100);
+ $this->model->save($this->product);
}
}
diff --git a/app/code/Magento/Catalog/Test/Unit/Model/ProductTest.php b/app/code/Magento/Catalog/Test/Unit/Model/ProductTest.php
index 92e5b8c7ecb21..483283e777118 100644
--- a/app/code/Magento/Catalog/Test/Unit/Model/ProductTest.php
+++ b/app/code/Magento/Catalog/Test/Unit/Model/ProductTest.php
@@ -200,6 +200,16 @@ class ProductTest extends \PHPUnit\Framework\TestCase
*/
private $extensionAttributes;
+ /**
+ * @var CacheInterface|\PHPUnit_Framework_MockObject_MockObject
+ */
+ private $cacheInterfaceMock;
+
+ /**
+ * @var \Magento\Eav\Model\Config|\PHPUnit_Framework_MockObject_MockObject
+ */
+ private $eavConfig;
+
/**
* @SuppressWarnings(PHPMD.ExcessiveMethodLength)
*/
@@ -246,8 +256,7 @@ protected function setUp()
\Magento\Framework\Model\ActionValidator\RemoveAction::class
);
$actionValidatorMock->expects($this->any())->method('isAllowed')->will($this->returnValue(true));
- $cacheInterfaceMock = $this->createMock(\Magento\Framework\App\CacheInterface::class);
-
+ $this->cacheInterfaceMock = $this->createMock(\Magento\Framework\App\CacheInterface::class);
$contextMock = $this->createPartialMock(
\Magento\Framework\Model\Context::class,
['getEventDispatcher', 'getCacheManager', 'getAppState', 'getActionValidator'], [], '', false
@@ -258,7 +267,7 @@ protected function setUp()
->will($this->returnValue($this->eventManagerMock));
$contextMock->expects($this->any())
->method('getCacheManager')
- ->will($this->returnValue($cacheInterfaceMock));
+ ->will($this->returnValue($this->cacheInterfaceMock));
$contextMock->expects($this->any())
->method('getActionValidator')
->will($this->returnValue($actionValidatorMock));
@@ -360,6 +369,7 @@ protected function setUp()
->setMethods(['create'])
->getMock();
$this->mediaConfig = $this->createMock(\Magento\Catalog\Model\Product\Media\Config::class);
+ $this->eavConfig = $this->createMock(\Magento\Eav\Model\Config::class);
$this->extensionAttributes = $this->getMockBuilder(ProductExtensionInterface::class)
->setMethods(['getStockItem'])
@@ -397,7 +407,8 @@ protected function setUp()
'catalogProductMediaConfig' => $this->mediaConfig,
'_filesystem' => $this->filesystemMock,
'_collectionFactory' => $this->collectionFactoryMock,
- 'data' => ['id' => 1]
+ 'data' => ['id' => 1],
+ 'eavConfig' => $this->eavConfig
]
);
}
@@ -502,6 +513,9 @@ public function testGetCategoryCollectionCollectionNull($initCategoryCollection,
$this->assertEquals($initCategoryCollection, $result);
}
+ /**
+ * @return array
+ */
public function getCategoryCollectionCollectionNullDataProvider()
{
return [
@@ -612,13 +626,16 @@ public function testReindex($productChanged, $isScheduled, $productFlatCount, $c
$this->model->reindex();
}
+ /**
+ * @return array
+ */
public function getProductReindexProvider()
{
- return array(
+ return [
'set 1' => [true, false, 1, 1],
'set 2' => [true, true, 1, 0],
'set 3' => [false, false, 1, 0]
- );
+ ];
}
public function testPriceReindexCallback()
@@ -1265,18 +1282,11 @@ public function testGetCustomAttributes()
{
$priceCode = 'price';
$colorAttributeCode = 'color';
- $interfaceAttribute = $this->createMock(\Magento\Framework\Api\MetadataObjectInterface::class);
- $interfaceAttribute->expects($this->once())
- ->method('getAttributeCode')
- ->willReturn($priceCode);
- $colorAttribute = $this->createMock(\Magento\Framework\Api\MetadataObjectInterface::class);
- $colorAttribute->expects($this->once())
- ->method('getAttributeCode')
- ->willReturn($colorAttributeCode);
- $customAttributesMetadata = [$interfaceAttribute, $colorAttribute];
-
- $this->metadataServiceMock->expects($this->once())
- ->method('getCustomAttributesMetadata')
+ $customAttributesMetadata = [$priceCode => 'attribute1', $colorAttributeCode => 'attribute2'];
+
+ $this->metadataServiceMock->expects($this->never())->method('getCustomAttributesMetadata');
+ $this->eavConfig->expects($this->once())
+ ->method('getEntityAttributes')
->willReturn($customAttributesMetadata);
$this->model->setData($priceCode, 10);
@@ -1284,20 +1294,20 @@ public function testGetCustomAttributes()
$this->assertEquals([], $this->model->getCustomAttributes());
//Set the color attribute;
- $this->model->setData($colorAttributeCode, "red");
+ $this->model->setData($colorAttributeCode, 'red');
$attributeValue = new \Magento\Framework\Api\AttributeValue();
$attributeValue2 = new \Magento\Framework\Api\AttributeValue();
$this->attributeValueFactory->expects($this->exactly(2))->method('create')
->willReturnOnConsecutiveCalls($attributeValue, $attributeValue2);
$this->assertEquals(1, count($this->model->getCustomAttributes()));
$this->assertNotNull($this->model->getCustomAttribute($colorAttributeCode));
- $this->assertEquals("red", $this->model->getCustomAttribute($colorAttributeCode)->getValue());
+ $this->assertEquals('red', $this->model->getCustomAttribute($colorAttributeCode)->getValue());
//Change the attribute value, should reflect in getCustomAttribute
- $this->model->setData($colorAttributeCode, "blue");
+ $this->model->setCustomAttribute($colorAttributeCode, 'blue');
$this->assertEquals(1, count($this->model->getCustomAttributes()));
$this->assertNotNull($this->model->getCustomAttribute($colorAttributeCode));
- $this->assertEquals("blue", $this->model->getCustomAttribute($colorAttributeCode)->getValue());
+ $this->assertEquals('blue', $this->model->getCustomAttribute($colorAttributeCode)->getValue());
}
/**
@@ -1393,7 +1403,20 @@ public function testGetFinalPricePreset()
$qty = 1;
$this->model->setQty($qty);
$this->model->setFinalPrice($finalPrice);
- $this->productTypeInstanceMock->expects($this->never())->method('priceFactory');
+ $productTypePriceMock = $this->createPartialMock(
+ \Magento\Catalog\Model\Product\Type\Price::class,
+ ['getFinalPrice']
+ );
+ $productTypePriceMock->expects($this->any())
+ ->method('getFinalPrice')
+ ->with($qty, $this->model)
+ ->will($this->returnValue($finalPrice));
+
+ $this->productTypeInstanceMock->expects($this->any())
+ ->method('priceFactory')
+ ->with($this->model->getTypeId())
+ ->will($this->returnValue($productTypePriceMock));
+
$this->assertEquals($finalPrice, $this->model->getFinalPrice($qty));
}
@@ -1434,4 +1457,17 @@ public function testGetOptionByIdForProductWithoutOptions()
{
$this->assertNull($this->model->getOptionById(100));
}
+
+ public function testGetCacheTags()
+ {
+ //If entity is identified getCacheTags has to return the same values
+ //as getIdentities
+ $this->model->setId(null);
+ $this->assertEquals([Product::CACHE_TAG], $this->model->getCacheTags());
+ $this->model->setId(1);
+ $this->assertEquals(
+ $this->model->getIdentities(),
+ $this->model->getCacheTags()
+ );
+ }
}
diff --git a/app/code/Magento/Catalog/Test/Unit/Model/ProductTypes/ConfigTest.php b/app/code/Magento/Catalog/Test/Unit/Model/ProductTypes/ConfigTest.php
index e6de1d33e564d..fb289c7beaac6 100644
--- a/app/code/Magento/Catalog/Test/Unit/Model/ProductTypes/ConfigTest.php
+++ b/app/code/Magento/Catalog/Test/Unit/Model/ProductTypes/ConfigTest.php
@@ -68,6 +68,9 @@ public function testGetType($value, $expected)
$this->assertEquals($expected, $this->config->getType('global'));
}
+ /**
+ * @return array
+ */
public function getTypeDataProvider()
{
return [
diff --git a/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/AbstractTest.php b/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/AbstractTest.php
index 64416301faa06..96336d2b0706a 100644
--- a/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/AbstractTest.php
+++ b/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/AbstractTest.php
@@ -44,11 +44,13 @@ public function testWalkAttributes()
$code = 'test_attr';
$set = 10;
+ $storeId = 100;
$object = $this->createPartialMock(\Magento\Catalog\Model\Product::class, ['__wakeup']);
$object->setData('test_attr', 'test_attr');
$object->setData('attribute_set_id', $set);
+ $object->setData('store_id', $storeId);
$entityType = new \Magento\Framework\DataObject();
$entityType->setEntityTypeCode('test');
diff --git a/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/AttributeTest.php b/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/AttributeTest.php
new file mode 100644
index 0000000000000..29a579bbae5c7
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/AttributeTest.php
@@ -0,0 +1,230 @@
+selectMock = $this->getMockBuilder(Select::class)
+ ->disableOriginalConstructor()
+ ->setMethods(['from', 'where', 'join', 'deleteFromSelect'])
+ ->getMock();
+
+ $this->connectionMock = $this->getMockBuilder(Adapter::class)->getMockForAbstractClass();
+ $this->connectionMock->expects($this->once())->method('select')->willReturn($this->selectMock);
+ $this->connectionMock->expects($this->once())->method('query')->willReturn($this->selectMock);
+ $this->connectionMock->expects($this->once())->method('delete')->willReturn($this->selectMock);
+ $this->selectMock->expects($this->once())->method('from')->willReturnSelf();
+ $this->selectMock->expects($this->once())->method('join')->willReturnSelf();
+ $this->selectMock->expects($this->any())->method('where')->willReturnSelf();
+ $this->selectMock->expects($this->any())->method('deleteFromSelect')->willReturnSelf();
+
+ $this->resourceMock = $this->getMockBuilder(ResourceConnection::class)
+ ->disableOriginalConstructor()
+ ->setMethods(['delete', 'getConnection'])
+ ->getMock();
+
+ $this->contextMock = $this->getMockBuilder(Context::class)->disableOriginalConstructor()->getMock();
+ $this->storeManagerMock = $this->getMockBuilder(StoreManagerInterface::class)->getMock();
+ $this->eavEntityTypeMock = $this->getMockBuilder(Type::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->eavConfigMock = $this->getMockBuilder(\Magento\Eav\Model\Config::class)
+ ->disableOriginalConstructor()
+ ->setMethods(['getAttribute'])
+ ->getMock();
+ $this->lockValidatorMock = $this->getMockBuilder(LockValidatorInterface::class)
+ ->disableOriginalConstructor()
+ ->setMethods(['validate'])
+ ->getMock();
+ $this->entityMetaDataInterfaceMock = $this->getMockBuilder(EntityMetadataInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ }
+
+ /**
+ * Sets object non-public property.
+ *
+ * @param mixed $object
+ * @param string $propertyName
+ * @param mixed $value
+ *
+ * @return void
+ */
+ private function setObjectProperty($object, string $propertyName, $value)
+ {
+ $reflectionClass = new \ReflectionClass($object);
+ $reflectionProperty = $reflectionClass->getProperty($propertyName);
+ $reflectionProperty->setAccessible(true);
+ $reflectionProperty->setValue($object, $value);
+ }
+
+ /**
+ * @return void
+ */
+ public function testDeleteEntity()
+ {
+ $entityAttributeId = 196;
+ $entityTypeId = 4;
+ $result = [
+ 'entity_attribute_id' => 196,
+ 'entity_type_id' => 4,
+ 'attribute_set_id'=> 4,
+ 'attribute_group_id' => 7,
+ 'attribute_id' => 177,
+ 'sort_order' => 3,
+ ];
+
+ $backendTableName = 'weee_tax';
+ $backendFieldName = 'value_id';
+
+ $attributeModel = $this->getMockBuilder(Attribute::class)
+ ->setMethods(['getEntityAttribute', 'getMetadataPool', 'getConnection', 'getTable'])
+ ->setConstructorArgs([
+ $this->contextMock,
+ $this->storeManagerMock,
+ $this->eavEntityTypeMock,
+ $this->eavConfigMock,
+ $this->lockValidatorMock,
+ null,
+ ])->getMock();
+ $attributeModel->expects($this->any())
+ ->method('getEntityAttribute')
+ ->with($entityAttributeId)
+ ->willReturn($result);
+ $metadataPoolMock = $this->getMockBuilder(MetadataPool::class)
+ ->disableOriginalConstructor()
+ ->setMethods(['getMetadata'])
+ ->getMock();
+
+ $this->setObjectProperty($attributeModel, 'metadataPool', $metadataPoolMock);
+
+ $eavAttributeMock = $this->getMockBuilder(\Magento\Eav\Model\Entity\Attribute\AbstractAttribute::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $eavAttributeMock->expects($this->any())->method('getId')->willReturn($result['attribute_id']);
+
+ $this->eavConfigMock->expects($this->any())
+ ->method('getAttribute')
+ ->with($entityTypeId, $result['attribute_id'])
+ ->willReturn($eavAttributeMock);
+
+ $abstractModelMock = $this->getMockBuilder(AbstractModel::class)
+ ->disableOriginalConstructor()
+ ->setMethods(['getEntityAttributeId','getEntityTypeId'])
+ ->getMockForAbstractClass();
+ $abstractModelMock->expects($this->any())->method('getEntityAttributeId')->willReturn($entityAttributeId);
+ $abstractModelMock->expects($this->any())->method('getEntityTypeId')->willReturn($entityTypeId);
+
+ $this->lockValidatorMock->expects($this->any())
+ ->method('validate')
+ ->with($eavAttributeMock, $result['attribute_set_id'])
+ ->willReturn(true);
+
+ $backendModelMock = $this->getMockBuilder(AbstractBackend::class)
+ ->disableOriginalConstructor()
+ ->setMethods(['getBackend', 'getTable', 'getEntityIdField'])
+ ->getMock();
+
+ $abstractAttributeMock = $this->getMockBuilder(AbstractAttribute::class)
+ ->disableOriginalConstructor()
+ ->setMethods(['getEntity'])
+ ->getMockForAbstractClass();
+
+ $eavAttributeMock->expects($this->any())->method('getBackend')->willReturn($backendModelMock);
+ $eavAttributeMock->expects($this->any())->method('getEntity')->willReturn($abstractAttributeMock);
+
+ $backendModelMock->expects($this->any())->method('getTable')->willReturn($backendTableName);
+ $backendModelMock->expects($this->once())->method('getEntityIdField')->willReturn($backendFieldName);
+
+ $metadataPoolMock->expects($this->any())
+ ->method('getMetadata')
+ ->with(ProductInterface::class)
+ ->willReturn($this->entityMetaDataInterfaceMock);
+
+ $this->entityMetaDataInterfaceMock->expects($this->any())
+ ->method('getLinkField')
+ ->willReturn('row_id');
+
+ $attributeModel->expects($this->any())->method('getConnection')->willReturn($this->connectionMock);
+ $attributeModel->expects($this->any())
+ ->method('getTable')
+ ->with('eav_entity_attribute')
+ ->willReturn('eav_entity_attribute');
+
+ $attributeModel->deleteEntity($abstractModelMock);
+ }
+}
diff --git a/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/CategoryTest.php b/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/CategoryTest.php
index 4812751792f18..2ac0b65c22e03 100644
--- a/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/CategoryTest.php
+++ b/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/CategoryTest.php
@@ -7,6 +7,7 @@
namespace Magento\Catalog\Test\Unit\Model\ResourceModel;
use Magento\Catalog\Model\Factory;
+use Magento\Catalog\Model\Indexer\Category\Product\Processor;
use Magento\Catalog\Model\ResourceModel\Category;
use Magento\Catalog\Model\ResourceModel\Category\CollectionFactory;
use Magento\Eav\Model\Config;
@@ -91,6 +92,11 @@ class CategoryTest extends \PHPUnit\Framework\TestCase
*/
private $serializerMock;
+ /**
+ * @var Processor|\PHPUnit_Framework_MockObject_MockObject
+ */
+ private $indexerProcessorMock;
+
/**
* {@inheritDoc}
*/
@@ -121,6 +127,9 @@ protected function setUp()
$this->collectionFactoryMock = $this->getMockBuilder(CollectionFactory::class)
->disableOriginalConstructor()
->getMock();
+ $this->indexerProcessorMock = $this->getMockBuilder(Processor::class)
+ ->disableOriginalConstructor()
+ ->getMock();
$this->serializerMock = $this->getMockBuilder(Json::class)->getMock();
@@ -132,7 +141,8 @@ protected function setUp()
$this->treeFactoryMock,
$this->collectionFactoryMock,
[],
- $this->serializerMock
+ $this->serializerMock,
+ $this->indexerProcessorMock
);
}
diff --git a/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/ConfigTest.php b/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/ConfigTest.php
new file mode 100644
index 0000000000000..abbcef942373e
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/ConfigTest.php
@@ -0,0 +1,107 @@
+resource = $this->createMock(\Magento\Framework\App\ResourceConnection::class);
+ $this->storeManager = $this->createMock(\Magento\Store\Model\StoreManagerInterface::class);
+ $this->eavConfig = $this->createMock(\Magento\Eav\Model\Config::class);
+
+ $this->model = $objectManager->getObject(
+ \Magento\Catalog\Model\ResourceModel\Config::class,
+ [
+ 'resource' => $this->resource,
+ 'storeManager' => $this->storeManager,
+ 'eavConfig' => $this->eavConfig,
+ ]
+ );
+
+ parent::setUp();
+ }
+
+ public function testGetAttributesUsedForSortBy()
+ {
+ $expression = 'someExpression';
+ $storeId = 1;
+ $entityTypeId = 4;
+
+ $connectionMock = $this->createMock(\Magento\Framework\DB\Adapter\AdapterInterface::class);
+ $selectMock = $this->createMock(\Magento\Framework\DB\Select::class);
+ $storeMock = $this->createMock(\Magento\Store\Api\Data\StoreInterface::class);
+ $entityTypeMock = $this->createMock(\Magento\Eav\Model\Entity\Type::class);
+
+ $this->resource->expects($this->atLeastOnce())->method('getConnection')->willReturn($connectionMock);
+
+ $connectionMock->expects($this->once())->method('getCheckSql')
+ ->with('al.value IS NULL', 'main_table.frontend_label', 'al.value')
+ ->willReturn($expression);
+ $connectionMock->expects($this->atLeastOnce())->method('select')->willReturn($selectMock);
+
+ $this->resource->expects($this->exactly(3))->method('getTableName')->withConsecutive(
+ ['eav_attribute'],
+ ['catalog_eav_attribute'],
+ ['eav_attribute_label']
+ )->willReturnOnConsecutiveCalls('eav_attribute', 'catalog_eav_attribute', 'eav_attribute_label');
+
+ $this->storeManager->expects($this->once())->method('getStore')->willReturn($storeMock);
+ $storeMock->expects($this->once())->method('getId')->willReturn($storeId);
+
+ $this->eavConfig->expects($this->once())->method('getEntityType')->willReturn($entityTypeMock);
+ $entityTypeMock->expects($this->once())->method('getId')->willReturn($entityTypeId);
+
+ $selectMock->expects($this->once())->method('from')
+ ->with(['main_table' => 'eav_attribute'])->willReturn($selectMock);
+ $selectMock->expects($this->once())->method('join')->with(
+ ['additional_table' => 'catalog_eav_attribute'],
+ 'main_table.attribute_id = additional_table.attribute_id'
+ )->willReturn($selectMock);
+ $selectMock->expects($this->once())->method('joinLeft')
+ ->with(
+ ['al' => 'eav_attribute_label'],
+ 'al.attribute_id = main_table.attribute_id AND al.store_id = ' . $storeId,
+ ['store_label' => $expression]
+ )->willReturn($selectMock);
+ $selectMock->expects($this->exactly(2))->method('where')->withConsecutive(
+ ['main_table.entity_type_id = ?', $entityTypeId],
+ ['additional_table.used_for_sort_by = ?', 1]
+ )->willReturn($selectMock);
+
+ $connectionMock->expects($this->once())->method('fetchAll')->with($selectMock);
+
+ $this->model->getAttributesUsedForSortBy();
+ }
+}
diff --git a/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Layer/Filter/PriceTest.php b/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Layer/Filter/PriceTest.php
deleted file mode 100644
index 9fba7d833c25a..0000000000000
--- a/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Layer/Filter/PriceTest.php
+++ /dev/null
@@ -1,48 +0,0 @@
-getMockBuilder(\Magento\Framework\Model\ResourceModel\Db\Context::class)
- ->disableOriginalConstructor()
- ->getMock();
- $this->resourceMock = $this->getMockBuilder(\Magento\Framework\App\ResourceConnection::class)
- ->disableOriginalConstructor()
- ->getMock();
- $contextMock->expects($this->once())->method('getResources')->willReturn($this->resourceMock);
- $this->model = $objectManagerHelper->getObject(
- \Magento\Catalog\Model\ResourceModel\Layer\Filter\Price::class,
- [
- 'context' => $contextMock
- ]
- );
- }
-
- public function testGetMainTable()
- {
- $expectedTableName = 'expectedTableName';
- $this->resourceMock->expects($this->once())->method('getTableName')->willReturn($expectedTableName);
- $this->assertEquals($expectedTableName, $this->model->getMainTable());
- }
-}
diff --git a/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Product/CollectionTest.php b/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Product/CollectionTest.php
index c73e772de3702..dbbb3fb29513b 100644
--- a/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Product/CollectionTest.php
+++ b/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Product/CollectionTest.php
@@ -6,6 +6,7 @@
namespace Magento\Catalog\Test\Unit\Model\ResourceModel\Product;
use Magento\Catalog\Model\ResourceModel\Product\Collection\ProductLimitationFactory;
+use Magento\Framework\DB\Select;
/**
* @SuppressWarnings(PHPMD.CouplingBetweenObjects)
@@ -239,7 +240,7 @@ public function testAddMediaGalleryData()
$mediaGalleriesMock = [[$linkField => $rowId]];
$itemMock = $this->getMockBuilder(\Magento\Catalog\Model\Product::class)
->disableOriginalConstructor()
- ->setMethods(['getData'])
+ ->setMethods(['getOrigData'])
->getMock();
$attributeMock = $this->getMockBuilder(\Magento\Eav\Model\Entity\Attribute\AbstractAttribute::class)
->disableOriginalConstructor()
@@ -254,8 +255,10 @@ public function testAddMediaGalleryData()
$this->galleryResourceMock->expects($this->once())->method('createBatchBaseSelect')->willReturn($selectMock);
$attributeMock->expects($this->once())->method('getAttributeId')->willReturn($attributeId);
$this->entityMock->expects($this->once())->method('getAttribute')->willReturn($attributeMock);
- $itemMock->expects($this->atLeastOnce())->method('getData')->willReturn($rowId);
- $selectMock->expects($this->once())->method('where')->with('entity.' . $linkField . ' IN (?)', [$rowId]);
+ $itemMock->expects($this->atLeastOnce())->method('getOrigData')->willReturn($rowId);
+ $selectMock->expects($this->once())->method('reset')->with(Select::ORDER)->willReturnSelf();
+ $selectMock->expects($this->once())->method('where')->with('entity.' . $linkField . ' IN (?)', [$rowId])
+ ->willReturnSelf();
$this->metadataPoolMock->expects($this->once())->method('getMetadata')->willReturn($metadataMock);
$metadataMock->expects($this->once())->method('getLinkField')->willReturn($linkField);
diff --git a/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Product/GalleryTest.php b/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Product/GalleryTest.php
index dfed4e4f37385..47ef3c999125f 100644
--- a/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Product/GalleryTest.php
+++ b/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Product/GalleryTest.php
@@ -281,6 +281,9 @@ public function testBindValueToEntityRecordExists()
$this->resource->bindValueToEntity($valueId, $entityId);
}
+ /**
+ * @SuppressWarnings(PHPMD.ExcessiveMethodLength)
+ */
public function testLoadGallery()
{
$productId = 5;
@@ -329,7 +332,8 @@ public function testLoadGallery()
'main.value_id = entity.value_id',
['entity_id']
)->willReturnSelf();
- $this->product->expects($this->at(0))->method('getData')->with('entity_id')->willReturn($productId);
+ $this->product->expects($this->at(0))->method('getData')
+ ->with('entity_id')->willReturn($productId);
$this->product->expects($this->at(1))->method('getStoreId')->will($this->returnValue($storeId));
$this->connection->expects($this->exactly(2))->method('quoteInto')->withConsecutive(
['value.store_id = ?'],
@@ -338,26 +342,50 @@ public function testLoadGallery()
'value.store_id = ' . $storeId,
'default_value.store_id = ' . 0
);
+ $this->connection->expects($this->any())->method('getIfNullSql')->will(
+ $this->returnValueMap([
+ [
+ '`value`.`label`',
+ '`default_value`.`label`',
+ 'IFNULL(`value`.`label`, `default_value`.`label`)'
+ ],
+ [
+ '`value`.`position`',
+ '`default_value`.`position`',
+ 'IFNULL(`value`.`position`, `default_value`.`position`)'
+ ],
+ [
+ '`value`.`disabled`',
+ '`default_value`.`disabled`',
+ 'IFNULL(`value`.`disabled`, `default_value`.`disabled`)'
+ ]
+ ])
+ );
$this->select->expects($this->at(2))->method('joinLeft')->with(
['value' => $getTableReturnValue],
$quoteInfoReturnValue,
- [
- 'label',
- 'position',
- 'disabled'
- ]
+ []
)->willReturnSelf();
$this->select->expects($this->at(3))->method('joinLeft')->with(
['default_value' => $getTableReturnValue],
$quoteDefaultInfoReturnValue,
- ['label_default' => 'label', 'position_default' => 'position', 'disabled_default' => 'disabled']
+ []
)->willReturnSelf();
- $this->select->expects($this->at(4))->method('where')->with(
+ $this->select->expects($this->at(4))->method('columns')->with([
+ 'label' => 'IFNULL(`value`.`label`, `default_value`.`label`)',
+ 'position' => 'IFNULL(`value`.`position`, `default_value`.`position`)',
+ 'disabled' => 'IFNULL(`value`.`disabled`, `default_value`.`disabled`)',
+ 'label_default' => 'default_value.label',
+ 'position_default' => 'default_value.position',
+ 'disabled_default' => 'default_value.disabled'
+ ])->willReturnSelf();
+ $this->select->expects($this->at(5))->method('where')->with(
'main.attribute_id = ?',
$attributeId
)->willReturnSelf();
- $this->select->expects($this->at(5))->method('where')->with('main.disabled = 0')->willReturnSelf();
- $this->select->expects($this->at(7))->method('where')
+ $this->select->expects($this->at(6))->method('where')
+ ->with('main.disabled = 0')->willReturnSelf();
+ $this->select->expects($this->at(8))->method('where')
->with('entity.entity_id = ?', $productId)
->willReturnSelf();
$this->select->expects($this->once())->method('order')
diff --git a/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Product/Indexer/LinkedProductSelectBuilderByIndexPriceTest.php b/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Product/Indexer/LinkedProductSelectBuilderByIndexPriceTest.php
index 6b908d317aa5b..6f3d8e1a84b17 100644
--- a/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Product/Indexer/LinkedProductSelectBuilderByIndexPriceTest.php
+++ b/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Product/Indexer/LinkedProductSelectBuilderByIndexPriceTest.php
@@ -7,6 +7,9 @@
use Magento\Catalog\Model\ResourceModel\Product\BaseSelectProcessorInterface;
+/**
+ * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
+ */
class LinkedProductSelectBuilderByIndexPriceTest extends \PHPUnit\Framework\TestCase
{
/**
@@ -56,12 +59,26 @@ protected function setUp()
$this->getMockBuilder(\Magento\Catalog\Model\ResourceModel\Product\BaseSelectProcessorInterface::class)
->disableOriginalConstructor()
->getMockForAbstractClass();
+
+ $this->indexScopeResolverMock = $this->createMock(
+ \Magento\Framework\Search\Request\IndexScopeResolverInterface::class
+ );
+ $this->dimensionMock = $this->createMock(\Magento\Framework\Indexer\Dimension::class);
+ $this->dimensionFactoryMock = $this->createMock(\Magento\Framework\Indexer\DimensionFactory::class);
+ $this->dimensionFactoryMock->method('create')->willReturn($this->dimensionMock);
+ $storeMock = $this->createMock(\Magento\Store\Api\Data\StoreInterface::class);
+ $storeMock->method('getId')->willReturn(1);
+ $storeMock->method('getWebsiteId')->willReturn(1);
+ $this->storeManagerMock->method('getStore')->willReturn($storeMock);
+
$this->model = new \Magento\Catalog\Model\ResourceModel\Product\Indexer\LinkedProductSelectBuilderByIndexPrice(
$this->storeManagerMock,
$this->resourceMock,
$this->customerSessionMock,
$this->metadataPoolMock,
- $this->baseSelectProcessorMock
+ $this->baseSelectProcessorMock,
+ $this->indexScopeResolverMock,
+ $this->dimensionFactoryMock
);
}
@@ -79,12 +96,12 @@ public function testBuild()
$storeMock = $this->getMockBuilder(\Magento\Store\Api\Data\StoreInterface::class)
->getMockForAbstractClass();
$this->storeManagerMock->expects($this->once())->method('getStore')->willReturn($storeMock);
- $this->customerSessionMock->expects($this->once())->method('getCustomerGroupId');
+ $this->customerSessionMock->expects($this->once())->method('getCustomerGroupId')->willReturn(1);
$connection->expects($this->any())->method('select')->willReturn($select);
$select->expects($this->any())->method('from')->willReturnSelf();
$select->expects($this->any())->method('joinInner')->willReturnSelf();
$select->expects($this->any())->method('where')->willReturnSelf();
- $select->expects($this->once())->method('order')->willReturnSelf();
+ $select->expects($this->exactly(2))->method('order')->willReturnSelf();
$select->expects($this->once())->method('limit')->willReturnSelf();
$this->resourceMock->expects($this->any())->method('getConnection')->willReturn($connection);
$this->metadataPoolMock->expects($this->once())->method('getMetadata')->willReturn($metadata);
diff --git a/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Product/Indexer/Price/IndexTableRowSizeEstimatorTest.php b/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Product/Indexer/Price/IndexTableRowSizeEstimatorTest.php
index e5720b4f0536c..c0ecc4370816b 100644
--- a/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Product/Indexer/Price/IndexTableRowSizeEstimatorTest.php
+++ b/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Product/Indexer/Price/IndexTableRowSizeEstimatorTest.php
@@ -38,7 +38,7 @@ protected function setUp()
public function testEstimateRowSize()
{
- $expectedValue = 2400000;
+ $expectedValue = 4000000;
$this->websiteManagementMock->expects($this->once())->method('getCount')->willReturn(100);
$collectionMock = $this->createMock(\Magento\Customer\Model\ResourceModel\Group\Collection::class);
diff --git a/app/code/Magento/Catalog/Test/Unit/Model/View/Asset/ImageTest.php b/app/code/Magento/Catalog/Test/Unit/Model/View/Asset/ImageTest.php
index 517b5949ee8ea..405c1ced44ba3 100644
--- a/app/code/Magento/Catalog/Test/Unit/Model/View/Asset/ImageTest.php
+++ b/app/code/Magento/Catalog/Test/Unit/Model/View/Asset/ImageTest.php
@@ -145,6 +145,9 @@ public function testGetUrl($filePath, $miscParams)
);
}
+ /**
+ * @return array
+ */
public function getPathDataProvider()
{
return [
diff --git a/app/code/Magento/Catalog/Test/Unit/Model/View/Asset/PlaceholderTest.php b/app/code/Magento/Catalog/Test/Unit/Model/View/Asset/PlaceholderTest.php
index 96a6c15e35651..58007145d21a4 100644
--- a/app/code/Magento/Catalog/Test/Unit/Model/View/Asset/PlaceholderTest.php
+++ b/app/code/Magento/Catalog/Test/Unit/Model/View/Asset/PlaceholderTest.php
@@ -147,6 +147,9 @@ public function testGetUrl($imageType, $placeholderPath)
$this->assertEquals($expectedResult, $imageModel->getUrl());
}
+ /**
+ * @return array
+ */
public function getPathDataProvider()
{
return [
diff --git a/app/code/Magento/Catalog/Test/Unit/Plugin/Block/TopmenuTest.php b/app/code/Magento/Catalog/Test/Unit/Plugin/Block/TopmenuTest.php
index 2d67db77d430b..c5a3e5dab7678 100644
--- a/app/code/Magento/Catalog/Test/Unit/Plugin/Block/TopmenuTest.php
+++ b/app/code/Magento/Catalog/Test/Unit/Plugin/Block/TopmenuTest.php
@@ -87,7 +87,7 @@ protected function setUp()
\Magento\Catalog\Model\ResourceModel\Category\Collection::class
);
$this->categoryCollectionFactoryMock = $this->createPartialMock(
- \Magento\Catalog\Model\ResourceModel\Category\CollectionFactory::class,
+ \Magento\Catalog\Model\ResourceModel\Category\StateDependentCollectionFactory::class,
['create']
);
diff --git a/app/code/Magento/Catalog/Test/Unit/Plugin/Model/Attribute/Backend/AttributeValidationTest.php b/app/code/Magento/Catalog/Test/Unit/Plugin/Model/Attribute/Backend/AttributeValidationTest.php
new file mode 100644
index 0000000000000..944dc234e928c
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Unit/Plugin/Model/Attribute/Backend/AttributeValidationTest.php
@@ -0,0 +1,153 @@
+attributeMock = $this->getMockBuilder(AbstractBackend::class)
+ ->setMethods(['getAttributeCode'])
+ ->getMockForAbstractClass();
+ $this->subjectMock = $this->getMockBuilder(AbstractBackend::class)
+ ->setMethods(['getAttribute'])
+ ->getMockForAbstractClass();
+ $this->subjectMock->expects($this->any())
+ ->method('getAttribute')
+ ->willReturn($this->attributeMock);
+
+ $this->storeMock = $this->getMockBuilder(StoreInterface::class)
+ ->setMethods(['getId'])
+ ->getMockForAbstractClass();
+ $this->storeManagerMock = $this->getMockBuilder(StoreManagerInterface::class)
+ ->setMethods(['getStore'])
+ ->getMockForAbstractClass();
+ $this->storeManagerMock->expects($this->any())
+ ->method('getStore')
+ ->willReturn($this->storeMock);
+
+ $this->entityMock = $this->getMockBuilder(DataObject::class)
+ ->setMethods(['getData'])
+ ->getMock();
+
+ $this->allowedEntityTypes = [$this->entityMock];
+
+ $this->proceedMock = function () {
+ $this->isProceedMockCalled = true;
+ };
+
+ $this->attributeValidation = $objectManager->getObject(
+ AttributeValidation::class,
+ [
+ 'storeManager' => $this->storeManagerMock,
+ 'allowedEntityTypes' => $this->allowedEntityTypes,
+ ]
+ );
+ }
+
+ /**
+ * @param bool $shouldProceedRun
+ * @param bool $defaultStoreUsed
+ * @param null|int|string $storeId
+ * @dataProvider aroundValidateDataProvider
+ * @throws \Magento\Framework\Exception\NoSuchEntityException
+ * @return void
+ */
+ public function testAroundValidate(bool $shouldProceedRun, bool $defaultStoreUsed, $storeId)
+ {
+ $this->isProceedMockCalled = false;
+ $attributeCode = 'code';
+
+ $this->storeMock->expects($this->once())
+ ->method('getId')
+ ->willReturn($storeId);
+ if ($defaultStoreUsed) {
+ $this->attributeMock->expects($this->once())
+ ->method('getAttributeCode')
+ ->willReturn($attributeCode);
+ $this->entityMock->expects($this->at(0))
+ ->method('getData')
+ ->willReturn([$attributeCode => null]);
+ $this->entityMock->expects($this->at(1))
+ ->method('getData')
+ ->with($attributeCode)
+ ->willReturn(null);
+ }
+
+ $this->attributeValidation->aroundValidate($this->subjectMock, $this->proceedMock, $this->entityMock);
+ $this->assertSame($shouldProceedRun, $this->isProceedMockCalled);
+ }
+
+ /**
+ * Data provider for testAroundValidate
+ * @return array
+ */
+ public function aroundValidateDataProvider(): array
+ {
+ return [
+ [true, false, '0'],
+ [true, false, 0],
+ [true, false, null],
+ [false, true, 1],
+ ];
+ }
+}
diff --git a/app/code/Magento/Catalog/Test/Unit/Plugin/Model/AttributeSetRepository/RemoveProductsTest.php b/app/code/Magento/Catalog/Test/Unit/Plugin/Model/AttributeSetRepository/RemoveProductsTest.php
new file mode 100644
index 0000000000000..712aeba59dffe
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Unit/Plugin/Model/AttributeSetRepository/RemoveProductsTest.php
@@ -0,0 +1,87 @@
+collectionFactory = $this->getMockBuilder(CollectionFactory::class)
+ ->disableOriginalConstructor()
+ ->setMethods(['create'])
+ ->getMock();
+ $this->testSubject = $objectManager->getObject(
+ RemoveProducts::class,
+ [
+ 'collectionFactory' => $this->collectionFactory,
+ ]
+ );
+ }
+
+ /**
+ * Test plugin will delete all related products for given attribute set.
+ */
+ public function testAfterDelete()
+ {
+ $attributeSetId = '1';
+
+ /** @var Collection|\PHPUnit_Framework_MockObject_MockObject $collection */
+ $collection = $this->getMockBuilder(Collection::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $collection->expects(self::once())
+ ->method('addFieldToFilter')
+ ->with(self::identicalTo('attribute_set_id'), self::identicalTo(['eq' => $attributeSetId]));
+ $collection->expects(self::once())
+ ->method('delete');
+
+ $this->collectionFactory->expects(self::once())
+ ->method('create')
+ ->willReturn($collection);
+
+ /** @var AttributeSetRepositoryInterface|\PHPUnit_Framework_MockObject_MockObject $attributeSetRepository */
+ $attributeSetRepository = $this->getMockBuilder(AttributeSetRepositoryInterface::class)
+ ->disableOriginalConstructor()
+ ->getMockForAbstractClass();
+
+ /** @var AttributeSetInterface|\PHPUnit_Framework_MockObject_MockObject $attributeSet */
+ $attributeSet = $this->getMockBuilder(AttributeSetInterface::class)
+ ->setMethods(['getId'])
+ ->disableOriginalConstructor()
+ ->getMockForAbstractClass();
+ $attributeSet->expects(self::once())
+ ->method('getId')
+ ->willReturn($attributeSetId);
+
+ self::assertTrue($this->testSubject->afterDelete($attributeSetRepository, true, $attributeSet));
+ }
+}
diff --git a/app/code/Magento/Catalog/Test/Unit/Pricing/Price/BasePriceTest.php b/app/code/Magento/Catalog/Test/Unit/Pricing/Price/BasePriceTest.php
index 25c3c3ab24ad8..78cc028b51de7 100644
--- a/app/code/Magento/Catalog/Test/Unit/Pricing/Price/BasePriceTest.php
+++ b/app/code/Magento/Catalog/Test/Unit/Pricing/Price/BasePriceTest.php
@@ -108,6 +108,9 @@ public function testGetValue($specialPriceValue, $expectedResult)
$this->assertSame($expectedResult, $this->basePrice->getValue());
}
+ /**
+ * @return array
+ */
public function getValueDataProvider()
{
return [[77, 77], [0, 0], [false, 99]];
diff --git a/app/code/Magento/Catalog/Test/Unit/Pricing/Price/CustomOptionPriceTest.php b/app/code/Magento/Catalog/Test/Unit/Pricing/Price/CustomOptionPriceTest.php
index f3436b8f9f09f..4a206d023ec16 100644
--- a/app/code/Magento/Catalog/Test/Unit/Pricing/Price/CustomOptionPriceTest.php
+++ b/app/code/Magento/Catalog/Test/Unit/Pricing/Price/CustomOptionPriceTest.php
@@ -77,6 +77,11 @@ protected function setUp()
);
}
+ /**
+ * @param array $optionsData
+ *
+ * @return array
+ */
protected function setupOptions(array $optionsData)
{
$options = [];
@@ -105,6 +110,11 @@ protected function setupOptions(array $optionsData)
return $options;
}
+ /**
+ * @param $optionsData
+ *
+ * @return array
+ */
protected function setupSingleValueOptions($optionsData)
{
$options = [];
@@ -279,7 +289,7 @@ protected function getOptionValueMock($price)
{
$optionValueMock = $this->getMockBuilder(\Magento\Catalog\Model\Product\Option\Value::class)
->disableOriginalConstructor()
- ->setMethods(['getPriceType', 'getPrice', 'getId', '__wakeup'])
+ ->setMethods(['getPriceType', 'getPrice', 'getId', '__wakeup', 'getOption', 'getData'])
->getMock();
$optionValueMock->expects($this->any())
->method('getPriceType')
@@ -288,6 +298,29 @@ protected function getOptionValueMock($price)
->method('getPrice')
->with($this->equalTo(true))
->will($this->returnValue($price));
+
+ $optionValueMock->expects($this->any())
+ ->method('getData')
+ ->with(\Magento\Catalog\Model\Product\Option\Value::KEY_PRICE)
+ ->willReturn($price);
+
+ $optionMock = $this->getMockBuilder(\Magento\Catalog\Model\Product\Option::class)
+ ->disableOriginalConstructor()
+ ->setMethods(['getProduct'])
+ ->getMock();
+
+ $optionValueMock->expects($this->any())->method('getOption')->willReturn($optionMock);
+
+ $optionMock->expects($this->any())->method('getProduct')->willReturn($this->product);
+
+ $priceMock = $this->getMockBuilder(\Magento\Framework\Pricing\Price\PriceInterface::class)
+ ->disableOriginalConstructor()
+ ->setMethods(['getValue'])
+ ->getMockForAbstractClass();
+ $priceMock->method('getValue')->willReturn($price);
+
+ $this->priceInfo->method('getPrice')->willReturn($priceMock);
+
return $optionValueMock;
}
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 d04bb4c681e67..1c50271976d15 100644
--- a/app/code/Magento/Catalog/Test/Unit/Pricing/Price/MinimalTierPriceCalculatorTest.php
+++ b/app/code/Magento/Catalog/Test/Unit/Pricing/Price/MinimalTierPriceCalculatorTest.php
@@ -61,6 +61,9 @@ public function setUp()
);
}
+ /**
+ * @return int
+ */
private function getValueTierPricesExistShouldReturnMinTierPrice()
{
$minPrice = 5;
diff --git a/app/code/Magento/Catalog/Test/Unit/Pricing/Price/TierPriceTest.php b/app/code/Magento/Catalog/Test/Unit/Pricing/Price/TierPriceTest.php
index 12fe4e2bdae97..64a6324a35620 100644
--- a/app/code/Magento/Catalog/Test/Unit/Pricing/Price/TierPriceTest.php
+++ b/app/code/Magento/Catalog/Test/Unit/Pricing/Price/TierPriceTest.php
@@ -390,4 +390,41 @@ public function dataProviderGetSavePercent()
['basePrice' => '20.80', 'tierPrice' => '18.72', 'savedPercent' => '10']
];
}
+
+ /**
+ * @param null|string|float $quantity
+ * @param float $expectedValue
+ * @dataProvider getQuantityDataProvider
+ */
+ public function testGetQuantity($quantity, $expectedValue)
+ {
+ $tierPrice = new TierPrice(
+ $this->product,
+ $quantity,
+ $this->calculator,
+ $this->priceCurrencyMock,
+ $this->session,
+ $this->groupManagement,
+ $this->customerGroupRetriever
+ );
+
+ $this->assertEquals($expectedValue, $tierPrice->getQuantity());
+ }
+
+ /**
+ * @return array
+ */
+ public function getQuantityDataProvider()
+ {
+ return [
+ [null, 1],
+ ['one', 1],
+ ['', 1],
+ [4, 4],
+ [4.5, 4.5],
+ ['0.7', 0.7],
+ ['0.0000000', 1]
+ ];
+
+ }
}
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 fc45a2e0c2146..42f537228ddf8 100644
--- a/app/code/Magento/Catalog/Test/Unit/Pricing/Render/FinalPriceBoxTest.php
+++ b/app/code/Magento/Catalog/Test/Unit/Pricing/Render/FinalPriceBoxTest.php
@@ -246,7 +246,8 @@ public function testRenderMsrpEnabled()
//assert price wrapper
$this->assertEquals(
- 'test
',
+ 'test
',
$result
);
}
@@ -346,6 +347,9 @@ public function testHasSpecialPrice($regularPrice, $finalPrice, $expectedResult)
$this->assertEquals($expectedResult, $this->object->hasSpecialPrice());
}
+ /**
+ * @return array
+ */
public function hasSpecialPriceProvider()
{
return [
diff --git a/app/code/Magento/Catalog/Test/Unit/Pricing/Render/PriceBoxTest.php b/app/code/Magento/Catalog/Test/Unit/Pricing/Render/PriceBoxTest.php
index 986a1f7710919..e4d531e91fa07 100644
--- a/app/code/Magento/Catalog/Test/Unit/Pricing/Render/PriceBoxTest.php
+++ b/app/code/Magento/Catalog/Test/Unit/Pricing/Render/PriceBoxTest.php
@@ -88,6 +88,9 @@ public function testGetCanDisplayQty($typeCode, $expected)
$this->assertEquals($expected, $this->object->getCanDisplayQty($product));
}
+ /**
+ * @return array
+ */
public function getCanDisplayQtyDataProvider()
{
return [
diff --git a/app/code/Magento/Catalog/Test/Unit/Ui/Component/Product/MassActionTest.php b/app/code/Magento/Catalog/Test/Unit/Ui/Component/Product/MassActionTest.php
new file mode 100644
index 0000000000000..2122543720854
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Unit/Ui/Component/Product/MassActionTest.php
@@ -0,0 +1,259 @@
+objectManager = new ObjectManager($this);
+
+ $this->contextMock = $this->getMockBuilder(ContextInterface::class)
+ ->getMockForAbstractClass();
+ $this->authorizationMock = $this->getMockBuilder(AuthorizationInterface::class)
+ ->getMockForAbstractClass();
+
+ $this->massAction = $this->objectManager->getObject(
+ MassAction::class,
+ [
+ 'authorization' => $this->authorizationMock,
+ 'context' => $this->contextMock,
+ 'data' => [],
+ ]
+ );
+ }
+
+ /**
+ * @return void
+ */
+ public function testGetComponentName()
+ {
+ $this->assertTrue($this->massAction->getComponentName() === MassAction::NAME);
+ }
+
+ /**
+ * @param string $componentName
+ * @param array $componentData
+ * @param bool $isAllowed
+ * @param bool $expectActionConfig
+ * @return void
+ * @dataProvider getPrepareDataProvider
+ */
+ public function testPrepare(
+ string $componentName,
+ array $componentData,
+ bool $isAllowed = true,
+ bool $expectActionConfig = true
+ ) {
+ $processor = $this->getMockBuilder(\Magento\Framework\View\Element\UiComponent\Processor::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->contextMock->expects($this->atLeastOnce())->method('getProcessor')->willReturn($processor);
+ /** @var \Magento\Ui\Component\MassAction $action */
+ $action = $this->objectManager->getObject(
+ \Magento\Ui\Component\MassAction::class,
+ [
+ 'context' => $this->contextMock,
+ 'data' => [
+ 'name' => $componentName,
+ 'config' => $componentData,
+ ]
+ ]
+ );
+ $this->authorizationMock->method('isAllowed')
+ ->willReturn($isAllowed);
+ $this->massAction->addComponent('action', $action);
+ $this->massAction->prepare();
+ $expected = $expectActionConfig ? ['actions' => [$action->getConfiguration()]] : [];
+ $this->assertEquals($expected, $this->massAction->getConfiguration());
+ }
+
+ /**
+ * @return array
+ * @SuppressWarnings(PHPMD.ExcessiveMethodLength)
+ */
+ public function getPrepareDataProvider() : array
+ {
+ return [
+ [
+ 'test_component1',
+ [
+ 'type' => 'first_action',
+ 'label' => 'First Action',
+ 'url' => '/module/controller/firstAction',
+ ],
+ ],
+ [
+ 'test_component2',
+ [
+ 'type' => 'second_action',
+ 'label' => 'Second Action',
+ 'actions' => [
+ [
+ 'type' => 'second_sub_action1',
+ 'label' => 'Second Sub Action 1',
+ 'url' => '/module/controller/secondSubAction1',
+ ],
+ [
+ 'type' => 'second_sub_action2',
+ 'label' => 'Second Sub Action 2',
+ 'url' => '/module/controller/secondSubAction2',
+ ],
+ ],
+ ],
+ ],
+ [
+ 'status_component',
+ [
+ 'type' => 'status',
+ 'label' => 'Status',
+ 'actions' => [
+ [
+ 'type' => 'enable',
+ 'label' => 'Second Sub Action 1',
+ 'url' => '/module/controller/enable',
+ ],
+ [
+ 'type' => 'disable',
+ 'label' => 'Second Sub Action 2',
+ 'url' => '/module/controller/disable',
+ ],
+ ],
+ ],
+ ],
+ [
+ 'status_component_not_allowed',
+ [
+ 'type' => 'status',
+ 'label' => 'Status',
+ 'actions' => [
+ [
+ 'type' => 'enable',
+ 'label' => 'Second Sub Action 1',
+ 'url' => '/module/controller/enable',
+ ],
+ [
+ 'type' => 'disable',
+ 'label' => 'Second Sub Action 2',
+ 'url' => '/module/controller/disable',
+ ],
+ ],
+ ],
+ false,
+ false,
+ ],
+ [
+ 'delete_component',
+ [
+ 'type' => 'delete',
+ 'label' => 'First Action',
+ 'url' => '/module/controller/delete',
+ ],
+ ],
+ [
+ 'delete_component_not_allowed',
+ [
+ 'type' => 'delete',
+ 'label' => 'First Action',
+ 'url' => '/module/controller/delete',
+ ],
+ false,
+ false,
+ ],
+ [
+ 'attributes_component',
+ [
+ 'type' => 'delete',
+ 'label' => 'First Action',
+ 'url' => '/module/controller/attributes',
+ ],
+ ],
+ [
+ 'attributes_component_not_allowed',
+ [
+ 'type' => 'delete',
+ 'label' => 'First Action',
+ 'url' => '/module/controller/attributes',
+ ],
+ false,
+ false,
+ ],
+ ];
+ }
+
+ /**
+ * @param bool $expected
+ * @param string $actionType
+ * @param int $callNum
+ * @param string $resource
+ * @param bool $isAllowed
+ * @return void
+ * @dataProvider isActionAllowedDataProvider
+ */
+ public function testIsActionAllowed(
+ bool $expected,
+ string $actionType,
+ int $callNum,
+ string $resource = '',
+ bool $isAllowed = true
+ ) {
+ $this->authorizationMock->expects($this->exactly($callNum))
+ ->method('isAllowed')
+ ->with($resource)
+ ->willReturn($isAllowed);
+
+ $this->assertEquals($expected, $this->massAction->isActionAllowed($actionType));
+ }
+
+ /**
+ * @return array
+ */
+ public function isActionAllowedDataProvider(): array
+ {
+ return [
+ 'other' => [true, 'other', 0,],
+ 'delete-allowed' => [true, 'delete', 1, 'Magento_Catalog::products'],
+ 'delete-not-allowed' => [false, 'delete', 1, 'Magento_Catalog::products', false],
+ 'status-allowed' => [true, 'status', 1, 'Magento_Catalog::products'],
+ 'status-not-allowed' => [false, 'status', 1, 'Magento_Catalog::products', false],
+ 'attributes-allowed' => [true, 'attributes', 1, 'Magento_Catalog::update_attributes'],
+ 'attributes-not-allowed' => [false, 'attributes', 1, 'Magento_Catalog::update_attributes', false],
+ ];
+ }
+}
diff --git a/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/CatalogEavValidationRulesTest.php b/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/CatalogEavValidationRulesTest.php
index 9b0ade2b1288f..57b277a786ea3 100644
--- a/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/CatalogEavValidationRulesTest.php
+++ b/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/CatalogEavValidationRulesTest.php
@@ -53,6 +53,9 @@ public function testBuild($frontendInput, $frontendClass, array $eavConfig, arra
$this->assertEquals($expectedResult, $this->catalogEavValidationRules->build($attribute, $eavConfig));
}
+ /**
+ * @return array
+ */
public function buildDataProvider()
{
$data['required'] = true;
diff --git a/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/AbstractModifierTest.php b/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/AbstractModifierTest.php
index af10eeea42fd3..473f1aea33618 100644
--- a/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/AbstractModifierTest.php
+++ b/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/AbstractModifierTest.php
@@ -63,7 +63,8 @@ protected function setUp()
'getAttributes',
'getStore',
'getAttributeDefaultValue',
- 'getExistsStoreValueFlag'
+ 'getExistsStoreValueFlag',
+ 'isLockedAttribute'
])->getMockForAbstractClass();
$this->storeMock = $this->getMockBuilder(StoreInterface::class)
->setMethods(['load', 'getId', 'getConfig'])
@@ -81,9 +82,6 @@ protected function setUp()
$this->arrayManagerMock->expects($this->any())
->method('set')
->willReturnArgument(1);
- $this->arrayManagerMock->expects($this->any())
- ->method('merge')
- ->willReturnArgument(1);
$this->arrayManagerMock->expects($this->any())
->method('remove')
->willReturnArgument(1);
diff --git a/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/AttributeSetTest.php b/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/AttributeSetTest.php
index c22dde0b456ac..6280de9d3a611 100644
--- a/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/AttributeSetTest.php
+++ b/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/AttributeSetTest.php
@@ -5,6 +5,7 @@
*/
namespace Magento\Catalog\Test\Unit\Ui\DataProvider\Product\Form\Modifier;
+use Magento\Catalog\Ui\DataProvider\Product\Form\Modifier\AbstractModifier;
use Magento\Catalog\Ui\DataProvider\Product\Form\Modifier\AttributeSet;
use Magento\Eav\Model\ResourceModel\Entity\Attribute\Set\CollectionFactory;
use Magento\Eav\Model\ResourceModel\Entity\Attribute\Set\Collection;
@@ -84,7 +85,34 @@ protected function createModel()
public function testModifyMeta()
{
- $this->assertNotEmpty($this->getModel()->modifyMeta(['test_group' => []]));
+ $modifyMeta = $this->getModel()->modifyMeta(['test_group' => []]);
+ $this->assertNotEmpty($modifyMeta);
+ }
+
+ /**
+ * @param bool $locked
+ * @return void
+ * @dataProvider modifyMetaLockedDataProvider
+ */
+ public function testModifyMetaLocked(bool $locked)
+ {
+ $this->productMock->expects($this->any())
+ ->method('isLockedAttribute')
+ ->willReturn($locked);
+ $modifyMeta = $this->getModel()->modifyMeta([AbstractModifier::DEFAULT_GENERAL_PANEL => []]);
+ $children = $modifyMeta[AbstractModifier::DEFAULT_GENERAL_PANEL]['children'];
+ $this->assertEquals(
+ $locked,
+ $children['attribute_set_id']['arguments']['data']['config']['disabled']
+ );
+ }
+
+ /**
+ * @return array
+ */
+ public function modifyMetaLockedDataProvider()
+ {
+ return [[true], [false]];
}
public function testModifyMetaToBeEmpty()
diff --git a/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/CategoriesTest.php b/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/CategoriesTest.php
index 4daff7e7930e3..cc3dda6e2d7b1 100644
--- a/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/CategoriesTest.php
+++ b/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/CategoriesTest.php
@@ -114,6 +114,48 @@ public function testModifyMeta()
$this->assertArrayHasKey($groupCode, $this->getModel()->modifyMeta($meta));
}
+ /**
+ * @param bool $locked
+ * @return void
+ * @dataProvider modifyMetaLockedDataProvider
+ */
+ public function testModifyMetaLocked(bool $locked)
+ {
+ $groupCode = 'test_group_code';
+ $meta = [
+ $groupCode => [
+ 'children' => [
+ 'category_ids' => [
+ 'sortOrder' => 10,
+ ],
+ ],
+ ],
+ ];
+
+ $this->arrayManagerMock->expects($this->any())
+ ->method('findPath')
+ ->willReturn('path');
+
+ $this->productMock->expects($this->any())
+ ->method('isLockedAttribute')
+ ->willReturn($locked);
+
+ $this->arrayManagerMock->expects($this->any())
+ ->method('merge')
+ ->willReturnArgument(2);
+
+ $modifyMeta = $this->createModel()->modifyMeta($meta);
+ $this->assertEquals($locked, $modifyMeta['arguments']['data']['config']['disabled']);
+ }
+
+ /**
+ * @return array
+ */
+ public function modifyMetaLockedDataProvider()
+ {
+ return [[true], [false]];
+ }
+
public function testModifyMetaWithCaching()
{
$this->arrayManagerMock->expects($this->exactly(2))
diff --git a/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/CustomOptionsTest.php b/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/CustomOptionsTest.php
index 921a8dcdfe6b8..dd9819cdbc5ab 100644
--- a/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/CustomOptionsTest.php
+++ b/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/CustomOptionsTest.php
@@ -154,7 +154,16 @@ public function testModifyMeta()
->method('getAll')
->willReturn([]);
- $this->assertArrayHasKey(CustomOptions::GROUP_CUSTOM_OPTIONS_NAME, $this->getModel()->modifyMeta([]));
+ $meta = $this->getModel()->modifyMeta([]);
+
+ $this->assertArrayHasKey(CustomOptions::GROUP_CUSTOM_OPTIONS_NAME, $meta);
+
+ $buttonAdd = $meta['custom_options']['children']['container_header']['children']['button_add'];
+ $buttonAddTargetName = $buttonAdd['arguments']['data']['config']['actions'][0]['targetName'];
+ $expectedTargetName = '${ $.ns }.${ $.ns }.' . CustomOptions::GROUP_CUSTOM_OPTIONS_NAME
+ . '.' . CustomOptions::GRID_OPTIONS_NAME;
+
+ $this->assertEquals($expectedTargetName, $buttonAddTargetName);
}
/**
diff --git a/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/EavTest.php b/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/EavTest.php
index d2adf50035cc8..b65aa5fc245d6 100755
--- a/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/EavTest.php
+++ b/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/EavTest.php
@@ -5,11 +5,10 @@
*/
namespace Magento\Catalog\Test\Unit\Ui\DataProvider\Product\Form\Modifier;
-use Magento\Catalog\Model\Product\Type;
use Magento\Catalog\Ui\DataProvider\Product\Form\Modifier\Eav;
use Magento\Eav\Model\Config;
+use Magento\Eav\Model\Entity\Attribute\Source\SourceInterface;
use Magento\Framework\App\RequestInterface;
-use Magento\Framework\EntityManager\EventManager;
use Magento\Store\Model\StoreManagerInterface;
use Magento\Store\Api\Data\StoreInterface;
use Magento\Ui\DataProvider\EavValidationRules;
@@ -19,6 +18,7 @@
use Magento\Catalog\Model\ResourceModel\Eav\Attribute as EavAttribute;
use Magento\Eav\Model\Entity\Type as EntityType;
use Magento\Eav\Model\ResourceModel\Entity\Attribute\Collection as AttributeCollection;
+use Magento\Eav\Model\ResourceModel\Entity\Attribute\CollectionFactory as AttributeCollectionFactory;
use Magento\Ui\DataProvider\Mapper\FormElement as FormElementMapper;
use Magento\Ui\DataProvider\Mapper\MetaProperties as MetaPropertiesMapper;
use Magento\Framework\Api\SearchCriteriaBuilder;
@@ -87,6 +87,11 @@ class EavTest extends AbstractModifierTest
*/
private $entityTypeMock;
+ /**
+ * @var AttributeCollectionFactory|\PHPUnit_Framework_MockObject_MockObject
+ */
+ private $attributeCollectionFactoryMock;
+
/**
* @var AttributeCollection|\PHPUnit_Framework_MockObject_MockObject
*/
@@ -225,6 +230,10 @@ protected function setUp()
$this->entityTypeMock = $this->getMockBuilder(EntityType::class)
->disableOriginalConstructor()
->getMock();
+ $this->attributeCollectionFactoryMock = $this->getMockBuilder(AttributeCollectionFactory::class)
+ ->disableOriginalConstructor()
+ ->setMethods(['create'])
+ ->getMock();
$this->attributeCollectionMock = $this->getMockBuilder(AttributeCollection::class)
->disableOriginalConstructor()
->getMock();
@@ -256,7 +265,15 @@ protected function setUp()
$this->searchResultsMock = $this->getMockBuilder(SearchResultsInterface::class)
->getMockForAbstractClass();
$this->eavAttributeMock = $this->getMockBuilder(Attribute::class)
- ->setMethods(['load', 'getAttributeGroupCode', 'getApplyTo', 'getFrontendInput', 'getAttributeCode'])
+ ->setMethods([
+ 'load',
+ 'getAttributeGroupCode',
+ 'getApplyTo',
+ 'getFrontendInput',
+ 'getAttributeCode',
+ 'usesSource',
+ 'getSource'
+ ])
->disableOriginalConstructor()
->getMock();
$this->productAttributeMock = $this->getMockBuilder(ProductAttributeInterface::class)
@@ -323,7 +340,7 @@ protected function setUp()
$this->eavAttributeMock->expects($this->any())
->method('load')
->willReturnSelf();
-
+
$this->eav =$this->getModel();
$this->objectManager->setBackwardCompatibleProperty(
$this->eav,
@@ -352,7 +369,8 @@ protected function createModel()
'attributeRepository' => $this->attributeRepositoryMock,
'arrayManager' => $this->arrayManagerMock,
'eavAttributeFactory' => $this->eavAttributeFactoryMock,
- '_eventManager' => $this->eventManagerMock
+ '_eventManager' => $this->eventManagerMock,
+ 'attributeCollectionFactory' => $this->attributeCollectionFactoryMock
]);
}
@@ -366,139 +384,131 @@ public function testModifyData()
]
];
- $this->locatorMock->expects($this->any())
- ->method('getProduct')
+ $this->attributeCollectionFactoryMock->expects($this->once())->method('create')
+ ->willReturn($this->attributeCollectionMock);
+
+ $this->attributeCollectionMock->expects($this->any())->method('getItems')
+ ->willReturn([
+ $this->eavAttributeMock
+ ]);
+
+ $this->locatorMock->expects($this->any())->method('getProduct')
->willReturn($this->productMock);
- $this->productMock->expects($this->any())
- ->method('getId')
+ $this->productMock->expects($this->any())->method('getId')
->willReturn(1);
- $this->productMock->expects($this->once())
- ->method('getAttributeSetId')
+ $this->productMock->expects($this->once())->method('getAttributeSetId')
->willReturn(4);
- $this->productMock->expects($this->once())
- ->method('getData')
+ $this->productMock->expects($this->once())->method('getData')
->with(ProductAttributeInterface::CODE_PRICE)->willReturn('19.9900');
- $this->searchCriteriaBuilderMock->expects($this->any())
- ->method('addFilter')
+ $this->searchCriteriaBuilderMock->expects($this->any())->method('addFilter')
->willReturnSelf();
- $this->searchCriteriaBuilderMock->expects($this->any())
- ->method('create')
+ $this->searchCriteriaBuilderMock->expects($this->any())->method('create')
->willReturn($this->searchCriteriaMock);
- $this->attributeGroupRepositoryMock->expects($this->any())
- ->method('getList')
+ $this->attributeGroupRepositoryMock->expects($this->any())->method('getList')
->willReturn($this->searchCriteriaMock);
- $this->searchCriteriaMock->expects($this->once())
- ->method('getItems')
+ $this->searchCriteriaMock->expects($this->once())->method('getItems')
->willReturn([$this->attributeGroupMock]);
- $this->sortOrderBuilderMock->expects($this->once())
- ->method('setField')
+ $this->sortOrderBuilderMock->expects($this->once())->method('setField')
->willReturnSelf();
- $this->sortOrderBuilderMock->expects($this->once())
- ->method('setAscendingDirection')
+ $this->sortOrderBuilderMock->expects($this->once())->method('setAscendingDirection')
->willReturnSelf();
$dataObjectMock = $this->createMock(\Magento\Framework\Api\AbstractSimpleObject::class);
- $this->sortOrderBuilderMock->expects($this->once())
- ->method('create')
+ $this->sortOrderBuilderMock->expects($this->once())->method('create')
->willReturn($dataObjectMock);
- $this->searchCriteriaBuilderMock->expects($this->any())
- ->method('addFilter')
+ $this->searchCriteriaBuilderMock->expects($this->any())->method('addFilter')
->willReturnSelf();
- $this->searchCriteriaBuilderMock->expects($this->once())
- ->method('addSortOrder')
+ $this->searchCriteriaBuilderMock->expects($this->once())->method('addSortOrder')
->willReturnSelf();
- $this->searchCriteriaBuilderMock->expects($this->any())
- ->method('create')
+ $this->searchCriteriaBuilderMock->expects($this->any())->method('create')
->willReturn($this->searchCriteriaMock);
- $this->attributeRepositoryMock->expects($this->once())
- ->method('getList')
+ $this->attributeRepositoryMock->expects($this->once())->method('getList')
->with($this->searchCriteriaMock)
->willReturn($this->searchResultsMock);
- $this->eavAttributeMock->expects($this->any())
- ->method('getAttributeGroupCode')
+ $this->eavAttributeMock->expects($this->any())->method('getAttributeGroupCode')
->willReturn('product-details');
- $this->eavAttributeMock->expects($this->once())
- ->method('getApplyTo')
+ $this->eavAttributeMock->expects($this->once())->method('getApplyTo')
->willReturn([]);
- $this->eavAttributeMock->expects($this->once())
- ->method('getFrontendInput')
+ $this->eavAttributeMock->expects($this->once())->method('getFrontendInput')
->willReturn('price');
- $this->eavAttributeMock->expects($this->any())
- ->method('getAttributeCode')
+ $this->eavAttributeMock->expects($this->any())->method('getAttributeCode')
->willReturn(ProductAttributeInterface::CODE_PRICE);
- $this->searchResultsMock->expects($this->once())
- ->method('getItems')
+ $this->searchResultsMock->expects($this->once())->method('getItems')
->willReturn([$this->eavAttributeMock]);
- $this->storeMock->expects(($this->once()))
- ->method('getBaseCurrencyCode')
+ $this->storeMock->expects(($this->once()))->method('getBaseCurrencyCode')
->willReturn('en_US');
- $this->storeManagerMock->expects($this->once())
- ->method('getStore')
+ $this->storeManagerMock->expects($this->once())->method('getStore')
->willReturn($this->storeMock);
- $this->currencyMock->expects($this->once())
- ->method('toCurrency')
+ $this->currencyMock->expects($this->once())->method('toCurrency')
->willReturn('19.99');
- $this->currencyLocaleMock->expects($this->once())
- ->method('getCurrency')
+ $this->currencyLocaleMock->expects($this->once())->method('getCurrency')
->willReturn($this->currencyMock);
$this->assertEquals($sourceData, $this->eav->modifyData([]));
}
/**
- * @param int $productId
+ * @param int|null $productId
* @param bool $productRequired
- * @param string $attrValue
+ * @param string|null $attrValue
* @param array $expected
+ * @param bool $locked
+ * @return void
* @covers \Magento\Catalog\Ui\DataProvider\Product\Form\Modifier\Eav::isProductExists
* @covers \Magento\Catalog\Ui\DataProvider\Product\Form\Modifier\Eav::setupAttributeMeta
* @dataProvider setupAttributeMetaDataProvider
*/
- public function testSetupAttributeMetaDefaultAttribute($productId, $productRequired, $attrValue, $expected)
- {
- $configPath = 'arguments/data/config';
+ public function testSetupAttributeMetaDefaultAttribute(
+ $productId,
+ bool $productRequired,
+ $attrValue,
+ array $expected,
+ bool $locked = false
+ ) {
+ $configPath = 'arguments/data/config';
$groupCode = 'product-details';
$sortOrder = '0';
+ $attributeOptions = [
+ ['value' => 1, 'label' => 'Int label'],
+ ['value' => 1.5, 'label' => 'Float label'],
+ ['value' => true, 'label' => 'Boolean label'],
+ ['value' => 'string', 'label' => 'String label'],
+ ['value' => ['test1', 'test2'], 'label' => 'Array label']
+ ];
+ $attributeOptionsExpected = [
+ ['value' => '1', 'label' => 'Int label'],
+ ['value' => '1.5', 'label' => 'Float label'],
+ ['value' => '1', 'label' => 'Boolean label'],
+ ['value' => 'string', 'label' => 'String label'],
+ ['value' => ['test1', 'test2'], 'label' => 'Array label']
+ ];
- $this->productMock->expects($this->any())
- ->method('getId')
- ->willReturn($productId);
-
- $this->productAttributeMock->expects($this->any())
- ->method('getIsRequired')
- ->willReturn($productRequired);
-
- $this->productAttributeMock->expects($this->any())
- ->method('getDefaultValue')
- ->willReturn('required_value');
-
- $this->productAttributeMock->expects($this->any())
- ->method('getAttributeCode')
- ->willReturn('code');
-
- $this->productAttributeMock->expects($this->any())
- ->method('getValue')
- ->willReturn('value');
+ $this->productMock->method('getId')->willReturn($productId);
+ $this->productMock->expects($this->any())->method('isLockedAttribute')->willReturn($locked);
+ $this->productAttributeMock->method('getIsRequired')->willReturn($productRequired);
+ $this->productAttributeMock->method('getDefaultValue')->willReturn('required_value');
+ $this->productAttributeMock->method('getAttributeCode')->willReturn('code');
+ $this->productAttributeMock->method('getValue')->willReturn('value');
$attributeMock = $this->getMockBuilder(AttributeInterface::class)
->setMethods(['getValue'])
->disableOriginalConstructor()
->getMockForAbstractClass();
- $attributeMock->expects($this->any())
- ->method('getValue')
- ->willReturn($attrValue);
+ $attributeMock->method('getValue')->willReturn($attrValue);
+ $this->productMock->method('getCustomAttribute')->willReturn($attributeMock);
+ $this->eavAttributeMock->method('usesSource')->willReturn(true);
- $this->productMock->expects($this->any())
- ->method('getCustomAttribute')
- ->willReturn($attributeMock);
+ $attributeSource = $this->getMockBuilder(SourceInterface::class)->getMockForAbstractClass();
+ $attributeSource->method('getAllOptions')->willReturn($attributeOptions);
- $this->arrayManagerMock->expects($this->any())
- ->method('set')
+ $this->eavAttributeMock->method('getSource')->willReturn($attributeSource);
+
+ $this->arrayManagerMock->method('set')
->with(
$configPath,
[],
@@ -508,14 +518,20 @@ public function testSetupAttributeMetaDefaultAttribute($productId, $productRequi
$this->arrayManagerMock->expects($this->any())
->method('merge')
+ ->with(
+ $this->anything(),
+ $this->anything(),
+ $this->callback(
+ function ($value) use ($attributeOptionsExpected) {
+ return isset($value['options']) ? $value['options'] === $attributeOptionsExpected : true;
+ }
+ )
+ )
->willReturn($expected);
- $this->arrayManagerMock->expects($this->any())
- ->method('get')
- ->willReturn([]);
+ $this->arrayManagerMock->method('get')->willReturn([]);
- $this->arrayManagerMock->expects($this->any())
- ->method('exists');
+ $this->arrayManagerMock->method('exists')->willReturn(true);
$this->assertEquals(
$expected,
@@ -525,84 +541,105 @@ public function testSetupAttributeMetaDefaultAttribute($productId, $productRequi
/**
* @return array
+ * @SuppressWarnings(PHPMD.ExcessiveMethodLength)
*/
public function setupAttributeMetaDataProvider()
{
return [
'default_null_prod_not_new_and_required' => [
- 'productId' => 1,
- 'productRequired' => true,
- 'attrValue' => 'val',
- 'expected' => [
- 'dataType' => null,
- 'formElement' => null,
- 'visible' => null,
- 'required' => true,
- 'notice' => null,
- 'default' => null,
- 'label' => null,
- 'code' => 'code',
- 'source' => 'product-details',
- 'scopeLabel' => '',
- 'globalScope' => false,
- 'sortOrder' => 0
+ 'productId' => 1,
+ 'productRequired' => true,
+ 'attrValue' => 'val',
+ 'expected' => [
+ 'dataType' => null,
+ 'formElement' => null,
+ 'visible' => null,
+ 'required' => true,
+ 'notice' => null,
+ 'default' => null,
+ 'label' => null,
+ 'code' => 'code',
+ 'source' => 'product-details',
+ 'scopeLabel' => '',
+ 'globalScope' => false,
+ 'sortOrder' => 0
],
],
+ 'default_null_prod_not_new_locked_and_required' => [
+ 'productId' => 1,
+ 'productRequired' => true,
+ 'attrValue' => 'val',
+ 'expected' => [
+ 'dataType' => null,
+ 'formElement' => null,
+ 'visible' => null,
+ 'required' => true,
+ 'notice' => null,
+ 'default' => null,
+ 'label' => null,
+ 'code' => 'code',
+ 'source' => 'product-details',
+ 'scopeLabel' => '',
+ 'globalScope' => false,
+ 'sortOrder' => 0,
+ ],
+ 'locked' => true,
+ ],
'default_null_prod_not_new_and_not_required' => [
- 'productId' => 1,
- 'productRequired' => false,
- 'attrValue' => 'val',
- 'expected' => [
- 'dataType' => null,
- 'formElement' => null,
- 'visible' => null,
- 'required' => false,
- 'notice' => null,
- 'default' => null,
- 'label' => null,
- 'code' => 'code',
- 'source' => 'product-details',
- 'scopeLabel' => '',
- 'globalScope' => false,
- 'sortOrder' => 0
+ 'productId' => 1,
+ 'productRequired' => false,
+ 'attrValue' => 'val',
+ 'expected' => [
+ 'dataType' => null,
+ 'formElement' => null,
+ 'visible' => null,
+ 'required' => false,
+ 'notice' => null,
+ 'default' => null,
+ 'label' => null,
+ 'code' => 'code',
+ 'source' => 'product-details',
+ 'scopeLabel' => '',
+ 'globalScope' => false,
+ 'sortOrder' => 0
],
],
'default_null_prod_new_and_not_required' => [
- 'productId' => null,
- 'productRequired' => false,
- 'attrValue' => null,
- 'expected' => [
- 'dataType' => null,
- 'formElement' => null,
- 'visible' => null,
- 'required' => false,
- 'notice' => null,
- 'default' => 'required_value',
- 'label' => null,
- 'code' => 'code',
- 'source' => 'product-details',
- 'scopeLabel' => '',
- 'globalScope' => false,
- 'sortOrder' => 0
+ 'productId' => null,
+ 'productRequired' => false,
+ 'attrValue' => null,
+ 'expected' => [
+ 'dataType' => null,
+ 'formElement' => null,
+ 'visible' => null,
+ 'required' => false,
+ 'notice' => null,
+ 'default' => 'required_value',
+ 'label' => null,
+ 'code' => 'code',
+ 'source' => 'product-details',
+ 'scopeLabel' => '',
+ 'globalScope' => false,
+ 'sortOrder' => 0
],
],
'default_null_prod_new_and_required' => [
- 'productId' => null,
- 'productRequired' => false,
- 'attrValue' => null,
- 'expected' => [
- 'dataType' => null,
- 'formElement' => null,
- 'visible' => null,
- 'required' => false,
- 'notice' => null,
- 'default' => 'required_value',
- 'label' => null,
- 'code' => 'code',
- 'source' => 'product-details',
- 'scopeLabel' => '',
- 'globalScope' => false,
- 'sortOrder' => 0
+ 'productId' => null,
+ 'productRequired' => false,
+ 'attrValue' => null,
+ 'expected' => [
+ 'dataType' => null,
+ 'formElement' => null,
+ 'visible' => null,
+ 'required' => false,
+ 'notice' => null,
+ 'default' => 'required_value',
+ 'label' => null,
+ 'code' => 'code',
+ 'source' => 'product-details',
+ 'scopeLabel' => '',
+ 'globalScope' => false,
+ 'sortOrder' => 0
],
]
];
diff --git a/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/GeneralTest.php b/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/GeneralTest.php
index b4460b314513b..0f247e7cca85f 100644
--- a/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/GeneralTest.php
+++ b/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/GeneralTest.php
@@ -5,8 +5,11 @@
*/
namespace Magento\Catalog\Test\Unit\Ui\DataProvider\Product\Form\Modifier;
-use Magento\Catalog\Model\Product\Type;
+use Magento\Catalog\Api\Data\ProductAttributeInterface;
use Magento\Catalog\Ui\DataProvider\Product\Form\Modifier\General;
+use Magento\Eav\Api\AttributeRepositoryInterface;
+use Magento\Eav\Api\Data\AttributeInterface;
+use Magento\Framework\Stdlib\ArrayManager;
/**
* Class GeneralTest
@@ -15,6 +18,38 @@
*/
class GeneralTest extends AbstractModifierTest
{
+ /**
+ * @var AttributeRepositoryInterface|\PHPUnit_Framework_MockObject_MockObject
+ */
+ private $attributeRepositoryMock;
+
+ /**
+ * @var General
+ */
+ private $generalModifier;
+
+ /**
+ * @inheritdoc
+ */
+ protected function setUp()
+ {
+ parent::setUp();
+
+ $this->attributeRepositoryMock = $this->getMockBuilder(AttributeRepositoryInterface::class)
+ ->getMockForAbstractClass();
+
+ $arrayManager = $this->objectManager->getObject(ArrayManager::class);
+
+ $this->generalModifier = $this->objectManager->getObject(
+ General::class,
+ [
+ 'attributeRepository' => $this->attributeRepositoryMock,
+ 'locator' => $this->locatorMock,
+ 'arrayManager' => $arrayManager,
+ ]
+ );
+ }
+
/**
* {@inheritdoc}
*/
@@ -26,8 +61,14 @@ protected function createModel()
]);
}
+ /**
+ * @return void
+ */
public function testModifyMeta()
{
+ $this->arrayManagerMock->expects($this->any())
+ ->method('merge')
+ ->willReturnArgument(2);
$this->assertNotEmpty($this->getModel()->modifyMeta([
'first_panel_code' => [
'arguments' => [
@@ -40,4 +81,59 @@ public function testModifyMeta()
]
]));
}
+
+ /**
+ * @param array $data
+ * @param int $defaultStatusValue
+ * @param array $expectedResult
+ * @dataProvider modifyDataDataProvider
+ * @return void
+ */
+ public function testModifyDataNewProduct(array $data, int $defaultStatusValue, array $expectedResult)
+ {
+ $attributeMock = $this->getMockBuilder(AttributeInterface::class)
+ ->getMockForAbstractClass();
+ $attributeMock
+ ->method('getDefaultValue')
+ ->willReturn($defaultStatusValue);
+ $this->attributeRepositoryMock
+ ->method('get')
+ ->with(
+ ProductAttributeInterface::ENTITY_TYPE_CODE,
+ ProductAttributeInterface::CODE_STATUS
+ )
+ ->willReturn($attributeMock);
+ $this->assertSame($expectedResult, $this->generalModifier->modifyData($data));
+ }
+
+ /**
+ * @return array
+ */
+ public function modifyDataDataProvider(): array
+ {
+ return [
+ 'With default status value' => [
+ 'data' => [],
+ 'defaultStatusAttributeValue' => 5,
+ 'expectedResult' => [
+ null => [
+ General::DATA_SOURCE_DEFAULT => [
+ ProductAttributeInterface::CODE_STATUS => 5,
+ ],
+ ],
+ ],
+ ],
+ 'Without default status value' => [
+ 'data' => [],
+ 'defaultStatusAttributeValue' => 0,
+ 'expectedResult' => [
+ null => [
+ General::DATA_SOURCE_DEFAULT => [
+ ProductAttributeInterface::CODE_STATUS => 1,
+ ],
+ ],
+ ],
+ ],
+ ];
+ }
}
diff --git a/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/ScheduleDesignUpdateTest.php b/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/ScheduleDesignUpdateTest.php
index d4d4136bf4157..855a756b550a6 100644
--- a/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/ScheduleDesignUpdateTest.php
+++ b/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/ScheduleDesignUpdateTest.php
@@ -22,8 +22,14 @@ protected function createModel()
]);
}
+ /**
+ * @return void
+ */
public function testModifyMeta()
{
+ $this->arrayManagerMock->expects($this->any())
+ ->method('merge')
+ ->willReturnArgument(1);
$this->assertSame([], $this->getModel()->modifyMeta([]));
}
diff --git a/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/WebsitesTest.php b/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/WebsitesTest.php
index 997b66861c21b..f38e2a0d426d1 100644
--- a/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/WebsitesTest.php
+++ b/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/WebsitesTest.php
@@ -76,7 +76,10 @@ class WebsitesTest extends AbstractModifierTest
protected function setUp()
{
- $this->objectManager = new ObjectManager($this);
+ parent::setUp();
+ $this->productMock->expects($this->any())
+ ->method('getId')
+ ->willReturn(self::PRODUCT_ID);
$this->assignedWebsites = [self::SECOND_WEBSITE_ID];
$this->websiteMock = $this->getMockBuilder(\Magento\Store\Model\Website::class)
->setMethods(['getId', 'getName'])
@@ -87,35 +90,28 @@ protected function setUp()
->disableOriginalConstructor()
->getMock();
$this->websiteRepositoryMock = $this->getMockBuilder(\Magento\Store\Api\WebsiteRepositoryInterface::class)
- ->setMethods(['getList', 'getDefault'])
+ ->setMethods(['getDefault'])
->getMockForAbstractClass();
$this->websiteRepositoryMock->expects($this->any())
->method('getDefault')
->willReturn($this->websiteMock);
- $this->websiteRepositoryMock->expects($this->any())
- ->method('getList')
- ->willReturn([$this->websiteMock, $this->secondWebsiteMock]);
$this->groupRepositoryMock = $this->getMockBuilder(\Magento\Store\Api\GroupRepositoryInterface::class)
->setMethods(['getList'])
->getMockForAbstractClass();
$this->storeRepositoryMock = $this->getMockBuilder(\Magento\Store\Api\StoreRepositoryInterface::class)
->setMethods(['getList'])
->getMockForAbstractClass();
- $this->locatorMock = $this->getMockBuilder(\Magento\Catalog\Model\Locator\LocatorInterface::class)
- ->setMethods(['getProduct', 'getWebsiteIds'])
- ->getMockForAbstractClass();
$this->productMock = $this->getMockBuilder(\Magento\Catalog\Api\Data\ProductInterface::class)
->setMethods(['getId'])
->getMockForAbstractClass();
- $this->locatorMock->expects($this->any())
- ->method('getProduct')
- ->willReturn($this->productMock);
$this->locatorMock->expects($this->any())
->method('getWebsiteIds')
->willReturn($this->assignedWebsites);
$this->storeManagerMock = $this->getMockBuilder(\Magento\Store\Model\StoreManagerInterface::class)
- ->setMethods(['isSingleStoreMode'])
+ ->setMethods(['isSingleStoreMode', 'getWebsites'])
->getMockForAbstractClass();
+ $this->storeManagerMock->method('getWebsites')
+ ->willReturn([$this->websiteMock, $this->secondWebsiteMock]);
$this->storeManagerMock->expects($this->any())
->method('isSingleStoreMode')
->willReturn(false);
@@ -148,9 +144,6 @@ protected function setUp()
$this->storeRepositoryMock->expects($this->any())
->method('getList')
->willReturn([$this->storeViewMock]);
- $this->productMock->expects($this->any())
- ->method('getId')
- ->willReturn(self::PRODUCT_ID);
$this->secondWebsiteMock->expects($this->any())
->method('getId')
->willReturn($this->assignedWebsites[0]);
diff --git a/app/code/Magento/Catalog/Test/Unit/ViewModel/Product/BreadcrumbsTest.php b/app/code/Magento/Catalog/Test/Unit/ViewModel/Product/BreadcrumbsTest.php
new file mode 100644
index 0000000000000..126a747292c2a
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Unit/ViewModel/Product/BreadcrumbsTest.php
@@ -0,0 +1,128 @@
+catalogHelper = $this->getMockBuilder(CatalogHelper::class)
+ ->setMethods(['getProduct'])
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->scopeConfig = $this->getMockBuilder(ScopeConfigInterface::class)
+ ->setMethods(['getValue', 'isSetFlag'])
+ ->disableOriginalConstructor()
+ ->getMockForAbstractClass();
+
+ $this->viewModel = $this->getObjectManager()->getObject(
+ Breadcrumbs::class,
+ [
+ 'catalogData' => $this->catalogHelper,
+ 'scopeConfig' => $this->scopeConfig,
+ ]
+ );
+ }
+
+ /**
+ * @return void
+ */
+ public function testGetCategoryUrlSuffix()
+ {
+ $this->scopeConfig->expects($this->once())
+ ->method('getValue')
+ ->with('catalog/seo/category_url_suffix', \Magento\Store\Model\ScopeInterface::SCOPE_STORE)
+ ->willReturn('.html');
+
+ $this->assertEquals('.html', $this->viewModel->getCategoryUrlSuffix());
+ }
+
+ /**
+ * @return void
+ */
+ public function testIsCategoryUsedInProductUrl()
+ {
+ $this->scopeConfig->expects($this->once())
+ ->method('isSetFlag')
+ ->with('catalog/seo/product_use_categories', \Magento\Store\Model\ScopeInterface::SCOPE_STORE)
+ ->willReturn(false);
+
+ $this->assertFalse($this->viewModel->isCategoryUsedInProductUrl());
+ }
+
+ /**
+ * @dataProvider productDataProvider
+ *
+ * @param Product|null $product
+ * @param string $expectedName
+ * @return void
+ */
+ public function testGetProductName($product, $expectedName)
+ {
+ $this->catalogHelper->expects($this->atLeastOnce())
+ ->method('getProduct')
+ ->willReturn($product);
+
+ $this->assertEquals($expectedName, $this->viewModel->getProductName());
+ }
+
+ /**
+ * @return array
+ */
+ public function productDataProvider()
+ {
+ return [
+ [$this->getObjectManager()->getObject(Product::class, ['data' => ['name' => 'Test']]), 'Test'],
+ [null, ''],
+ ];
+ }
+
+ /**
+ * @return ObjectManager
+ */
+ private function getObjectManager()
+ {
+ if (null === $this->objectManager) {
+ $this->objectManager = new ObjectManager($this);
+ }
+
+ return $this->objectManager;
+ }
+}
diff --git a/app/code/Magento/Catalog/Ui/Component/FilterFactory.php b/app/code/Magento/Catalog/Ui/Component/FilterFactory.php
index fcc500c891607..dd8eaffb0a658 100644
--- a/app/code/Magento/Catalog/Ui/Component/FilterFactory.php
+++ b/app/code/Magento/Catalog/Ui/Component/FilterFactory.php
@@ -71,8 +71,6 @@ public function create($attribute, $context, $config = [])
*/
protected function getFilterType($attribute)
{
- return isset($this->filterMap[$attribute->getFrontendInput()])
- ? $this->filterMap[$attribute->getFrontendInput()]
- : $this->filterMap['default'];
+ return $this->filterMap[$attribute->getFrontendInput()] ?? $this->filterMap['default'];
}
}
diff --git a/app/code/Magento/Catalog/Ui/Component/Listing/Columns.php b/app/code/Magento/Catalog/Ui/Component/Listing/Columns.php
index c96498b054d25..8ea6d8b9e5a06 100644
--- a/app/code/Magento/Catalog/Ui/Component/Listing/Columns.php
+++ b/app/code/Magento/Catalog/Ui/Component/Listing/Columns.php
@@ -80,6 +80,6 @@ public function prepare()
*/
protected function getFilterType($frontendInput)
{
- return isset($this->filterMap[$frontendInput]) ? $this->filterMap[$frontendInput] : $this->filterMap['default'];
+ return $this->filterMap[$frontendInput] ?? $this->filterMap['default'];
}
}
diff --git a/app/code/Magento/Catalog/Ui/Component/Product/MassAction.php b/app/code/Magento/Catalog/Ui/Component/Product/MassAction.php
new file mode 100644
index 0000000000000..37ce6df5c8625
--- /dev/null
+++ b/app/code/Magento/Catalog/Ui/Component/Product/MassAction.php
@@ -0,0 +1,98 @@
+authorization = $authorization;
+ parent::__construct($context, $components, $data);
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function prepare()
+ {
+ $config = $this->getConfiguration();
+
+ foreach ($this->getChildComponents() as $actionComponent) {
+ $actionType = $actionComponent->getConfiguration()['type'];
+ if ($this->isActionAllowed($actionType)) {
+ $config['actions'][] = $actionComponent->getConfiguration();
+ }
+ }
+ $origConfig = $this->getConfiguration();
+ if ($origConfig !== $config) {
+ $config = array_replace_recursive($config, $origConfig);
+ }
+
+ $this->setData('config', $config);
+ $this->components = [];
+
+ parent::prepare();
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function getComponentName(): string
+ {
+ return static::NAME;
+ }
+
+ /**
+ * Check if the given type of action is allowed.
+ *
+ * @param string $actionType
+ * @return bool
+ */
+ public function isActionAllowed(string $actionType): bool
+ {
+ $isAllowed = true;
+ switch ($actionType) {
+ case 'delete':
+ case 'status':
+ $isAllowed = $this->authorization->isAllowed('Magento_Catalog::products');
+ break;
+ case 'attributes':
+ $isAllowed = $this->authorization->isAllowed('Magento_Catalog::update_attributes');
+ break;
+ default:
+ break;
+ }
+
+ return $isAllowed;
+ }
+}
diff --git a/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/AdvancedPricing.php b/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/AdvancedPricing.php
index a8378c364a63e..37b0b328a522b 100644
--- a/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/AdvancedPricing.php
+++ b/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/AdvancedPricing.php
@@ -7,14 +7,15 @@
use Magento\Catalog\Api\Data\ProductAttributeInterface;
use Magento\Catalog\Model\Locator\LocatorInterface;
-use Magento\Customer\Model\Customer\Source\GroupSourceInterface;
-use Magento\Directory\Helper\Data;
-use Magento\Framework\App\ObjectManager;
-use Magento\Store\Model\StoreManagerInterface;
use Magento\Customer\Api\GroupManagementInterface;
use Magento\Customer\Api\GroupRepositoryInterface;
+use Magento\Customer\Model\Customer\Source\GroupSourceInterface;
+use Magento\Directory\Helper\Data;
use Magento\Framework\Api\SearchCriteriaBuilder;
+use Magento\Framework\App\ObjectManager;
use Magento\Framework\Module\Manager as ModuleManager;
+use Magento\Framework\Stdlib\ArrayManager;
+use Magento\Store\Model\StoreManagerInterface;
use Magento\Ui\Component\Container;
use Magento\Ui\Component\Form\Element\DataType\Number;
use Magento\Ui\Component\Form\Element\DataType\Price;
@@ -23,7 +24,6 @@
use Magento\Ui\Component\Form\Element\Select;
use Magento\Ui\Component\Form\Field;
use Magento\Ui\Component\Modal;
-use Magento\Framework\Stdlib\ArrayManager;
/**
* Class AdvancedPricing
@@ -432,7 +432,8 @@ private function getTierPriceStructure($tierPricePath)
'dndConfig' => [
'enabled' => false,
],
- 'disabled' => false,
+ 'disabled' =>
+ $this->arrayManager->get($tierPricePath . '/arguments/data/config/disabled', $this->meta),
'required' => false,
'sortOrder' =>
$this->arrayManager->get($tierPricePath . '/arguments/data/config/sortOrder', $this->meta),
@@ -500,7 +501,8 @@ private function getTierPriceStructure($tierPricePath)
'validation' => [
'required-entry' => true,
'validate-greater-than-zero' => true,
- 'validate-digits' => true,
+ 'validate-digits' => false,
+ 'validate-number' => true,
],
],
],
@@ -622,7 +624,7 @@ private function customizeAdvancedPricing()
'componentType' => Modal::NAME,
'dataScope' => '',
'provider' => 'product_form.product_form_data_source',
- 'onCancel' => 'actionDone',
+ 'onCancel' => 'closeModal',
'options' => [
'title' => __('Advanced Pricing'),
'buttons' => [
diff --git a/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Alerts.php b/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Alerts.php
new file mode 100644
index 0000000000000..1f154d3204454
--- /dev/null
+++ b/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Alerts.php
@@ -0,0 +1,172 @@
+scopeConfig = $scopeConfig;
+ $this->layoutFactory = $layoutFactory;
+ }
+
+ /**
+ * {@inheritdoc}
+ * @since 101.0.0
+ */
+ public function modifyData(array $data)
+ {
+ return $data;
+ }
+
+ /**
+ * {@inheritdoc}
+ * @since 101.0.0
+ */
+ public function modifyMeta(array $meta)
+ {
+ if (!$this->canShowTab()) {
+ return $meta;
+ }
+
+ $meta = array_replace_recursive(
+ $meta,
+ [
+ 'alerts' => [
+ 'arguments' => [
+ 'data' => [
+ 'config' => [
+ 'additionalClasses' => 'admin__fieldset-section',
+ 'label' => __('Product Alerts'),
+ 'collapsible' => true,
+ 'componentType' => Fieldset::NAME,
+ 'dataScope' => static::DATA_SCOPE,
+ 'sortOrder' =>
+ $this->getNextGroupSortOrder(
+ $meta,
+ self::$previousGroup,
+ self::$sortOrder
+ ),
+ ],
+ ],
+ ],
+ 'children' => [
+ static::DATA_SCOPE_STOCK => $this->getAlertStockFieldset(),
+ static::DATA_SCOPE_PRICE => $this->getAlertPriceFieldset()
+ ],
+ ],
+ ]
+ );
+
+ return $meta;
+ }
+
+ /**
+ * @return bool
+ */
+ private function canShowTab()
+ {
+ $alertPriceAllow = $this->scopeConfig->getValue(
+ 'catalog/productalert/allow_price',
+ ScopeInterface::SCOPE_STORE
+ );
+ $alertStockAllow = $this->scopeConfig->getValue(
+ 'catalog/productalert/allow_stock',
+ ScopeInterface::SCOPE_STORE
+ );
+
+ return ($alertPriceAllow || $alertStockAllow);
+ }
+
+ /**
+ * Prepares config for the alert stock products fieldset
+ * @return array
+ */
+ private function getAlertStockFieldset()
+ {
+ return [
+ 'arguments' => [
+ 'data' => [
+ 'config' => [
+ 'label' => __('Alert stock'),
+ 'componentType' => 'container',
+ 'component' => 'Magento_Ui/js/form/components/html',
+ 'additionalClasses' => 'admin__fieldset-note',
+ 'content' =>
+ '' . __('Alert Stock') . ' ' .
+ $this->layoutFactory->create()->createBlock(
+ Stock::class
+ )->toHtml(),
+ ]
+ ]
+ ]
+ ];
+ }
+
+ /**
+ * Prepares config for the alert price products fieldset
+ * @return array
+ */
+ private function getAlertPriceFieldset()
+ {
+ return [
+ 'arguments' => [
+ 'data' => [
+ 'config' => [
+ 'label' => __('Alert price'),
+ 'componentType' => 'container',
+ 'component' => 'Magento_Ui/js/form/components/html',
+ 'additionalClasses' => 'admin__fieldset-note',
+ 'content' =>
+ '' . __('Alert Price') . ' ' .
+ $this->layoutFactory->create()->createBlock(
+ Price::class
+ )->toHtml(),
+ ]
+ ]
+ ]
+ ];
+ }
+}
diff --git a/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/AttributeSet.php b/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/AttributeSet.php
index a1aacc91f2e47..0733d21bf47d7 100644
--- a/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/AttributeSet.php
+++ b/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/AttributeSet.php
@@ -108,6 +108,7 @@ public function modifyMeta(array $meta)
self::ATTRIBUTE_SET_FIELD_ORDER
),
'multiple' => false,
+ 'disabled' => $this->locator->getProduct()->isLockedAttribute('attribute_set_id'),
];
}
diff --git a/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Attributes.php b/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Attributes.php
index aec6549f400fc..683a96133ad30 100644
--- a/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Attributes.php
+++ b/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Attributes.php
@@ -182,6 +182,11 @@ private function customizeAddAttributeModal(array $meta)
. '.create_new_attribute_modal',
'actionName' => 'toggleModal',
],
+ [
+ 'targetName' => 'product_form.product_form.add_attribute_modal'
+ . '.create_new_attribute_modal.product_attribute_add_form',
+ 'actionName' => 'destroyInserted'
+ ],
[
'targetName'
=> 'product_form.product_form.add_attribute_modal'
diff --git a/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Categories.php b/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Categories.php
index 7456c1bfef91f..2dad7e8495b11 100644
--- a/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Categories.php
+++ b/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Categories.php
@@ -228,6 +228,7 @@ protected function customizeCategoriesField(array $meta)
'componentType' => 'container',
'component' => 'Magento_Ui/js/form/components/group',
'scopeLabel' => __('[GLOBAL]'),
+ 'disabled' => $this->locator->getProduct()->isLockedAttribute($fieldCode),
],
],
],
@@ -288,6 +289,7 @@ protected function customizeCategoriesField(array $meta)
'source' => 'product_details',
'displayArea' => 'insideGroup',
'sortOrder' => 20,
+ 'dataScope' => $fieldCode,
],
],
]
diff --git a/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/CustomOptions.php b/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/CustomOptions.php
index 73fecd17c69ce..e557c8a377681 100755
--- a/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/CustomOptions.php
+++ b/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/CustomOptions.php
@@ -348,7 +348,8 @@ protected function getHeaderContainerConfig($sortOrder)
'sortOrder' => 20,
'actions' => [
[
- 'targetName' => 'ns = ${ $.ns }, index = ' . static::GRID_OPTIONS_NAME,
+ 'targetName' => '${ $.ns }.${ $.ns }.' . static::GROUP_CUSTOM_OPTIONS_NAME
+ . '.' . static::GRID_OPTIONS_NAME,
'actionName' => 'processingAddChild',
]
]
@@ -922,7 +923,7 @@ protected function getPriceFieldConfig($sortOrder)
'addbeforePool' => $this->productOptionsPrice->prefixesToOptionArray(),
'sortOrder' => $sortOrder,
'validation' => [
- 'validate-zero-or-greater' => true
+ 'validate-number' => true
],
],
],
@@ -1045,6 +1046,7 @@ protected function getFileExtensionFieldConfig($sortOrder)
'data' => [
'config' => [
'label' => __('Compatible File Extensions'),
+ 'notice' => __('Enter separated extensions, like: png, jpg, gif.'),
'componentType' => Field::NAME,
'formElement' => Input::NAME,
'dataScope' => static::FIELD_FILE_EXTENSION_NAME,
diff --git a/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Eav.php b/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Eav.php
index 970d485267ca0..c56d3d2d7d354 100755
--- a/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Eav.php
+++ b/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Eav.php
@@ -31,6 +31,7 @@
use Magento\Ui\Component\Form\Fieldset;
use Magento\Ui\DataProvider\Mapper\FormElement as FormElementMapper;
use Magento\Ui\DataProvider\Mapper\MetaProperties as MetaPropertiesMapper;
+use Magento\Eav\Model\ResourceModel\Entity\Attribute\CollectionFactory as AttributeCollectionFactory;
/**
* Class Eav
@@ -39,6 +40,7 @@
*
* @SuppressWarnings(PHPMD.CouplingBetweenObjects)
* @SuppressWarnings(PHPMD.TooManyFields)
+ * @SuppressWarnings(PHPMD.ExcessiveClassComplexity)
* @since 101.0.0
*/
class Eav extends AbstractModifier
@@ -187,6 +189,17 @@ class Eav extends AbstractModifier
*/
private $localeCurrency;
+ /**
+ * internal cache for attribute models
+ * @var array
+ */
+ private $attributesCache = [];
+
+ /**
+ * @var AttributeCollectionFactory
+ */
+ private $attributeCollectionFactory;
+
/**
* @param LocatorInterface $locator
* @param CatalogEavValidationRules $catalogEavValidationRules
@@ -207,6 +220,7 @@ class Eav extends AbstractModifier
* @param DataPersistorInterface $dataPersistor
* @param array $attributesToDisable
* @param array $attributesToEliminate
+ * @param AttributeCollectionFactory $attributeCollectionFactory
* @SuppressWarnings(PHPMD.ExcessiveParameterList)
*/
public function __construct(
@@ -228,7 +242,8 @@ public function __construct(
ScopeOverriddenValue $scopeOverriddenValue,
DataPersistorInterface $dataPersistor,
$attributesToDisable = [],
- $attributesToEliminate = []
+ $attributesToEliminate = [],
+ AttributeCollectionFactory $attributeCollectionFactory = null
) {
$this->locator = $locator;
$this->catalogEavValidationRules = $catalogEavValidationRules;
@@ -249,6 +264,8 @@ public function __construct(
$this->dataPersistor = $dataPersistor;
$this->attributesToDisable = $attributesToDisable;
$this->attributesToEliminate = $attributesToEliminate;
+ $this->attributeCollectionFactory = $attributeCollectionFactory
+ ?: \Magento\Framework\App\ObjectManager::getInstance()->get(AttributeCollectionFactory::class);
}
/**
@@ -265,7 +282,7 @@ public function modifyMeta(array $meta)
if ($attributes) {
$meta[$groupCode]['children'] = $this->getAttributesMeta($attributes, $groupCode);
$meta[$groupCode]['arguments']['data']['config']['componentType'] = Fieldset::NAME;
- $meta[$groupCode]['arguments']['data']['config']['label'] = __('%1', $group->getAttributeGroupName());
+ $meta[$groupCode]['arguments']['data']['config']['label'] = __($group->getAttributeGroupName());
$meta[$groupCode]['arguments']['data']['config']['collapsible'] = true;
$meta[$groupCode]['arguments']['data']['config']['dataScope'] = self::DATA_SCOPE_PRODUCT;
$meta[$groupCode]['arguments']['data']['config']['sortOrder'] =
@@ -485,39 +502,59 @@ private function getAttributeSetId()
private function getAttributes()
{
if (!$this->attributes) {
- foreach ($this->getGroups() as $group) {
- $this->attributes[$this->calculateGroupCode($group)] = $this->loadAttributes($group);
- }
+ $this->attributes = $this->loadAttributesForGroups($this->getGroups());
}
return $this->attributes;
}
/**
- * Loading product attributes from group
+ * Loads attributes for specified groups at once
*
- * @param AttributeGroupInterface $group
- * @return ProductAttributeInterface[]
+ * @param AttributeGroupInterface[] ...$groups
+ * @return @return ProductAttributeInterface[]
*/
- private function loadAttributes(AttributeGroupInterface $group)
+ private function loadAttributesForGroups(array $groups)
{
$attributes = [];
+ $groupIds = [];
+
+ foreach ($groups as $group) {
+ $groupIds[$group->getAttributeGroupId()] = $this->calculateGroupCode($group);
+ $attributes[$this->calculateGroupCode($group)] = [];
+ }
+
+ $collection = $this->attributeCollectionFactory->create();
+ $collection->setAttributeGroupFilter(array_keys($groupIds));
+
+ $mapAttributeToGroup = [];
+
+ foreach ($collection->getItems() as $attribute) {
+ $mapAttributeToGroup[$attribute->getAttributeId()] = $attribute->getAttributeGroupId();
+ }
+
$sortOrder = $this->sortOrderBuilder
->setField('sort_order')
->setAscendingDirection()
->create();
+
$searchCriteria = $this->searchCriteriaBuilder
- ->addFilter(AttributeGroupInterface::GROUP_ID, $group->getAttributeGroupId())
+ ->addFilter(AttributeGroupInterface::GROUP_ID, array_keys($groupIds), 'in')
->addFilter(ProductAttributeInterface::IS_VISIBLE, 1)
->addSortOrder($sortOrder)
->create();
+
$groupAttributes = $this->attributeRepository->getList($searchCriteria)->getItems();
+
$productType = $this->getProductType();
+
foreach ($groupAttributes as $attribute) {
$applyTo = $attribute->getApplyTo();
$isRelated = !$applyTo || in_array($productType, $applyTo);
if ($isRelated) {
- $attributes[] = $attribute;
+ $attributeGroupId = $mapAttributeToGroup[$attribute->getAttributeId()];
+ $attributeGroupCode = $groupIds[$attributeGroupId];
+ $attributes[$attributeGroupCode][] = $attribute;
}
}
@@ -553,7 +590,7 @@ private function getPreviousSetAttributes()
*/
private function isProductExists()
{
- return (bool) $this->locator->getProduct()->getId();
+ return (bool)$this->locator->getProduct()->getId();
}
/**
@@ -572,7 +609,7 @@ private function isProductExists()
public function setupAttributeMeta(ProductAttributeInterface $attribute, $groupCode, $sortOrder)
{
$configPath = ltrim(static::META_CONFIG_PATH, ArrayManager::DEFAULT_PATH_DELIMITER);
-
+ $attributeCode = $attribute->getAttributeCode();
$meta = $this->arrayManager->set($configPath, [], [
'dataType' => $attribute->getFrontendInput(),
'formElement' => $this->getFormElementsMapValue($attribute->getFrontendInput()),
@@ -581,7 +618,7 @@ public function setupAttributeMeta(ProductAttributeInterface $attribute, $groupC
'notice' => $attribute->getNote(),
'default' => (!$this->isProductExists()) ? $attribute->getDefaultValue() : null,
'label' => $attribute->getDefaultFrontendLabel(),
- 'code' => $attribute->getAttributeCode(),
+ 'code' => $attributeCode,
'source' => $groupCode,
'scopeLabel' => $this->getScopeLabel($attribute),
'globalScope' => $this->isScopeGlobal($attribute),
@@ -591,8 +628,9 @@ public function setupAttributeMeta(ProductAttributeInterface $attribute, $groupC
// TODO: Refactor to $attribute->getOptions() when MAGETWO-48289 is done
$attributeModel = $this->getAttributeModel($attribute);
if ($attributeModel->usesSource()) {
+ $options = $attributeModel->getSource()->getAllOptions();
$meta = $this->arrayManager->merge($configPath, $meta, [
- 'options' => $attributeModel->getSource()->getAllOptions(),
+ 'options' => $this->convertOptionsValueToString($options),
]);
}
@@ -610,7 +648,8 @@ public function setupAttributeMeta(ProductAttributeInterface $attribute, $groupC
]);
}
- if (in_array($attribute->getAttributeCode(), $this->attributesToDisable)) {
+ $product = $this->locator->getProduct();
+ if (in_array($attributeCode, $this->attributesToDisable) || $product->isLockedAttribute($attributeCode)) {
$meta = $this->arrayManager->merge($configPath, $meta, [
'disabled' => true,
]);
@@ -645,6 +684,22 @@ public function setupAttributeMeta(ProductAttributeInterface $attribute, $groupC
return $meta;
}
+ /**
+ * Convert options value to string
+ *
+ * @param array $options
+ * @return array
+ */
+ private function convertOptionsValueToString(array $options): array
+ {
+ array_walk($options, function (&$value) {
+ if (isset($value['value']) && is_scalar($value['value'])) {
+ $value['value'] = (string)$value['value'];
+ }
+ });
+ return $options;
+ }
+
/**
* @param ProductAttributeInterface $attribute
* @param array $meta
@@ -800,7 +855,7 @@ private function getFormElementsMapValue($value)
{
$valueMap = $this->formElementMapper->getMappings();
- return isset($valueMap[$value]) ? $valueMap[$value] : $value;
+ return $valueMap[$value] ?? $value;
}
/**
@@ -854,6 +909,9 @@ private function canDisplayUseDefault(ProductAttributeInterface $attribute)
$attributeCode = $attribute->getAttributeCode();
/** @var Product $product */
$product = $this->locator->getProduct();
+ if ($product->isLockedAttribute($attributeCode)) {
+ return false;
+ }
if (isset($this->canDisplayUseDefault[$attributeCode])) {
return $this->canDisplayUseDefault[$attributeCode];
@@ -888,7 +946,13 @@ private function isScopeGlobal($attribute)
*/
private function getAttributeModel($attribute)
{
- return $this->eavAttributeFactory->create()->load($attribute->getAttributeId());
+ $attributeId = $attribute->getAttributeId();
+
+ if (!array_key_exists($attributeId, $this->attributesCache)) {
+ $this->attributesCache[$attributeId] = $this->eavAttributeFactory->create()->load($attributeId);
+ }
+
+ return $this->attributesCache[$attributeId];
}
/**
diff --git a/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/General.php b/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/General.php
index ea69ebf4dda24..ec3ef58ded569 100755
--- a/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/General.php
+++ b/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/General.php
@@ -7,6 +7,7 @@
use Magento\Catalog\Api\Data\ProductAttributeInterface;
use Magento\Catalog\Model\Locator\LocatorInterface;
+use Magento\Eav\Api\AttributeRepositoryInterface;
use Magento\Ui\Component\Form;
use Magento\Framework\Stdlib\ArrayManager;
@@ -35,16 +36,25 @@ class General extends AbstractModifier
*/
private $localeCurrency;
+ /**
+ * @var AttributeRepositoryInterface
+ */
+ private $attributeRepository;
+
/**
* @param LocatorInterface $locator
* @param ArrayManager $arrayManager
+ * @param AttributeRepositoryInterface|null $attributeRepository
*/
public function __construct(
LocatorInterface $locator,
- ArrayManager $arrayManager
+ ArrayManager $arrayManager,
+ AttributeRepositoryInterface $attributeRepository = null
) {
$this->locator = $locator;
$this->arrayManager = $arrayManager;
+ $this->attributeRepository = $attributeRepository
+ ?: \Magento\Framework\App\ObjectManager::getInstance()->get(AttributeRepositoryInterface::class);
}
/**
@@ -58,7 +68,12 @@ public function modifyData(array $data)
$modelId = $this->locator->getProduct()->getId();
if (!isset($data[$modelId][static::DATA_SOURCE_DEFAULT][ProductAttributeInterface::CODE_STATUS])) {
- $data[$modelId][static::DATA_SOURCE_DEFAULT][ProductAttributeInterface::CODE_STATUS] = '1';
+ $attributeStatus = $this->attributeRepository->get(
+ ProductAttributeInterface::ENTITY_TYPE_CODE,
+ ProductAttributeInterface::CODE_STATUS
+ );
+ $data[$modelId][static::DATA_SOURCE_DEFAULT][ProductAttributeInterface::CODE_STATUS] =
+ $attributeStatus->getDefaultValue() ?: 1;
}
return $data;
@@ -106,7 +121,7 @@ protected function customizeAdvancedPriceFormat(array $data)
$value[ProductAttributeInterface::CODE_TIER_PRICE_FIELD_PRICE] =
$this->formatPrice($value[ProductAttributeInterface::CODE_TIER_PRICE_FIELD_PRICE]);
$value[ProductAttributeInterface::CODE_TIER_PRICE_FIELD_PRICE_QTY] =
- (int)$value[ProductAttributeInterface::CODE_TIER_PRICE_FIELD_PRICE_QTY];
+ (float) $value[ProductAttributeInterface::CODE_TIER_PRICE_FIELD_PRICE_QTY];
}
}
@@ -187,7 +202,7 @@ protected function customizeStatusField(array $meta)
protected function customizeWeightField(array $meta)
{
$weightPath = $this->arrayManager->findPath(ProductAttributeInterface::CODE_WEIGHT, $meta, null, 'children');
-
+ $disabled = $this->arrayManager->get($weightPath . '/arguments/data/config/disabled', $meta);
if ($weightPath) {
$meta = $this->arrayManager->merge(
$weightPath . static::META_CONFIG_PATH,
@@ -199,7 +214,7 @@ protected function customizeWeightField(array $meta)
],
'additionalClasses' => 'admin__field-small',
'addafter' => $this->locator->getStore()->getConfig('general/locale/weight_unit'),
- 'imports' => [
+ 'imports' => $disabled ? [] : [
'disabled' => '!${$.provider}:' . self::DATA_SCOPE_PRODUCT
. '.product_has_weight:value'
]
@@ -239,6 +254,7 @@ protected function customizeWeightField(array $meta)
],
],
'value' => (int)$this->locator->getProduct()->getTypeInstance()->hasWeight(),
+ 'disabled' => $disabled,
]
);
}
@@ -264,23 +280,36 @@ protected function customizeNewDateRangeField(array $meta)
if ($fromFieldPath && $toFieldPath) {
$fromContainerPath = $this->arrayManager->slicePath($fromFieldPath, 0, -2);
$toContainerPath = $this->arrayManager->slicePath($toFieldPath, 0, -2);
+ $commonFieldsMeta = [
+ 'outputDateTimeToISO' => false,
+ 'inputDateTimeFormat' => 'YYYY-MM-DD h:mm',
+ 'options' => [
+ 'showsTime' => true,
+ ]
+ ];
$meta = $this->arrayManager->merge(
$fromFieldPath . self::META_CONFIG_PATH,
$meta,
- [
- 'label' => __('Set Product as New From'),
- 'additionalClasses' => 'admin__field-date',
- ]
+ array_merge(
+ [
+ 'label' => __('Set Product as New From'),
+ 'additionalClasses' => 'admin__field-date',
+ ],
+ $commonFieldsMeta
+ )
);
$meta = $this->arrayManager->merge(
$toFieldPath . self::META_CONFIG_PATH,
$meta,
- [
- 'label' => __('To'),
- 'scopeLabel' => null,
- 'additionalClasses' => 'admin__field-date',
- ]
+ array_merge(
+ [
+ 'label' => __('To'),
+ 'scopeLabel' => null,
+ 'additionalClasses' => 'admin__field-date',
+ ],
+ $commonFieldsMeta
+ )
);
$meta = $this->arrayManager->merge(
$fromContainerPath . self::META_CONFIG_PATH,
diff --git a/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Websites.php b/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Websites.php
index 298da3d5cd6f2..8166c42a5a8b1 100644
--- a/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Websites.php
+++ b/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Websites.php
@@ -135,7 +135,6 @@ public function modifyMeta(array $meta)
'collapsible' => true,
'componentType' => Form\Fieldset::NAME,
'dataScope' => self::DATA_SCOPE_PRODUCT,
- 'disabled' => false,
'sortOrder' => $this->getNextGroupSortOrder(
$meta,
'search-engine-optimization',
@@ -176,9 +175,11 @@ protected function getFieldsForFieldset()
$label = __('Websites');
$defaultWebsiteId = $this->websiteRepository->getDefault()->getId();
+ $isOnlyOneWebsiteAvailable = count($websitesList) === 1;
foreach ($websitesList as $website) {
$isChecked = in_array($website['id'], $websiteIds)
- || ($defaultWebsiteId == $website['id'] && $isNewProduct);
+ || ($defaultWebsiteId == $website['id'] && $isNewProduct)
+ || $isOnlyOneWebsiteAvailable;
$children[$website['id']] = [
'arguments' => [
'data' => [
@@ -196,6 +197,7 @@ protected function getFieldsForFieldset()
'false' => '0',
],
'value' => $isChecked ? (string)$website['id'] : '0',
+ 'disabled' => $this->locator->getProduct()->isLockedAttribute('website_ids'),
],
],
],
@@ -397,8 +399,9 @@ protected function getWebsitesList()
$this->websitesList = [];
$groupList = $this->groupRepository->getList();
$storesList = $this->storeRepository->getList();
+ $websiteList = $this->storeManager->getWebsites(true);
- foreach ($this->websiteRepository->getList() as $website) {
+ foreach ($websiteList as $website) {
$websiteId = $website->getId();
if (!$websiteId) {
continue;
diff --git a/app/code/Magento/Catalog/Ui/DataProvider/Product/Listing/Collector/Image.php b/app/code/Magento/Catalog/Ui/DataProvider/Product/Listing/Collector/Image.php
index 2fc9ef76aa00d..216bc16968fcb 100644
--- a/app/code/Magento/Catalog/Ui/DataProvider/Product/Listing/Collector/Image.php
+++ b/app/code/Magento/Catalog/Ui/DataProvider/Product/Listing/Collector/Image.php
@@ -11,6 +11,7 @@
use Magento\Catalog\Api\Data\ProductRender\ImageInterfaceFactory;
use Magento\Catalog\Api\Data\ProductRenderInterface;
use Magento\Catalog\Helper\ImageFactory;
+use Magento\Catalog\Model\Product\Image\NotLoadInfoImageException;
use Magento\Catalog\Ui\DataProvider\Product\ProductRenderCollectorInterface;
use Magento\Framework\App\State;
use Magento\Framework\View\DesignInterface;
@@ -102,7 +103,12 @@ public function collect(ProductInterface $product, ProductRenderInterface $produ
[$this, "emulateImageCreating"],
[$product, $imageCode, (int) $productRender->getStoreId(), $image]
);
- $resizedInfo = $helper->getResizedImageInfo();
+
+ try {
+ $resizedInfo = $helper->getResizedImageInfo();
+ } catch (NotLoadInfoImageException $exception) {
+ $resizedInfo = [$helper->getWidth(), $helper->getHeight()];
+ }
$image->setCode($imageCode);
$image->setHeight($helper->getHeight());
diff --git a/app/code/Magento/Catalog/Ui/DataProvider/Product/ProductCollection.php b/app/code/Magento/Catalog/Ui/DataProvider/Product/ProductCollection.php
new file mode 100644
index 0000000000000..f4334bc25efd8
--- /dev/null
+++ b/app/code/Magento/Catalog/Ui/DataProvider/Product/ProductCollection.php
@@ -0,0 +1,28 @@
+_productLimitationFilters->setUsePriceIndex(false);
+ return $this->_productLimitationPrice(true);
+ }
+}
diff --git a/app/code/Magento/Catalog/ViewModel/Product/Breadcrumbs.php b/app/code/Magento/Catalog/ViewModel/Product/Breadcrumbs.php
new file mode 100644
index 0000000000000..e897c330b7e0f
--- /dev/null
+++ b/app/code/Magento/Catalog/ViewModel/Product/Breadcrumbs.php
@@ -0,0 +1,115 @@
+catalogData = $catalogData;
+ $this->scopeConfig = $scopeConfig;
+ $this->json = $json ?: ObjectManager::getInstance()->get(Json::class);
+ $this->escaper = $escaper ?: ObjectManager::getInstance()->get(Escaper::class);
+ }
+
+ /**
+ * Returns category URL suffix.
+ *
+ * @return mixed
+ */
+ public function getCategoryUrlSuffix()
+ {
+ return $this->scopeConfig->getValue(
+ 'catalog/seo/category_url_suffix',
+ \Magento\Store\Model\ScopeInterface::SCOPE_STORE
+ );
+ }
+
+ /**
+ * Checks if categories path is used for product URLs.
+ *
+ * @return bool
+ */
+ public function isCategoryUsedInProductUrl()
+ {
+ return $this->scopeConfig->isSetFlag(
+ 'catalog/seo/product_use_categories',
+ \Magento\Store\Model\ScopeInterface::SCOPE_STORE
+ );
+ }
+
+ /**
+ * Returns product name.
+ *
+ * @return string
+ */
+ public function getProductName()
+ {
+ return $this->catalogData->getProduct() !== null
+ ? $this->catalogData->getProduct()->getName()
+ : '';
+ }
+
+ /**
+ * Returns breadcrumb json.
+ *
+ * @return string
+ */
+ public function getJsonConfiguration()
+ {
+ return $this->escaper->escapeHtml($this->json->serialize([
+ 'breadcrumbs' => [
+ 'categoryUrlSuffix' => $this->escaper->escapeHtml($this->getCategoryUrlSuffix()),
+ 'userCategoryPathInUrl' => (int)$this->isCategoryUsedInProductUrl(),
+ 'product' => $this->getProductName()
+ ]
+ ]));
+ }
+}
diff --git a/app/code/Magento/Catalog/composer.json b/app/code/Magento/Catalog/composer.json
index 391d6065fd867..4535e527d2dec 100644
--- a/app/code/Magento/Catalog/composer.json
+++ b/app/code/Magento/Catalog/composer.json
@@ -2,39 +2,39 @@
"name": "magento/module-catalog",
"description": "N/A",
"require": {
- "php": "7.0.2|7.0.4|~7.0.6|~7.1.0",
+ "php": "~7.0.13|~7.1.0",
"magento/module-store": "100.2.*",
- "magento/module-eav": "100.2.*",
- "magento/module-cms": "101.1.*",
+ "magento/module-eav": "101.0.*",
+ "magento/module-cms": "102.0.*",
"magento/module-indexer": "100.2.*",
- "magento/module-customer": "100.2.*",
+ "magento/module-customer": "101.0.*",
"magento/module-theme": "100.2.*",
"magento/module-checkout": "100.2.*",
"magento/module-backend": "100.2.*",
- "magento/module-widget": "100.2.*",
- "magento/module-wishlist": "100.2.*",
+ "magento/module-widget": "101.0.*",
+ "magento/module-wishlist": "101.0.*",
"magento/module-tax": "100.2.*",
"magento/module-msrp": "100.2.*",
"magento/module-catalog-inventory": "100.2.*",
"magento/module-directory": "100.2.*",
- "magento/module-catalog-rule": "100.2.*",
+ "magento/module-catalog-rule": "101.0.*",
"magento/module-product-alert": "100.2.*",
- "magento/module-url-rewrite": "100.2.*",
+ "magento/module-url-rewrite": "101.0.*",
"magento/module-catalog-url-rewrite": "100.2.*",
"magento/module-page-cache": "100.2.*",
- "magento/module-quote": "100.2.*",
- "magento/module-config": "100.2.*",
+ "magento/module-quote": "101.0.*",
+ "magento/module-config": "101.0.*",
"magento/module-media-storage": "100.2.*",
- "magento/framework": "100.2.*",
- "magento/module-ui": "100.2.*"
+ "magento/framework": "101.0.*",
+ "magento/module-ui": "101.0.*"
},
"suggest": {
"magento/module-cookie": "100.2.*",
- "magento/module-sales": "100.2.*",
+ "magento/module-sales": "101.0.*",
"magento/module-catalog-sample-data": "Sample Data version:100.2.*"
},
"type": "magento2-module",
- "version": "101.1.0-dev",
+ "version": "102.0.6",
"license": [
"OSL-3.0",
"AFL-3.0"
diff --git a/app/code/Magento/Catalog/etc/adminhtml/di.xml b/app/code/Magento/Catalog/etc/adminhtml/di.xml
index 790bd163a6f17..9739ee28a6dae 100644
--- a/app/code/Magento/Catalog/etc/adminhtml/di.xml
+++ b/app/code/Magento/Catalog/etc/adminhtml/di.xml
@@ -78,6 +78,11 @@
+
+
+ \Magento\Catalog\Ui\DataProvider\Product\ProductCollection
+
+
@@ -86,6 +91,7 @@
- Magento\Catalog\Ui\DataProvider\Product\AddStoreFieldToCollection
+ \Magento\Catalog\Ui\DataProvider\Product\ProductCollectionFactory
@@ -143,6 +149,10 @@
- Magento\Catalog\Ui\DataProvider\Product\Form\Modifier\Attributes
- 120
+ -
+
- Magento\Catalog\Ui\DataProvider\Product\Form\Modifier\Alerts
+ - 130
+
-
- Magento\Catalog\Ui\DataProvider\Product\Form\Modifier\TierPrice
- 150
@@ -180,4 +190,7 @@
Magento\Catalog\Model\Attribute\ScopeOverriddenValue
+
+
+
diff --git a/app/code/Magento/Catalog/etc/adminhtml/events.xml b/app/code/Magento/Catalog/etc/adminhtml/events.xml
index f4fd7fc30398c..ad83f5898237a 100644
--- a/app/code/Magento/Catalog/etc/adminhtml/events.xml
+++ b/app/code/Magento/Catalog/etc/adminhtml/events.xml
@@ -9,4 +9,7 @@
+
+
+
diff --git a/app/code/Magento/Catalog/etc/adminhtml/menu.xml b/app/code/Magento/Catalog/etc/adminhtml/menu.xml
index aa910e6d5ade4..cfcce3a26cbec 100644
--- a/app/code/Magento/Catalog/etc/adminhtml/menu.xml
+++ b/app/code/Magento/Catalog/etc/adminhtml/menu.xml
@@ -12,7 +12,6 @@
-
diff --git a/app/code/Magento/Catalog/etc/adminhtml/system.xml b/app/code/Magento/Catalog/etc/adminhtml/system.xml
index e42eab787e3fc..1f78de93b369d 100644
--- a/app/code/Magento/Catalog/etc/adminhtml/system.xml
+++ b/app/code/Magento/Catalog/etc/adminhtml/system.xml
@@ -36,10 +36,10 @@
Recently Viewed/Compared Products
-
+
Lifetime of products in Recently Viewed Widget
-
+
Lifetime of products in Recently Compared Widget
@@ -83,7 +83,7 @@
Magento\Catalog\Model\Indexer\Product\Flat\System\Config\Mode
Magento\Config\Model\Config\Source\Yesno
-
+
Product Listing Sort by
Magento\Catalog\Model\Config\Source\ListSort
@@ -103,7 +103,6 @@
1
Magento\Catalog\Model\Config\CatalogClone\Media\Image
-
Magento\Config\Model\Config\Backend\Image
catalog/product/placeholder
catalog/product/placeholder
diff --git a/app/code/Magento/Catalog/etc/config.xml b/app/code/Magento/Catalog/etc/config.xml
index 3569c0a27b83f..74f7a0e082dd6 100644
--- a/app/code/Magento/Catalog/etc/config.xml
+++ b/app/code/Magento/Catalog/etc/config.xml
@@ -52,6 +52,11 @@
php,exe
+
+
+ none
+
+
diff --git a/app/code/Magento/Catalog/etc/di.xml b/app/code/Magento/Catalog/etc/di.xml
index 114d46f63fdd3..c64ed54608542 100644
--- a/app/code/Magento/Catalog/etc/di.xml
+++ b/app/code/Magento/Catalog/etc/di.xml
@@ -70,6 +70,7 @@
+
@@ -224,7 +225,8 @@
-
+
+
@@ -356,6 +358,7 @@
- Magento\Catalog\Pricing\Price\BasePrice
- Magento\Catalog\Pricing\Price\CustomOptionPrice
- Magento\Catalog\Pricing\Price\ConfiguredPrice
+ - Magento\Catalog\Pricing\Price\ConfiguredRegularPrice
@@ -652,12 +655,14 @@
- Magento\Catalog\Model\Product\Gallery\CreateHandler
- Magento\Catalog\Model\Category\Link\SaveHandler
- Magento\Catalog\Model\Product\Website\SaveHandler
+ - Magento\Catalog\Model\Product\Attribute\Backend\TierPrice\SaveHandler
-
- Magento\Catalog\Model\Product\Option\SaveHandler
- Magento\Catalog\Model\Product\Gallery\UpdateHandler
- Magento\Catalog\Model\Category\Link\SaveHandler
- Magento\Catalog\Model\Product\Website\SaveHandler
+ - Magento\Catalog\Model\Product\Attribute\Backend\TierPrice\UpdateHandler
@@ -809,6 +814,9 @@
Magento\Catalog\Model\ResourceModel\AttributePersistor
+
+
+
@@ -854,6 +862,7 @@
- Magento\Catalog\Model\Api\SearchCriteria\CollectionProcessor\FilterProcessor\ProductCategoryFilter
- Magento\Catalog\Model\Api\SearchCriteria\CollectionProcessor\FilterProcessor\ProductStoreFilter
+ - Magento\Catalog\Model\Api\SearchCriteria\CollectionProcessor\FilterProcessor\ProductStoreFilter
- Magento\Catalog\Model\Api\SearchCriteria\CollectionProcessor\FilterProcessor\ProductWebsiteFilter
@@ -903,6 +912,7 @@
Magento\Catalog\Model\ResourceModel\Product\CompositeBaseSelectProcessor
+ Magento\Catalog\Model\Indexer\Product\Price\PriceTableResolver
@@ -1057,4 +1067,80 @@
Magento\Catalog\Api\ProductRepositoryInterface\Proxy
+
+
+ indexer
+
+
+
+
+ Magento\Catalog\Model\Api\SearchCriteria\CollectionProcessor\ConditionProcessor\ConditionBuilder\EavAttributeCondition
+ Magento\Catalog\Model\Api\SearchCriteria\CollectionProcessor\ConditionProcessor\ConditionBuilder\NativeAttributeCondition
+
+
+
+
+
+
+ - Magento\Store\Model\Indexer\WebsiteDimensionProvider
+
+ - Magento\Customer\Model\Indexer\CustomerGroupDimensionProvider
+
+
+
+
+
+ Magento\Catalog\Model\Indexer\Product\Price\PriceTableResolver\Proxy
+ Magento\Store\Model\StoreManagerInterface\Proxy
+ Magento\Framework\App\Http\Context\Proxy
+
+
+
+
+
+
+
+
+
+ Magento\Catalog\Model\Indexer\Product\Price\PriceTableResolver
+
+
+
+
+
+ Magento\Catalog\Model\ResourceModel\Product\Indexer\TemporaryTableStrategy
+
+
+
+
+ indexer
+ Magento\Framework\Indexer\ScopeResolver\IndexScopeResolver
+
+
+
+
+
+ - Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\CustomOptionPriceModifier
+
+
+
+
+
+ virtual
+
+
+
+
+
+ - Magento\Catalog\Model\Indexer\Product\Price\ModeSwitcher
+
+
+
+
+
+
+ - catalog_product_price
+
+
+
diff --git a/app/code/Magento/Catalog/etc/events.xml b/app/code/Magento/Catalog/etc/events.xml
index 3fdb554e65b62..63bd574894339 100644
--- a/app/code/Magento/Catalog/etc/events.xml
+++ b/app/code/Magento/Catalog/etc/events.xml
@@ -56,7 +56,6 @@
-
diff --git a/app/code/Magento/Catalog/etc/frontend/di.xml b/app/code/Magento/Catalog/etc/frontend/di.xml
index 63fc11c08d8bb..2e98c980f5686 100644
--- a/app/code/Magento/Catalog/etc/frontend/di.xml
+++ b/app/code/Magento/Catalog/etc/frontend/di.xml
@@ -79,4 +79,20 @@
recently_compared_product
+
+
+
+ -
+
- \Magento\Framework\View\Element\Message\Renderer\BlockRenderer::CODE
+ -
+
- Magento_Catalog::messages/addCompareSuccessMessage.phtml
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/etc/module.xml b/app/code/Magento/Catalog/etc/module.xml
index 18671a32bb4fb..23e130aa8a991 100644
--- a/app/code/Magento/Catalog/etc/module.xml
+++ b/app/code/Magento/Catalog/etc/module.xml
@@ -6,7 +6,7 @@
*/
-->
-
+
diff --git a/app/code/Magento/Catalog/etc/product_types.xml b/app/code/Magento/Catalog/etc/product_types.xml
index fe4922ab8fa1f..fdcb67ae484d2 100644
--- a/app/code/Magento/Catalog/etc/product_types.xml
+++ b/app/code/Magento/Catalog/etc/product_types.xml
@@ -7,11 +7,13 @@
-->
+
+
diff --git a/app/code/Magento/Catalog/etc/webapi_rest/di.xml b/app/code/Magento/Catalog/etc/webapi_rest/di.xml
index 1d2b013f2035d..49c5eff91ee49 100644
--- a/app/code/Magento/Catalog/etc/webapi_rest/di.xml
+++ b/app/code/Magento/Catalog/etc/webapi_rest/di.xml
@@ -16,4 +16,8 @@
+
+
+
+
diff --git a/app/code/Magento/Catalog/etc/webapi_soap/di.xml b/app/code/Magento/Catalog/etc/webapi_soap/di.xml
index 98a8ef4de8408..2a5d60222e9f8 100644
--- a/app/code/Magento/Catalog/etc/webapi_soap/di.xml
+++ b/app/code/Magento/Catalog/etc/webapi_soap/di.xml
@@ -15,4 +15,8 @@
+
+
+
+
diff --git a/app/code/Magento/Catalog/etc/widget.xml b/app/code/Magento/Catalog/etc/widget.xml
index ff3773981d407..ea53f5215b7e6 100644
--- a/app/code/Magento/Catalog/etc/widget.xml
+++ b/app/code/Magento/Catalog/etc/widget.xml
@@ -292,7 +292,7 @@
-
+
diff --git a/app/code/Magento/Catalog/i18n/en_US.csv b/app/code/Magento/Catalog/i18n/en_US.csv
index de9f5e1975870..35a2c224c4ed2 100644
--- a/app/code/Magento/Catalog/i18n/en_US.csv
+++ b/app/code/Magento/Catalog/i18n/en_US.csv
@@ -13,8 +13,8 @@ Position,Position
Day,Day
Month,Month
Year,Year
-"from ","from "
-"to ","to "
+from,from
+to,to
[GLOBAL],[GLOBAL]
[WEBSITE],[WEBSITE]
"[STORE VIEW]","[STORE VIEW]"
@@ -516,6 +516,9 @@ Groups,Groups
"Maximum image width","Maximum image width"
"Maximum image height","Maximum image height"
"Maximum number of characters:","Maximum number of characters:"
+"Maximum %1 characters", "Maximum %1 characters"
+"too many", "too many"
+"remaining", "remaining"
"start typing to search template","start typing to search template"
"Product online","Product online"
"Product offline","Product offline"
@@ -615,7 +618,7 @@ Submit,Submit
"We don't recognize or support this file extension type.","We don't recognize or support this file extension type."
"Configure Product","Configure Product"
OK,OK
-"This value does not follow the specified format (for example, 200X300).","This value does not follow the specified format (for example, 200X300)."
+"This value does not follow the specified format (for example, 200x300).","This value does not follow the specified format (for example, 200x300)."
"Select type of option.","Select type of option."
"Please add rows to option.","Please add rows to option."
"Please select items.","Please select items."
diff --git a/app/code/Magento/Catalog/view/adminhtml/layout/CATALOG_PRODUCT_COMPOSITE_CONFIGURE.xml b/app/code/Magento/Catalog/view/adminhtml/layout/CATALOG_PRODUCT_COMPOSITE_CONFIGURE.xml
index 92663f0ce5c44..f4a5bbba8f571 100644
--- a/app/code/Magento/Catalog/view/adminhtml/layout/CATALOG_PRODUCT_COMPOSITE_CONFIGURE.xml
+++ b/app/code/Magento/Catalog/view/adminhtml/layout/CATALOG_PRODUCT_COMPOSITE_CONFIGURE.xml
@@ -9,11 +9,11 @@
-
-
-
-
-
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/view/adminhtml/layout/catalog_product_attribute_edit.xml b/app/code/Magento/Catalog/view/adminhtml/layout/catalog_product_attribute_edit.xml
index c3328455be8ab..94e9425593404 100644
--- a/app/code/Magento/Catalog/view/adminhtml/layout/catalog_product_attribute_edit.xml
+++ b/app/code/Magento/Catalog/view/adminhtml/layout/catalog_product_attribute_edit.xml
@@ -10,12 +10,12 @@
-
-
-
+
+
+
-
-
+
+
diff --git a/app/code/Magento/Catalog/view/adminhtml/layout/catalog_product_attribute_edit_popup.xml b/app/code/Magento/Catalog/view/adminhtml/layout/catalog_product_attribute_edit_popup.xml
index a46f6e5b0e8b4..ff241dca1dfe6 100755
--- a/app/code/Magento/Catalog/view/adminhtml/layout/catalog_product_attribute_edit_popup.xml
+++ b/app/code/Magento/Catalog/view/adminhtml/layout/catalog_product_attribute_edit_popup.xml
@@ -13,11 +13,11 @@
-
-
-
-
-
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/view/adminhtml/layout/catalog_product_set_block.xml b/app/code/Magento/Catalog/view/adminhtml/layout/catalog_product_set_block.xml
index 22e16cac8cef2..44884897461a8 100644
--- a/app/code/Magento/Catalog/view/adminhtml/layout/catalog_product_set_block.xml
+++ b/app/code/Magento/Catalog/view/adminhtml/layout/catalog_product_set_block.xml
@@ -26,7 +26,7 @@
-
+
Set
left
diff --git a/app/code/Magento/Catalog/view/adminhtml/requirejs-config.js b/app/code/Magento/Catalog/view/adminhtml/requirejs-config.js
index 5ffc587f65bec..0677b0a5811c2 100644
--- a/app/code/Magento/Catalog/view/adminhtml/requirejs-config.js
+++ b/app/code/Magento/Catalog/view/adminhtml/requirejs-config.js
@@ -6,15 +6,29 @@
var config = {
map: {
'*': {
- categoryForm: 'Magento_Catalog/catalog/category/form',
- newCategoryDialog: 'Magento_Catalog/js/new-category-dialog',
- categoryTree: 'Magento_Catalog/js/category-tree',
- productGallery: 'Magento_Catalog/js/product-gallery',
- baseImage: 'Magento_Catalog/catalog/base-image-uploader',
- productAttributes: 'Magento_Catalog/catalog/product-attributes'
+ categoryForm: 'Magento_Catalog/catalog/category/form',
+ newCategoryDialog: 'Magento_Catalog/js/new-category-dialog',
+ categoryTree: 'Magento_Catalog/js/category-tree',
+ productGallery: 'Magento_Catalog/js/product-gallery',
+ baseImage: 'Magento_Catalog/catalog/base-image-uploader',
+ productAttributes: 'Magento_Catalog/catalog/product-attributes',
+ categoryCheckboxTree: 'Magento_Catalog/js/category-checkbox-tree'
}
},
deps: [
'Magento_Catalog/catalog/product'
- ]
+ ],
+ config: {
+ mixins: {
+ 'Magento_Catalog/js/components/use-parent-settings/select': {
+ 'Magento_Catalog/js/components/use-parent-settings/toggle-disabled-mixin': true
+ },
+ 'Magento_Catalog/js/components/use-parent-settings/textarea': {
+ 'Magento_Catalog/js/components/use-parent-settings/toggle-disabled-mixin': true
+ },
+ 'Magento_Catalog/js/components/use-parent-settings/single-checkbox': {
+ 'Magento_Catalog/js/components/use-parent-settings/toggle-disabled-mixin': true
+ }
+ }
+ }
};
diff --git a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/category/checkboxes/tree.phtml b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/category/checkboxes/tree.phtml
index 740d389735974..00a1580923a7b 100644
--- a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/category/checkboxes/tree.phtml
+++ b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/category/checkboxes/tree.phtml
@@ -5,187 +5,33 @@
*/
// @codingStandardsIgnoreFile
-
-?>
-
-
-
-
-
- categoryLoader.buildHash = function(node)
+
diff --git a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/category/tree.phtml b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/category/tree.phtml
index a3cf4b478ad3b..9865589556e7b 100644
--- a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/category/tree.phtml
+++ b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/category/tree.phtml
@@ -59,6 +59,8 @@
}
};
+ var treeRoot = '#tree-div';
+
/**
* Fix ext compatibility with prototype 1.6
*/
@@ -491,7 +493,7 @@
if (data.error) {
reRenderTree();
} else {
- $(obj.tree.container.dom).trigger('categoryMove.tree');
+ $(treeRoot).trigger('categoryMove.tree');
}
$('.page-main-actions').next('.messages').remove();
$('.page-main-actions').next('#messages').remove();
diff --git a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/attribute/form.phtml b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/attribute/form.phtml
index 74cf8f5f3a70b..124194519b978 100644
--- a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/attribute/form.phtml
+++ b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/attribute/form.phtml
@@ -21,13 +21,13 @@
= $block->getChildHtml('form') ?>
-
-
-
= /* @escapeNotVerified */ $block->getFormScripts() ?>
diff --git a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/attribute/js.phtml b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/attribute/js.phtml
index 69bc847c30f46..3cbfa0f29d74f 100644
--- a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/attribute/js.phtml
+++ b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/attribute/js.phtml
@@ -55,7 +55,7 @@ function bindAttributeInputType()
{
checkOptionsPanelVisibility();
switchDefaultValueField();
- if($('frontend_input') && ($('frontend_input').value=='select' || $('frontend_input').value=='multiselect' || $('frontend_input').value=='price')){
+ if($('frontend_input') && ($('frontend_input').value=='boolean' || $('frontend_input').value=='select' || $('frontend_input').value=='multiselect' || $('frontend_input').value=='price')){
if($('is_filterable') && !$('is_filterable').getAttribute('readonly')){
$('is_filterable').disabled = false;
}
@@ -196,9 +196,11 @@ function switchDefaultValueField()
helper('Magento\Catalog\Helper\Data')->getAttributeHiddenFields() as $type => $fields): ?>
case '= /* @escapeNotVerified */ $type ?>':
+ var isFrontTabHidden = false;
getFrontTab().hide();
+ isFrontTabHidden = true;
defaultValueTextVisibility =
defaultValueTextareaVisibility =
@@ -210,6 +212,10 @@ function switchDefaultValueField()
setRowVisibility('= /* @escapeNotVerified */ $one ?>', false);
+
+ if (!isFrontTabHidden){
+ getFrontTab().show();
+ }
break;
diff --git a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/attribute/options.phtml b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/attribute/options.phtml
index a0041d2e02988..f812a27f87ad9 100644
--- a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/attribute/options.phtml
+++ b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/attribute/options.phtml
@@ -57,7 +57,7 @@ $stores = $block->getStoresSortedBySortOrder();
diff --git a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/attribute_set.phtml b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/attribute_set.phtml
index 6a5f6c4648494..a7e8564e7a1d8 100644
--- a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/attribute_set.phtml
+++ b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/attribute_set.phtml
@@ -14,7 +14,7 @@
<% } %>
<% if (!data.term && data.items.length && !data.allShown()) { %>
diff --git a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/options/type/file.phtml b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/options/type/file.phtml
index 693c98fc02cab..9f0fc0c569d6c 100644
--- a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/options/type/file.phtml
+++ b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/options/type/file.phtml
@@ -41,6 +41,7 @@
+
x %2 px. ',
' ',
diff --git a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/tab/inventory.phtml b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/tab/inventory.phtml
index 15c33c56e3ac6..2c62bbf8db3e9 100644
--- a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/tab/inventory.phtml
+++ b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/tab/inventory.phtml
@@ -27,7 +27,7 @@
getFieldValue('manage_stock') == 0): ?> selected="selected">= /* @escapeNotVerified */ __('No') ?>
- getFieldValue('use_config_manage_stock') || $block->IsNew()) ? 'checked="checked"' : '' ?>
+ getFieldValue('use_config_manage_stock') || $block->isNew()) ? 'checked="checked"' : '' ?>
onclick="toggleValueElements(this, this.parentNode);" = /* @escapeNotVerified */ $_readonly ?>>
= /* @escapeNotVerified */ __('Use Config Settings') ?>
isReadonly()): ?>
@@ -67,7 +67,7 @@ toggleValueElements($('inventory_use_config_manage_stock'), $('inventory_use_con
>
- getFieldValue('use_config_min_qty') || $block->IsNew()) ? 'checked="checked"' : '' ?>
+ getFieldValue('use_config_min_qty') || $block->isNew()) ? 'checked="checked"' : '' ?>
onclick="toggleValueElements(this, this.parentNode);" = /* @escapeNotVerified */ $_readonly ?>>
= /* @escapeNotVerified */ __('Use Config Settings') ?>
@@ -94,7 +94,7 @@ toggleValueElements($('inventory_use_config_min_qty'), $('inventory_use_config_m
name="= /* @escapeNotVerified */ $block->getFieldSuffix() ?>[stock_data][min_sale_qty]"
value="= /* @escapeNotVerified */ $block->getFieldValue('min_sale_qty') * 1 ?>" = /* @escapeNotVerified */ $_readonly ?>>
- getFieldValue('use_config_min_sale_qty') || $block->IsNew()) ? 'checked="checked"' : '' ?>
+ getFieldValue('use_config_min_sale_qty') || $block->isNew()) ? 'checked="checked"' : '' ?>
onclick="toggleValueElements(this, this.parentNode);" class="checkbox" = /* @escapeNotVerified */ $_readonly ?>>
= /* @escapeNotVerified */ __('Use Config Settings') ?>
@@ -117,7 +117,7 @@ toggleValueElements($('inventory_use_config_min_sale_qty'), $('inventory_use_con
>
- getFieldValue('use_config_max_sale_qty') || $block->IsNew()) ? 'checked="checked"' : '' ?>
+ getFieldValue('use_config_max_sale_qty') || $block->isNew()) ? 'checked="checked"' : '' ?>
onclick="toggleValueElements(this, this.parentNode);" class="checkbox" = /* @escapeNotVerified */ $_readonly ?>>
= /* @escapeNotVerified */ __('Use Config Settings') ?>
@@ -182,7 +182,7 @@ toggleValueElements($('inventory_use_config_max_sale_qty'), $('inventory_use_con
- getFieldValue('use_config_backorders') || $block->IsNew()) ? 'checked="checked"' : '' ?>
+ getFieldValue('use_config_backorders') || $block->isNew()) ? 'checked="checked"' : '' ?>
onclick="toggleValueElements(this, this.parentNode);" = /* @escapeNotVerified */ $_readonly ?>>
= /* @escapeNotVerified */ __('Use Config Settings') ?>
@@ -207,7 +207,7 @@ toggleValueElements($('inventory_use_config_backorders'), $('inventory_use_confi
>
- getFieldValue('use_config_notify_stock_qty') || $block->IsNew()) ? 'checked="checked"' : '' ?>
+ getFieldValue('use_config_notify_stock_qty') || $block->isNew()) ? 'checked="checked"' : '' ?>
onclick="toggleValueElements(this, this.parentNode);" = /* @escapeNotVerified */ $_readonly ?>>
= /* @escapeNotVerified */ __('Use Config Settings') ?>
@@ -238,7 +238,7 @@ toggleValueElements($('inventory_use_config_notify_stock_qty'), $('inventory_use
- getFieldValue('use_config_enable_qty_inc') || $block->IsNew()) ? 'checked="checked"' : '' ?>
+ getFieldValue('use_config_enable_qty_inc') || $block->isNew()) ? 'checked="checked"' : '' ?>
onclick="toggleValueElements(this, this.parentNode);" = /* @escapeNotVerified */ $_readonly ?>>
= /* @escapeNotVerified */ __('Use Config Settings') ?>
@@ -262,7 +262,7 @@ toggleValueElements($('inventory_use_config_enable_qty_increments'), $('inventor
>
- getFieldValue('use_config_qty_increments') || $block->IsNew()) ? 'checked="checked"' : '' ?>
+ getFieldValue('use_config_qty_increments') || $block->isNew()) ? 'checked="checked"' : '' ?>
onclick="toggleValueElements(this, this.parentNode);" = /* @escapeNotVerified */ $_readonly ?>>
= /* @escapeNotVerified */ __('Use Config Settings') ?>
diff --git a/app/code/Magento/Catalog/view/adminhtml/ui_component/category_form.xml b/app/code/Magento/Catalog/view/adminhtml/ui_component/category_form.xml
index 4eb0b986edfb1..793ed3b1bfc93 100644
--- a/app/code/Magento/Catalog/view/adminhtml/ui_component/category_form.xml
+++ b/app/code/Magento/Catalog/view/adminhtml/ui_component/category_form.xml
@@ -42,7 +42,7 @@
-
+
false
@@ -256,7 +256,6 @@
- group
-
- true
- - true
@@ -297,7 +296,6 @@
- group
-
- true
- - true
@@ -459,34 +457,34 @@
-
+
string
Theme
- ${ $.parentName }.custom_use_parent_settings:checked
+ ${ $.parentName }.custom_use_parent_settings:checked
-
+
string
Layout
- ${ $.parentName }.custom_use_parent_settings:checked
+ ${ $.parentName }.custom_use_parent_settings:checked
-
+
string
Layout Update XML
- ns = ${ $.ns }, index = custom_use_parent_settings :checked
+ ${ $.parentName }.custom_use_parent_settings:checked
-
+
-
- 0
@@ -499,7 +497,7 @@
boolean
Apply Design to Products
- ns = ${ $.ns }, index = custom_use_parent_settings:checked
+ ${ $.parentName }.custom_use_parent_settings:checked
diff --git a/app/code/Magento/Catalog/view/adminhtml/ui_component/product_attribute_add_form.xml b/app/code/Magento/Catalog/view/adminhtml/ui_component/product_attribute_add_form.xml
index 772bc1e6ec5d7..f795fcabe701c 100644
--- a/app/code/Magento/Catalog/view/adminhtml/ui_component/product_attribute_add_form.xml
+++ b/app/code/Magento/Catalog/view/adminhtml/ui_component/product_attribute_add_form.xml
@@ -147,7 +147,6 @@
- true
- true
- container
- - attribute_options.position
@@ -184,12 +183,8 @@
-
- true
-
text
false
- position
diff --git a/app/code/Magento/Catalog/view/adminhtml/ui_component/product_listing.xml b/app/code/Magento/Catalog/view/adminhtml/ui_component/product_listing.xml
index 09332d66633f1..65090fa3ac461 100644
--- a/app/code/Magento/Catalog/view/adminhtml/ui_component/product_listing.xml
+++ b/app/code/Magento/Catalog/view/adminhtml/ui_component/product_listing.xml
@@ -48,7 +48,9 @@
-
+
diff --git a/app/code/Magento/Catalog/view/adminhtml/web/catalog/category/form.js b/app/code/Magento/Catalog/view/adminhtml/web/catalog/category/form.js
index 0f6689b88db06..0a04358e41123 100644
--- a/app/code/Magento/Catalog/view/adminhtml/web/catalog/category/form.js
+++ b/app/code/Magento/Catalog/view/adminhtml/web/catalog/category/form.js
@@ -14,6 +14,7 @@ define([
options: {
categoryIdSelector: 'input[name="id"]',
categoryPathSelector: 'input[name="path"]',
+ categoryParentSelector: 'input[name="parent"]',
refreshUrl: config.refreshUrl
},
@@ -45,6 +46,7 @@ define([
} else {
$(this.options.categoryIdSelector).val(data.id).change();
$(this.options.categoryPathSelector).val(data.path).change();
+ $(this.options.categoryParentSelector).val(data.parentId).change();
}
}
};
diff --git a/app/code/Magento/Catalog/view/adminhtml/web/catalog/product/edit/attribute.js b/app/code/Magento/Catalog/view/adminhtml/web/catalog/product/edit/attribute.js
new file mode 100644
index 0000000000000..407fd1fe28e39
--- /dev/null
+++ b/app/code/Magento/Catalog/view/adminhtml/web/catalog/product/edit/attribute.js
@@ -0,0 +1,18 @@
+/**
+ * Copyright © Magento, Inc. All rights reserved.
+ * See COPYING.txt for license details.
+ */
+
+define([
+ 'jquery',
+ 'mage/mage'
+], function ($) {
+ 'use strict';
+
+ return function (config, element) {
+
+ $(element).mage('form').mage('validation', {
+ validationUrl: config.validationUrl
+ });
+ };
+});
diff --git a/app/code/Magento/Catalog/view/adminhtml/web/component/image-size-field.js b/app/code/Magento/Catalog/view/adminhtml/web/component/image-size-field.js
index 3ebd4bdf9c804..11a1a65cbab47 100644
--- a/app/code/Magento/Catalog/view/adminhtml/web/component/image-size-field.js
+++ b/app/code/Magento/Catalog/view/adminhtml/web/component/image-size-field.js
@@ -26,7 +26,7 @@ define([
return !!(m && m[1] > 0 && m[2] > 0);
},
- $.mage.__('This value does not follow the specified format (for example, 200X300).')
+ $.mage.__('This value does not follow the specified format (for example, 200x300).')
);
return Abstract.extend({
diff --git a/app/code/Magento/Catalog/view/adminhtml/web/js/category-checkbox-tree.js b/app/code/Magento/Catalog/view/adminhtml/web/js/category-checkbox-tree.js
new file mode 100644
index 0000000000000..1d64418e36ff5
--- /dev/null
+++ b/app/code/Magento/Catalog/view/adminhtml/web/js/category-checkbox-tree.js
@@ -0,0 +1,279 @@
+/**
+ * Copyright © Magento, Inc. All rights reserved.
+ * See COPYING.txt for license details.
+ */
+
+define([
+ 'jquery',
+ 'prototype',
+ 'extjs/ext-tree-checkbox',
+ 'mage/adminhtml/form'
+], function (jQuery) {
+ 'use strict';
+
+ return function (config) {
+ var tree,
+ options = {
+ dataUrl: config.dataUrl,
+ divId: config.divId,
+ rootVisible: config.rootVisible,
+ useAjax: config.useAjax,
+ currentNodeId: config.currentNodeId,
+ jsFormObject: window[config.jsFormObject],
+ name: config.name,
+ checked: config.checked,
+ allowDrop: config.allowDrop,
+ rootId: config.rootId,
+ expanded: config.expanded,
+ categoryId: config.categoryId,
+ treeJson: config.treeJson
+ },
+ data = {},
+ parameters = {},
+ root = {},
+ key = '';
+
+ /* eslint-disable */
+ /**
+ * Fix ext compatibility with prototype 1.6
+ */
+ Ext.lib.Event.getTarget = function (e) {// eslint-disable-line no-undef
+ var ee = e.browserEvent || e;
+
+ return ee.target ? Event.element(ee) : null;
+ };
+
+ /**
+ * @param {Object} el
+ * @param {Object} config
+ */
+ Ext.tree.TreePanel.Enhanced = function (el, config) {// eslint-disable-line no-undef
+ Ext.tree.TreePanel.Enhanced.superclass.constructor.call(this, el, config);// eslint-disable-line no-undef
+ };
+
+ Ext.extend(Ext.tree.TreePanel.Enhanced, Ext.tree.TreePanel, {// eslint-disable-line no-undef
+ /* eslint-enable */
+ /**
+ * @param {Object} config
+ * @param {Boolean} firstLoad
+ */
+ loadTree: function (config, firstLoad) {// eslint-disable-line no-shadow
+ parameters = config.parameters,
+ data = config.data,
+ root = new Ext.tree.TreeNode(parameters);// eslint-disable-line no-undef
+
+ if (typeof parameters.rootVisible != 'undefined') {
+ this.rootVisible = parameters.rootVisible * 1;
+ }
+
+ this.nodeHash = {};
+ this.setRootNode(root);
+
+ if (firstLoad) {
+ this.addListener('click', this.categoryClick.createDelegate(this));
+ }
+
+ this.loader.buildCategoryTree(root, data);
+ this.el.dom.innerHTML = '';
+ // render the tree
+ this.render();
+ },
+
+ /**
+ * @param {Object} node
+ */
+ categoryClick: function (node) {
+ node.getUI().check(!node.getUI().checked());
+ }
+ });
+
+ jQuery(function () {
+ var categoryLoader = new Ext.tree.TreeLoader({// eslint-disable-line no-undef
+ dataUrl: config.dataUrl
+ });
+
+ /**
+ * @param {Object} response
+ * @param {Object} parent
+ * @param {Function} callback
+ */
+ categoryLoader.processResponse = function (response, parent, callback) {
+ config = JSON.parse(response.responseText);
+
+ this.buildCategoryTree(parent, config);
+
+ if (typeof callback === 'function') {
+ callback(this, parent);
+ }
+ };
+
+ /**
+ * @param {Object} config
+ * @returns {Object}
+ */
+ categoryLoader.createNode = function (config) {// eslint-disable-line no-shadow
+ var node;
+
+ config.uiProvider = Ext.tree.CheckboxNodeUI;// eslint-disable-line no-undef
+
+ if (config.children && !config.children.length) {
+ delete config.children;
+ node = new Ext.tree.AsyncTreeNode(config);// eslint-disable-line no-undef
+ } else {
+ node = new Ext.tree.TreeNode(config);// eslint-disable-line no-undef
+ }
+
+ return node;
+ };
+
+ /**
+ * @param {Object} parent
+ * @param {Object} config
+ * @param {Integer} i
+ */
+ categoryLoader.processCategoryTree = function (parent, config, i) {// eslint-disable-line no-shadow
+ var node,
+ _node = {};
+
+ config[i].uiProvider = Ext.tree.CheckboxNodeUI;// eslint-disable-line no-undef
+
+ _node = Object.clone(config[i]);
+
+ if (_node.children && !_node.children.length) {
+ delete _node.children;
+ node = new Ext.tree.AsyncTreeNode(_node);// eslint-disable-line no-undef
+ } else {
+ node = new Ext.tree.TreeNode(config[i]);// eslint-disable-line no-undef
+ }
+ parent.appendChild(node);
+ node.loader = node.getOwnerTree().loader;
+
+ if (_node.children) {
+ categoryLoader.buildCategoryTree(node, _node.children);
+ }
+ };
+
+ /**
+ * @param {Object} parent
+ * @param {Object} config
+ * @returns {void}
+ */
+ categoryLoader.buildCategoryTree = function (parent, config) {// eslint-disable-line no-shadow
+ var i = 0;
+
+ if (!config) {
+ return null;
+ }
+
+ if (parent && config && config.length) {
+ for (i; i < config.length; i++) {
+ categoryLoader.processCategoryTree(parent, config, i);
+ }
+ }
+ };
+
+ /**
+ *
+ * @param {Object} hash
+ * @param {Object} node
+ * @returns {Object}
+ */
+ categoryLoader.buildHashChildren = function (hash, node) {// eslint-disable-line no-shadow
+ var i = 0,
+ len;
+
+ // eslint-disable-next-line no-extra-parens
+ if ((node.childNodes.length > 0) || (node.loaded === false && node.loading === false)) {
+ hash.children = [];
+
+ for (i, len = node.childNodes.length; i < len; i++) {
+ /* eslint-disable */
+ if (!hash.children) {
+ hash.children = [];
+ }
+ /* eslint-enable */
+ hash.children.push(this.buildHash(node.childNodes[i]));
+ }
+ }
+
+ return hash;
+ };
+
+ /**
+ * @param {Object} node
+ * @returns {Object}
+ */
+ categoryLoader.buildHash = function (node) {
+ var hash = {};
+
+ hash = this.toArray(node.attributes);
+
+ return categoryLoader.buildHashChildren(hash, node);
+ };
+
+ /**
+ * @param {Object} attributes
+ * @returns {Object}
+ */
+ categoryLoader.toArray = function (attributes) {
+ data = {};
+
+ for (key in attributes) {
+
+ if (attributes[key]) {
+ data[key] = attributes[key];
+ }
+ }
+
+ return data;
+ };
+
+ categoryLoader.on('beforeload', function (treeLoader, node) {
+ treeLoader.baseParams.id = node.attributes.id;
+ treeLoader.baseParams.selected = options.jsFormObject.updateElement.value;
+ });
+
+ /* eslint-disable */
+ categoryLoader.on('load', function () {
+ varienWindowOnload();
+ });
+
+ tree = new Ext.tree.TreePanel.Enhanced(options.divId, {
+ animate: false,
+ loader: categoryLoader,
+ enableDD: false,
+ containerScroll: true,
+ selModel: new Ext.tree.CheckNodeMultiSelectionModel(),
+ rootVisible: options.rootVisible,
+ useAjax: options.useAjax,
+ currentNodeId: options.currentNodeId,
+ addNodeTo: false,
+ rootUIProvider: Ext.tree.CheckboxNodeUI
+ });
+
+ tree.on('check', function (node) {
+ options.jsFormObject.updateElement.value = this.getChecked().join(', ');
+ varienElementMethods.setHasChanges(node.getUI().checkbox);
+ }, tree);
+
+ // set the root node
+ //jscs:disable requireCamelCaseOrUpperCaseIdentifiers
+ parameters = {
+ text: options.name,
+ draggable: false,
+ checked: options.checked,
+ uiProvider: Ext.tree.CheckboxNodeUI,
+ allowDrop: options.allowDrop,
+ id: options.rootId,
+ expanded: options.expanded,
+ category_id: options.categoryId
+ };
+ //jscs:enable requireCamelCaseOrUpperCaseIdentifiers
+
+ tree.loadTree({
+ parameters: parameters, data: options.treeJson
+ }, true);
+ /* eslint-enable */
+ });
+ };
+});
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 99b1252b8f781..561e23b974462 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,9 +5,10 @@
define([
'jquery',
+ 'mageUtils',
'jquery/ui',
'jquery/jstree/jquery.jstree'
-], function ($) {
+], function ($, utils) {
'use strict';
$.widget('mage.categoryTree', {
@@ -88,9 +89,10 @@ define([
if (!node) {
return result;
}
+
result = {
data: {
- title: node.name + ' (' + node['product_count'] + ')'
+ title: utils.unescape(node.name) + ' (' + node['product_count'] + ')'
},
attr: {
'class': node.cls + (!!node.disabled ? ' disabled' : '') //eslint-disable-line no-extra-boolean-cast
diff --git a/app/code/Magento/Catalog/view/adminhtml/web/js/components/dynamic-rows-tier-price.js b/app/code/Magento/Catalog/view/adminhtml/web/js/components/dynamic-rows-tier-price.js
index 9201c1c8e0fb4..b5c0e7a95d401 100644
--- a/app/code/Magento/Catalog/view/adminhtml/web/js/components/dynamic-rows-tier-price.js
+++ b/app/code/Magento/Catalog/view/adminhtml/web/js/components/dynamic-rows-tier-price.js
@@ -9,6 +9,10 @@ define([
], function (_, DynamicRows) {
'use strict';
+ /**
+ * @deprecated Parent method contains labels sorting.
+ * @see Magento_Ui/js/dynamic-rows/dynamic-rows
+ */
return DynamicRows.extend({
/**
diff --git a/app/code/Magento/Catalog/view/adminhtml/web/js/components/use-parent-settings/select.js b/app/code/Magento/Catalog/view/adminhtml/web/js/components/use-parent-settings/select.js
new file mode 100644
index 0000000000000..1ddb24f3eefbb
--- /dev/null
+++ b/app/code/Magento/Catalog/view/adminhtml/web/js/components/use-parent-settings/select.js
@@ -0,0 +1,15 @@
+/**
+ * Copyright © Magento, Inc. All rights reserved.
+ * See COPYING.txt for license details.
+ */
+
+/**
+ * @api
+ */
+define([
+ 'Magento_Ui/js/form/element/select'
+], function (Component) {
+ 'use strict';
+
+ return Component;
+});
diff --git a/app/code/Magento/Catalog/view/adminhtml/web/js/components/use-parent-settings/single-checkbox.js b/app/code/Magento/Catalog/view/adminhtml/web/js/components/use-parent-settings/single-checkbox.js
new file mode 100644
index 0000000000000..0f166d3b45582
--- /dev/null
+++ b/app/code/Magento/Catalog/view/adminhtml/web/js/components/use-parent-settings/single-checkbox.js
@@ -0,0 +1,15 @@
+/**
+ * Copyright © Magento, Inc. All rights reserved.
+ * See COPYING.txt for license details.
+ */
+
+/**
+ * @api
+ */
+define([
+ 'Magento_Ui/js/form/element/single-checkbox'
+], function (Component) {
+ 'use strict';
+
+ return Component;
+});
diff --git a/app/code/Magento/Catalog/view/adminhtml/web/js/components/use-parent-settings/textarea.js b/app/code/Magento/Catalog/view/adminhtml/web/js/components/use-parent-settings/textarea.js
new file mode 100644
index 0000000000000..3ef2bb21241a7
--- /dev/null
+++ b/app/code/Magento/Catalog/view/adminhtml/web/js/components/use-parent-settings/textarea.js
@@ -0,0 +1,15 @@
+/**
+ * Copyright © Magento, Inc. All rights reserved.
+ * See COPYING.txt for license details.
+ */
+
+/**
+ * @api
+ */
+define([
+ 'Magento_Ui/js/form/element/textarea'
+], function (Component) {
+ 'use strict';
+
+ return Component;
+});
diff --git a/app/code/Magento/Catalog/view/adminhtml/web/js/components/use-parent-settings/toggle-disabled-mixin.js b/app/code/Magento/Catalog/view/adminhtml/web/js/components/use-parent-settings/toggle-disabled-mixin.js
new file mode 100644
index 0000000000000..d140cc0fad74e
--- /dev/null
+++ b/app/code/Magento/Catalog/view/adminhtml/web/js/components/use-parent-settings/toggle-disabled-mixin.js
@@ -0,0 +1,62 @@
+/**
+ * Copyright © Magento, Inc. All rights reserved.
+ * See COPYING.txt for license details.
+ */
+
+define([
+ 'underscore'
+], function (_) {
+ 'use strict';
+
+ var mixin = {
+ defaults: {
+ imports: {
+ toggleDisabled: '${ $.parentName }.custom_use_parent_settings:checked'
+ },
+ useParent: false,
+ useDefaults: false
+ },
+
+ /**
+ * Disable form input if settings for parent section is used
+ * or default value is applied.
+ *
+ * @param {Boolean} isUseParent
+ */
+ toggleDisabled: function (isUseParent) {
+ var disabled = this.useParent = isUseParent;
+
+ if (!disabled && !_.isUndefined(this.service)) {
+ disabled = !!this.isUseDefault();
+ }
+
+ this.saveUseDefaults();
+ this.disabled(disabled);
+ },
+
+ /**
+ * Stores original state of the field.
+ */
+ saveUseDefaults: function () {
+ this.useDefaults = this.disabled();
+ },
+
+ /** @inheritdoc */
+ setInitialValue: function () {
+ this._super();
+ this.isUseDefault(this.useDefaults);
+
+ return this;
+ },
+
+ /** @inheritdoc */
+ toggleUseDefault: function (state) {
+ this._super();
+ this.disabled(state || this.useParent);
+ }
+ };
+
+ return function (target) {
+ return target.extend(mixin);
+ };
+});
diff --git a/app/code/Magento/Catalog/view/adminhtml/web/js/custom-options-type.js b/app/code/Magento/Catalog/view/adminhtml/web/js/custom-options-type.js
index b46d09a7323b1..353c28d0c9421 100644
--- a/app/code/Magento/Catalog/view/adminhtml/web/js/custom-options-type.js
+++ b/app/code/Magento/Catalog/view/adminhtml/web/js/custom-options-type.js
@@ -103,11 +103,6 @@ define([
if (component) {
component.visible(visible);
-
- /*eslint-disable max-depth */
- if (_.isFunction(component.clear)) {
- component.clear();
- }
}
}
}, this);
diff --git a/app/code/Magento/Catalog/view/adminhtml/web/js/form/element/input.js b/app/code/Magento/Catalog/view/adminhtml/web/js/form/element/input.js
index 51ffeaea0fc0c..2f6703cc92eac 100644
--- a/app/code/Magento/Catalog/view/adminhtml/web/js/form/element/input.js
+++ b/app/code/Magento/Catalog/view/adminhtml/web/js/form/element/input.js
@@ -54,9 +54,16 @@ define([
if (!_.isEmpty(this.suffixName) || _.isNumber(this.suffixName)) {
suffixName = '.' + this.suffixName;
}
- this.dataScope = 'data.' + this.prefixName + '.' + this.elementName + suffixName;
- this.links.value = this.provider + ':' + this.dataScope;
+ this.exportDataLink = 'data.' + this.prefixName + '.' + this.elementName + suffixName;
+ this.exports.value = this.provider + ':' + this.exportDataLink;
+ },
+
+ /** @inheritdoc */
+ destroy: function () {
+ this._super();
+
+ this.source.remove(this.exportDataLink);
},
/**
diff --git a/app/code/Magento/Catalog/view/adminhtml/web/js/options.js b/app/code/Magento/Catalog/view/adminhtml/web/js/options.js
index 5239eb207efca..6ea005915763c 100644
--- a/app/code/Magento/Catalog/view/adminhtml/web/js/options.js
+++ b/app/code/Magento/Catalog/view/adminhtml/web/js/options.js
@@ -13,17 +13,22 @@ define([
'jquery/ui',
'prototype',
'form',
- 'validation'
+ 'validation',
+ 'mage/translate'
], function (jQuery, mageTemplate, rg) {
'use strict';
return function (config) {
- var attributeOption = {
+ var optionPanel = jQuery('#manage-options-panel'),
+ optionsValues = [],
+ editForm = jQuery('#edit_form'),
+ attributeOption = {
table: $('attribute-options-table'),
itemCount: 0,
totalItems: 0,
rendered: 0,
template: mageTemplate('#row-template'),
+ newOptionClass: 'new-option',
isReadOnly: config.isReadOnly,
add: function (data, render) {
var isNewOption = false,
@@ -32,7 +37,8 @@ define([
if (typeof data.id == 'undefined') {
data = {
'id': 'option_' + this.itemCount,
- 'sort_order': this.itemCount + 1
+ 'sort_order': this.itemCount + 1,
+ 'rowClasses': this.newOptionClass
};
isNewOption = true;
}
@@ -84,6 +90,10 @@ define([
this.totalItems--;
this.updateItemsCountField();
}
+
+ if (element.hasClassName(this.newOptionClass)) {
+ element.remove();
+ }
},
updateItemsCountField: function () {
$('option-count-check').value = this.totalItems > 0 ? '1' : '';
@@ -144,7 +154,7 @@ define([
attributeOption.remove(event);
});
- jQuery('#manage-options-panel').on('render', function () {
+ optionPanel.on('render', function () {
attributeOption.ignoreValidate();
if (attributeOption.rendered) {
@@ -170,7 +180,31 @@ define([
});
});
}
+ editForm.on('submit', function () {
+ optionPanel.find('input')
+ .each(function () {
+ if (this.disabled) {
+ return;
+ }
+ if (this.type === 'checkbox' || this.type === 'radio') {
+ if (this.checked) {
+ optionsValues.push(this.name + '=' + jQuery(this).val());
+ }
+ } else {
+ optionsValues.push(this.name + '=' + jQuery(this).val());
+ }
+ });
+ jQuery(' ')
+ .attr({
+ type: 'hidden',
+ name: 'serialized_options'
+ })
+ .val(JSON.stringify(optionsValues))
+ .prependTo(editForm);
+ optionPanel.find('table')
+ .replaceWith(jQuery('').text(jQuery.mage.__('Sending attribute values as package.')));
+ });
window.attributeOption = attributeOption;
window.optionDefaultInputType = attributeOption.getOptionInputType();
diff --git a/app/code/Magento/Catalog/view/adminhtml/web/js/product/weight-handler.js b/app/code/Magento/Catalog/view/adminhtml/web/js/product/weight-handler.js
index 475c9d2dc0601..1c79331253251 100644
--- a/app/code/Magento/Catalog/view/adminhtml/web/js/product/weight-handler.js
+++ b/app/code/Magento/Catalog/view/adminhtml/web/js/product/weight-handler.js
@@ -67,7 +67,7 @@ define([
},
/**
- * Has weight swither
+ * Has weight switcher
* @returns {*}
*/
hasWeightSwither: function () {
diff --git a/app/code/Magento/Catalog/view/adminhtml/web/template/form/element/action-delete.html b/app/code/Magento/Catalog/view/adminhtml/web/template/form/element/action-delete.html
index d4cfb02611416..9a52dcefa3042 100644
--- a/app/code/Magento/Catalog/view/adminhtml/web/template/form/element/action-delete.html
+++ b/app/code/Magento/Catalog/view/adminhtml/web/template/form/element/action-delete.html
@@ -7,7 +7,7 @@
+ alt: $file.name,
+ title: $file.name">
diff --git a/app/code/Magento/Catalog/view/base/templates/product/price/amount/default.phtml b/app/code/Magento/Catalog/view/base/templates/product/price/amount/default.phtml
index 86168c742c0f1..ce1561e382eed 100644
--- a/app/code/Magento/Catalog/view/base/templates/product/price/amount/default.phtml
+++ b/app/code/Magento/Catalog/view/base/templates/product/price/amount/default.phtml
@@ -19,9 +19,8 @@
= ($block->getPriceDisplayLabel()) ? 'data-label="' . $block->getPriceDisplayLabel() . $block->getPriceDisplayInclExclTaxes() . '"' : '' ?>
data-price-amount="= /* @escapeNotVerified */ $block->getDisplayValue() ?>"
data-price-type="= /* @escapeNotVerified */ $block->getPriceType() ?>"
- class="price-wrapper = /* @escapeNotVerified */ $block->getPriceWrapperCss() ?>">
- = /* @escapeNotVerified */ $block->formatCurrency($block->getDisplayValue(), (bool)$block->getIncludeContainer()) ?>
-
+ class="price-wrapper = /* @escapeNotVerified */ $block->getPriceWrapperCss() ?>"
+ >= /* @escapeNotVerified */ $block->formatCurrency($block->getDisplayValue(), (bool)$block->getIncludeContainer()) ?>
hasAdjustmentsHtml()): ?>
= $block->getAdjustmentsHtml() ?>
diff --git a/app/code/Magento/Catalog/view/base/templates/product/price/configured_price.phtml b/app/code/Magento/Catalog/view/base/templates/product/price/configured_price.phtml
index 98b713be685d0..33cd071a27f84 100644
--- a/app/code/Magento/Catalog/view/base/templates/product/price/configured_price.phtml
+++ b/app/code/Magento/Catalog/view/base/templates/product/price/configured_price.phtml
@@ -6,19 +6,58 @@
?>
getZone() == 'item_view') ? true : false;
+$idSuffix = $block->getIdSuffix() ? $block->getIdSuffix() : '';
/** @var \Magento\Catalog\Pricing\Price\ConfiguredPrice $configuredPrice */
$configuredPrice = $block->getPrice();
-$schema = ($block->getZone() == 'item_view') ? true : false;
-$priceLabel = ($block->getPriceLabel() !== null)
- ? $block->getPriceLabel()
- : '';
+/** @var \Magento\Catalog\Pricing\Price\ConfiguredRegularPrice $configuredRegularPrice */
+$configuredRegularPrice = $block->getPriceType(
+ \Magento\Catalog\Pricing\Price\ConfiguredPriceInterface::CONFIGURED_REGULAR_PRICE_CODE
+);
?>
-
- = /* @escapeNotVerified */ $block->renderAmount($configuredPrice->getAmount(), [
- 'display_label' => $priceLabel,
- 'price_id' => $block->getPriceId('product-price-'),
- 'price_type' => 'finalPrice',
- 'include_container' => true,
- 'schema' => $schema
- ]); ?>
-
+getAmount()->getValue() < $configuredRegularPrice->getAmount()->getValue()) : ?>
+
+
+ = /* @noEscape */ $block->renderAmount(
+ $configuredPrice->getAmount(),
+ [
+ 'display_label' => $block->escapeHtml(__('Special Price')),
+ 'price_id' => $block->escapeHtml($block->getPriceId('product-price-' . $idSuffix)),
+ 'price_type' => 'finalPrice',
+ 'include_container' => true,
+ 'schema' => $schema
+ ]
+ ); ?>
+
+
+ = /* @noEscape */ $block->renderAmount(
+ $configuredRegularPrice->getAmount(),
+ [
+ 'display_label' => $block->escapeHtml(__('Regular Price')),
+ 'price_id' => $block->escapeHtml($block->getPriceId('old-price-' . $idSuffix)),
+ 'price_type' => 'oldPrice',
+ 'include_container' => true,
+ 'skip_adjustments' => true
+ ]
+ ); ?>
+
+
+
+ getPriceLabel() !== null)
+ ? $block->getPriceLabel()
+ : '';
+ ?>
+
+ = /* @noEscape */ $block->renderAmount(
+ $configuredPrice->getAmount(),
+ [
+ 'display_label' => $block->escapeHtml($priceLabel),
+ 'price_id' => $block->escapeHtml($block->getPriceId('product-price-' . $idSuffix)),
+ 'price_type' => 'finalPrice',
+ 'include_container' => true,
+ 'schema' => $schema
+ ]
+ ); ?>
+
+
diff --git a/app/code/Magento/Catalog/view/base/templates/product/price/default.phtml b/app/code/Magento/Catalog/view/base/templates/product/price/default.phtml
index 065472c686129..b414f02a3d6fb 100644
--- a/app/code/Magento/Catalog/view/base/templates/product/price/default.phtml
+++ b/app/code/Magento/Catalog/view/base/templates/product/price/default.phtml
@@ -11,8 +11,6 @@
getSaleableItem()->getId();
-
/** ex: \Magento\Catalog\Pricing\Price\RegularPrice */
/** @var \Magento\Framework\Pricing\Price\PriceInterface $priceModel */
$priceModel = $block->getPriceType('regular_price');
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 72d9124173898..6e281bdef7afb 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
@@ -11,8 +11,6 @@
getSaleableItem()->getId();
-
/** ex: \Magento\Catalog\Pricing\Price\RegularPrice */
/** @var \Magento\Framework\Pricing\Price\PriceInterface $priceModel */
$priceModel = $block->getPriceType('regular_price');
diff --git a/app/code/Magento/Catalog/view/base/web/js/price-options.js b/app/code/Magento/Catalog/view/base/web/js/price-options.js
index ceeea4c878622..e18abe3af38a6 100644
--- a/app/code/Magento/Catalog/view/base/web/js/price-options.js
+++ b/app/code/Magento/Catalog/view/base/web/js/price-options.js
@@ -20,8 +20,10 @@ define([
optionConfig: {},
optionHandlers: {},
optionTemplate: '<%= data.label %>' +
- '<% if (data.finalPrice.value) { %>' +
+ '<% if (data.finalPrice.value > 0) { %>' +
' +<%- data.finalPrice.formatted %>' +
+ '<% } else if (data.finalPrice.value < 0) { %>' +
+ ' <%- data.finalPrice.formatted %>' +
'<% } %>',
controlContainer: 'dd'
};
diff --git a/app/code/Magento/Catalog/view/base/web/js/price-utils.js b/app/code/Magento/Catalog/view/base/web/js/price-utils.js
index 7c4280e64930f..e2ea42f7d5fe3 100644
--- a/app/code/Magento/Catalog/view/base/web/js/price-utils.js
+++ b/app/code/Magento/Catalog/view/base/web/js/price-utils.js
@@ -79,7 +79,7 @@ define([
am = Number(Math.round(Math.abs(amount - i) + 'e+' + precision) + ('e-' + precision));
r = (j ? i.substr(0, j) + groupSymbol : '') +
i.substr(j).replace(re, '$1' + groupSymbol) +
- (precision ? decimalSymbol + am.toFixed(2).replace(/-/, 0).slice(2) : '');
+ (precision ? decimalSymbol + am.toFixed(precision).replace(/-/, 0).slice(2) : '');
return pattern.replace('%s', r).replace(/^\s\s*/, '').replace(/\s\s*$/, '');
}
diff --git a/app/code/Magento/Catalog/view/base/web/template/product/link.html b/app/code/Magento/Catalog/view/base/web/template/product/link.html
index 98255e1b8a9e2..2c70300f7aec3 100644
--- a/app/code/Magento/Catalog/view/base/web/template/product/link.html
+++ b/app/code/Magento/Catalog/view/base/web/template/product/link.html
@@ -4,6 +4,7 @@
* See COPYING.txt for license details.
*/
-->
-
diff --git a/app/code/Magento/Catalog/view/base/web/template/product/list/columns/image.html b/app/code/Magento/Catalog/view/base/web/template/product/list/columns/image.html
index 318a6ceed69d1..cf76762b1ff58 100644
--- a/app/code/Magento/Catalog/view/base/web/template/product/list/columns/image.html
+++ b/app/code/Magento/Catalog/view/base/web/template/product/list/columns/image.html
@@ -11,6 +11,7 @@
class="product-image-photo"
attr="src: getImageUrl($row()),
alt: getLabel($row()),
+ title: getLabel($row()),
width: getResizedImageWidth($row()),
height: getResizedImageHeight($row())"/>
diff --git a/app/code/Magento/Catalog/view/base/web/template/product/list/columns/image_with_borders.html b/app/code/Magento/Catalog/view/base/web/template/product/list/columns/image_with_borders.html
index 2baa9926df5f1..68b7f4e386896 100644
--- a/app/code/Magento/Catalog/view/base/web/template/product/list/columns/image_with_borders.html
+++ b/app/code/Magento/Catalog/view/base/web/template/product/list/columns/image_with_borders.html
@@ -14,7 +14,7 @@
data-bind="style: {'padding-bottom': getHeight($row())/getWidth($row()) * 100 + '%'}">
+ alt: getLabel($row()), title: getLabel($row())}" />
diff --git a/app/code/Magento/Catalog/view/frontend/layout/catalog_category_view.xml b/app/code/Magento/Catalog/view/frontend/layout/catalog_category_view.xml
index 00c71d6163153..5fee1d8447e5a 100644
--- a/app/code/Magento/Catalog/view/frontend/layout/catalog_category_view.xml
+++ b/app/code/Magento/Catalog/view/frontend/layout/catalog_category_view.xml
@@ -19,7 +19,7 @@
-
+
itemscope itemtype="http://schema.org/Product"
+
+
+
+ Magento\Catalog\ViewModel\Product\Breadcrumbs
+
+
+
@@ -130,7 +137,7 @@
-
+
getDescription
description
diff --git a/app/code/Magento/Catalog/view/frontend/layout/checkout_cart_item_renderers.xml b/app/code/Magento/Catalog/view/frontend/layout/checkout_cart_item_renderers.xml
index 6ba591a2c21ee..adf64f13fdb9c 100644
--- a/app/code/Magento/Catalog/view/frontend/layout/checkout_cart_item_renderers.xml
+++ b/app/code/Magento/Catalog/view/frontend/layout/checkout_cart_item_renderers.xml
@@ -8,7 +8,7 @@
-
+
diff --git a/app/code/Magento/Catalog/view/frontend/requirejs-config.js b/app/code/Magento/Catalog/view/frontend/requirejs-config.js
index b588600b7db87..55df18afeb024 100644
--- a/app/code/Magento/Catalog/view/frontend/requirejs-config.js
+++ b/app/code/Magento/Catalog/view/frontend/requirejs-config.js
@@ -18,5 +18,12 @@ var config = {
priceUtils: 'Magento_Catalog/js/price-utils',
catalogAddToCart: 'Magento_Catalog/js/catalog-add-to-cart'
}
+ },
+ config: {
+ mixins: {
+ 'Magento_Theme/js/view/breadcrumbs': {
+ 'Magento_Catalog/js/product/breadcrumbs': true
+ }
+ }
}
};
diff --git a/app/code/Magento/Catalog/view/frontend/templates/messages/addCompareSuccessMessage.phtml b/app/code/Magento/Catalog/view/frontend/templates/messages/addCompareSuccessMessage.phtml
new file mode 100644
index 0000000000000..5f44c42e17c57
--- /dev/null
+++ b/app/code/Magento/Catalog/view/frontend/templates/messages/addCompareSuccessMessage.phtml
@@ -0,0 +1,14 @@
+
+= $block->escapeHtml(__(
+ 'You added product %1 to the comparison list .',
+ $block->getData('product_name'),
+ $block->getData('compare_list_url')),
+ ['a']
+);
diff --git a/app/code/Magento/Catalog/view/frontend/templates/navigation/left.phtml b/app/code/Magento/Catalog/view/frontend/templates/navigation/left.phtml
index fa70e15135578..01820361744e0 100644
--- a/app/code/Magento/Catalog/view/frontend/templates/navigation/left.phtml
+++ b/app/code/Magento/Catalog/view/frontend/templates/navigation/left.phtml
@@ -28,6 +28,7 @@
= /* @escapeNotVerified */ __('Category') ?>
+
getIsActive()): ?>
diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/breadcrumbs.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/breadcrumbs.phtml
new file mode 100644
index 0000000000000..c54ce5340851c
--- /dev/null
+++ b/app/code/Magento/Catalog/view/frontend/templates/product/breadcrumbs.phtml
@@ -0,0 +1,10 @@
+getData('viewModel');
+?>
+
diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/compare/list.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/compare/list.phtml
index 6a2dd1f27d4a9..a70ceadd12244 100644
--- a/app/code/Magento/Catalog/view/frontend/templates/product/compare/list.phtml
+++ b/app/code/Magento/Catalog/view/frontend/templates/product/compare/list.phtml
@@ -8,9 +8,9 @@
/* @var $block \Magento\Catalog\Block\Product\Compare\ListCompare */
?>
-getItems()->getSize() ?>
-
-
+getItems()->getSize() ?>
+
+
= /* @escapeNotVerified */ __('Print This Page') ?>
@@ -24,14 +24,14 @@
= /* @escapeNotVerified */ __('Compare Products') ?>
-
- getItems() as $_item): ?>
-
+
+ getItems() as $item): ?>
+
= /* @escapeNotVerified */ __('Remove Product') ?>
-
- helper('Magento\Catalog\Helper\Product\Compare');?>
-
+ helper(\Magento\Catalog\Helper\Product\Compare::class);?>
+
= /* @escapeNotVerified */ __('Remove Product') ?>
@@ -41,44 +41,44 @@
-
- helper('Magento\Catalog\Helper\Output'); ?>
-
- getItems() as $_item): ?>
-
+
+ helper(\Magento\Catalog\Helper\Output::class); ?>
+
+ getItems() as $item): ?>
+
= /* @escapeNotVerified */ __('Product') ?>
-
- = $block->getImage($_item, 'product_comparison_list')->toHtml() ?>
+
+ = $block->getImage($item, 'product_comparison_list')->toHtml() ?>
-
- = /* @escapeNotVerified */ $_helper->productAttribute($_item, $_item->getName(), 'name') ?>
+
+ = /* @escapeNotVerified */ $helper->productAttribute($item, $item->getName(), 'name') ?>
- = $block->getReviewsSummaryHtml($_item, 'short') ?>
- = /* @escapeNotVerified */ $block->getProductPrice($_item, '-compare-list-top') ?>
-
+ = $block->getReviewsSummaryHtml($item, 'short') ?>
+ = /* @escapeNotVerified */ $block->getProductPrice($item, '-compare-list-top') ?>
+
- isSaleable()): ?>
-
- getIsSalable()): ?>
+ getIsSalable()): ?>
= /* @escapeNotVerified */ __('In stock') ?>
= /* @escapeNotVerified */ __('Out of stock') ?>
- helper('Magento\Wishlist\Helper\Data')->isAllow()) : ?>
+ helper(\Magento\Wishlist\Helper\Data::class)->isAllow()) : ?>
@@ -89,39 +89,41 @@
- getAttributes() as $_attribute): ?>
-
-
- getItems() as $_item): ?>
-
-
-
- = $block->escapeHtml($_attribute->getStoreLabel() ? $_attribute->getStoreLabel() : __($_attribute->getFrontendLabel())) ?>
-
-
-
-
-
- getAttributeCode()) {
- case "price": ?>
- getProductPrice(
- $_item,
- '-compare-list-' . $_attribute->getCode()
- )
- ?>
-
- getImage($_item, 'product_small_image')->toHtml(); ?>
+ getAttributes() as $attribute): ?>
+
+ hasAttributeValueForProducts($attribute)): ?>
+
+ getItems() as $item): ?>
+
+
+
+ = $block->escapeHtml($attribute->getStoreLabel() ? $attribute->getStoreLabel() : __($attribute->getFrontendLabel())) ?>
+
+
+
+
+
+ getAttributeCode()) {
+ case "price": ?>
+ getProductPrice(
+ $item,
+ '-compare-list-' . $attribute->getCode()
+ )
+ ?>
+
+ getImage($item, 'product_small_image')->toHtml(); ?>
+
+ = /* @escapeNotVerified */ $helper->productAttribute($item, $block->getProductAttributeValue($item, $attribute), $attribute->getAttributeCode()) ?>
- = /* @escapeNotVerified */ $_helper->productAttribute($_item, $block->getProductAttributeValue($_item, $_attribute), $_attribute->getAttributeCode()) ?>
-
-
-
-
-
+ } ?>
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/list.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/list.phtml
index baf849a295e0b..41dbdd86f75f9 100644
--- a/app/code/Magento/Catalog/view/frontend/templates/product/list.phtml
+++ b/app/code/Magento/Catalog/view/frontend/templates/product/list.phtml
@@ -47,7 +47,7 @@ $_helper = $this->helper('Magento\Catalog\Helper\Output');
= /* @escapeNotVerified */ ($iterator++ == 1) ? '
' : ' ' ?>
-
+
getImage($_product, $image);
if ($pos != null) {
@@ -78,7 +78,7 @@ $_helper = $this->helper('Magento\Catalog\Helper\Output');
>
isSaleable()): ?>
getAddToCartPostParams($_product); ?>
-