diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000000000..37cfad78c270b --- /dev/null +++ b/.editorconfig @@ -0,0 +1,12 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_size = 4 +indent_style = space +insert_final_newline = true +trim_trailing_whitespace = true + +[*.md] +trim_trailing_whitespace = false diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000000000..00763eb56e0c6 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,206 @@ +/app/code/Magento/AdminNotification/ @paliarush +/app/code/Magento/Backend/ @paliarush +/app/code/Magento/User/ @paliarush +/lib/internal/Magento/Framework/App/ @buskamuza +/lib/internal/Magento/Framework/Controller/ @buskamuza +/lib/internal/Magento/Framework/Flag/ @buskamuza +/lib/internal/Magento/Framework/HTTP/ @buskamuza +/lib/internal/Magento/Framework/Logger/ @buskamuza +/lib/internal/Magento/Framework/Message/ @buskamuza +/lib/internal/Magento/Framework/Notification/ @buskamuza +/lib/internal/Magento/Framework/Session/ @buskamuza +/lib/internal/Magento/Framework/Url/ @buskamuza +/app/code/Magento/Cms/ @melnikovi +/app/code/Magento/CmsUrlRewrite/ @melnikovi +/app/code/Magento/Contact/ @melnikovi +/app/code/Magento/Email/ @melnikovi +/app/code/Magento/Variable/ @melnikovi +/app/code/Magento/Widget/ @melnikovi +/lib/internal/Magento/Framework/Cache/ @kokoc +/app/code/Magento/CacheInvalidate/ @kokoc +/app/code/Magento/CatalogInventory/ @tariqjawed83 @maghamed +/app/code/Magento/Bundle/ @akaplya +/app/code/Magento/BundleImportExport/ @akaplya +/app/code/Magento/Catalog/ @akaplya +/app/code/Magento/CatalogAnalytics/ @akaplya +/app/code/Magento/CatalogImportExport/ @akaplya +/app/code/Magento/CatalogSearch/ @kokoc +/app/code/Magento/CatalogUrlRewrite/ @akaplya +/app/code/Magento/ConfigurableImportExport/ @akaplya +/app/code/Magento/ConfigurableProduct/ @akaplya +/app/code/Magento/Downloadable/ @akaplya +/app/code/Magento/DownloadableImportExport/ @akaplya +/app/code/Magento/GroupedImportExport/ @akaplya +/app/code/Magento/GroupedProduct/ @akaplya +/app/code/Magento/LayeredNavigation/ @kokoc +/app/code/Magento/ProductVideo/ @akaplya +/app/code/Magento/Review/ @akaplya +/app/code/Magento/Swatches/ @akaplya +/app/code/Magento/SwatchesLayeredNavigation/ @kokoc +/app/code/Magento/Checkout/ @paliarush +/app/code/Magento/CheckoutAgreements/ @paliarush +/app/code/Magento/GiftMessage/ @paliarush +/app/code/Magento/InstantPurchase/ @paliarush +/app/code/Magento/Multishipping/ @joni-jones +/app/code/Magento/Quote/ @paliarush +/app/code/Magento/QuoteAnalytics/ @paliarush +/lib/internal/Magento/Framework/Code/ @joni-jones +/lib/internal/Magento/Framework/Reflection/ @joni-jones +/lib/internal/Magento/Framework/Component/ @buskamuza +/app/code/Magento/Version/ @buskamuza +/lib/internal/Magento/Framework/Config/ @paliarush +/app/code/Magento/Config/ @paliarush +/lib/internal/Magento/Framework/Console/ @joni-jones +/lib/internal/Magento/Framework/Process/ @joni-jones +/lib/internal/Magento/Framework/Shell/ @joni-jones +/app/code/Magento/Cookie/ @kokoc +/lib/internal/Magento/Framework/Crontab/ @tariqjawed83 @buskamuza +/app/code/Magento/Cron/ @tariqjawed83 @buskamuza +/app/code/Magento/Customer/ @paliarush +/app/code/Magento/CustomerAnalytics/ @paliarush +/app/code/Magento/CustomerImportExport/ @paliarush +/app/code/Magento/Persistent/ @paliarush +/app/code/Magento/Wishlist/ @paliarush +/lib/internal/Magento/Framework/DB/ @akaplya +/lib/internal/Magento/Framework/EntityManager/ @akaplya +/lib/internal/Magento/Framework/Indexer/ @akaplya +/lib/internal/Magento/Framework/Model/ @akaplya +/lib/internal/Magento/Framework/Mview/ @akaplya +/app/code/Magento/Eav/ @akaplya +/app/code/Magento/Indexer/ @akaplya +/lib/internal/Magento/Framework/Archive/ @joni-jones +/lib/internal/Magento/Framework/Convert/ @joni-jones +/lib/internal/Magento/Framework/Data/ @joni-jones +/lib/internal/Magento/Framework/DomDocument/ @joni-jones +/lib/internal/Magento/Framework/Json/ @joni-jones +/lib/internal/Magento/Framework/Math/ @joni-jones +/lib/internal/Magento/Framework/Parse/ @joni-jones +/lib/internal/Magento/Framework/Serialize/ @joni-jones +/lib/internal/Magento/Framework/Simplexml/ @joni-jones +/lib/internal/Magento/Framework/Stdlib/ @joni-jones +/lib/internal/Magento/Framework/Unserialize/ @joni-jones +/lib/internal/Magento/Framework/Xml/ @joni-jones +/lib/internal/Magento/Framework/XsltProcessor/ @joni-jones +/app/code/Magento/Deploy/ @kandy @buskamuza +/lib/internal/Magento/Framework/Profiler/ @kandy +/app/code/Magento/Developer/ @buskamuza +/app/code/Magento/Directory/ @buskamuza +/lib/internal/Magento/Framework/Exception/ @paliarush +/lib/internal/Magento/Framework/File/ @buskamuza +/lib/internal/Magento/Framework/Filesystem/ @buskamuza +/lib/internal/Magento/Framework/System/ @buskamuza +/lib/internal/Magento/Framework/Css/ @DrewML +/lib/internal/Magento/Framework/Option/ @DrewML +/lib/internal/Magento/Framework/RequireJs/ @DrewML +/lib/internal/Magento/Framework/View/ @melnikovi +/dev/tests/js/ @DrewML +/app/code/Magento/RequireJs/ @DrewML +/app/code/Magento/Theme/ @melnikovi +/app/code/Magento/Ui/ @melnikovi +/lib/internal/Magento/Framework/Intl/ @melnikovi +/lib/internal/Magento/Framework/Locale/ @melnikovi +/lib/internal/Magento/Framework/Phrase/ @melnikovi +/lib/internal/Magento/Framework/Translate/ @melnikovi +/app/code/Magento/Translation/ @melnikovi +/app/code/Magento/ImportExport/ @akaplya +/app/code/Magento/GoogleAdwords/ @buskamuza @melnikovi +/app/code/Magento/Newsletter/ @buskamuza @melnikovi +/app/code/Magento/ProductAlert/ @buskamuza @melnikovi +/app/code/Magento/Rss/ @buskamuza @melnikovi +/app/code/Magento/SendFriend/ @buskamuza @melnikovi +/app/code/Magento/Marketplace/ @buskamuza +/app/code/Magento/MediaStorage/ @buskamuza +/lib/internal/Magento/Framework/Amqp/ @tariqjawed83 @paliarush +/lib/internal/Magento/Framework/Bulk/ @tariqjawed83 @paliarush +/lib/internal/Magento/Framework/Communication/ @tariqjawed83 @paliarush +/app/code/Magento/Amqp/ @tariqjawed83 @paliarush +/app/code/Magento/AsynchronousOperations/ @tariqjawed83 @paliarush +/app/code/Magento/MessageQueue/ @tariqjawed83 @paliarush +/app/code/Magento/MysqlMq/ @tariqjawed83 @paliarush +/app/code/Magento/Sales/ @joni-jones +/app/code/Magento/SalesInventory/ @joni-jones +/app/code/Magento/SalesSequence/ @joni-jones +/lib/internal/Magento/Framework/Event/ @buskamuza @kandy +/lib/internal/Magento/Framework/Interception/ @buskamuza @kandy +/lib/internal/Magento/Framework/ObjectManager/ @buskamuza @kandy +/app/code/Magento/PageCache/ @Andrey @kokoc @paliarush +/app/code/Magento/Authorizenet/ @joni-jones +/app/code/Magento/Braintree/ @joni-jones +/app/code/Magento/OfflinePayments/ @joni-jones +/app/code/Magento/Payment/ @joni-jones +/app/code/Magento/Paypal/ @joni-jones +/app/code/Magento/Signifyd/ @joni-jones +/app/code/Magento/Vault/ @joni-jones +/lib/internal/Magento/Framework/Pricing/ @akaplya +/app/code/Magento/AdvancedPricingImportExport/ @akaplya +/app/code/Magento/CurrencySymbol/ @akaplya +/app/code/Magento/Msrp/ @akaplya +/app/code/Magento/Tax/ @akaplya +/app/code/Magento/TaxImportExport/ @akaplya +/app/code/Magento/Weee/ @akaplya +/app/code/Magento/CatalogRule/ @kokoc +/app/code/Magento/CatalogRuleConfigurable/ @kokoc +/app/code/Magento/CatalogWidget/ @kokoc +/app/code/Magento/Rule/ @kokoc +/app/code/Magento/SalesRule/ @akaplya +/app/code/Magento/ReleaseNotification/ @paliarush +/app/code/Magento/Analytics/ @tariqjawed83 @buskamuza +/app/code/Magento/GoogleAnalytics/ @tariqjawed83 @buskamuza +/app/code/Magento/NewRelicReporting/ @tariqjawed83 @buskamuza +/app/code/Magento/Reports/ @tariqjawed83 @buskamuza +/app/code/Magento/ReviewAnalytics/ @tariqjawed83 @buskamuza +/app/code/Magento/SalesAnalytics/ @tariqjawed83 @buskamuza +/app/code/Magento/WishlistAnalytics/ @tariqjawed83 @buskamuza +/app/code/Magento/GoogleOptimizer/ @paliarush +/app/code/Magento/Robots/ @paliarush +/app/code/Magento/Sitemap/ @paliarush +/lib/internal/Magento/Framework/Search/ @kokoc +/app/code/Magento/AdvancedSearch/ @kokoc +/app/code/Magento/Elasticsearch/ @kokoc +/app/code/Magento/Search/ @kokoc +/lib/internal/Magento/Framework/Acl/ @kokoc +/lib/internal/Magento/Framework/Authorization/ @kokoc +/lib/internal/Magento/Framework/Encryption/ @kokoc +/app/code/Magento/Authorization/ @kokoc +/app/code/Magento/Captcha/ @kokoc +/app/code/Magento/EncryptionKey/ @kokoc +/app/code/Magento/Security/ @kokoc +/lib/internal/Magento/Framework/Autoload/ @buskamuza +/lib/internal/Magento/Framework/Backup/ @buskamuza +/lib/internal/Magento/Framework/Composer/ @buskamuza +/lib/internal/Magento/Framework/Setup/ @buskamuza +/app/code/Magento/Backup/ @buskamuza +/setup/ @buskamuza +/app/code/Magento/Dhl/ @joni-jones +/app/code/Magento/Fedex/ @joni-jones +/app/code/Magento/OfflineShipping/ @joni-jones +/app/code/Magento/Shipping/ @joni-jones +/app/code/Magento/Ups/ @joni-jones +/app/code/Magento/Usps/ @joni-jones +/app/code/Magento/Store/ @akaplya +/lib/internal/Magento/Framework/TestFramework/ @paliarush +/dev/tests/integration/framework/ @buskamuza +/dev/tests/setup-integration/framework/ @paliarush +/dev/tests/static/framework/ @paliarush +/dev/tests/unit/ @paliarush +/dev/tests/api-functional/ @paliarush +/app/code/Magento/UrlRewrite/ @kokoc +/lib/internal/Magento/Framework/Image/ @buskamuza +/lib/internal/Magento/Framework/Mail/ @melnikovi +/lib/internal/Magento/Framework/Filter/ @melnikovi +/lib/internal/Magento/Framework/Validation/ @melnikovi +/lib/internal/Magento/Framework/Validator/ @melnikovi +/lib/internal/Magento/Framework/Api/ @paliarush +/lib/internal/Magento/Framework/GraphQL/ @paliarush +/lib/internal/Magento/Framework/Oauth/ @paliarush +/lib/internal/Magento/Framework/Webapi/ @paliarush +/app/code/Magento/GraphQL/ @paliarush +/app/code/Magento/Integration/ @paliarush +/app/code/Magento/Swagger/ @paliarush +/app/code/Magento/Webapi/ @paliarush +/app/code/Magento/WebapiSecurity/ @paliarush + +composer.json @buskamuza +*.js @DrewML +.htaccess* @akaplya +nginx.conf* @akaplya diff --git a/app/code/Magento/AdminNotification/README.md b/app/code/Magento/AdminNotification/README.md index a60ae33ff2d86..c22fef1bca97d 100644 --- a/app/code/Magento/AdminNotification/README.md +++ b/app/code/Magento/AdminNotification/README.md @@ -4,15 +4,25 @@ The Magento_AdminNotification module provides the ability to alert administrator ## Installation details +The Magento_AdminNotification module creates the following tables in the database: +- `adminnotification_inbox` +- `admin_system_messages` + Before disabling or uninstalling this module, note that the Magento_Indexer module depends on this module. -For information about module installation in Magento 2, see [Enable or disable modules](http://devdocs.magento.com/guides/v2.3/install-gde/install/cli/install-cli-subcommands-enable.html). +For information about module installation in Magento 2, see [Enable or disable modules](https://devdocs.magento.com/guides/v2.3/install-gde/install/cli/install-cli-subcommands-enable.html). + +## Extensibility + +Extension developers can interact with the Magento_AdminNotification module. For more information about the Magento extension mechanism, see [Magento plug-ins](https://devdocs.magento.com/guides/v2.3/extension-dev-guide/plugins.html). + +[The Magento dependency injection mechanism](https://devdocs.magento.com/guides/v2.3/extension-dev-guide/depend-inj.html) enables you to override the functionality of the Magento_AdminNotification module. ### Events This module observes the following events: - - `controller_action_predispatch` event in `Magento\AdminNotification\Observer\PredispatchAdminActionControllerObserver` + - `controller_action_predispatch` event in `Magento\AdminNotification\Observer\PredispatchAdminActionControllerObserver` file. ### Layouts @@ -21,10 +31,10 @@ This module introduces the following layouts and layout handles in the `view/adm - `adminhtml_notification_index` - `adminhtml_notification_block` -For more information about layouts in Magento 2, see the [Layout documentation](http://devdocs.magento.com/guides/v2.3/frontend-dev-guide/layouts/layout-overview.html). +For more information about layouts in Magento 2, see the [Layout documentation](https://devdocs.magento.com/guides/v2.3/frontend-dev-guide/layouts/layout-overview.html). ### UI components You can extend admin notifications using the `view/adminhtml/ui_component/notification_area.xml` configuration file. -For information about UI components in Magento 2, see [Overview of UI components](http://devdocs.magento.com/guides/v2.3/ui_comp_guide/bk-ui_comps.html). +For information about UI components in Magento 2, see [Overview of UI components](https://devdocs.magento.com/guides/v2.3/ui_comp_guide/bk-ui_comps.html). diff --git a/app/code/Magento/AdvancedPricingImportExport/Model/Import/AdvancedPricing.php b/app/code/Magento/AdvancedPricingImportExport/Model/Import/AdvancedPricing.php index 591e648547d61..974397226c56c 100644 --- a/app/code/Magento/AdvancedPricingImportExport/Model/Import/AdvancedPricing.php +++ b/app/code/Magento/AdvancedPricingImportExport/Model/Import/AdvancedPricing.php @@ -50,7 +50,13 @@ class AdvancedPricing extends \Magento\ImportExport\Model\Import\Entity\Abstract const VALIDATOR_WEBSITE = 'validator_website'; - const VALIDATOR_TEAR_PRICE = 'validator_tear_price'; + /** + * @deprecated + * @see VALIDATOR_TIER_PRICE + */ + private const VALIDATOR_TEAR_PRICE = 'validator_tier_price'; + + private const VALIDATOR_TIER_PRICE = 'validator_tier_price'; /** * Validation failure message template definitions. @@ -221,7 +227,7 @@ public function __construct( $this->_catalogProductEntity = $this->_resourceFactory->create()->getTable('catalog_product_entity'); $this->_oldSkus = $this->retrieveOldSkus(); $this->_validators[self::VALIDATOR_WEBSITE] = $websiteValidator; - $this->_validators[self::VALIDATOR_TEAR_PRICE] = $tierPriceValidator; + $this->_validators[self::VALIDATOR_TIER_PRICE] = $tierPriceValidator; $this->errorAggregator = $errorAggregator; foreach (array_merge($this->errorMessageTemplates, $this->_messageTemplates) as $errorCode => $message) { @@ -536,7 +542,7 @@ protected function getWebSiteId($websiteCode) */ protected function getCustomerGroupId($customerGroup) { - $customerGroups = $this->_getValidator(self::VALIDATOR_TEAR_PRICE)->getCustomerGroups(); + $customerGroups = $this->_getValidator(self::VALIDATOR_TIER_PRICE)->getCustomerGroups(); return $customerGroup == self::VALUE_ALL_GROUPS ? 0 : $customerGroups[$customerGroup]; } diff --git a/app/code/Magento/AdvancedPricingImportExport/README.md b/app/code/Magento/AdvancedPricingImportExport/README.md index 581a3a91c8a1a..8f33781932f9e 100644 --- a/app/code/Magento/AdvancedPricingImportExport/README.md +++ b/app/code/Magento/AdvancedPricingImportExport/README.md @@ -1 +1,9 @@ -The Magento_AdvancedPricingImportExport module handles the import and export of the advanced pricing. \ No newline at end of file +# Magento_AdvancedPricingImportExport module + +The Magento_AdvancedPricingImportExport module handles the import and export of the advanced pricing. + +## Extensibility + +Extension developers can interact with the Magento_AdvancedPricingImportExport module. For more information about the Magento extension mechanism, see [Magento plug-ins](https://devdocs.magento.com/guides/v2.3/extension-dev-guide/plugins.html). + +[The Magento dependency injection mechanism](https://devdocs.magento.com/guides/v2.3/extension-dev-guide/depend-inj.html) enables you to override the functionality of the Magento_AdvancedPricingImportExport module. \ No newline at end of file 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 2aa59c1cfb758..fd968a2682d58 100644 --- a/app/code/Magento/AdvancedPricingImportExport/Test/Unit/Model/Import/AdvancedPricingTest.php +++ b/app/code/Magento/AdvancedPricingImportExport/Test/Unit/Model/Import/AdvancedPricingTest.php @@ -15,6 +15,9 @@ */ class AdvancedPricingTest extends \Magento\ImportExport\Test\Unit\Model\Import\AbstractImportTestCase { + /** + * DB Table data + */ const TABLE_NAME = 'tableName'; const LINK_FIELD = 'linkField'; @@ -54,7 +57,7 @@ class AdvancedPricingTest extends \Magento\ImportExport\Test\Unit\Model\Import\A protected $websiteValidator; /** - * @var AdvancedPricing\Validator\TearPrice |\PHPUnit_Framework_MockObject_MockObject + * @var AdvancedPricing\Validator\TierPrice |\PHPUnit_Framework_MockObject_MockObject */ protected $tierPriceValidator; diff --git a/app/code/Magento/AdvancedSearch/README.md b/app/code/Magento/AdvancedSearch/README.md index 763d1917a8546..999721fca1c8f 100644 --- a/app/code/Magento/AdvancedSearch/README.md +++ b/app/code/Magento/AdvancedSearch/README.md @@ -1 +1,38 @@ -AdvancedSearch module introduces advanced search functionality and provides interfaces that allow to implement this functionality by 3rd party search engines +# Magento_AdvancedSearch module +The Magento_AdvancedSearch module introduces advanced search functionality and provides interfaces that allow third-party search engines to implement this functionality. + +## Installation details + +Before disabling or uninstalling this module, note that the following modules depends on this module: +- Magento_Elasticsearch +- Magento_Elasticsearch6 + +For information about module installation in Magento 2, see [Enable or disable modules](https://devdocs.magento.com/guides/v2.3/install-gde/install/cli/install-cli-subcommands-enable.html). + +## Extensibility + +Extension developers can interact with the Magento_AdvancedSearch module. For more information about the Magento extension mechanism, see [Magento plug-ins](https://devdocs.magento.com/guides/v2.3/extension-dev-guide/plugins.html). + +[The Magento dependency injection mechanism](https://devdocs.magento.com/guides/v2.3/extension-dev-guide/depend-inj.html) enables you to override the functionality of the Magento_AdvancedSearch module. + +### Events + +This module observes the following event: + + - `catalogsearch_query_save_after` in the `Magento\AdvancedSearch\Model\Recommendations\SaveSearchQueryRelationsObserver` file. + +For information about an event in Magento 2, see [Events and observers](http://devdocs.magento.com/guides/v2.3/extension-dev-guide/events-and-observers.html#events). + +### Layouts + +The module interacts with the following layout handles in the `view/adminhtml/layout` directory: + +- `catalog_search_block` +- `catalog_search_edit` +- `catalog_search_relatedgrid` + +The module interacts with the following layout handles in the `view/frontend/layout` directory: + +- `catalogsearch_result_index` + +For more information about layouts in Magento 2, see the [Layout documentation](https://devdocs.magento.com/guides/v2.3/frontend-dev-guide/layouts/layout-overview.html). \ No newline at end of file diff --git a/app/code/Magento/Amqp/README.md b/app/code/Magento/Amqp/README.md index a21624031d619..3613421aacbdc 100644 --- a/app/code/Magento/Amqp/README.md +++ b/app/code/Magento/Amqp/README.md @@ -1,3 +1,9 @@ -# Amqp +# Magento_Amqp module -**Amqp** provides functionality to publish/consume messages with Amqp. +Magento_Amqp module provides functionality to publish/consume messages with the Advanced Message Queuing Protocol (AMQP). + +## Extensibility + +Extension developers can interact with the Magento_Amqp module. For more information about the Magento extension mechanism, see [Magento plug-ins](https://devdocs.magento.com/guides/v2.3/extension-dev-guide/plugins.html). + +[The Magento dependency injection mechanism](https://devdocs.magento.com/guides/v2.3/extension-dev-guide/depend-inj.html) enables you to override the functionality of the Magento_Amqp module. \ No newline at end of file diff --git a/app/code/Magento/AmqpStore/Plugin/Framework/Amqp/Bulk/Exchange.php b/app/code/Magento/AmqpStore/Plugin/Framework/Amqp/Bulk/Exchange.php index c5db1f5300c29..37960a64d3861 100644 --- a/app/code/Magento/AmqpStore/Plugin/Framework/Amqp/Bulk/Exchange.php +++ b/app/code/Magento/AmqpStore/Plugin/Framework/Amqp/Bulk/Exchange.php @@ -91,7 +91,6 @@ public function beforeEnqueue(SubjectExchange $subject, $topic, array $envelopes if ($headers instanceof AMQPTable) { try { $headers->set('store_id', $storeId); - // phpcs:ignore Magento2.Exceptions.ThrowCatch } catch (AMQPInvalidArgumentException $ea) { $errorMessage = sprintf("Can't set storeId to amqp message. Error %s.", $ea->getMessage()); $this->logger->error($errorMessage); diff --git a/app/code/Magento/AmqpStore/README.md b/app/code/Magento/AmqpStore/README.md index 0f84c8ff3276e..88459a495401f 100644 --- a/app/code/Magento/AmqpStore/README.md +++ b/app/code/Magento/AmqpStore/README.md @@ -1,3 +1,9 @@ -# Amqp Store +# Magento_AmqpStore module -**AmqpStore** provides ability to specify store before publish messages with Amqp. +The Magento_AmqpStore module provides the ability to specify a store before publishing messages with the Advanced Message Queuing Protocol (AMQP). + +## Extensibility + +Extension developers can interact with the Magento_AmqpStore module using plugins. For more information about the Magento extension mechanism, see [Magento plug-ins](https://devdocs.magento.com/guides/v2.3/extension-dev-guide/plugins.html). + +[The Magento dependency injection mechanism](https://devdocs.magento.com/guides/v2.3/extension-dev-guide/depend-inj.html) enables you to override the functionality of the Magento_AmqpStore module. diff --git a/app/code/Magento/AsynchronousOperations/README.md b/app/code/Magento/AsynchronousOperations/README.md index fb7d53df1b81c..896fddedab5fa 100644 --- a/app/code/Magento/AsynchronousOperations/README.md +++ b/app/code/Magento/AsynchronousOperations/README.md @@ -1 +1,48 @@ - This component is designed to provide response for client who launched the bulk operation as soon as possible and postpone handling of operations moving them to background handler. \ No newline at end of file +# Magento_AsynchronousOperations module + +This component is designed to provide a response for a client that launched the bulk operation as soon as possible and postpone handling of operations moving them to the background handler. + +## Installation details + +The Magento_AsynchronousOperations module creates the following tables in the database: + +- `magento_bulk` +- `magento_operation` +- `magento_acknowledged_bulk` + +Before disabling or uninstalling this module, note that the following modules depends on this module: + +- Magento_WebapiAsync + +For information about module installation in Magento 2, see [Enable or disable modules](https://devdocs.magento.com/guides/v2.3/install-gde/install/cli/install-cli-subcommands-enable.html). + +## Extensibility + +Extension developers can interact with the Magento_AsynchronousOperations module. For more information about the Magento extension mechanism, see [Magento plug-ins](https://devdocs.magento.com/guides/v2.3/extension-dev-guide/plugins.html). + +[The Magento dependency injection mechanism](https://devdocs.magento.com/guides/v2.3/extension-dev-guide/depend-inj.html) enables you to override the functionality of the Magento_AsynchronousOperations module. + +### Layouts + +This module introduces the following layouts and layout handles in the `view/adminhtml/layout` directory: + +- `bulk_bulk_details` +- `bulk_bulk_details_modal` +- `bulk_index_index` + +For more information about layouts in Magento 2, see the [Layout documentation](https://devdocs.magento.com/guides/v2.3/frontend-dev-guide/layouts/layout-overview.html). + +### UI components + +You can extend Magento_AsynchronousOperations module using the following configuration files in the `view/adminhtml/ui_component/` directory: + +- `bulk_details_form` +- `bulk_details_form_modal` +- `bulk_listing` +- `failed_operation_listing` +- `failed_operation_modal_listing` +- `notification_area` +- `retriable_operation_listing` +- `retriable_operation_modal_listing` + +For information about UI components in Magento 2, see [Overview of UI components](https://devdocs.magento.com/guides/v2.3/ui_comp_guide/bk-ui_comps.html). diff --git a/app/code/Magento/Authorization/README.md b/app/code/Magento/Authorization/README.md index f74f5b6c70340..4b8afb4fef0d3 100644 --- a/app/code/Magento/Authorization/README.md +++ b/app/code/Magento/Authorization/README.md @@ -1,4 +1,20 @@ -# Authorization +# Magento_Authorization module -**Authorization** enables management of access control list roles and -rules in the application. +The Magento_Authorization module enables management of access control list roles and rules in the application. + +## Installation details + +The Magento_AdminNotification module creates the following tables in the database: + +- `authorization_role` +- `authorization_rule` + +Before disabling or uninstalling this module, note that the Magento_GraphQl module depends on this module. + +For information about module installation in Magento 2, see [Enable or disable modules](https://devdocs.magento.com/guides/v2.3/install-gde/install/cli/install-cli-subcommands-enable.html). + +## Extensibility + +Extension developers can interact with the Magento_Authorization module. For more information about the Magento extension mechanism, see [Magento plug-ins](https://devdocs.magento.com/guides/v2.3/extension-dev-guide/plugins.html). + +[The Magento dependency injection mechanism](https://devdocs.magento.com/guides/v2.3/extension-dev-guide/depend-inj.html) enables you to override the functionality of the Magento_Authorization module. diff --git a/app/code/Magento/Authorizenet/README.md b/app/code/Magento/Authorizenet/README.md index 380161d8b264e..62598837bee6d 100644 --- a/app/code/Magento/Authorizenet/README.md +++ b/app/code/Magento/Authorizenet/README.md @@ -1 +1,42 @@ +# Magento_Authorizenet module + The Magento_Authorizenet module implements the integration with the Authorize.Net payment gateway and makes the latter available as a payment method in Magento. + +## Extensibility + +Extension developers can interact with the Magento_Authorizenet module. For more information about the Magento extension mechanism, see [Magento plug-ins](https://devdocs.magento.com/guides/v2.3/extension-dev-guide/plugins.html). + +[The Magento dependency injection mechanism](https://devdocs.magento.com/guides/v2.3/extension-dev-guide/depend-inj.html) enables you to override the functionality of the Magento_Authorizenet module. + +### Events + +This module dispatches the following events: + + - `checkout_directpost_placeOrder` event in the `\Magento\Authorizenet\Controller\Directpost\Payment\Place::placeCheckoutOrder()` method. Parameters: + - `result` is a data object (`\Magento\Framework\DataObject` class). + - `action` is a controller object (`\Magento\Authorizenet\Controller\Directpost\Payment\Place`). + + - `order_cancel_after` event in the `\Magento\Authorizenet\Model\Directpost::declineOrder()` method. Parameters: + - `order` is an order object (`\Magento\Sales\Model\Order` class). + + +This module observes the following events: + + - `checkout_submit_all_after` event in the `Magento\Authorizenet\Observer\SaveOrderAfterSubmitObserver` file. + - `checkout_directpost_placeOrder` event in the `Magento\Authorizenet\Observer\AddFieldsToResponseObserver` file. + +For information about events in Magento 2, see [Events and observers](http://devdocs.magento.com/guides/v2.3/extension-dev-guide/events-and-observers.html#events). + +### Layouts + +This module introduces the following layouts and layout handles in the `view/adminhtml/layout` directory: + +- `adminhtml_authorizenet_directpost_payment_redirect` + +This module introduces the following layouts and layout handles in the `view/frontend/layout` directory: + +- `authorizenet_directpost_payment_backendresponse` +- `authorizenet_directpost_payment_redirect` +- `authorizenet_directpost_payment_response` + +For more information about layouts in Magento 2, see the [Layout documentation](https://devdocs.magento.com/guides/v2.3/frontend-dev-guide/layouts/layout-overview.html). diff --git a/app/code/Magento/AuthorizenetAcceptjs/Gateway/Command/RefundTransactionStrategyCommand.php b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Command/RefundTransactionStrategyCommand.php index 53a1f13fa8786..3cdfcf23ba607 100644 --- a/app/code/Magento/AuthorizenetAcceptjs/Gateway/Command/RefundTransactionStrategyCommand.php +++ b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Command/RefundTransactionStrategyCommand.php @@ -59,6 +59,7 @@ public function execute(array $commandSubject): void * @param array $commandSubject * @return string * @throws CommandException + * @throws \Magento\Framework\Exception\NotFoundException */ private function getCommand(array $commandSubject): string { @@ -66,12 +67,37 @@ private function getCommand(array $commandSubject): string ->execute($commandSubject) ->get(); - if ($details['transaction']['transactionStatus'] === 'capturedPendingSettlement') { + if ($this->canVoid($details, $commandSubject)) { return self::VOID; - } elseif ($details['transaction']['transactionStatus'] !== 'settledSuccessfully') { + } + + if ($details['transaction']['transactionStatus'] !== 'settledSuccessfully') { throw new CommandException(__('This transaction cannot be refunded with its current status.')); } return self::REFUND; } + + /** + * Checks if void command can be performed. + * + * @param array $details + * @param array $commandSubject + * @return bool + * @throws CommandException + */ + private function canVoid(array $details, array $commandSubject) :bool + { + if ($details['transaction']['transactionStatus'] === 'capturedPendingSettlement') { + if ((float) $details['transaction']['authAmount'] !== (float) $commandSubject['amount']) { + throw new CommandException( + __('The transaction has not been settled, a partial refund is not yet available.') + ); + } + + return true; + } + + return false; + } } diff --git a/app/code/Magento/AuthorizenetAcceptjs/Gateway/Response/ClosePartialTransactionHandler.php b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Response/ClosePartialTransactionHandler.php new file mode 100644 index 0000000000000..fd8af3d28c4d4 --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Response/ClosePartialTransactionHandler.php @@ -0,0 +1,27 @@ +getCreditmemo()->getInvoice()->canRefund(); + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/Gateway/Response/CloseTransactionHandler.php b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Response/CloseTransactionHandler.php index acbde62bacd77..fa9bf55462111 100644 --- a/app/code/Magento/AuthorizenetAcceptjs/Gateway/Response/CloseTransactionHandler.php +++ b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Response/CloseTransactionHandler.php @@ -47,7 +47,19 @@ public function handle(array $handlingSubject, array $response): void if ($payment instanceof Payment) { $payment->setIsTransactionClosed($this->closeTransaction); - $payment->setShouldCloseParentTransaction(true); + $payment->setShouldCloseParentTransaction($this->shouldCloseParentTransaction($payment)); } } + + /** + * Whether parent transaction should be closed. + * + * @param Payment $payment + * @return bool + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function shouldCloseParentTransaction(Payment $payment) + { + return true; + } } diff --git a/app/code/Magento/AuthorizenetAcceptjs/README.md b/app/code/Magento/AuthorizenetAcceptjs/README.md index b066f8a2d7509..b507f97a5a223 100644 --- a/app/code/Magento/AuthorizenetAcceptjs/README.md +++ b/app/code/Magento/AuthorizenetAcceptjs/README.md @@ -1 +1,29 @@ +# Magento_AuthorizenetAcceptjs module + The Magento_AuthorizenetAcceptjs module implements the integration with the Authorize.Net payment gateway and makes the latter available as a payment method in Magento. + +## Installation details + +Before disabling or uninstalling this module, note that the `Magento_AuthorizenetCardinal` module depends on this module. + +For information about module installation in Magento 2, see [Enable or disable modules](https://devdocs.magento.com/guides/v2.3/install-gde/install/cli/install-cli-subcommands-enable.html). + +## Structure + +`Gateway/` - the directory that contains payment gateway command interfaces and service classes. + +For information about typical file structure of a module in Magento 2, see [Module file structure](http://devdocs.magento.com/guides/v2.3/extension-dev-guide/build/module-file-structure.html#module-file-structure). + +## Extensibility + +Extension developers can interact with the Magento_AuthorizenetAcceptjs module. For more information about the Magento extension mechanism, see [Magento plug-ins](https://devdocs.magento.com/guides/v2.3/extension-dev-guide/plugins.html). + +[The Magento dependency injection mechanism](https://devdocs.magento.com/guides/v2.3/extension-dev-guide/depend-inj.html) enables you to override the functionality of the Magento_AuthorizenetAcceptjs module. + +### Events + +This module observes the following events: + +- `payment_method_assign_data_authorizenet_acceptjs` event in the `Magento\AuthorizenetAcceptjs\Observer\DataAssignObserver` file. + +For information about an event in Magento 2, see [Events and observers](https://devdocs.magento.com/guides/v2.3/extension-dev-guide/events-and-observers.html#events). diff --git a/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Command/RefundTransactionStrategyCommandTest.php b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Command/RefundTransactionStrategyCommandTest.php index df6d89d7bc585..79477b06e0e6c 100644 --- a/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Command/RefundTransactionStrategyCommandTest.php +++ b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Command/RefundTransactionStrategyCommandTest.php @@ -62,20 +62,75 @@ public function testCommandWillVoidWhenTransactionIsPendingSettlement() ->method('execute'); $this->commandPoolMock->method('get') - ->willReturnMap([ - ['get_transaction_details', $this->transactionDetailsCommandMock], - ['void', $this->commandMock] - ]); + ->willReturnMap( + [ + [ + 'get_transaction_details', + $this->transactionDetailsCommandMock + ], + [ + 'void', + $this->commandMock + ] + ] + ); $this->transactionResultMock->method('get') - ->willReturn([ - 'transaction' => [ - 'transactionStatus' => 'capturedPendingSettlement' + ->willReturn( + [ + 'transaction' => [ + 'transactionStatus' => 'capturedPendingSettlement', + 'authAmount' => '20.19', + ] ] - ]); + ); $buildSubject = [ - 'foo' => '123' + 'foo' => '123', + 'amount' => '20.19', + ]; + + $this->transactionDetailsCommandMock->expects($this->once()) + ->method('execute') + ->with($buildSubject) + ->willReturn($this->transactionResultMock); + + $this->command->execute($buildSubject); + } + + /** + * @expectedException \Magento\Payment\Gateway\Command\CommandException + * @expectedExceptionMessage The transaction has not been settled, a partial refund is not yet available. + */ + public function testCommandWillThrowExceptionWhenVoidTransactionIsPartial() + { + // Assert command is executed + $this->commandMock->expects($this->never()) + ->method('execute'); + + $this->commandPoolMock->method('get') + ->willReturnMap( + [ + [ + 'get_transaction_details', + $this->transactionDetailsCommandMock + ], + ] + ); + + $this->transactionResultMock->method('get') + ->willReturn( + [ + 'transaction' => [ + 'transactionStatus' => 'capturedPendingSettlement', + 'authAmount' => '20.19', + ] + ] + ); + + $buildSubject = [ + 'foo' => '123', + 'amount' => '10.19', ]; $this->transactionDetailsCommandMock->expects($this->once()) @@ -93,17 +148,27 @@ public function testCommandWillRefundWhenTransactionIsSettled() ->method('execute'); $this->commandPoolMock->method('get') - ->willReturnMap([ - ['get_transaction_details', $this->transactionDetailsCommandMock], - ['refund_settled', $this->commandMock] - ]); + ->willReturnMap( + [ + [ + 'get_transaction_details', + $this->transactionDetailsCommandMock + ], + [ + 'refund_settled', + $this->commandMock + ] + ] + ); $this->transactionResultMock->method('get') - ->willReturn([ - 'transaction' => [ - 'transactionStatus' => 'settledSuccessfully' + ->willReturn( + [ + 'transaction' => [ + 'transactionStatus' => 'settledSuccessfully' + ] ] - ]); + ); $buildSubject = [ 'foo' => '123' @@ -128,16 +193,23 @@ public function testCommandWillThrowExceptionWhenTransactionIsInInvalidState() ->method('execute'); $this->commandPoolMock->method('get') - ->willReturnMap([ - ['get_transaction_details', $this->transactionDetailsCommandMock], - ]); + ->willReturnMap( + [ + [ + 'get_transaction_details', + $this->transactionDetailsCommandMock + ], + ] + ); $this->transactionResultMock->method('get') - ->willReturn([ - 'transaction' => [ - 'transactionStatus' => 'somethingIsWrong' + ->willReturn( + [ + 'transaction' => [ + 'transactionStatus' => 'somethingIsWrong' + ] ] - ]); + ); $buildSubject = [ 'foo' => '123' diff --git a/app/code/Magento/AuthorizenetAcceptjs/etc/config.xml b/app/code/Magento/AuthorizenetAcceptjs/etc/config.xml index 7324421d3c14b..6fdbb98a78f8b 100644 --- a/app/code/Magento/AuthorizenetAcceptjs/etc/config.xml +++ b/app/code/Magento/AuthorizenetAcceptjs/etc/config.xml @@ -24,6 +24,7 @@ 0 1 1 + 1 1 1 1 diff --git a/app/code/Magento/AuthorizenetAcceptjs/etc/di.xml b/app/code/Magento/AuthorizenetAcceptjs/etc/di.xml index 145bcf22fd912..1bff19e15a65f 100644 --- a/app/code/Magento/AuthorizenetAcceptjs/etc/di.xml +++ b/app/code/Magento/AuthorizenetAcceptjs/etc/di.xml @@ -206,8 +206,7 @@ Magento\AuthorizenetAcceptjs\Gateway\Response\TransactionIdHandler - Magento\AuthorizenetAcceptjs\Gateway\Response\CloseParentTransactionHandler - Magento\AuthorizenetAcceptjs\Gateway\Response\CloseTransactionHandler + Magento\AuthorizenetAcceptjs\Gateway\Response\ClosePartialTransactionHandler diff --git a/app/code/Magento/AuthorizenetAcceptjs/i18n/en_US.csv b/app/code/Magento/AuthorizenetAcceptjs/i18n/en_US.csv index da518301652f4..3c5b677c88cc8 100644 --- a/app/code/Magento/AuthorizenetAcceptjs/i18n/en_US.csv +++ b/app/code/Magento/AuthorizenetAcceptjs/i18n/en_US.csv @@ -19,3 +19,4 @@ Authorize.net,Authorize.net "ccLast4","Last 4 Digits of Card" "There was an error while trying to process the refund.","There was an error while trying to process the refund." "This transaction cannot be refunded with its current status.","This transaction cannot be refunded with its current status." +"The transaction has not been settled, a partial refund is not yet available.","The transaction has not been settled, a partial refund is not yet available." diff --git a/app/code/Magento/AuthorizenetCardinal/README.md b/app/code/Magento/AuthorizenetCardinal/README.md index 2324f680bafc9..0bd63130471bd 100644 --- a/app/code/Magento/AuthorizenetCardinal/README.md +++ b/app/code/Magento/AuthorizenetCardinal/README.md @@ -1 +1,23 @@ -The AuthorizenetCardinal module provides a possibility to enable 3-D Secure 2.0 support for AuthorizenetAcceptjs payment integration. \ No newline at end of file +# Magento_AuthorizenetCardinal module + +Use the Magento_AuthorizenetCardinal module to enable 3D Secure 2.0 support for AuthorizenetAcceptjs payment integrations. + +## Structure + +`Gateway/` - the directory that contains payment gateway command interfaces and service classes. + +For information about typical file structure of a module in Magento 2, see [Module file structure](https://devdocs.magento.com/guides/v2.3/extension-dev-guide/build/module-file-structure.html#module-file-structure). + +## Extensibility + +Extension developers can interact with the Magento_AuthorizenetCardinal module. For more information about the Magento extension mechanism, see [Magento plug-ins](https://devdocs.magento.com/guides/v2.3/extension-dev-guide/plugins.html). + +[The Magento dependency injection mechanism](https://devdocs.magento.com/guides/v2.3/extension-dev-guide/depend-inj.html) enables you to override the functionality of the Magento_AuthorizenetCardinal module. + +### Events + +This module observes the following events: + +- `payment_method_assign_data_authorizenet_acceptjs` event in the `Magento\AuthorizenetCardinal\Observer\DataAssignObserver` file. + +For information about an event in Magento 2, see [Events and observers](https://devdocs.magento.com/guides/v2.3/extension-dev-guide/events-and-observers.html#events). diff --git a/app/code/Magento/AuthorizenetGraphQl/Model/AuthorizenetDataProvider.php b/app/code/Magento/AuthorizenetGraphQl/Model/AuthorizenetDataProvider.php index 207d21994308f..704f0af85da06 100644 --- a/app/code/Magento/AuthorizenetGraphQl/Model/AuthorizenetDataProvider.php +++ b/app/code/Magento/AuthorizenetGraphQl/Model/AuthorizenetDataProvider.php @@ -9,6 +9,7 @@ use Magento\QuoteGraphQl\Model\Cart\Payment\AdditionalDataProviderInterface; use Magento\Framework\Stdlib\ArrayManager; +use Magento\Framework\GraphQl\Exception\GraphQlInputException; /** * SetPaymentMethod additional data provider model for Authorizenet payment method @@ -36,10 +37,32 @@ public function __construct( * * @param array $data * @return array + * @throws GraphQlInputException */ public function getData(array $data): array { - $additionalData = $this->arrayManager->get(static::PATH_ADDITIONAL_DATA, $data) ?? []; + if (!isset($data[self::PATH_ADDITIONAL_DATA])) { + throw new GraphQlInputException( + __('Required parameter "authorizenet_acceptjs" for "payment_method" is missing.') + ); + } + if (!isset($data[self::PATH_ADDITIONAL_DATA]['opaque_data_descriptor'])) { + throw new GraphQlInputException( + __('Required parameter "opaque_data_descriptor" for "authorizenet_acceptjs" is missing.') + ); + } + if (!isset($data[self::PATH_ADDITIONAL_DATA]['opaque_data_value'])) { + throw new GraphQlInputException( + __('Required parameter "opaque_data_value" for "authorizenet_acceptjs" is missing.') + ); + } + if (!isset($data[self::PATH_ADDITIONAL_DATA]['cc_last_4'])) { + throw new GraphQlInputException( + __('Required parameter "cc_last_4" for "authorizenet_acceptjs" is missing.') + ); + } + + $additionalData = $this->arrayManager->get(static::PATH_ADDITIONAL_DATA, $data); foreach ($additionalData as $key => $value) { $additionalData[$this->convertSnakeCaseToCamelCase($key)] = $value; unset($additionalData[$key]); diff --git a/app/code/Magento/AuthorizenetGraphQl/README.md b/app/code/Magento/AuthorizenetGraphQl/README.md index 8b920e569341f..2af2b6a1024af 100644 --- a/app/code/Magento/AuthorizenetGraphQl/README.md +++ b/app/code/Magento/AuthorizenetGraphQl/README.md @@ -1,3 +1,9 @@ -# AuthorizenetGraphQl +# Magento_AuthorizenetGraphQl module - **AuthorizenetGraphQl** defines the data types needed to pass payment information data from the client to Magento. +The Magento_AuthorizenetGraphQl module defines the data types needed to pass payment information data from the client to Magento. + +## Extensibility + +Extension developers can interact with the Magento_AuthorizenetGraphQl module. For more information about the Magento extension mechanism, see [Magento plug-ins](https://devdocs.magento.com/guides/v2.3/extension-dev-guide/plugins.html). + +[The Magento dependency injection mechanism](https://devdocs.magento.com/guides/v2.3/extension-dev-guide/depend-inj.html) enables you to override the functionality of the Magento_AuthorizenetGraphQl module. diff --git a/app/code/Magento/Backend/Block/System/Store/Edit/Form/Store.php b/app/code/Magento/Backend/Block/System/Store/Edit/Form/Store.php index dfbf60e1524ff..fee756b5d66be 100644 --- a/app/code/Magento/Backend/Block/System/Store/Edit/Form/Store.php +++ b/app/code/Magento/Backend/Block/System/Store/Edit/Form/Store.php @@ -3,6 +3,9 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +declare(strict_types=1); + namespace Magento\Backend\Block\System\Store\Edit\Form; /** @@ -129,6 +132,7 @@ protected function _prepareStoreFieldset(\Magento\Framework\Data\Form $form) 'label' => __('Sort Order'), 'value' => $storeModel->getSortOrder(), 'required' => false, + 'class' => 'validate-number validate-zero-or-greater', 'disabled' => $storeModel->isReadOnly() ] ); diff --git a/app/code/Magento/Backend/Block/System/Store/Edit/Form/Website.php b/app/code/Magento/Backend/Block/System/Store/Edit/Form/Website.php index ec4e2cd8b444c..545620d99c4c5 100644 --- a/app/code/Magento/Backend/Block/System/Store/Edit/Form/Website.php +++ b/app/code/Magento/Backend/Block/System/Store/Edit/Form/Website.php @@ -3,6 +3,9 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +declare(strict_types=1); + namespace Magento\Backend\Block\System\Store\Edit\Form; /** @@ -85,6 +88,7 @@ protected function _prepareStoreFieldset(\Magento\Framework\Data\Form $form) 'label' => __('Sort Order'), 'value' => $websiteModel->getSortOrder(), 'required' => false, + 'class' => 'validate-number validate-zero-or-greater', 'disabled' => $websiteModel->isReadOnly() ] ); 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 8e0fce2b16cc9..28200323a3aec 100644 --- a/app/code/Magento/Backend/Block/Widget/Grid/Massaction/Extended.php +++ b/app/code/Magento/Backend/Block/Widget/Grid/Massaction/Extended.php @@ -3,9 +3,11 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - namespace Magento\Backend\Block\Widget\Grid\Massaction; +use Magento\Framework\Data\Collection\AbstractDb; +use Magento\Framework\DB\Select; + /** * Grid widget massaction block * @@ -13,7 +15,7 @@ * @deprecated 100.2.0 in favour of UI component implementation * @method \Magento\Quote\Model\Quote setHideFormElement(boolean $value) Hide Form element to prevent IE errors * @method boolean getHideFormElement() - * @author Magento Core Team + * @author Magento Core Team * @TODO MAGETWO-31510: Remove deprecated class * @since 100.0.2 */ @@ -156,7 +158,7 @@ public function getItemsJson() */ public function getCount() { - return sizeof($this->_items); + return count($this->_items); } /** @@ -248,6 +250,8 @@ public function getApplyButtonHtml() } /** + * Get mass action javascript code + * * @return string */ public function getJavaScript() @@ -264,6 +268,8 @@ public function getJavaScript() } /** + * Get grid ids in JSON format + * * @return string */ public function getGridIdsJson() @@ -281,15 +287,24 @@ public function getGridIdsJson() $massActionIdField = $this->getParentBlock()->getMassactionIdField(); } - $gridIds = $allIdsCollection->setPageSize(0)->getColumnValues($massActionIdField); - - if (!empty($gridIds)) { - return join(",", $gridIds); + if ($allIdsCollection instanceof AbstractDb) { + $idsSelect = clone $allIdsCollection->getSelect(); + $idsSelect->reset(Select::ORDER); + $idsSelect->reset(Select::LIMIT_COUNT); + $idsSelect->reset(Select::LIMIT_OFFSET); + $idsSelect->reset(Select::COLUMNS); + $idsSelect->columns($massActionIdField); + $idList = $allIdsCollection->getConnection()->fetchCol($idsSelect); + } else { + $idList = $allIdsCollection->setPageSize(0)->getColumnValues($massActionIdField); } - return ''; + + return implode(',', $idList); } /** + * Retrieve massaction block js object name + * * @return string */ public function getHtmlId() diff --git a/app/code/Magento/Backend/etc/adminhtml/system.xml b/app/code/Magento/Backend/etc/adminhtml/system.xml index c762dbf58de62..343ecc0ee3d58 100644 --- a/app/code/Magento/Backend/etc/adminhtml/system.xml +++ b/app/code/Magento/Backend/etc/adminhtml/system.xml @@ -23,7 +23,7 @@ Advanced advanced - Magento_Backend::advanced + Magento_Config::advanced Disable Modules Output Magento\Config\Block\System\Config\Form\Fieldset\Modules\DisableOutput @@ -145,7 +145,9 @@ Allow Symlinks Magento\Config\Model\Config\Source\Yesno - Warning! Enabling this feature is not recommended on production environments because it represents a potential security risk. + + Warning! Enabling this feature is not recommended on production environments because it represents a potential security risk.]]> + Minify Html @@ -435,7 +437,7 @@ Admin Session Lifetime (seconds) Please enter at least 60 and at most 31536000 (one year). Magento\Backend\Model\Config\SessionLifetime\BackendModel - validate-digits + validate-digits validate-digits-range digits-range-60-31536000 @@ -536,7 +538,7 @@ Enable HTTP Strict Transport Security (HSTS) Magento\Config\Model\Config\Source\Yesno Magento\Config\Model\Config\Backend\Secure - HTTP Strict Transport Security page for details.]]> + HTTP Strict Transport Security page for details.]]> 1 1 diff --git a/app/code/Magento/Backend/i18n/en_US.csv b/app/code/Magento/Backend/i18n/en_US.csv index bfedd56b14313..51fe8bfe542a2 100644 --- a/app/code/Magento/Backend/i18n/en_US.csv +++ b/app/code/Magento/Backend/i18n/en_US.csv @@ -258,7 +258,7 @@ Minute,Minute "To use this website you must first enable JavaScript in your browser.","To use this website you must first enable JavaScript in your browser." "This is only a demo store. You can browse and place orders, but nothing will be processed.","This is only a demo store. You can browse and place orders, but nothing will be processed." "Report an Issue","Report an Issue" -"Store View:","Store View:" +"Scope:","Scope:" "Stores Configuration","Stores Configuration" "Please confirm scope switching. All data that hasn't been saved will be lost.","Please confirm scope switching. All data that hasn't been saved will be lost." "Additional Cache Management","Additional Cache Management" @@ -333,7 +333,7 @@ Debug,Debug "Add Block Names to Hints","Add Block Names to Hints" "Template Settings","Template Settings" "Allow Symlinks","Allow Symlinks" -"Warning! Enabling this feature is not recommended on production environments because it represents a potential security risk.","Warning! Enabling this feature is not recommended on production environments because it represents a potential security risk." +"Warning! Enabling this feature is not recommended on production environments because it represents a potential security risk.","Warning! Enabling this feature is not recommended on production environments because it represents a potential security risk." "Minify Html","Minify Html" "Minification is not applied in developer mode.","Minification is not applied in developer mode." "Translate Inline","Translate Inline" 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 be309423c48d2..3cc2594a75801 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 @@ -21,10 +21,10 @@ = $block->escapeHtml(__('Return to ')) ?> - + = $block->escapeHtml(__('previous page')) ?>= $block->escapeHtml(__('.')) ?> - + = $block->escapeHtml(__('previous page')) ?>= $block->escapeHtml(__('.')) ?> diff --git a/app/code/Magento/Backend/view/adminhtml/templates/store/switcher.phtml b/app/code/Magento/Backend/view/adminhtml/templates/store/switcher.phtml index da18bc183759b..df9323a7276df 100644 --- a/app/code/Magento/Backend/view/adminhtml/templates/store/switcher.phtml +++ b/app/code/Magento/Backend/view/adminhtml/templates/store/switcher.phtml @@ -9,7 +9,7 @@ getWebsites()) : ?> - = $block->escapeHtml(__('Store View:')) ?> + = $block->escapeHtml(__('Scope:')) ?> getColumns() !== null ? count($block->getColumns()) : 0; canDisplayContainer()) : ?> - = $block->getLayout()->getMessagesBlock()->getGroupedHtml() ?> + = $block->getLayout()->getMessagesBlock()->getGroupedHtml() ?> diff --git a/app/code/Magento/Backup/Controller/Adminhtml/Index/Rollback.php b/app/code/Magento/Backup/Controller/Adminhtml/Index/Rollback.php index 7f450e7e313cc..91152bf51f026 100644 --- a/app/code/Magento/Backup/Controller/Adminhtml/Index/Rollback.php +++ b/app/code/Magento/Backup/Controller/Adminhtml/Index/Rollback.php @@ -127,7 +127,6 @@ public function execute() $adminSession->destroy(); $response->setRedirectUrl($this->getUrl('*')); - // phpcs:disable Magento2.Exceptions.ThrowCatch } catch (\Magento\Framework\Backup\Exception\CantLoadSnapshot $e) { $errorMsg = __('We can\'t find the backup file.'); } catch (\Magento\Framework\Backup\Exception\FtpConnectionFailed $e) { diff --git a/app/code/Magento/Braintree/Model/Paypal/Helper/OrderPlace.php b/app/code/Magento/Braintree/Model/Paypal/Helper/OrderPlace.php index 314404c79939c..f448cd4c19785 100644 --- a/app/code/Magento/Braintree/Model/Paypal/Helper/OrderPlace.php +++ b/app/code/Magento/Braintree/Model/Paypal/Helper/OrderPlace.php @@ -19,6 +19,7 @@ /** * Class OrderPlace * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) */ class OrderPlace extends AbstractHelper { @@ -79,9 +80,10 @@ public function __construct( public function execute(Quote $quote, array $agreement) { if (!$this->agreementsValidator->isValid($agreement)) { - throw new LocalizedException(__( + $errorMsg = __( "The order wasn't placed. First, agree to the terms and conditions, then try placing your order again." - )); + ); + throw new LocalizedException($errorMsg); } if ($this->getCheckoutMethod($quote) === Onepage::METHOD_GUEST) { @@ -91,12 +93,7 @@ public function execute(Quote $quote, array $agreement) $this->disabledQuoteAddressValidation($quote); $quote->collectTotals(); - try { - $this->cartManagement->placeOrder($quote->getId()); - } catch (\Exception $e) { - $this->orderCancellationService->execute($quote->getReservedOrderId()); - throw $e; - } + $this->cartManagement->placeOrder($quote->getId()); } /** diff --git a/app/code/Magento/Braintree/Observer/AddPaypalShortcuts.php b/app/code/Magento/Braintree/Observer/AddPaypalShortcuts.php index 132f4b92a3e2d..ea16745a24117 100644 --- a/app/code/Magento/Braintree/Observer/AddPaypalShortcuts.php +++ b/app/code/Magento/Braintree/Observer/AddPaypalShortcuts.php @@ -15,9 +15,27 @@ class AddPaypalShortcuts implements ObserverInterface { /** - * Block class + * Alias for mini-cart block. */ - const PAYPAL_SHORTCUT_BLOCK = \Magento\Braintree\Block\Paypal\Button::class; + private const PAYPAL_MINICART_ALIAS = 'mini_cart'; + + /** + * Alias for shopping cart page. + */ + private const PAYPAL_SHOPPINGCART_ALIAS = 'shopping_cart'; + + /** + * @var string[] + */ + private $buttonBlocks; + + /** + * @param string[] $buttonBlocks + */ + public function __construct(array $buttonBlocks = []) + { + $this->buttonBlocks = $buttonBlocks; + } /** * Add Braintree PayPal shortcut buttons @@ -35,7 +53,13 @@ public function execute(Observer $observer) /** @var ShortcutButtons $shortcutButtons */ $shortcutButtons = $observer->getEvent()->getContainer(); - $shortcut = $shortcutButtons->getLayout()->createBlock(self::PAYPAL_SHORTCUT_BLOCK); + if ($observer->getData('is_shopping_cart')) { + $shortcut = $shortcutButtons->getLayout() + ->createBlock($this->buttonBlocks[self::PAYPAL_SHOPPINGCART_ALIAS]); + } else { + $shortcut = $shortcutButtons->getLayout() + ->createBlock($this->buttonBlocks[self::PAYPAL_MINICART_ALIAS]); + } $shortcutButtons->addShortcut($shortcut); } diff --git a/app/code/Magento/Braintree/Plugin/DisableQuoteAddressValidation.php b/app/code/Magento/Braintree/Plugin/DisableQuoteAddressValidation.php new file mode 100644 index 0000000000000..03117a4e977a1 --- /dev/null +++ b/app/code/Magento/Braintree/Plugin/DisableQuoteAddressValidation.php @@ -0,0 +1,43 @@ +getPayment()->getMethod() == 'braintree_paypal' && + $quote->getCheckoutMethod() == CartManagementInterface::METHOD_GUEST) { + $billingAddress = $quote->getBillingAddress(); + $billingAddress->setShouldIgnoreValidation(true); + $quote->setBillingAddress($billingAddress); + } + return [$quote, $orderData]; + } +} diff --git a/app/code/Magento/Braintree/Plugin/OrderCancellation.php b/app/code/Magento/Braintree/Plugin/OrderCancellation.php index 90c72839d9777..4754e89e67ada 100644 --- a/app/code/Magento/Braintree/Plugin/OrderCancellation.php +++ b/app/code/Magento/Braintree/Plugin/OrderCancellation.php @@ -72,7 +72,9 @@ public function aroundPlaceOrder( ]; if (in_array($payment->getMethod(), $paymentCodes)) { $incrementId = $quote->getReservedOrderId(); - $this->orderCancellationService->execute($incrementId); + if ($incrementId) { + $this->orderCancellationService->execute($incrementId); + } } throw $e; diff --git a/app/code/Magento/Braintree/Test/Unit/Observer/AddPaypalShortcutsTest.php b/app/code/Magento/Braintree/Test/Unit/Observer/AddPaypalShortcutsTest.php index 377b4d3c650ae..7bf1722e317ef 100644 --- a/app/code/Magento/Braintree/Test/Unit/Observer/AddPaypalShortcutsTest.php +++ b/app/code/Magento/Braintree/Test/Unit/Observer/AddPaypalShortcutsTest.php @@ -19,9 +19,17 @@ */ class AddPaypalShortcutsTest extends \PHPUnit\Framework\TestCase { + /** + * Tests PayPal shortcuts observer. + */ public function testExecute() { - $addPaypalShortcuts = new AddPaypalShortcuts(); + $addPaypalShortcuts = new AddPaypalShortcuts( + [ + 'mini_cart' => 'Minicart-block', + 'shopping_cart' => 'Shoppingcart-block' + ] + ); /** @var Observer|\PHPUnit_Framework_MockObject_MockObject $observerMock */ $observerMock = $this->getMockBuilder(Observer::class) @@ -60,7 +68,7 @@ public function testExecute() $layoutMock->expects(self::once()) ->method('createBlock') - ->with(AddPaypalShortcuts::PAYPAL_SHORTCUT_BLOCK) + ->with('Minicart-block') ->willReturn($blockMock); $shortcutButtonsMock->expects(self::once()) diff --git a/app/code/Magento/Braintree/composer.json b/app/code/Magento/Braintree/composer.json index 5b5eeaf2b3dd7..58049f7bf0f93 100644 --- a/app/code/Magento/Braintree/composer.json +++ b/app/code/Magento/Braintree/composer.json @@ -22,11 +22,11 @@ "magento/module-sales": "*", "magento/module-ui": "*", "magento/module-vault": "*", - "magento/module-multishipping": "*" + "magento/module-multishipping": "*", + "magento/module-theme": "*" }, "suggest": { - "magento/module-checkout-agreements": "*", - "magento/module-theme": "*" + "magento/module-checkout-agreements": "*" }, "type": "magento2-module", "license": [ diff --git a/app/code/Magento/Braintree/etc/di.xml b/app/code/Magento/Braintree/etc/di.xml index 6f8b7d1d6c368..f6ad552fc9bef 100644 --- a/app/code/Magento/Braintree/etc/di.xml +++ b/app/code/Magento/Braintree/etc/di.xml @@ -627,4 +627,7 @@ + + + diff --git a/app/code/Magento/Braintree/etc/frontend/di.xml b/app/code/Magento/Braintree/etc/frontend/di.xml index d8d3a93b71dc3..330fa51258c44 100644 --- a/app/code/Magento/Braintree/etc/frontend/di.xml +++ b/app/code/Magento/Braintree/etc/frontend/di.xml @@ -55,6 +55,23 @@ BraintreePayPalFacade + + + + Magento_Braintree::paypal/button_shopping_cart.phtml + braintree.paypal.mini-cart + braintree-paypal-mini-cart + + + + + + + Magento\Braintree\Block\Paypal\Button + Magento\Braintree\Block\Paypal\ButtonShoppingCartVirtual + + + diff --git a/app/code/Magento/Braintree/view/frontend/templates/paypal/button.phtml b/app/code/Magento/Braintree/view/frontend/templates/paypal/button.phtml index 36eddcf5819d9..19dfed0255085 100644 --- a/app/code/Magento/Braintree/view/frontend/templates/paypal/button.phtml +++ b/app/code/Magento/Braintree/view/frontend/templates/paypal/button.phtml @@ -11,17 +11,14 @@ $id = $block->getContainerId() . random_int(0, PHP_INT_MAX); $config = [ - 'Magento_Braintree/js/paypal/button' => [ - 'id' => $id, - 'clientToken' => $block->getClientToken(), - 'displayName' => $block->getMerchantName(), - 'actionSuccess' => $block->getActionSuccess(), - 'environment' => $block->getEnvironment() - ] + 'id' => $id, + 'clientToken' => $block->getClientToken(), + 'displayName' => $block->getMerchantName(), + 'actionSuccess' => $block->getActionSuccess(), + 'environment' => $block->getEnvironment() ]; - ?> - getContainerId() . random_int(0, PHP_INT_MAX); + +$config = [ + 'Magento_Braintree/js/paypal/button_shopping_cart' => [ + 'id' => $id, + 'clientToken' => $block->getClientToken(), + 'displayName' => $block->getMerchantName(), + 'actionSuccess' => $block->getActionSuccess(), + 'environment' => $block->getEnvironment() + ] +]; + +?> + + + + diff --git a/app/code/Magento/Braintree/view/frontend/web/js/paypal/button_shopping_cart.js b/app/code/Magento/Braintree/view/frontend/web/js/paypal/button_shopping_cart.js new file mode 100644 index 0000000000000..9dd249998c152 --- /dev/null +++ b/app/code/Magento/Braintree/view/frontend/web/js/paypal/button_shopping_cart.js @@ -0,0 +1,36 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +define( + [ + 'Magento_Braintree/js/paypal/button', + 'Magento_Checkout/js/model/quote', + 'domReady!' + ], + function ( + Component, + quote + ) { + 'use strict'; + + return Component.extend({ + + /** + * Overrides amount with a value from quote. + * + * @returns {Object} + * @private + */ + getClientConfig: function (data) { + var config = this._super(data); + + if (config.amount !== quote.totals()['base_grand_total']) { + config.amount = quote.totals()['base_grand_total']; + } + + return config; + } + }); + } +); diff --git a/app/code/Magento/Braintree/view/frontend/web/js/view/payment/method-renderer/paypal.js b/app/code/Magento/Braintree/view/frontend/web/js/view/payment/method-renderer/paypal.js index c46e65ffb8abd..ae9f69c405c2b 100644 --- a/app/code/Magento/Braintree/view/frontend/web/js/view/payment/method-renderer/paypal.js +++ b/app/code/Magento/Braintree/view/frontend/web/js/view/payment/method-renderer/paypal.js @@ -187,17 +187,17 @@ define([ */ setBillingAddress: function (customer, address) { var billingAddress = { - street: [address.streetAddress], - city: address.locality, + street: [address.line1], + city: address.city, postcode: address.postalCode, - countryId: address.countryCodeAlpha2, + countryId: address.countryCode, email: customer.email, firstname: customer.firstName, lastname: customer.lastName, - telephone: customer.phone + telephone: customer.phone, + regionCode: address.state }; - billingAddress['region_code'] = address.region; billingAddress = createBillingAddress(billingAddress); quote.billingAddress(billingAddress); }, @@ -209,10 +209,12 @@ define([ beforePlaceOrder: function (payload) { this.setPaymentPayload(payload); - if ((this.isRequiredBillingAddress() || quote.billingAddress() === null) && - typeof payload.details.billingAddress !== 'undefined' - ) { - this.setBillingAddress(payload.details, payload.details.billingAddress); + if (this.isRequiredBillingAddress() || quote.billingAddress() === null) { + if (typeof payload.details.billingAddress !== 'undefined') { + this.setBillingAddress(payload.details, payload.details.billingAddress); + } else { + this.setBillingAddress(payload.details, payload.details.shippingAddress); + } } if (this.isSkipOrderReview()) { diff --git a/app/code/Magento/Bundle/Model/ResourceModel/Option/Collection.php b/app/code/Magento/Bundle/Model/ResourceModel/Option/Collection.php index e701b4cf9cc1d..5efbab94c9227 100644 --- a/app/code/Magento/Bundle/Model/ResourceModel/Option/Collection.php +++ b/app/code/Magento/Bundle/Model/ResourceModel/Option/Collection.php @@ -5,6 +5,8 @@ */ namespace Magento\Bundle\Model\ResourceModel\Option; +use Magento\Catalog\Model\Product\Attribute\Source\Status; + /** * Bundle Options Resource Collection * @api @@ -138,12 +140,10 @@ public function setPositionOrder() /** * Append selection to options - * stripBefore - indicates to reload - * appendAll - indicates do we need to filter by saleable and required custom options * * @param \Magento\Bundle\Model\ResourceModel\Selection\Collection $selectionsCollection - * @param bool $stripBefore - * @param bool $appendAll + * @param bool $stripBefore indicates to reload + * @param bool $appendAll indicates do we need to filter by saleable and required custom options * @return \Magento\Framework\DataObject[] */ public function appendSelections($selectionsCollection, $stripBefore = false, $appendAll = true) @@ -156,7 +156,9 @@ public function appendSelections($selectionsCollection, $stripBefore = false, $a foreach ($selectionsCollection->getItems() as $key => $selection) { $option = $this->getItemById($selection->getOptionId()); if ($option) { - if ($appendAll || $selection->isSalable() && !$selection->getRequiredOptions()) { + if ($appendAll || + ((int) $selection->getStatus()) === Status::STATUS_ENABLED && !$selection->getRequiredOptions() + ) { $selection->setOption($option); $option->addSelection($selection); } else { diff --git a/app/code/Magento/Bundle/Plugin/UpdatePriceInQuoteItemOptions.php b/app/code/Magento/Bundle/Plugin/UpdatePriceInQuoteItemOptions.php deleted file mode 100644 index d5aafb8ad2b61..0000000000000 --- a/app/code/Magento/Bundle/Plugin/UpdatePriceInQuoteItemOptions.php +++ /dev/null @@ -1,55 +0,0 @@ -serializer = $serializer; - } - - /** - * Update price on quote item options level - * - * @param OrigQuoteItem $subject - * @param AbstractItem $result - * @return AbstractItem - * - * @SuppressWarnings(PHPMD.UnusedFormalParameter) - */ - public function afterCalcRowTotal(OrigQuoteItem $subject, AbstractItem $result) - { - $bundleAttributes = $result->getProduct()->getCustomOption('bundle_selection_attributes'); - if ($bundleAttributes !== null) { - $actualPrice = $result->getPrice(); - $parsedValue = $this->serializer->unserialize($bundleAttributes->getValue()); - if (is_array($parsedValue) && array_key_exists('price', $parsedValue)) { - $parsedValue['price'] = $actualPrice; - } - $bundleAttributes->setValue($this->serializer->serialize($parsedValue)); - } - - return $result; - } -} diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/AdminAssociateBundleProductToWebsitesTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/AdminAssociateBundleProductToWebsitesTest.xml new file mode 100644 index 0000000000000..505a319c5c44f --- /dev/null +++ b/app/code/Magento/Bundle/Test/Mftf/Test/AdminAssociateBundleProductToWebsitesTest.xml @@ -0,0 +1,126 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/BundleProductWithTierPriceInCartTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/BundleProductWithTierPriceInCartTest.xml new file mode 100644 index 0000000000000..46c6114637af6 --- /dev/null +++ b/app/code/Magento/Bundle/Test/Mftf/Test/BundleProductWithTierPriceInCartTest.xml @@ -0,0 +1,80 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontSortBundleProductsByPriceTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontSortBundleProductsByPriceTest.xml new file mode 100644 index 0000000000000..18316e41241e4 --- /dev/null +++ b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontSortBundleProductsByPriceTest.xml @@ -0,0 +1,197 @@ + + + + + + + + + + + + + + + + + + + + + 100.00 + + + + + + + + + + + + + + + + + + + + + + + + 10.00 + + + + + + + + + + + + + + + + + + + + + + + + + 500.00 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontSpecialPriceBundleProductInCartTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontSpecialPriceBundleProductInCartTest.xml new file mode 100644 index 0000000000000..44ac68a2759f3 --- /dev/null +++ b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontSpecialPriceBundleProductInCartTest.xml @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + + + + + + true + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/code/Magento/Bundle/Ui/DataProvider/Product/Form/Modifier/BundlePrice.php b/app/code/Magento/Bundle/Ui/DataProvider/Product/Form/Modifier/BundlePrice.php index f431012dc3fa5..92326bb1521b4 100644 --- a/app/code/Magento/Bundle/Ui/DataProvider/Product/Form/Modifier/BundlePrice.php +++ b/app/code/Magento/Bundle/Ui/DataProvider/Product/Form/Modifier/BundlePrice.php @@ -39,9 +39,9 @@ public function __construct( $this->locator = $locator; $this->arrayManager = $arrayManager; } - + /** - * {@inheritdoc} + * @inheritdoc */ public function modifyMeta(array $meta) { @@ -64,7 +64,7 @@ public function modifyMeta(array $meta) $this->arrayManager->findPath( ProductAttributeInterface::CODE_PRICE, $meta, - null, + self::DEFAULT_GENERAL_PANEL . '/children', 'children' ) . static::META_CONFIG_PATH, $meta, @@ -94,7 +94,7 @@ public function modifyMeta(array $meta) } /** - * {@inheritdoc} + * @inheritdoc */ public function modifyData(array $data) { diff --git a/app/code/Magento/Bundle/etc/di.xml b/app/code/Magento/Bundle/etc/di.xml index 72155d922a25f..d0e956efee694 100644 --- a/app/code/Magento/Bundle/etc/di.xml +++ b/app/code/Magento/Bundle/etc/di.xml @@ -123,9 +123,6 @@ - - - diff --git a/app/code/Magento/Captcha/Observer/CheckContactUsFormObserver.php b/app/code/Magento/Captcha/Observer/CheckContactUsFormObserver.php index 91737c1a3d779..8c1da0e1ef104 100644 --- a/app/code/Magento/Captcha/Observer/CheckContactUsFormObserver.php +++ b/app/code/Magento/Captcha/Observer/CheckContactUsFormObserver.php @@ -9,6 +9,9 @@ use Magento\Framework\App\Request\DataPersistorInterface; use Magento\Framework\App\ObjectManager; +/** + * Class CheckContactUsFormObserver + */ class CheckContactUsFormObserver implements ObserverInterface { /** @@ -76,7 +79,7 @@ public function execute(\Magento\Framework\Event\Observer $observer) /** @var \Magento\Framework\App\Action\Action $controller */ $controller = $observer->getControllerAction(); if (!$captcha->isCorrect($this->captchaStringResolver->resolve($controller->getRequest(), $formId))) { - $this->messageManager->addError(__('Incorrect CAPTCHA.')); + $this->messageManager->addErrorMessage(__('Incorrect CAPTCHA.')); $this->getDataPersistor()->set($formId, $controller->getRequest()->getPostValue()); $this->_actionFlag->set('', \Magento\Framework\App\Action\Action::FLAG_NO_DISPATCH, true); $this->redirect->redirect($controller->getResponse(), 'contact/index/index'); diff --git a/app/code/Magento/Captcha/Observer/CheckForgotpasswordObserver.php b/app/code/Magento/Captcha/Observer/CheckForgotpasswordObserver.php index 0736c7514a568..623d11903926e 100644 --- a/app/code/Magento/Captcha/Observer/CheckForgotpasswordObserver.php +++ b/app/code/Magento/Captcha/Observer/CheckForgotpasswordObserver.php @@ -7,6 +7,9 @@ use Magento\Framework\Event\ObserverInterface; +/** + * Class CheckForgotpasswordObserver + */ class CheckForgotpasswordObserver implements ObserverInterface { /** @@ -69,7 +72,7 @@ public function execute(\Magento\Framework\Event\Observer $observer) /** @var \Magento\Framework\App\Action\Action $controller */ $controller = $observer->getControllerAction(); if (!$captchaModel->isCorrect($this->captchaStringResolver->resolve($controller->getRequest(), $formId))) { - $this->messageManager->addError(__('Incorrect CAPTCHA')); + $this->messageManager->addErrorMessage(__('Incorrect CAPTCHA')); $this->_actionFlag->set('', \Magento\Framework\App\Action\Action::FLAG_NO_DISPATCH, true); $this->redirect->redirect($controller->getResponse(), '*/*/forgotpassword'); } diff --git a/app/code/Magento/Captcha/Observer/CheckUserCreateObserver.php b/app/code/Magento/Captcha/Observer/CheckUserCreateObserver.php index 6d2ed4d1050ca..ef66116432f55 100644 --- a/app/code/Magento/Captcha/Observer/CheckUserCreateObserver.php +++ b/app/code/Magento/Captcha/Observer/CheckUserCreateObserver.php @@ -7,6 +7,11 @@ use Magento\Framework\Event\ObserverInterface; +/** + * Class CheckUserCreateObserver + * + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) + */ class CheckUserCreateObserver implements ObserverInterface { /** @@ -86,7 +91,7 @@ public function execute(\Magento\Framework\Event\Observer $observer) /** @var \Magento\Framework\App\Action\Action $controller */ $controller = $observer->getControllerAction(); if (!$captchaModel->isCorrect($this->captchaStringResolver->resolve($controller->getRequest(), $formId))) { - $this->messageManager->addError(__('Incorrect CAPTCHA')); + $this->messageManager->addErrorMessage(__('Incorrect CAPTCHA')); $this->_actionFlag->set('', \Magento\Framework\App\Action\Action::FLAG_NO_DISPATCH, true); $this->_session->setCustomerFormData($controller->getRequest()->getPostValue()); $url = $this->_urlManager->getUrl('*/*/create', ['_nosecret' => true]); diff --git a/app/code/Magento/Captcha/Observer/CheckUserEditObserver.php b/app/code/Magento/Captcha/Observer/CheckUserEditObserver.php index 9d3cd8d367093..872bbec4ffa56 100644 --- a/app/code/Magento/Captcha/Observer/CheckUserEditObserver.php +++ b/app/code/Magento/Captcha/Observer/CheckUserEditObserver.php @@ -11,13 +11,12 @@ use Magento\Framework\App\Config\ScopeConfigInterface; /** - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * Class CheckUserEditObserver + * + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) */ class CheckUserEditObserver implements ObserverInterface { - /** - * Form ID - */ const FORM_ID = 'user_edit'; /** @@ -96,7 +95,8 @@ public function __construct( * Check Captcha On Forgot Password Page * * @param \Magento\Framework\Event\Observer $observer - * @return $this + * @return $this|void + * @throws \Magento\Framework\Exception\SessionException */ public function execute(\Magento\Framework\Event\Observer $observer) { @@ -119,9 +119,9 @@ public function execute(\Magento\Framework\Event\Observer $observer) 'The account is locked. Please wait and try again or contact %1.', $this->scopeConfig->getValue('contact/email/recipient_email') ); - $this->messageManager->addError($message); + $this->messageManager->addErrorMessage($message); } - $this->messageManager->addError(__('Incorrect CAPTCHA')); + $this->messageManager->addErrorMessage(__('Incorrect CAPTCHA')); $this->actionFlag->set('', \Magento\Framework\App\Action\Action::FLAG_NO_DISPATCH, true); $this->redirect->redirect($controller->getResponse(), '*/*/edit'); } diff --git a/app/code/Magento/Captcha/Observer/CheckUserForgotPasswordBackendObserver.php b/app/code/Magento/Captcha/Observer/CheckUserForgotPasswordBackendObserver.php index 2de93dcf6b59b..e11e48a527169 100644 --- a/app/code/Magento/Captcha/Observer/CheckUserForgotPasswordBackendObserver.php +++ b/app/code/Magento/Captcha/Observer/CheckUserForgotPasswordBackendObserver.php @@ -7,6 +7,11 @@ use Magento\Framework\Event\ObserverInterface; +/** + * Class CheckUserForgotPasswordBackendObserver + * + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) + */ class CheckUserForgotPasswordBackendObserver implements ObserverInterface { /** @@ -76,7 +81,7 @@ public function execute(\Magento\Framework\Event\Observer $observer) ) { $this->_session->setEmail((string)$controller->getRequest()->getPost('email')); $this->_actionFlag->set('', \Magento\Framework\App\Action\Action::FLAG_NO_DISPATCH, true); - $this->messageManager->addError(__('Incorrect CAPTCHA')); + $this->messageManager->addErrorMessage(__('Incorrect CAPTCHA')); $controller->getResponse()->setRedirect( $controller->getUrl('*/*/forgotpassword', ['_nosecret' => true]) ); diff --git a/app/code/Magento/Captcha/Observer/CheckUserLoginObserver.php b/app/code/Magento/Captcha/Observer/CheckUserLoginObserver.php index dd4974c5d842c..27507423e77eb 100644 --- a/app/code/Magento/Captcha/Observer/CheckUserLoginObserver.php +++ b/app/code/Magento/Captcha/Observer/CheckUserLoginObserver.php @@ -6,10 +6,10 @@ namespace Magento\Captcha\Observer; +use Magento\Customer\Api\CustomerRepositoryInterface; use Magento\Customer\Model\AuthenticationInterface; use Magento\Framework\Event\ObserverInterface; use Magento\Framework\Exception\NoSuchEntityException; -use Magento\Customer\Api\CustomerRepositoryInterface; /** * Check captcha on user login page observer. @@ -64,6 +64,8 @@ class CheckUserLoginObserver implements ObserverInterface protected $authentication; /** + * CheckUserLoginObserver constructor. + * * @param \Magento\Captcha\Helper\Data $helper * @param \Magento\Framework\App\ActionFlag $actionFlag * @param \Magento\Framework\Message\ManagerInterface $messageManager @@ -125,8 +127,7 @@ private function getAuthentication() * Check captcha on user login page * * @param \Magento\Framework\Event\Observer $observer - * @throws NoSuchEntityException - * @return $this + * @return $this|void */ public function execute(\Magento\Framework\Event\Observer $observer) { @@ -143,10 +144,11 @@ public function execute(\Magento\Framework\Event\Observer $observer) try { $customer = $this->getCustomerRepository()->get($login); $this->getAuthentication()->processAuthenticationFailure($customer->getId()); + // phpcs:ignore Magento2.CodeAnalysis.EmptyBlock } catch (NoSuchEntityException $e) { //do nothing as customer existence is validated later in authenticate method } - $this->messageManager->addError(__('Incorrect CAPTCHA')); + $this->messageManager->addErrorMessage(__('Incorrect CAPTCHA')); $this->_actionFlag->set('', \Magento\Framework\App\Action\Action::FLAG_NO_DISPATCH, true); $this->_session->setUsername($login); $beforeUrl = $this->_session->getBeforeAuthUrl(); diff --git a/app/code/Magento/Captcha/Test/Unit/Observer/CheckContactUsFormObserverTest.php b/app/code/Magento/Captcha/Test/Unit/Observer/CheckContactUsFormObserverTest.php index 08f76aa74ac6d..83bfb2910f9f8 100644 --- a/app/code/Magento/Captcha/Test/Unit/Observer/CheckContactUsFormObserverTest.php +++ b/app/code/Magento/Captcha/Test/Unit/Observer/CheckContactUsFormObserverTest.php @@ -69,7 +69,10 @@ protected function setUp() $this->messageManagerMock = $this->createMock(\Magento\Framework\Message\ManagerInterface::class); $this->redirectMock = $this->createMock(\Magento\Framework\App\Response\RedirectInterface::class); $this->captchaStringResolverMock = $this->createMock(\Magento\Captcha\Observer\CaptchaStringResolver::class); - $this->sessionMock = $this->createPartialMock(\Magento\Framework\Session\SessionManager::class, ['addError']); + $this->sessionMock = $this->createPartialMock( + \Magento\Framework\Session\SessionManager::class, + ['addErrorMessage'] + ); $this->dataPersistorMock = $this->getMockBuilder(\Magento\Framework\App\Request\DataPersistorInterface::class) ->getMockForAbstractClass(); @@ -116,7 +119,7 @@ public function testCheckContactUsFormWhenCaptchaIsRequiredAndValid() $this->helperMock->expects($this->any()) ->method('getCaptcha') ->with($formId)->willReturn($this->captchaMock); - $this->sessionMock->expects($this->never())->method('addError'); + $this->sessionMock->expects($this->never())->method('addErrorMessage'); $this->checkContactUsFormObserver->execute( new \Magento\Framework\Event\Observer(['controller_action' => $controller]) @@ -163,7 +166,7 @@ public function testCheckContactUsFormRedirectsCustomerWithWarningMessageWhenCap ->method('getCaptcha') ->with($formId) ->willReturn($this->captchaMock); - $this->messageManagerMock->expects($this->once())->method('addError')->with($warningMessage); + $this->messageManagerMock->expects($this->once())->method('addErrorMessage')->with($warningMessage); $this->actionFlagMock->expects($this->once()) ->method('set') ->with('', \Magento\Framework\App\Action\Action::FLAG_NO_DISPATCH, true); diff --git a/app/code/Magento/Captcha/Test/Unit/Observer/CheckForgotpasswordObserverTest.php b/app/code/Magento/Captcha/Test/Unit/Observer/CheckForgotpasswordObserverTest.php index b05a3b2e34af0..93b58191cc334 100644 --- a/app/code/Magento/Captcha/Test/Unit/Observer/CheckForgotpasswordObserverTest.php +++ b/app/code/Magento/Captcha/Test/Unit/Observer/CheckForgotpasswordObserverTest.php @@ -138,7 +138,7 @@ public function testCheckForgotpasswordRedirects() )->will( $this->returnValue($this->_captcha) ); - $this->_messageManager->expects($this->once())->method('addError')->with($warningMessage); + $this->_messageManager->expects($this->once())->method('addErrorMessage')->with($warningMessage); $this->_actionFlag->expects( $this->once() )->method( diff --git a/app/code/Magento/Captcha/Test/Unit/Observer/CheckUserCreateObserverTest.php b/app/code/Magento/Captcha/Test/Unit/Observer/CheckUserCreateObserverTest.php index 8dc67437f4879..a57faabda99eb 100644 --- a/app/code/Magento/Captcha/Test/Unit/Observer/CheckUserCreateObserverTest.php +++ b/app/code/Magento/Captcha/Test/Unit/Observer/CheckUserCreateObserverTest.php @@ -151,7 +151,7 @@ public function testCheckUserCreateRedirectsError() )->will( $this->returnValue($this->_captcha) ); - $this->_messageManager->expects($this->once())->method('addError')->with($warningMessage); + $this->_messageManager->expects($this->once())->method('addErrorMessage')->with($warningMessage); $this->_actionFlag->expects( $this->once() )->method( diff --git a/app/code/Magento/Captcha/Test/Unit/Observer/CheckUserEditObserverTest.php b/app/code/Magento/Captcha/Test/Unit/Observer/CheckUserEditObserverTest.php index 26fd8fd928c56..0f08e5c569dfc 100644 --- a/app/code/Magento/Captcha/Test/Unit/Observer/CheckUserEditObserverTest.php +++ b/app/code/Magento/Captcha/Test/Unit/Observer/CheckUserEditObserverTest.php @@ -146,7 +146,7 @@ public function testExecute() $message = __('The account is locked. Please wait and try again or contact %1.', $email); $this->messageManagerMock->expects($this->exactly(2)) - ->method('addError') + ->method('addErrorMessage') ->withConsecutive([$message], [__('Incorrect CAPTCHA')]); $this->actionFlagMock->expects($this->once()) diff --git a/app/code/Magento/Captcha/Test/Unit/Observer/CheckUserLoginObserverTest.php b/app/code/Magento/Captcha/Test/Unit/Observer/CheckUserLoginObserverTest.php index 19dc096b9ef66..0499ec3255c51 100644 --- a/app/code/Magento/Captcha/Test/Unit/Observer/CheckUserLoginObserverTest.php +++ b/app/code/Magento/Captcha/Test/Unit/Observer/CheckUserLoginObserverTest.php @@ -145,7 +145,7 @@ public function testExecute() ->with($customerId); $this->messageManagerMock->expects($this->once()) - ->method('addError') + ->method('addErrorMessage') ->with(__('Incorrect CAPTCHA')); $this->actionFlagMock->expects($this->once()) diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Set/Main/Formset.php b/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Set/Main/Formset.php index 295ea141b8eef..484f630d4d03e 100644 --- a/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Set/Main/Formset.php +++ b/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Set/Main/Formset.php @@ -3,10 +3,17 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Catalog\Block\Adminhtml\Product\Attribute\Set\Main; use Magento\Backend\Block\Widget\Form; +/** + * Form attribute set + * + * Class \Magento\Catalog\Block\Adminhtml\Product\Attribute\Set\Main\Formset + */ class Formset extends \Magento\Backend\Block\Widget\Form\Generic { /** @@ -43,7 +50,7 @@ protected function _prepareForm() /** @var \Magento\Framework\Data\Form $form */ $form = $this->_formFactory->create(); - $fieldset = $form->addFieldset('set_name', ['legend' => __('Edit Attribute Set Name')]); + $fieldset = $form->addFieldset('set_name', ['legend' => $this->getAttributeSetLabel()]); $fieldset->addField( 'attribute_set_name', 'text', @@ -84,4 +91,18 @@ protected function _prepareForm() $form->setOnsubmit('return false;'); $this->setForm($form); } + + /** + * Get Attribute Set Label + * + * @return \Magento\Framework\Phrase + */ + private function getAttributeSetLabel() + { + if ($this->getRequest()->getParam('id', false)) { + return __('Edit Attribute Set Name'); + } + + return __('Attribute Set Information'); + } } diff --git a/app/code/Magento/Catalog/Block/Product/ImageFactory.php b/app/code/Magento/Catalog/Block/Product/ImageFactory.php index aa303af656a5b..172cd794edfb9 100644 --- a/app/code/Magento/Catalog/Block/Product/ImageFactory.php +++ b/app/code/Magento/Catalog/Block/Product/ImageFactory.php @@ -160,6 +160,8 @@ public function create(Product $product, string $imageId, array $attributes = nu ); } + $attributes = $attributes === null ? [] : $attributes; + $data = [ 'data' => [ 'template' => 'Magento_Catalog::product/image_with_borders.phtml', diff --git a/app/code/Magento/Catalog/Block/Product/View/Details.php b/app/code/Magento/Catalog/Block/Product/View/Details.php index e76c5bf201334..38925e9ae3cd7 100644 --- a/app/code/Magento/Catalog/Block/Product/View/Details.php +++ b/app/code/Magento/Catalog/Block/Product/View/Details.php @@ -37,11 +37,11 @@ public function getGroupSortedChildNames(string $groupName, string $callback): a $alias = $layout->getElementAlias($childName); $sortOrder = (int)$this->getChildData($alias, 'sort_order') ?? 0; - $childNamesSortOrder[$sortOrder] = $childName; + $childNamesSortOrder[$childName] = $sortOrder; } - ksort($childNamesSortOrder, SORT_NUMERIC); + asort($childNamesSortOrder, SORT_NUMERIC); - return $childNamesSortOrder; + return array_keys($childNamesSortOrder); } } diff --git a/app/code/Magento/Catalog/Block/Widget/Link.php b/app/code/Magento/Catalog/Block/Widget/Link.php index 85e50dbd3dc27..a25af297111d2 100644 --- a/app/code/Magento/Catalog/Block/Widget/Link.php +++ b/app/code/Magento/Catalog/Block/Widget/Link.php @@ -4,17 +4,15 @@ * See COPYING.txt for license details. */ -/** - * Widget to display catalog link - * - * @author Magento Core Team - */ namespace Magento\Catalog\Block\Widget; use Magento\CatalogUrlRewrite\Model\ProductUrlRewriteGenerator; use Magento\UrlRewrite\Model\UrlFinderInterface; use Magento\UrlRewrite\Service\V1\Data\UrlRewrite; +/** + * Render the URL of given entity + */ class Link extends \Magento\Framework\View\Element\Html\Link implements \Magento\Widget\Block\BlockInterface { /** @@ -63,10 +61,9 @@ public function __construct( /** * Prepare url using passed id path and return it - * or return false if path was not found in url rewrites. * * @throws \RuntimeException - * @return string|false + * @return string|false if path was not found in url rewrites. * @SuppressWarnings(PHPMD.NPathComplexity) */ public function getHref() @@ -93,7 +90,7 @@ public function getHref() if ($rewrite) { $href = $store->getUrl('', ['_direct' => $rewrite->getRequestPath()]); - if (strpos($href, '___store') === false) { + if ($this->addStoreCodeParam($store, $href)) { $href .= (strpos($href, '?') === false ? '?' : '&') . '___store=' . $store->getCode(); } } @@ -102,6 +99,22 @@ public function getHref() return $this->_href; } + /** + * Checks whether store code query param should be appended to the URL + * + * @param \Magento\Store\Model\Store $store + * @param string $url + * @return bool + * @throws \Magento\Framework\Exception\NoSuchEntityException + */ + private function addStoreCodeParam(\Magento\Store\Model\Store $store, string $url): bool + { + return $this->getStoreId() + && !$store->isUseStoreInUrl() + && $store->getId() !== $this->_storeManager->getStore()->getId() + && strpos($url, '___store') === false; + } + /** * Parse id_path * @@ -121,6 +134,7 @@ protected function parseIdPath($idPath) /** * Prepare label using passed text as parameter. + * * If anchor text was not specified get entity name from DB. * * @return string @@ -150,9 +164,8 @@ public function getLabel() /** * Render block HTML - * or return empty string if url can't be prepared * - * @return string + * @return string empty string if url can't be prepared */ protected function _toHtml() { diff --git a/app/code/Magento/Catalog/Model/Category.php b/app/code/Magento/Catalog/Model/Category.php index 9d6d7e41ff34e..4ddfd1f3b63a8 100644 --- a/app/code/Magento/Catalog/Model/Category.php +++ b/app/code/Magento/Catalog/Model/Category.php @@ -131,6 +131,7 @@ class Category extends \Magento\Catalog\Model\AbstractModel implements 'page_layout', 'custom_layout_update', 'custom_apply_to_products', + 'custom_use_parent_settings', ]; /** @@ -331,9 +332,11 @@ protected function getCustomAttributesCodes() * @throws \Magento\Framework\Exception\LocalizedException * @return \Magento\Catalog\Model\ResourceModel\Category * @deprecated because resource models should be used directly + * phpcs:disable Generic.CodeAnalysis.UselessOverridingMethod */ protected function _getResource() { + //phpcs:enable Generic.CodeAnalysis.UselessOverridingMethod return parent::_getResource(); } // phpcs:enable @@ -448,7 +451,9 @@ public function move($parentId, $afterCategoryId) if ($this->flatState->isFlatEnabled()) { $flatIndexer = $this->indexerRegistry->get(Indexer\Category\Flat\State::INDEXER_ID); if (!$flatIndexer->isScheduled()) { - $flatIndexer->reindexList([$this->getId(), $oldParentId, $parentId]); + $sameLevelCategories = explode(',', $this->getParentCategory()->getChildren()); + $list = array_unique(array_merge($sameLevelCategories, [$this->getId(), $oldParentId, $parentId])); + $flatIndexer->reindexList($list); } } $productIndexer = $this->indexerRegistry->get(Indexer\Category\Product::INDEXER_ID); 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 6a035a4681a54..4880214e5c6a6 100644 --- a/app/code/Magento/Catalog/Model/Category/Attribute/Backend/Image.php +++ b/app/code/Magento/Catalog/Model/Category/Attribute/Backend/Image.php @@ -121,11 +121,15 @@ public function beforeSave($object) if ($this->fileResidesOutsideCategoryDir($value)) { // use relative path for image attribute so we know it's outside of category dir when we fetch it + // phpcs:ignore Magento2.Functions.DiscouragedFunction + $value[0]['url'] = parse_url($value[0]['url'], PHP_URL_PATH); $value[0]['name'] = $value[0]['url']; } if ($imageName = $this->getUploadedImageName($value)) { - $imageName = $this->checkUniqueImageName($imageName); + if (!$this->fileResidesOutsideCategoryDir($value)) { + $imageName = $this->checkUniqueImageName($imageName); + } $object->setData($this->additionalData . $attributeName, $value); $object->setData($attributeName, $imageName); } elseif (!is_string($value)) { @@ -182,7 +186,7 @@ private function fileResidesOutsideCategoryDir($value) return false; } - return strpos($fileUrl, $baseMediaDir) === 0; + return strpos($fileUrl, $baseMediaDir) !== false; } /** diff --git a/app/code/Magento/Catalog/Model/Category/FileInfo.php b/app/code/Magento/Catalog/Model/Category/FileInfo.php index d77f472c6be90..76b6a2e75d0ea 100644 --- a/app/code/Magento/Catalog/Model/Category/FileInfo.php +++ b/app/code/Magento/Catalog/Model/Category/FileInfo.php @@ -10,6 +10,8 @@ use Magento\Framework\Filesystem; use Magento\Framework\Filesystem\Directory\WriteInterface; use Magento\Framework\Filesystem\Directory\ReadInterface; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Store\Model\StoreManagerInterface; /** * Class FileInfo @@ -48,16 +50,26 @@ class FileInfo */ private $pubDirectory; + /** + * Store manager + * + * @var \Magento\Store\Model\StoreManagerInterface + */ + private $storeManager; + /** * @param Filesystem $filesystem * @param Mime $mime + * @param StoreManagerInterface $storeManager */ public function __construct( Filesystem $filesystem, - Mime $mime + Mime $mime, + StoreManagerInterface $storeManager ) { $this->filesystem = $filesystem; $this->mime = $mime; + $this->storeManager = $storeManager; } /** @@ -152,7 +164,8 @@ public function isExist($fileName) */ private function getFilePath($fileName) { - $filePath = ltrim($fileName, '/'); + $filePath = $this->removeStorePath($fileName); + $filePath = ltrim($filePath, '/'); $mediaDirectoryRelativeSubpath = $this->getMediaDirectoryPathRelativeToBaseDirectoryPath($filePath); $isFileNameBeginsWithMediaDirectoryPath = $this->isBeginsWithMediaDirectoryPath($fileName); @@ -177,7 +190,8 @@ private function getFilePath($fileName) */ public function isBeginsWithMediaDirectoryPath($fileName) { - $filePath = ltrim($fileName, '/'); + $filePath = $this->removeStorePath($fileName); + $filePath = ltrim($filePath, '/'); $mediaDirectoryRelativeSubpath = $this->getMediaDirectoryPathRelativeToBaseDirectoryPath($filePath); $isFileNameBeginsWithMediaDirectoryPath = strpos($filePath, (string) $mediaDirectoryRelativeSubpath) === 0; @@ -185,6 +199,30 @@ public function isBeginsWithMediaDirectoryPath($fileName) return $isFileNameBeginsWithMediaDirectoryPath; } + /** + * Clean store path in case if it's exists + * + * @param string $path + * @return string + */ + private function removeStorePath(string $path): string + { + $result = $path; + try { + $storeUrl = $this->storeManager->getStore()->getBaseUrl(); + } catch (NoSuchEntityException $e) { + return $result; + } + // phpcs:ignore Magento2.Functions.DiscouragedFunction + $path = parse_url($path, PHP_URL_PATH); + // phpcs:ignore Magento2.Functions.DiscouragedFunction + $storePath = parse_url($storeUrl, PHP_URL_PATH); + $storePath = rtrim($storePath, '/'); + + $result = preg_replace('/^' . preg_quote($storePath, '/') . '/', '', $path); + return $result; + } + /** * Get media directory subpath relative to base directory path * diff --git a/app/code/Magento/Catalog/Model/Category/Tree.php b/app/code/Magento/Catalog/Model/Category/Tree.php index 0a9cb25d7b0e5..2b77e19fe3b8d 100644 --- a/app/code/Magento/Catalog/Model/Category/Tree.php +++ b/app/code/Magento/Catalog/Model/Category/Tree.php @@ -5,7 +5,16 @@ */ namespace Magento\Catalog\Model\Category; +use Magento\Catalog\Api\Data\CategoryTreeInterface; +use Magento\Catalog\Api\Data\CategoryTreeInterfaceFactory; +use Magento\Catalog\Model\Category; +use Magento\Catalog\Model\ResourceModel\Category\Collection; +use Magento\Catalog\Model\ResourceModel\Category\TreeFactory; +use Magento\Framework\App\ObjectManager; use Magento\Framework\Data\Tree\Node; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Store\Model\StoreManagerInterface; /** * Retrieve category data represented in tree structure @@ -18,54 +27,54 @@ class Tree protected $categoryTree; /** - * @var \Magento\Store\Model\StoreManagerInterface + * @var StoreManagerInterface */ protected $storeManager; /** - * @var \Magento\Catalog\Model\ResourceModel\Category\Collection + * @var Collection */ protected $categoryCollection; /** - * @var \Magento\Catalog\Api\Data\CategoryTreeInterfaceFactory + * @var CategoryTreeInterfaceFactory */ protected $treeFactory; /** - * @var \Magento\Catalog\Model\ResourceModel\Category\TreeFactory + * @var TreeFactory */ private $treeResourceFactory; /** * @param \Magento\Catalog\Model\ResourceModel\Category\Tree $categoryTree - * @param \Magento\Store\Model\StoreManagerInterface $storeManager - * @param \Magento\Catalog\Model\ResourceModel\Category\Collection $categoryCollection - * @param \Magento\Catalog\Api\Data\CategoryTreeInterfaceFactory $treeFactory - * @param \Magento\Catalog\Model\ResourceModel\Category\TreeFactory|null $treeResourceFactory + * @param StoreManagerInterface $storeManager + * @param Collection $categoryCollection + * @param CategoryTreeInterfaceFactory $treeFactory + * @param TreeFactory|null $treeResourceFactory */ public function __construct( \Magento\Catalog\Model\ResourceModel\Category\Tree $categoryTree, - \Magento\Store\Model\StoreManagerInterface $storeManager, - \Magento\Catalog\Model\ResourceModel\Category\Collection $categoryCollection, - \Magento\Catalog\Api\Data\CategoryTreeInterfaceFactory $treeFactory, - \Magento\Catalog\Model\ResourceModel\Category\TreeFactory $treeResourceFactory = null + StoreManagerInterface $storeManager, + Collection $categoryCollection, + CategoryTreeInterfaceFactory $treeFactory, + TreeFactory $treeResourceFactory = null ) { $this->categoryTree = $categoryTree; $this->storeManager = $storeManager; $this->categoryCollection = $categoryCollection; $this->treeFactory = $treeFactory; - $this->treeResourceFactory = $treeResourceFactory ?? \Magento\Framework\App\ObjectManager::getInstance() - ->get(\Magento\Catalog\Model\ResourceModel\Category\TreeFactory::class); + $this->treeResourceFactory = $treeResourceFactory ?? ObjectManager::getInstance() + ->get(TreeFactory::class); } /** * Get root node by category. * - * @param \Magento\Catalog\Model\Category|null $category + * @param Category|null $category * @return Node|null - * @throws \Magento\Framework\Exception\LocalizedException - * @throws \Magento\Framework\Exception\NoSuchEntityException + * @throws LocalizedException + * @throws NoSuchEntityException */ public function getRootNode($category = null) { @@ -86,19 +95,19 @@ public function getRootNode($category = null) /** * Get node by category. * - * @param \Magento\Catalog\Model\Category $category + * @param Category $category * @return Node - * @throws \Magento\Framework\Exception\LocalizedException - * @throws \Magento\Framework\Exception\NoSuchEntityException + * @throws LocalizedException + * @throws NoSuchEntityException */ - protected function getNode(\Magento\Catalog\Model\Category $category) + protected function getNode(Category $category) { $nodeId = $category->getId(); $categoryTree = $this->treeResourceFactory->create(); $node = $categoryTree->loadNode($nodeId); $node->loadChildren(); $this->prepareCollection(); - $this->categoryTree->addCollectionData($this->categoryCollection); + $categoryTree->addCollectionData($this->categoryCollection); return $node; } @@ -106,8 +115,8 @@ protected function getNode(\Magento\Catalog\Model\Category $category) * Prepare category collection. * * @return void - * @throws \Magento\Framework\Exception\LocalizedException - * @throws \Magento\Framework\Exception\NoSuchEntityException + * @throws LocalizedException + * @throws NoSuchEntityException */ protected function prepareCollection() { @@ -128,16 +137,16 @@ protected function prepareCollection() /** * Get tree by node. * - * @param \Magento\Framework\Data\Tree\Node $node + * @param Node $node * @param int $depth * @param int $currentLevel - * @return \Magento\Catalog\Api\Data\CategoryTreeInterface + * @return CategoryTreeInterface */ public function getTree($node, $depth = null, $currentLevel = 0) { - /** @var \Magento\Catalog\Api\Data\CategoryTreeInterface[] $children */ + /** @var CategoryTreeInterface[] $children */ $children = $this->getChildren($node, $depth, $currentLevel); - /** @var \Magento\Catalog\Api\Data\CategoryTreeInterface $tree */ + /** @var CategoryTreeInterface $tree */ $tree = $this->treeFactory->create(); $tree->setId($node->getId()) ->setParentId($node->getParentId()) @@ -153,10 +162,10 @@ public function getTree($node, $depth = null, $currentLevel = 0) /** * Get node children. * - * @param \Magento\Framework\Data\Tree\Node $node + * @param Node $node * @param int $depth * @param int $currentLevel - * @return \Magento\Catalog\Api\Data\CategoryTreeInterface[]|[] + * @return CategoryTreeInterface[]|[] */ protected function getChildren($node, $depth, $currentLevel) { 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 eb59acb56c356..eee347c36910d 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 @@ -152,7 +152,7 @@ private function switchTables(): void * * @return $this */ - public function execute(): self + public function execute(): Full { $this->createTables(); $this->clearReplicaTables(); diff --git a/app/code/Magento/Catalog/Model/Indexer/Product/Price.php b/app/code/Magento/Catalog/Model/Indexer/Product/Price.php index c9936f7e6c691..b703ba82a4052 100644 --- a/app/code/Magento/Catalog/Model/Indexer/Product/Price.php +++ b/app/code/Magento/Catalog/Model/Indexer/Product/Price.php @@ -5,43 +5,56 @@ */ namespace Magento\Catalog\Model\Indexer\Product; +use Magento\Catalog\Model\Category as CategoryModel; +use Magento\Catalog\Model\Indexer\Product\Price\Action\Full as FullAction; +use Magento\Catalog\Model\Indexer\Product\Price\Action\Row as RowAction; +use Magento\Catalog\Model\Indexer\Product\Price\Action\Rows as RowsAction; +use Magento\Catalog\Model\Product as ProductModel; +use Magento\Framework\Indexer\ActionInterface as IndexerActionInterface; use Magento\Framework\Indexer\CacheContext; +use Magento\Framework\Mview\ActionInterface as MviewActionInterface; -class Price implements \Magento\Framework\Indexer\ActionInterface, \Magento\Framework\Mview\ActionInterface +/** + * Price indexer + */ +class Price implements IndexerActionInterface, MviewActionInterface { /** - * @var \Magento\Catalog\Model\Indexer\Product\Price\Action\Row + * @var RowAction */ protected $_productPriceIndexerRow; /** - * @var \Magento\Catalog\Model\Indexer\Product\Price\Action\Rows + * @var RowsAction */ protected $_productPriceIndexerRows; /** - * @var \Magento\Catalog\Model\Indexer\Product\Price\Action\Full + * @var FullAction */ protected $_productPriceIndexerFull; /** - * @var \Magento\Framework\Indexer\CacheContext + * @var CacheContext */ private $cacheContext; /** - * @param Price\Action\Row $productPriceIndexerRow - * @param Price\Action\Rows $productPriceIndexerRows - * @param Price\Action\Full $productPriceIndexerFull + * @param RowAction $productPriceIndexerRow + * @param RowsAction $productPriceIndexerRows + * @param FullAction $productPriceIndexerFull + * @param CacheContext $cacheContext */ public function __construct( - \Magento\Catalog\Model\Indexer\Product\Price\Action\Row $productPriceIndexerRow, - \Magento\Catalog\Model\Indexer\Product\Price\Action\Rows $productPriceIndexerRows, - \Magento\Catalog\Model\Indexer\Product\Price\Action\Full $productPriceIndexerFull + RowAction $productPriceIndexerRow, + RowsAction $productPriceIndexerRows, + FullAction $productPriceIndexerFull, + CacheContext $cacheContext ) { $this->_productPriceIndexerRow = $productPriceIndexerRow; $this->_productPriceIndexerRows = $productPriceIndexerRows; $this->_productPriceIndexerFull = $productPriceIndexerFull; + $this->cacheContext = $cacheContext; } /** @@ -53,7 +66,7 @@ public function __construct( public function execute($ids) { $this->_productPriceIndexerRows->execute($ids); - $this->getCacheContext()->registerEntities(\Magento\Catalog\Model\Product::CACHE_TAG, $ids); + $this->cacheContext->registerEntities(ProductModel::CACHE_TAG, $ids); } /** @@ -64,10 +77,10 @@ public function execute($ids) public function executeFull() { $this->_productPriceIndexerFull->execute(); - $this->getCacheContext()->registerTags( + $this->cacheContext->registerTags( [ - \Magento\Catalog\Model\Category::CACHE_TAG, - \Magento\Catalog\Model\Product::CACHE_TAG + CategoryModel::CACHE_TAG, + ProductModel::CACHE_TAG ] ); } @@ -81,6 +94,7 @@ public function executeFull() public function executeList(array $ids) { $this->_productPriceIndexerRows->execute($ids); + $this->cacheContext->registerEntities(ProductModel::CACHE_TAG, $ids); } /** @@ -92,20 +106,6 @@ public function executeList(array $ids) public function executeRow($id) { $this->_productPriceIndexerRow->execute($id); - } - - /** - * Get cache context - * - * @return \Magento\Framework\Indexer\CacheContext - * @deprecated 100.0.11 - */ - protected function getCacheContext() - { - if (!($this->cacheContext instanceof CacheContext)) { - return \Magento\Framework\App\ObjectManager::getInstance()->get(CacheContext::class); - } else { - return $this->cacheContext; - } + $this->cacheContext->registerEntities(ProductModel::CACHE_TAG, [$id]); } } diff --git a/app/code/Magento/Catalog/Model/Plugin/SetPageLayoutDefaultValue.php b/app/code/Magento/Catalog/Model/Plugin/SetPageLayoutDefaultValue.php new file mode 100644 index 0000000000000..b812de1dfc2ae --- /dev/null +++ b/app/code/Magento/Catalog/Model/Plugin/SetPageLayoutDefaultValue.php @@ -0,0 +1,50 @@ +defaultValue = $defaultValue; + } + + /** + * Sets the default value for Category Design Layout in data provider if provided + * + * @param DataProvider $subject + * @param array $result + * @return array + * + * @throws NoSuchEntityException + */ + public function afterGetDefaultMetaData(DataProvider $subject, array $result): array + { + $currentCategory = $subject->getCurrentCategory(); + + if ($currentCategory && !$currentCategory->getId() && array_key_exists('page_layout', $result)) { + $result['page_layout']['default'] = $this->defaultValue ?: null; + } + + return $result; + } +} diff --git a/app/code/Magento/Catalog/Model/Product.php b/app/code/Magento/Catalog/Model/Product.php index fc9fffb2a7e9a..1b7552c82276d 100644 --- a/app/code/Magento/Catalog/Model/Product.php +++ b/app/code/Magento/Catalog/Model/Product.php @@ -832,12 +832,14 @@ public function getStoreIds() if (!$this->hasStoreIds()) { $storeIds = []; if ($websiteIds = $this->getWebsiteIds()) { - if ($this->_storeManager->isSingleStoreMode()) { + if (!$this->isObjectNew() && $this->_storeManager->isSingleStoreMode()) { $websiteIds = array_keys($websiteIds); } foreach ($websiteIds as $websiteId) { $websiteStores = $this->_storeManager->getWebsite($websiteId)->getStoreIds(); - $storeIds = array_merge($storeIds, $websiteStores); + foreach ($websiteStores as $websiteStore) { + $storeIds []= $websiteStore; + } } } $this->setStoreIds($storeIds); @@ -920,9 +922,9 @@ public function beforeSave() //Validate changing of design. $userType = $this->getUserContext()->getUserType(); if (( - $userType === UserContextInterface::USER_TYPE_ADMIN + $userType === UserContextInterface::USER_TYPE_ADMIN || $userType === UserContextInterface::USER_TYPE_INTEGRATION - ) + ) && !$this->getAuthorization()->isAllowed('Magento_Catalog::edit_product_design') ) { $this->setData('custom_design', $this->getOrigData('custom_design')); diff --git a/app/code/Magento/Catalog/Model/Product/Attribute/OptionManagement.php b/app/code/Magento/Catalog/Model/Product/Attribute/OptionManagement.php index 8b638feafaafc..b797308c30fb0 100644 --- a/app/code/Magento/Catalog/Model/Product/Attribute/OptionManagement.php +++ b/app/code/Magento/Catalog/Model/Product/Attribute/OptionManagement.php @@ -43,17 +43,6 @@ 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, true)) { - return false; - } - } return $this->eavOptionManagement->add( \Magento\Catalog\Api\Data\ProductAttributeInterface::ENTITY_TYPE_CODE, $attributeCode, diff --git a/app/code/Magento/Catalog/Model/Product/Gallery/GalleryManagement.php b/app/code/Magento/Catalog/Model/Product/Gallery/GalleryManagement.php index c993e51c8bc09..9e5cf084c25a1 100644 --- a/app/code/Magento/Catalog/Model/Product/Gallery/GalleryManagement.php +++ b/app/code/Magento/Catalog/Model/Product/Gallery/GalleryManagement.php @@ -71,10 +71,8 @@ public function create($sku, ProductAttributeMediaGalleryEntryInterface $entry) $product->setMediaGalleryEntries($existingMediaGalleryEntries); try { $product = $this->productRepository->save($product); - // phpcs:ignore Magento2.Exceptions.ThrowCatch } catch (InputException $inputException) { throw $inputException; - // phpcs:ignore Magento2.Exceptions.ThrowCatch } catch (\Exception $e) { throw new StateException(__("The product can't be saved.")); } diff --git a/app/code/Magento/Catalog/Model/Product/Image/ParamsBuilder.php b/app/code/Magento/Catalog/Model/Product/Image/ParamsBuilder.php index 4a55714a27ec5..4b7a623b15c19 100644 --- a/app/code/Magento/Catalog/Model/Product/Image/ParamsBuilder.php +++ b/app/code/Magento/Catalog/Model/Product/Image/ParamsBuilder.php @@ -68,11 +68,12 @@ public function __construct( * Build image params * * @param array $imageArguments + * @param int $scopeId * @return array * @SuppressWarnings(PHPMD.NPathComplexity) * @SuppressWarnings(PHPMD.CyclomaticComplexity) */ - public function build(array $imageArguments): array + public function build(array $imageArguments, int $scopeId = null): array { $miscParams = [ 'image_type' => $imageArguments['type'] ?? null, @@ -81,7 +82,7 @@ public function build(array $imageArguments): array ]; $overwritten = $this->overwriteDefaultValues($imageArguments); - $watermark = isset($miscParams['image_type']) ? $this->getWatermark($miscParams['image_type']) : []; + $watermark = isset($miscParams['image_type']) ? $this->getWatermark($miscParams['image_type'], $scopeId) : []; return array_merge($miscParams, $overwritten, $watermark); } @@ -117,27 +118,32 @@ private function overwriteDefaultValues(array $imageArguments): array * Get watermark * * @param string $type + * @param int $scopeId * @return array */ - private function getWatermark(string $type): array + private function getWatermark(string $type, int $scopeId = null): array { $file = $this->scopeConfig->getValue( "design/watermark/{$type}_image", - ScopeInterface::SCOPE_STORE + ScopeInterface::SCOPE_STORE, + $scopeId ); if ($file) { $size = $this->scopeConfig->getValue( "design/watermark/{$type}_size", - ScopeInterface::SCOPE_STORE + ScopeInterface::SCOPE_STORE, + $scopeId ); $opacity = $this->scopeConfig->getValue( "design/watermark/{$type}_imageOpacity", - ScopeInterface::SCOPE_STORE + ScopeInterface::SCOPE_STORE, + $scopeId ); $position = $this->scopeConfig->getValue( "design/watermark/{$type}_position", - ScopeInterface::SCOPE_STORE + ScopeInterface::SCOPE_STORE, + $scopeId ); $width = !empty($size['width']) ? $size['width'] : null; $height = !empty($size['height']) ? $size['height'] : null; diff --git a/app/code/Magento/Catalog/Model/Product/Option.php b/app/code/Magento/Catalog/Model/Product/Option.php index b4a4ec08d390d..4f730834412e4 100644 --- a/app/code/Magento/Catalog/Model/Product/Option.php +++ b/app/code/Magento/Catalog/Model/Product/Option.php @@ -167,6 +167,8 @@ protected function _getResource() } /** + * Construct function + * * @return void */ protected function _construct() @@ -215,6 +217,8 @@ public function hasValues($type = null) } /** + * Get values + * * @return ProductCustomOptionValuesInterface[]|null */ public function getValues() @@ -345,7 +349,8 @@ public function groupFactory($type) } /** - * {@inheritdoc} + * @inheritdoc + * * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @since 101.0.0 */ @@ -396,6 +401,8 @@ public function beforeSave() } /** + * After save + * * @return \Magento\Framework\Model\AbstractModel * @throws \Magento\Framework\Exception\LocalizedException */ @@ -424,7 +431,8 @@ public function afterSave() /** * Return price. If $flag is true and price is percent - * return converted percent to price + * + * Return converted percent to price * * @param bool $flag * @return float @@ -555,7 +563,7 @@ protected function _clearReferences() } /** - * {@inheritdoc} + * @inheritdoc */ protected function _getValidationRulesBeforeSave() { @@ -649,6 +657,8 @@ public function getSku() } /** + * Get file extension + * * @return string|null */ public function getFileExtension() @@ -657,6 +667,8 @@ public function getFileExtension() } /** + * Get Max Characters + * * @return int|null */ public function getMaxCharacters() @@ -665,6 +677,8 @@ public function getMaxCharacters() } /** + * Get image size X + * * @return int|null */ public function getImageSizeX() @@ -673,6 +687,8 @@ public function getImageSizeX() } /** + * Get image size Y + * * @return int|null */ public function getImageSizeY() @@ -780,6 +796,8 @@ public function setSku($sku) } /** + * Set File Extension + * * @param string $fileExtension * @return $this */ @@ -789,6 +807,8 @@ public function setFileExtension($fileExtension) } /** + * Set Max Characters + * * @param int $maxCharacters * @return $this */ @@ -798,6 +818,8 @@ public function setMaxCharacters($maxCharacters) } /** + * Set Image Size X + * * @param int $imageSizeX * @return $this */ @@ -807,6 +829,8 @@ public function setImageSizeX($imageSizeX) } /** + * Set Image Size Y + * * @param int $imageSizeY * @return $this */ @@ -816,6 +840,8 @@ public function setImageSizeY($imageSizeY) } /** + * Set value + * * @param ProductCustomOptionValuesInterface[] $values * @return $this */ @@ -826,7 +852,7 @@ public function setValues(array $values = null) } /** - * {@inheritdoc} + * @inheritdoc * * @return \Magento\Catalog\Api\Data\ProductCustomOptionExtensionInterface|null */ @@ -852,6 +878,8 @@ public function getRegularPrice() } /** + * Get Product Option Collection + * * @param Product $product * @return \Magento\Framework\Model\ResourceModel\Db\Collection\AbstractCollection */ @@ -882,7 +910,7 @@ public function getProductOptionCollection(Product $product) } /** - * {@inheritdoc} + * @inheritdoc * * @param \Magento\Catalog\Api\Data\ProductCustomOptionExtensionInterface $extensionAttributes * @return $this @@ -894,6 +922,8 @@ public function setExtensionAttributes( } /** + * Get option repository + * * @return Option\Repository */ private function getOptionRepository() @@ -906,6 +936,8 @@ private function getOptionRepository() } /** + * Get metadata pool + * * @return \Magento\Framework\EntityManager\MetadataPool */ private function getMetadataPool() @@ -931,7 +963,7 @@ private function cleanFileExtensions() preg_match_all('/(?[a-z0-9]+)/i', strtolower($rawExtensions), $matches); if (!empty($matches)) { $extensions = implode(', ', array_unique($matches['extensions'])); + $this->setFileExtension($extensions); } - $this->setFileExtension($extensions); } } diff --git a/app/code/Magento/Catalog/Model/Product/Price/TierPriceStorage.php b/app/code/Magento/Catalog/Model/Product/Price/TierPriceStorage.php index 3ee064670a460..36ef1826462b0 100644 --- a/app/code/Magento/Catalog/Model/Product/Price/TierPriceStorage.php +++ b/app/code/Magento/Catalog/Model/Product/Price/TierPriceStorage.php @@ -7,11 +7,15 @@ namespace Magento\Catalog\Model\Product\Price; use Magento\Catalog\Api\Data\TierPriceInterface; +use Magento\Catalog\Api\TierPriceStorageInterface; +use Magento\Catalog\Model\Indexer\Product\Price\Processor as PriceIndexerProcessor; +use Magento\Catalog\Model\Product\Price\Validation\TierPriceValidator; +use Magento\Catalog\Model\ProductIdLocatorInterface; /** * Tier price storage. */ -class TierPriceStorage implements \Magento\Catalog\Api\TierPriceStorageInterface +class TierPriceStorage implements TierPriceStorageInterface { /** * Tier price resource model. @@ -23,7 +27,7 @@ class TierPriceStorage implements \Magento\Catalog\Api\TierPriceStorageInterface /** * Tier price validator. * - * @var \Magento\Catalog\Model\Product\Price\Validation\TierPriceValidator + * @var TierPriceValidator */ private $tierPriceValidator; @@ -35,65 +39,38 @@ class TierPriceStorage implements \Magento\Catalog\Api\TierPriceStorageInterface private $tierPriceFactory; /** - * Price indexer. + * Price index processor. * - * @var \Magento\Catalog\Model\Indexer\Product\Price + * @var PriceIndexerProcessor */ - private $priceIndexer; + private $priceIndexProcessor; /** * Product ID locator. * - * @var \Magento\Catalog\Model\ProductIdLocatorInterface + * @var ProductIdLocatorInterface */ private $productIdLocator; - /** - * Page cache config. - * - * @var \Magento\PageCache\Model\Config - */ - private $config; - - /** - * Cache type list. - * - * @var \Magento\Framework\App\Cache\TypeListInterface - */ - private $typeList; - - /** - * Indexer chunk value. - * - * @var int - */ - private $indexerChunkValue = 500; - /** * @param TierPricePersistence $tierPricePersistence - * @param \Magento\Catalog\Model\Product\Price\Validation\TierPriceValidator $tierPriceValidator + * @param TierPriceValidator $tierPriceValidator * @param TierPriceFactory $tierPriceFactory - * @param \Magento\Catalog\Model\Indexer\Product\Price $priceIndexer - * @param \Magento\Catalog\Model\ProductIdLocatorInterface $productIdLocator - * @param \Magento\PageCache\Model\Config $config - * @param \Magento\Framework\App\Cache\TypeListInterface $typeList + * @param PriceIndexerProcessor $priceIndexProcessor + * @param ProductIdLocatorInterface $productIdLocator */ public function __construct( TierPricePersistence $tierPricePersistence, - \Magento\Catalog\Model\Product\Price\Validation\TierPriceValidator $tierPriceValidator, + TierPriceValidator $tierPriceValidator, TierPriceFactory $tierPriceFactory, - \Magento\Catalog\Model\Indexer\Product\Price $priceIndexer, - \Magento\Catalog\Model\ProductIdLocatorInterface $productIdLocator, - \Magento\PageCache\Model\Config $config, - \Magento\Framework\App\Cache\TypeListInterface $typeList + PriceIndexerProcessor $priceIndexProcessor, + ProductIdLocatorInterface $productIdLocator ) { $this->tierPricePersistence = $tierPricePersistence; $this->tierPriceValidator = $tierPriceValidator; $this->tierPriceFactory = $tierPriceFactory; - $this->priceIndexer = $priceIndexer; + $this->priceIndexProcessor = $priceIndexProcessor; $this->productIdLocator = $productIdLocator; - $this->config = $config; - $this->typeList = $typeList; } /** @@ -113,16 +90,18 @@ public function update(array $prices) { $affectedIds = $this->retrieveAffectedProductIdsForPrices($prices); $skus = array_unique( - array_map(function ($price) { - return $price->getSku(); - }, $prices) + array_map( + function (TierPriceInterface $price) { + return $price->getSku(); + }, + $prices + ) ); $result = $this->tierPriceValidator->retrieveValidationResult($prices, $this->getExistingPrices($skus, true)); $prices = $this->removeIncorrectPrices($prices, $result->getFailedRowIds()); $formattedPrices = $this->retrieveFormattedPrices($prices); $this->tierPricePersistence->update($formattedPrices); $this->reindexPrices($affectedIds); - $this->invalidateFullPageCache(); return $result->getFailedItems(); } @@ -138,7 +117,6 @@ public function replace(array $prices) $formattedPrices = $this->retrieveFormattedPrices($prices); $this->tierPricePersistence->replace($formattedPrices, $affectedIds); $this->reindexPrices($affectedIds); - $this->invalidateFullPageCache(); return $result->getFailedItems(); } @@ -154,7 +132,6 @@ public function delete(array $prices) $priceIds = $this->retrieveAffectedPriceIds($prices); $this->tierPricePersistence->delete($priceIds); $this->reindexPrices($affectedIds); - $this->invalidateFullPageCache(); return $result->getFailedItems(); } @@ -166,7 +143,7 @@ public function delete(array $prices) * @param bool $groupBySku [optional] * @return array */ - private function getExistingPrices(array $skus, $groupBySku = false) + private function getExistingPrices(array $skus, bool $groupBySku = false): array { $ids = $this->retrieveAffectedIds($skus); $rawPrices = $this->tierPricePersistence->get($ids); @@ -194,7 +171,7 @@ private function getExistingPrices(array $skus, $groupBySku = false) * @param array $prices * @return array */ - private function retrieveFormattedPrices(array $prices) + private function retrieveFormattedPrices(array $prices): array { $formattedPrices = []; @@ -215,12 +192,15 @@ private function retrieveFormattedPrices(array $prices) * @param TierPriceInterface[] $prices * @return array */ - private function retrieveAffectedProductIdsForPrices(array $prices) + private function retrieveAffectedProductIdsForPrices(array $prices): array { $skus = array_unique( - array_map(function ($price) { - return $price->getSku(); - }, $prices) + array_map( + function (TierPriceInterface $price) { + return $price->getSku(); + }, + $prices + ) ); return $this->retrieveAffectedIds($skus); @@ -232,15 +212,15 @@ private function retrieveAffectedProductIdsForPrices(array $prices) * @param array $skus * @return array */ - private function retrieveAffectedIds(array $skus) + private function retrieveAffectedIds(array $skus): array { $affectedIds = []; foreach ($this->productIdLocator->retrieveProductIdsBySkus($skus) as $productId) { - $affectedIds = array_merge($affectedIds, array_keys($productId)); + $affectedIds[] = array_keys($productId); } - return array_unique($affectedIds); + return $affectedIds ? array_unique(array_merge(...$affectedIds)) : []; } /** @@ -249,7 +229,7 @@ private function retrieveAffectedIds(array $skus) * @param array $prices * @return array */ - private function retrieveAffectedPriceIds(array $prices) + private function retrieveAffectedPriceIds(array $prices): array { $affectedIds = $this->retrieveAffectedProductIdsForPrices($prices); $formattedPrices = $this->retrieveFormattedPrices($prices); @@ -270,7 +250,7 @@ private function retrieveAffectedPriceIds(array $prices) * @param array $existingPrices * @return int|null */ - private function retrievePriceId(array $price, array $existingPrices) + private function retrievePriceId(array $price, array $existingPrices): ?int { $linkField = $this->tierPricePersistence->getEntityLinkField(); @@ -281,7 +261,7 @@ private function retrievePriceId(array $price, array $existingPrices) && $this->isCorrectPriceValue($existingPrice, $price) && $existingPrice[$linkField] == $price[$linkField] ) { - return $existingPrice['value_id']; + return (int) $existingPrice['value_id']; } } @@ -295,7 +275,7 @@ private function retrievePriceId(array $price, array $existingPrices) * @param array $price * @return bool */ - private function isCorrectPriceValue(array $existingPrice, array $price) + private function isCorrectPriceValue(array $existingPrice, array $price): bool { return ($existingPrice['value'] != 0 && $existingPrice['value'] == $price['value']) || ($existingPrice['percentage_value'] !== null @@ -308,7 +288,7 @@ private function isCorrectPriceValue(array $existingPrice, array $price) * @param array $skus * @return array */ - private function buildSkuByIdLookup($skus) + private function buildSkuByIdLookup(array $skus): array { $lookup = []; foreach ($this->productIdLocator->retrieveProductIdsBySkus($skus) as $sku => $ids) { @@ -320,28 +300,16 @@ private function buildSkuByIdLookup($skus) return $lookup; } - /** - * Invalidate full page cache. - * - * @return void - */ - private function invalidateFullPageCache() - { - if ($this->config->isEnabled()) { - $this->typeList->invalidate('full_page'); - } - } - /** * Reindex prices. * * @param array $ids * @return void */ - private function reindexPrices(array $ids) + private function reindexPrices(array $ids): void { - foreach (array_chunk($ids, $this->indexerChunkValue) as $affectedIds) { - $this->priceIndexer->execute($affectedIds); + if (!empty($ids)) { + $this->priceIndexProcessor->reindexList($ids); } } @@ -352,7 +320,7 @@ private function reindexPrices(array $ids) * @param array $ids * @return array */ - private function removeIncorrectPrices(array $prices, array $ids) + private function removeIncorrectPrices(array $prices, array $ids): array { foreach ($ids as $id) { unset($prices[$id]); diff --git a/app/code/Magento/Catalog/Model/Product/TierPriceManagement.php b/app/code/Magento/Catalog/Model/Product/TierPriceManagement.php index f2da1e770279e..f078349c2a8f4 100644 --- a/app/code/Magento/Catalog/Model/Product/TierPriceManagement.php +++ b/app/code/Magento/Catalog/Model/Product/TierPriceManagement.php @@ -182,16 +182,19 @@ public function getList($sku, $customerGroupId) : $customerGroupId); $prices = []; - foreach ($product->getData('tier_price') as $price) { - if ((is_numeric($customerGroupId) && (int) $price['cust_group'] === (int) $customerGroupId) - || ($customerGroupId === 'all' && $price['all_groups']) - ) { - /** @var \Magento\Catalog\Api\Data\ProductTierPriceInterface $tierPrice */ - $tierPrice = $this->priceFactory->create(); - $tierPrice->setValue($price[$priceKey]) - ->setQty($price['price_qty']) - ->setCustomerGroupId($cgi); - $prices[] = $tierPrice; + $tierPrices = $product->getData('tier_price'); + if ($tierPrices !== null) { + foreach ($tierPrices as $price) { + if ((is_numeric($customerGroupId) && (int) $price['cust_group'] === (int) $customerGroupId) + || ($customerGroupId === 'all' && $price['all_groups']) + ) { + /** @var \Magento\Catalog\Api\Data\ProductTierPriceInterface $tierPrice */ + $tierPrice = $this->priceFactory->create(); + $tierPrice->setValue($price[$priceKey]) + ->setQty($price['price_qty']) + ->setCustomerGroupId($cgi); + $prices[] = $tierPrice; + } } } return $prices; diff --git a/app/code/Magento/Catalog/Model/Product/Type/Price.php b/app/code/Magento/Catalog/Model/Product/Type/Price.php index dc73baef3f768..814865fa75917 100644 --- a/app/code/Magento/Catalog/Model/Product/Type/Price.php +++ b/app/code/Magento/Catalog/Model/Product/Type/Price.php @@ -204,7 +204,7 @@ public function getChildFinalPrice($product, $productQty, $childProduct, $childP } /** - * Gets the 'tear_price' array from the product + * Gets the 'tier_price' array from the product * * @param Product $product * @param string $key @@ -596,7 +596,10 @@ public function calculatePrice( ) { \Magento\Framework\Profiler::start('__PRODUCT_CALCULATE_PRICE__'); if ($wId instanceof Store) { + $sId = $wId->getId(); $wId = $wId->getWebsiteId(); + } else { + $sId = $this->_storeManager->getWebsite($wId)->getDefaultGroup()->getDefaultStoreId(); } $finalPrice = $basePrice; @@ -610,7 +613,7 @@ public function calculatePrice( ); if ($rulePrice === false) { - $date = $this->_localeDate->date(null, null, false); + $date = $this->_localeDate->scopeDate($sId); $rulePrice = $this->_ruleFactory->create()->getRulePrice($date, $wId, $gId, $productId); } diff --git a/app/code/Magento/Catalog/Model/ProductAttributeGroupRepository.php b/app/code/Magento/Catalog/Model/ProductAttributeGroupRepository.php index f43ff45b93efa..a63780715c713 100644 --- a/app/code/Magento/Catalog/Model/ProductAttributeGroupRepository.php +++ b/app/code/Magento/Catalog/Model/ProductAttributeGroupRepository.php @@ -4,12 +4,16 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); namespace Magento\Catalog\Model; use Magento\Framework\Exception\NoSuchEntityException; use Magento\Framework\Exception\StateException; +/** + * Class \Magento\Catalog\Model\ProductAttributeGroupRepository + */ class ProductAttributeGroupRepository implements \Magento\Catalog\Api\ProductAttributeGroupRepositoryInterface { /** @@ -43,15 +47,21 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc */ public function save(\Magento\Eav\Api\Data\AttributeGroupInterface $group) { + /** @var \Magento\Catalog\Model\Product\Attribute\Group $group */ + $extensionAttributes = $group->getExtensionAttributes(); + if ($extensionAttributes) { + $group->setSortOrder($extensionAttributes->getSortOrder()); + $group->setAttributeGroupCode($extensionAttributes->getAttributeGroupCode()); + } return $this->groupRepository->save($group); } /** - * {@inheritdoc} + * @inheritdoc */ public function getList(\Magento\Framework\Api\SearchCriteriaInterface $searchCriteria) { @@ -59,7 +69,7 @@ public function getList(\Magento\Framework\Api\SearchCriteriaInterface $searchCr } /** - * {@inheritdoc} + * @inheritdoc */ public function get($groupId) { @@ -75,7 +85,7 @@ public function get($groupId) } /** - * {@inheritdoc} + * @inheritdoc */ public function deleteById($groupId) { @@ -86,7 +96,7 @@ public function deleteById($groupId) } /** - * {@inheritdoc} + * @inheritdoc */ public function delete(\Magento\Eav\Api\Data\AttributeGroupInterface $group) { diff --git a/app/code/Magento/Catalog/Model/ProductRepository.php b/app/code/Magento/Catalog/Model/ProductRepository.php index e961db42d99fe..d656a0a9ac5b4 100644 --- a/app/code/Magento/Catalog/Model/ProductRepository.php +++ b/app/code/Magento/Catalog/Model/ProductRepository.php @@ -735,7 +735,7 @@ private function getCollectionProcessor() { if (!$this->collectionProcessor) { $this->collectionProcessor = \Magento\Framework\App\ObjectManager::getInstance()->get( - 'Magento\Catalog\Model\Api\SearchCriteria\ProductCollectionProcessor' + \Magento\Catalog\Model\Api\SearchCriteria\ProductCollectionProcessor::class ); } return $this->collectionProcessor; @@ -845,7 +845,6 @@ private function saveProduct($product): void throw new CouldNotSaveException(__($e->getMessage())); } catch (LocalizedException $e) { throw $e; - // phpcs:disable Magento2.Exceptions.ThrowCatch } catch (\Exception $e) { throw new CouldNotSaveException( __('The product was unable to be saved. Please try again.'), diff --git a/app/code/Magento/Catalog/Model/ResourceModel/AbstractResource.php b/app/code/Magento/Catalog/Model/ResourceModel/AbstractResource.php index 3d7f863b7c0d3..3946be32184ec 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/AbstractResource.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/AbstractResource.php @@ -15,6 +15,7 @@ /** * Catalog entity abstract model * + * phpcs:disable Magento2.Classes.AbstractApi * @api * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) @@ -468,7 +469,7 @@ protected function _getOrigObject($object) * * @param AbstractAttribute $attribute * @param mixed $value New value of the attribute. - * @param array &$origData + * @param array $origData * @return bool */ protected function _canUpdateAttribute(AbstractAttribute $attribute, $value, array &$origData) @@ -560,15 +561,19 @@ public function getAttributeRawValue($entityId, $attribute, $store) $store = (int) $store; if ($typedAttributes) { foreach ($typedAttributes as $table => $_attributes) { + $defaultJoinCondition = [ + $connection->quoteInto('default_value.attribute_id IN (?)', array_keys($_attributes)), + "default_value.{$this->getLinkField()} = e.{$this->getLinkField()}", + 'default_value.store_id = 0', + ]; + $select = $connection->select() - ->from(['default_value' => $table], ['attribute_id']) - ->join( - ['e' => $this->getTable($this->getEntityTable())], - 'e.' . $this->getLinkField() . ' = ' . 'default_value.' . $this->getLinkField(), - '' - )->where('default_value.attribute_id IN (?)', array_keys($_attributes)) - ->where("e.entity_id = :entity_id") - ->where('default_value.store_id = ?', 0); + ->from(['e' => $this->getTable($this->getEntityTable())], []) + ->joinLeft( + ['default_value' => $table], + implode(' AND ', $defaultJoinCondition), + [] + )->where("e.entity_id = :entity_id"); $bind = ['entity_id' => $entityId]; @@ -578,6 +583,11 @@ public function getAttributeRawValue($entityId, $attribute, $store) 'default_value.value', 'store_value.value' ); + $attributeIdExpr = $connection->getCheckSql( + 'store_value.attribute_id IS NULL', + 'default_value.attribute_id', + 'store_value.attribute_id' + ); $joinCondition = [ $connection->quoteInto('store_value.attribute_id IN (?)', array_keys($_attributes)), "store_value.{$this->getLinkField()} = e.{$this->getLinkField()}", @@ -587,23 +597,28 @@ public function getAttributeRawValue($entityId, $attribute, $store) $select->joinLeft( ['store_value' => $table], implode(' AND ', $joinCondition), - ['attr_value' => $valueExpr] + ['attribute_id' => $attributeIdExpr, 'attr_value' => $valueExpr] ); $bind['store_id'] = $store; } else { - $select->columns(['attr_value' => 'value'], 'default_value'); + $select->columns( + ['attribute_id' => 'attribute_id', 'attr_value' => 'value'], + 'default_value' + ); } $result = $connection->fetchPairs($select, $bind); foreach ($result as $attrId => $value) { - $attrCode = $typedAttributes[$table][$attrId]; - $attributesData[$attrCode] = $value; + if ($attrId !== '') { + $attrCode = $typedAttributes[$table][$attrId]; + $attributesData[$attrCode] = $value; + } } } } - if (is_array($attributesData) && sizeof($attributesData) == 1) { + if (is_array($attributesData) && count($attributesData) == 1) { $attributesData = array_shift($attributesData); } diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Eav/Attribute.php b/app/code/Magento/Catalog/Model/ResourceModel/Eav/Attribute.php index e7c98b218f5ad..23f612582f42e 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Eav/Attribute.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Eav/Attribute.php @@ -193,7 +193,6 @@ public function beforeSave() if ($this->_data[self::KEY_IS_GLOBAL] != $this->_origData[self::KEY_IS_GLOBAL]) { try { $this->attrLockValidator->validate($this); - // phpcs:ignore Magento2.Exceptions.ThrowCatch } catch (\Magento\Framework\Exception\LocalizedException $exception) { throw new \Magento\Framework\Exception\LocalizedException( __('Do not change the scope. %1', $exception->getMessage()) diff --git a/app/code/Magento/Catalog/Observer/InvalidateCacheOnCategoryDesignChange.php b/app/code/Magento/Catalog/Observer/InvalidateCacheOnCategoryDesignChange.php new file mode 100644 index 0000000000000..0b30ff4f1c038 --- /dev/null +++ b/app/code/Magento/Catalog/Observer/InvalidateCacheOnCategoryDesignChange.php @@ -0,0 +1,102 @@ +cacheTypeList = $cacheTypeList; + $this->scopeConfig = $scopeConfig; + } + + /** + * Get default category design attribute values + * + * @return array + */ + private function getDefaultAttributeValues() + { + return [ + 'custom_apply_to_products' => '0', + 'custom_use_parent_settings' => '0', + 'page_layout' => $this->scopeConfig->getValue( + 'web/default_layouts/default_category_layout', + \Magento\Store\Model\ScopeInterface::SCOPE_STORE + ) + ]; + } + + /** + * Invalidate cache on category design attribute value changed + * + * @param \Magento\Framework\Event\Observer $observer + */ + public function execute(Observer $observer) + { + $category = $observer->getEvent()->getEntity(); + if (!$category->isObjectNew()) { + foreach ($category->getDesignAttributes() as $designAttribute) { + if ($this->isCategoryAttributeChanged($designAttribute->getAttributeCode(), $category)) { + $this->cacheTypeList->invalidate( + [ + \Magento\PageCache\Model\Cache\Type::TYPE_IDENTIFIER, + \Magento\Framework\App\Cache\Type\Layout::TYPE_IDENTIFIER + ] + ); + break; + } + } + } + } + + /** + * Check if category attribute changed + * + * @param string $attributeCode + * @param \Magento\Catalog\Api\Data\CategoryInterface $category + * @return bool + */ + private function isCategoryAttributeChanged($attributeCode, $category) + { + if (!array_key_exists($attributeCode, $category->getOrigData())) { + $defaultValue = $this->getDefaultAttributeValues()[$attributeCode] ?? null; + if ($category->getData($attributeCode) !== $defaultValue) { + return true; + } + } else { + if ($category->dataHasChangedFor($attributeCode)) { + return true; + } + } + + return false; + } +} diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminCategoryActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminCategoryActionGroup.xml index 12bf5179a07d0..a114ea3edd563 100644 --- a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminCategoryActionGroup.xml +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminCategoryActionGroup.xml @@ -396,4 +396,44 @@ + + + + Requires navigation to category creation/edit page. Assign products to category - using "Products in Category" tab. + + + + + + + + + + + + + + + Deletes all children categories of Default Root Category. + + + + + diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductActionGroup.xml index 8b7567bb5158b..0bb73e7416b07 100644 --- a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductActionGroup.xml +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductActionGroup.xml @@ -422,6 +422,10 @@ + + + + diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductGridActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductGridActionGroup.xml index 85441bafdc93c..6a260bbf22522 100644 --- a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductGridActionGroup.xml +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductGridActionGroup.xml @@ -13,7 +13,7 @@ Sets the Admin Products grid view to the 'Default View'. - + @@ -120,7 +120,7 @@ Filters the Admin Products grid by the 'Enabled' Status. PLEASE NOTE: The Filter is Hardcoded. - + @@ -286,6 +286,27 @@ + + + Delete products by keyword + + + + + + + + + + + + + + + + + + @@ -314,7 +335,7 @@ Filters the ID column in Ascending Order. - + @@ -400,5 +421,6 @@ + diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontCategoryActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontCategoryActionGroup.xml index 341a00d3158d6..9393669f6e46d 100644 --- a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontCategoryActionGroup.xml +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontCategoryActionGroup.xml @@ -78,6 +78,15 @@ + + + EXTENDS:StorefrontCheckCategorySimpleProduct. Removes 'AssertProductPrice', 'moveMouseOverProduct', 'AssertAddToCart' + + + + + + diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontCategoryPageSortProductActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontCategoryPageSortProductActionGroup.xml new file mode 100644 index 0000000000000..64dd2c97a382f --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontCategoryPageSortProductActionGroup.xml @@ -0,0 +1,32 @@ + + + + + + + Select "Sort by" parameter for sorting Products on Category page + + + + + + + + + Set Ascending Direction for sorting Products on Category page + + + + + + Set Descending Direction for sorting Products on Category page + + + + diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontCheckProductPriceInCategoryActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontCheckProductPriceInCategoryActionGroup.xml index bdab0572324b3..ac33727564505 100644 --- a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontCheckProductPriceInCategoryActionGroup.xml +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontCheckProductPriceInCategoryActionGroup.xml @@ -17,4 +17,14 @@ + + + Validate that the provided Product Price is correct on category page. + + + + + + + diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/CategoryData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/CategoryData.xml index 13951a0d197d1..6ffb4e1902424 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Data/CategoryData.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Data/CategoryData.xml @@ -117,4 +117,14 @@ true true + + Default Category + + + + CategoryExportImport + + + CustomAttributeCategoryNonAnchor + diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/CustomAttributeData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/CustomAttributeData.xml index 1684bd0c8a2c3..7bd392f0aa74a 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Data/CustomAttributeData.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Data/CustomAttributeData.xml @@ -51,4 +51,8 @@ short_description Short Fixedtest 555 + + is_anchor + 0 + diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/FrontendLabelData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/FrontendLabelData.xml index a2bdaa7dbc62f..e4ffdbde4368d 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Data/FrontendLabelData.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Data/FrontendLabelData.xml @@ -20,4 +20,8 @@ 0 attributeThree + + 0 + attributeExportImport + diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/ImageContentData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/ImageContentData.xml index 1f4b1470098e2..6e40499d0efeb 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Data/ImageContentData.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Data/ImageContentData.xml @@ -18,4 +18,7 @@ image/png magento-logo.png + + magento-logo.png + diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/ProductAttributeData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/ProductAttributeData.xml index 6bbca45741c75..1986821f899cf 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Data/ProductAttributeData.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Data/ProductAttributeData.xml @@ -368,4 +368,17 @@ swatch_visual visual_swatch + + Color + color_attr + + + Size + size_attr + + + + attribute + ProductAttributeFrontendLabelForExportImport + diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/ProductAttributeMediaGalleryEntryData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/ProductAttributeMediaGalleryEntryData.xml index 98c9a70e6aad4..75b4ef773a934 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Data/ProductAttributeMediaGalleryEntryData.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Data/ProductAttributeMediaGalleryEntryData.xml @@ -30,4 +30,9 @@ false MagentoLogoImageContent + + + Magento Logo + MagentoLogoImageContentExportImport + diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/ProductAttributeOptionData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/ProductAttributeOptionData.xml index fcb56cf298a98..bb0e85bcbb40b 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Data/ProductAttributeOptionData.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Data/ProductAttributeOptionData.xml @@ -86,4 +86,11 @@ White white + + + option1 + + + option2 + diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/ProductData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/ProductData.xml index 517ab253b8238..e122615eb8aa4 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Data/ProductData.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Data/ProductData.xml @@ -351,6 +351,11 @@ adobe-base jpg + + test_image + test_image.jpg + test_image + 霁产品 simple @@ -1165,6 +1170,15 @@ EavStock10 CustomAttributeProductAttribute + + + api-simple-one-export-import + Api Simple Product One Export Import + + + api-simple-two-export-import + Api Simple Product Two Export Import + simple-product_ simple diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminCatalogStorefrontConfigSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCatalogStorefrontConfigSection.xml new file mode 100644 index 0000000000000..d0200f1e0a5b0 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCatalogStorefrontConfigSection.xml @@ -0,0 +1,16 @@ + + + + + + + + + + diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryContentSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryContentSection.xml index e3d224904671b..1cb095974d0fd 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryContentSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryContentSection.xml @@ -24,5 +24,6 @@ + diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddImageToWYSIWYGProductTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddImageToWYSIWYGProductTest.xml index f3d3e653b260b..ee105320c5f29 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddImageToWYSIWYGProductTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddImageToWYSIWYGProductTest.xml @@ -16,9 +16,6 @@ - - - @@ -78,6 +75,8 @@ + + diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckPaginationInStorefrontTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckPaginationInStorefrontTest.xml index f40a62c164ecc..1b72458747067 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckPaginationInStorefrontTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckPaginationInStorefrontTest.xml @@ -41,6 +41,16 @@ + + + + + + + + + + @@ -67,18 +77,36 @@ + + + + + + + + + + - + + + + + + + + + - + @@ -86,91 +114,92 @@ - + + - - + + - + - - + + - - + - - + + + - - + + - - + + - + - - + + - - + + - - + + - - - + + + - + - + - - + + - - + + - + - + diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminGridPageNumberAfterSaveAndCloseActionTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminGridPageNumberAfterSaveAndCloseActionTest.xml index 672205d935dab..df6ddfa169029 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminGridPageNumberAfterSaveAndCloseActionTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminGridPageNumberAfterSaveAndCloseActionTest.xml @@ -15,12 +15,9 @@ - - + + - - - diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassProductPriceUpdateTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassProductPriceUpdateTest.xml index 4d581bae700d7..f5ad5b8079d1f 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassProductPriceUpdateTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassProductPriceUpdateTest.xml @@ -10,7 +10,7 @@ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> - + @@ -24,8 +24,8 @@ - - + + diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductCustomURLKeyPreservedWhenAssignedToCategoryWithoutCustomURLKey.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductCustomURLKeyPreservedWhenAssignedToCategoryWithoutCustomURLKey.xml new file mode 100644 index 0000000000000..bae81513de632 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductCustomURLKeyPreservedWhenAssignedToCategoryWithoutCustomURLKey.xml @@ -0,0 +1,117 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminSortingByWebsitesTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminSortingByWebsitesTest.xml index eb014ca7f884d..5b6207e135796 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminSortingByWebsitesTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminSortingByWebsitesTest.xml @@ -40,6 +40,8 @@ + + diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/DisplayRefreshCacheAfterChangingCategoryPageLayoutTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/DisplayRefreshCacheAfterChangingCategoryPageLayoutTest.xml new file mode 100644 index 0000000000000..5a94dd4f04d24 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/DisplayRefreshCacheAfterChangingCategoryPageLayoutTest.xml @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontCatalogNavigationMenuUIDesktopTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontCatalogNavigationMenuUIDesktopTest.xml new file mode 100644 index 0000000000000..ae54b72a5a702 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontCatalogNavigationMenuUIDesktopTest.xml @@ -0,0 +1,336 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Category A + + + + + + + + + TEST + + + _test2 + + + test 3 + + + Category with several products + + + test 5 + + + test 8 + + + This is a very very very very very looong title + + + test 6 + + + test 7 + + + test 4 + + + Category with image + + + test 0 + + + Category with description & custom title + + + Category with children + + + level 1 test category very very very long name + + + + level 1 test category name + + + + level 1 with children + + + + level 2 with children + + + + level 3 test + + + + level 4 + + + + level 4 test + + + + level 5 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontCheckDefaultNumberProductsToDisplayTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontCheckDefaultNumberProductsToDisplayTest.xml new file mode 100644 index 0000000000000..ac2605ff5f3e2 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontCheckDefaultNumberProductsToDisplayTest.xml @@ -0,0 +1,201 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontPurchaseProductWithCustomOptionsWithLongValuesTitle.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontPurchaseProductWithCustomOptionsWithLongValuesTitle.xml index 294dcb8c1b81a..e9e23cf157a26 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontPurchaseProductWithCustomOptionsWithLongValuesTitle.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontPurchaseProductWithCustomOptionsWithLongValuesTitle.xml @@ -17,6 +17,9 @@ + + + @@ -114,6 +117,6 @@ - + diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/VerifyCategoryProductAndProductCategoryPartialReindexTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/VerifyCategoryProductAndProductCategoryPartialReindexTest.xml new file mode 100644 index 0000000000000..6184a220f047c --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/VerifyCategoryProductAndProductCategoryPartialReindexTest.xml @@ -0,0 +1,226 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/code/Magento/Catalog/Test/Unit/Block/Widget/LinkTest.php b/app/code/Magento/Catalog/Test/Unit/Block/Widget/LinkTest.php index 8333ed22e1da0..dcbd3161733aa 100644 --- a/app/code/Magento/Catalog/Test/Unit/Block/Widget/LinkTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Block/Widget/LinkTest.php @@ -5,54 +5,81 @@ */ namespace Magento\Catalog\Test\Unit\Block\Widget; +use Exception; +use Magento\Catalog\Block\Widget\Link; +use Magento\Catalog\Model\ResourceModel\AbstractResource; use Magento\CatalogUrlRewrite\Model\ProductUrlRewriteGenerator; +use Magento\Framework\App\Config\ReinitableConfigInterface; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\Framework\Url; +use Magento\Framework\Url\ModifierInterface; +use Magento\Framework\View\Element\Template\Context; +use Magento\Store\Model\Store; +use Magento\Store\Model\StoreManagerInterface; +use Magento\UrlRewrite\Model\UrlFinderInterface; use Magento\UrlRewrite\Service\V1\Data\UrlRewrite; +use PHPUnit\Framework\TestCase; +use PHPUnit_Framework_MockObject_MockObject; +use ReflectionClass; +use RuntimeException; -class LinkTest extends \PHPUnit\Framework\TestCase +/** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + * @SuppressWarnings(PHPMD.UnusedLocalVariable) + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class LinkTest extends TestCase { /** - * @var \PHPUnit_Framework_MockObject_MockObject|\Magento\Store\Model\StoreManagerInterface + * @var PHPUnit_Framework_MockObject_MockObject|StoreManagerInterface */ protected $storeManager; /** - * @var \PHPUnit_Framework_MockObject_MockObject|\Magento\UrlRewrite\Model\UrlFinderInterface + * @var PHPUnit_Framework_MockObject_MockObject|UrlFinderInterface */ protected $urlFinder; /** - * @var \Magento\Catalog\Block\Widget\Link + * @var Link */ protected $block; /** - * @var \Magento\Catalog\Model\ResourceModel\AbstractResource|\PHPUnit_Framework_MockObject_MockObject + * @var AbstractResource|PHPUnit_Framework_MockObject_MockObject */ protected $entityResource; + /** + * @inheritDoc + */ protected function setUp() { - $this->storeManager = $this->createMock(\Magento\Store\Model\StoreManagerInterface::class); - $this->urlFinder = $this->createMock(\Magento\UrlRewrite\Model\UrlFinderInterface::class); + $this->storeManager = $this->createMock(StoreManagerInterface::class); + $this->urlFinder = $this->createMock(UrlFinderInterface::class); - $context = $this->createMock(\Magento\Framework\View\Element\Template\Context::class); + $context = $this->createMock(Context::class); $context->expects($this->any()) ->method('getStoreManager') ->will($this->returnValue($this->storeManager)); $this->entityResource = - $this->createMock(\Magento\Catalog\Model\ResourceModel\AbstractResource::class); - - $this->block = (new ObjectManager($this))->getObject(\Magento\Catalog\Block\Widget\Link::class, [ - 'context' => $context, - 'urlFinder' => $this->urlFinder, - 'entityResource' => $this->entityResource - ]); + $this->createMock(AbstractResource::class); + + $this->block = (new ObjectManager($this))->getObject( + Link::class, + [ + 'context' => $context, + 'urlFinder' => $this->urlFinder, + 'entityResource' => $this->entityResource + ] + ); } /** - * @expectedException \RuntimeException + * Tests getHref with wrong id_path + * + * @expectedException RuntimeException * @expectedExceptionMessage Parameter id_path is not set. */ public function testGetHrefWithoutSetIdPath() @@ -61,7 +88,9 @@ public function testGetHrefWithoutSetIdPath() } /** - * @expectedException \RuntimeException + * Tests getHref with wrong id_path + * + * @expectedException RuntimeException * @expectedExceptionMessage Wrong id_path structure. */ public function testGetHrefIfSetWrongIdPath() @@ -70,27 +99,30 @@ public function testGetHrefIfSetWrongIdPath() $this->block->getHref(); } + /** + * Tests getHref with wrong store ID + * + * @expectedException Exception + */ public function testGetHrefWithSetStoreId() { $this->block->setData('id_path', 'type/id'); $this->block->setData('store_id', 'store_id'); - $this->storeManager->expects($this->once()) - ->method('getStore')->with('store_id') - // interrupt test execution - ->will($this->throwException(new \Exception())); - - try { - $this->block->getHref(); - } catch (\Exception $e) { - } + ->method('getStore') + ->with('store_id') + ->will($this->throwException(new Exception())); + $this->block->getHref(); } + /** + * Tests getHref with not found URL + */ public function testGetHrefIfRewriteIsNotFound() { $this->block->setData('id_path', 'entity_type/entity_id'); - $store = $this->createPartialMock(\Magento\Store\Model\Store::class, ['getId', '__wakeUp']); + $store = $this->createPartialMock(Store::class, ['getId', '__wakeUp']); $store->expects($this->any()) ->method('getId'); @@ -105,52 +137,107 @@ public function testGetHrefIfRewriteIsNotFound() } /** - * @param string $url - * @param string $separator + * Tests getHref whether it should include the store code or not + * * @dataProvider dataProviderForTestGetHrefWithoutUrlStoreSuffix + * @param string $path + * @param int|null $storeId + * @param bool $includeStoreCode + * @param string $expected + * @throws \ReflectionException */ - public function testGetHrefWithoutUrlStoreSuffix($url, $separator) - { - $storeId = 15; - $storeCode = 'store-code'; - $requestPath = 'request-path'; + public function testStoreCodeShouldBeIncludedInURLOnlyIfItIsConfiguredSo( + string $path, + ?int $storeId, + bool $includeStoreCode, + string $expected + ) { $this->block->setData('id_path', 'entity_type/entity_id'); - - $rewrite = $this->createMock(\Magento\UrlRewrite\Service\V1\Data\UrlRewrite::class); - $rewrite->expects($this->once()) - ->method('getRequestPath') - ->will($this->returnValue($requestPath)); - - $store = $this->createPartialMock( - \Magento\Store\Model\Store::class, - ['getId', 'getUrl', 'getCode', '__wakeUp'] + $this->block->setData('store_id', $storeId); + $objectManager = new ObjectManager($this); + + $rewrite = $this->createPartialMock(UrlRewrite::class, ['getRequestPath']); + $url = $this->createPartialMock(Url::class, ['setScope', 'getUrl']); + $urlModifier = $this->getMockForAbstractClass(ModifierInterface::class); + $config = $this->getMockForAbstractClass(ReinitableConfigInterface::class); + $store = $objectManager->getObject( + Store::class, + [ + 'storeManager' => $this->storeManager, + 'url' => $url, + 'config' => $config + ] ); - $store->expects($this->once()) - ->method('getId') - ->will($this->returnValue($storeId)); - $store->expects($this->once()) + $property = (new ReflectionClass(get_class($store)))->getProperty('urlModifier'); + $property->setAccessible(true); + $property->setValue($store, $urlModifier); + + $urlModifier->expects($this->any()) + ->method('execute') + ->willReturnArgument(0); + $config->expects($this->any()) + ->method('getValue') + ->willReturnMap( + [ + [Store::XML_PATH_USE_REWRITES, ReinitableConfigInterface::SCOPE_TYPE_DEFAULT, null, true], + [ + Store::XML_PATH_STORE_IN_URL, + ReinitableConfigInterface::SCOPE_TYPE_DEFAULT, + null, $includeStoreCode + ] + ] + ); + + $url->expects($this->any()) + ->method('setScope') + ->willReturnSelf(); + + $url->expects($this->any()) ->method('getUrl') - ->with('', ['_direct' => $requestPath]) - ->will($this->returnValue($url)); - $store->expects($this->once()) - ->method('getCode') - ->will($this->returnValue($storeCode)); + ->willReturnCallback( + function ($route, $params) use ($storeId) { + $baseUrl = rtrim($this->storeManager->getStore($storeId)->getBaseUrl(), '/'); + return $baseUrl .'/' . ltrim($params['_direct'], '/'); + } + ); - $this->storeManager->expects($this->once()) - ->method('getStore') - ->will($this->returnValue($store)); + $store->addData(['store_id' => 1, 'code' => 'french']); - $this->urlFinder->expects($this->once())->method('findOneByData') - ->with([ + $store2 = clone $store; + $store2->addData(['store_id' => 2, 'code' => 'german']); + + $this->storeManager + ->expects($this->any()) + ->method('getStore') + ->willReturnMap( + [ + [null, $store], + [1, $store], + [2, $store2], + ] + ); + + $this->urlFinder->expects($this->once()) + ->method('findOneByData') + ->with( + [ UrlRewrite::ENTITY_ID => 'entity_id', UrlRewrite::ENTITY_TYPE => 'entity_type', - UrlRewrite::STORE_ID => $storeId, - ]) + UrlRewrite::STORE_ID => $this->storeManager->getStore($storeId)->getStoreId(), + ] + ) ->will($this->returnValue($rewrite)); - $this->assertEquals($url . $separator . '___store=' . $storeCode, $this->block->getHref()); + $rewrite->expects($this->once()) + ->method('getRequestPath') + ->will($this->returnValue($path)); + + $this->assertEquals($expected, $this->block->getHref()); } + /** + * Tests getLabel with custom text + */ public function testGetLabelWithCustomText() { $customText = 'Some text'; @@ -158,6 +245,9 @@ public function testGetLabelWithCustomText() $this->assertEquals($customText, $this->block->getLabel()); } + /** + * Tests getLabel without custom text + */ public function testGetLabelWithoutCustomText() { $category = 'Some text'; @@ -178,17 +268,25 @@ public function testGetLabelWithoutCustomText() public function dataProviderForTestGetHrefWithoutUrlStoreSuffix() { return [ - ['url', '?'], - ['url?some_parameter', '&'], + ['/accessories.html', null, true, 'french/accessories.html'], + ['/accessories.html', null, false, '/accessories.html'], + ['/accessories.html', 1, true, 'french/accessories.html'], + ['/accessories.html', 1, false, '/accessories.html'], + ['/accessories.html', 2, true, 'german/accessories.html'], + ['/accessories.html', 2, false, '/accessories.html?___store=german'], + ['/accessories.html?___store=german', 2, false, '/accessories.html?___store=german'], ]; } + /** + * Tests getHref with product entity and additional category id in the id_path + */ public function testGetHrefWithForProductWithCategoryIdParameter() { $storeId = 15; $this->block->setData('id_path', ProductUrlRewriteGenerator::ENTITY_TYPE . '/entity_id/category_id'); - $store = $this->createPartialMock(\Magento\Store\Model\Store::class, ['getId', '__wakeUp']); + $store = $this->createPartialMock(Store::class, ['getId', '__wakeUp']); $store->expects($this->any()) ->method('getId') ->will($this->returnValue($storeId)); @@ -197,13 +295,16 @@ public function testGetHrefWithForProductWithCategoryIdParameter() ->method('getStore') ->will($this->returnValue($store)); - $this->urlFinder->expects($this->once())->method('findOneByData') - ->with([ - UrlRewrite::ENTITY_ID => 'entity_id', - UrlRewrite::ENTITY_TYPE => ProductUrlRewriteGenerator::ENTITY_TYPE, - UrlRewrite::STORE_ID => $storeId, - UrlRewrite::METADATA => ['category_id' => 'category_id'], - ]) + $this->urlFinder->expects($this->once()) + ->method('findOneByData') + ->with( + [ + UrlRewrite::ENTITY_ID => 'entity_id', + UrlRewrite::ENTITY_TYPE => ProductUrlRewriteGenerator::ENTITY_TYPE, + UrlRewrite::STORE_ID => $storeId, + UrlRewrite::METADATA => ['category_id' => 'category_id'], + ] + ) ->will($this->returnValue(false)); $this->block->getHref(); diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Category/Attribute/Backend/ImageTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Category/Attribute/Backend/ImageTest.php index dc74cdfc642e3..a76ae5244076f 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 @@ -179,27 +179,19 @@ public function testBeforeSaveAttributeFileNameOutsideOfCategoryDir() { $model = $this->setUpModelForAfterSave(); $model->setAttribute($this->attribute); - - $mediaDirectoryMock = $this->createMock(WriteInterface::class); - $this->filesystem->expects($this->once()) - ->method('getDirectoryWrite') - ->with(DirectoryList::MEDIA) - ->willReturn($mediaDirectoryMock); + $imagePath = '/pub/media/wysiwyg/test123.jpg'; $this->filesystem - ->expects($this->once()) + ->expects($this->exactly(2)) ->method('getUri') ->with(DirectoryList::MEDIA) ->willReturn('pub/media'); - $mediaDirectoryMock->expects($this->once()) - ->method('getAbsolutePath') - ->willReturn('/pub/media/wysiwyg/test123.jpg'); $object = new \Magento\Framework\DataObject( [ 'test_attribute' => [ [ 'name' => 'test123.jpg', - 'url' => '/pub/media/wysiwyg/test123.jpg', + 'url' => $imagePath, ], ], ] @@ -207,9 +199,9 @@ public function testBeforeSaveAttributeFileNameOutsideOfCategoryDir() $model->beforeSave($object); - $this->assertEquals('test123.jpg', $object->getTestAttribute()); + $this->assertEquals($imagePath, $object->getTestAttribute()); $this->assertEquals( - [['name' => '/pub/media/wysiwyg/test123.jpg', 'url' => '/pub/media/wysiwyg/test123.jpg']], + [['name' => $imagePath, 'url' => $imagePath]], $object->getData('_additional_data_test_attribute') ); } diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Category/FileInfoTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Category/FileInfoTest.php index 6c6a69ec39c85..71f5ca33d1303 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/Category/FileInfoTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/Category/FileInfoTest.php @@ -13,6 +13,8 @@ use Magento\Framework\Filesystem\Directory\WriteInterface; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; +use Magento\Store\Model\StoreManagerInterface; +use Magento\Store\Model\Store; /** * Test for Magento\Catalog\Model\Category\FileInfo class. @@ -44,6 +46,16 @@ class FileInfoTest extends TestCase */ private $pubDirectory; + /** + * @var StoreManagerInterface|MockObject + */ + private $storeManager; + + /** + * @var Store|MockObject + */ + private $store; + /** * @var FileInfo */ @@ -60,6 +72,16 @@ protected function setUp() $this->pubDirectory = $pubDirectory = $this->getMockBuilder(ReadInterface::class) ->getMockForAbstractClass(); + $this->store = $this->getMockBuilder(Store::class) + ->disableOriginalConstructor() + ->getMock(); + $this->storeManager = $this->getMockBuilder(StoreManagerInterface::class) + ->setMethods(['getStore']) + ->getMockForAbstractClass(); + $this->storeManager->expects($this->any()) + ->method('getStore') + ->willReturn($this->store); + $this->filesystem = $this->getMockBuilder(Filesystem::class) ->disableOriginalConstructor() ->getMock(); @@ -94,7 +116,8 @@ function ($arg) use ($baseDirectory, $pubDirectory) { $this->model = new FileInfo( $this->filesystem, - $this->mime + $this->mime, + $this->storeManager ); } 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 3003c2f8085e4..b2e1401479301 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 @@ -65,11 +65,11 @@ public function testGetAffectedFields() $attribute->expects($this->any())->method('getAttributeId')->will($this->returnValue($attributeId)); $attribute->expects($this->any())->method('isStatic')->will($this->returnValue(false)); $attribute->expects($this->any())->method('getBackendTable')->will($this->returnValue('table')); - $attribute->expects($this->any())->method('getName')->will($this->returnValue('tear_price')); + $attribute->expects($this->any())->method('getName')->will($this->returnValue('tier_price')); $this->_model->setAttribute($attribute); $object = new \Magento\Framework\DataObject(); - $object->setTearPrice([['price_id' => 10]]); + $object->setTierPrice([['price_id' => 10]]); $object->setId(555); $this->assertEquals( diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Product/Price/TierPriceStorageTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Product/Price/TierPriceStorageTest.php index a97f2281125a6..34f43b725da57 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/Product/Price/TierPriceStorageTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/Product/Price/TierPriceStorageTest.php @@ -6,46 +6,44 @@ namespace Magento\Catalog\Test\Unit\Model\Product\Price; +use Magento\Catalog\Api\Data\TierPriceInterface; +use Magento\Catalog\Model\Indexer\Product\Price\Processor as PriceIndexerProcessor; +use Magento\Catalog\Model\Product\Price\TierPriceFactory; +use Magento\Catalog\Model\Product\Price\TierPricePersistence; +use Magento\Catalog\Model\Product\Price\Validation\Result as PriceValidationResult; +use Magento\Catalog\Model\Product\Price\Validation\TierPriceValidator; +use Magento\Catalog\Model\ProductIdLocatorInterface; + /** * TierPriceStorage test. */ class TierPriceStorageTest extends \PHPUnit\Framework\TestCase { /** - * @var \Magento\Catalog\Model\Product\Price\TierPricePersistence|\PHPUnit_Framework_MockObject_MockObject + * @var TierPricePersistence|\PHPUnit_Framework_MockObject_MockObject */ private $tierPricePersistence; /** - * @var \Magento\Catalog\Model\Product\Price\Validation\TierPriceValidator|\PHPUnit_Framework_MockObject_MockObject + * @var TierPriceValidator|\PHPUnit_Framework_MockObject_MockObject */ private $tierPriceValidator; /** - * @var \Magento\Catalog\Model\Product\Price\TierPriceFactory|\PHPUnit_Framework_MockObject_MockObject + * @var TierPriceFactory|\PHPUnit_Framework_MockObject_MockObject */ private $tierPriceFactory; /** - * @var \Magento\Catalog\Model\Indexer\Product\Price|\PHPUnit_Framework_MockObject_MockObject + * @var PriceIndexerProcessor|\PHPUnit_Framework_MockObject_MockObject */ - private $priceIndexer; + private $priceIndexProcessor; /** - * @var \Magento\Catalog\Model\ProductIdLocatorInterface|\PHPUnit_Framework_MockObject_MockObject + * @var ProductIdLocatorInterface|\PHPUnit_Framework_MockObject_MockObject */ private $productIdLocator; - /** - * @var \Magento\PageCache\Model\Config|\PHPUnit_Framework_MockObject_MockObject - */ - private $config; - - /** - * @var \Magento\Framework\App\Cache\TypeListInterface|\PHPUnit_Framework_MockObject_MockObject - */ - private $typeList; - /** * @var \Magento\Catalog\Model\Product\Price\TierPriceStorage */ @@ -56,36 +54,13 @@ class TierPriceStorageTest extends \PHPUnit\Framework\TestCase */ protected function setUp() { - $this->tierPricePersistence = $this->getMockBuilder( - \Magento\Catalog\Model\Product\Price\TierPricePersistence::class - ) - ->disableOriginalConstructor() - ->getMock(); - $this->tierPricePersistence->expects($this->any()) - ->method('getEntityLinkField') - ->willReturn('row_id'); - $this->tierPriceValidator = $this->getMockBuilder( - \Magento\Catalog\Model\Product\Price\Validation\TierPriceValidator::class - ) - ->disableOriginalConstructor() - ->getMock(); - $this->tierPriceFactory = $this->getMockBuilder( - \Magento\Catalog\Model\Product\Price\TierPriceFactory::class - ) - ->disableOriginalConstructor() - ->getMock(); - $this->priceIndexer = $this->getMockBuilder(\Magento\Catalog\Model\Indexer\Product\Price::class) - ->disableOriginalConstructor() - ->getMock(); - $this->productIdLocator = $this->getMockBuilder(\Magento\Catalog\Model\ProductIdLocatorInterface::class) - ->disableOriginalConstructor() - ->getMockForAbstractClass(); - $this->config = $this->getMockBuilder(\Magento\PageCache\Model\Config::class) - ->disableOriginalConstructor() - ->getMock(); - $this->typeList = $this->getMockBuilder(\Magento\Framework\App\Cache\TypeListInterface::class) - ->disableOriginalConstructor() - ->getMockForAbstractClass(); + $this->tierPricePersistence = $this->createMock(TierPricePersistence::class); + $this->tierPricePersistence->method('getEntityLinkField') + ->willReturn('entity_id'); + $this->tierPriceValidator = $this->createMock(TierPriceValidator::class); + $this->tierPriceFactory = $this->createMock(TierPriceFactory::class); + $this->priceIndexProcessor = $this->createMock(PriceIndexerProcessor::class); + $this->productIdLocator = $this->createMock(ProductIdLocatorInterface::class); $objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); $this->tierPriceStorage = $objectManager->getObject( @@ -94,10 +69,8 @@ protected function setUp() 'tierPricePersistence' => $this->tierPricePersistence, 'tierPriceValidator' => $this->tierPriceValidator, 'tierPriceFactory' => $this->tierPriceFactory, - 'priceIndexer' => $this->priceIndexer, + 'priceIndexProcessor' => $this->priceIndexProcessor, 'productIdLocator' => $this->productIdLocator, - 'config' => $this->config, - 'typeList' => $this->typeList, ] ); } @@ -125,7 +98,7 @@ public function testGet() [ [ 'value_id' => 1, - 'row_id' => 2, + 'entity_id' => 2, 'all_groups' => 1, 'customer_group_id' => 0, 'qty' => 2.0000, @@ -135,7 +108,7 @@ public function testGet() ], [ 'value_id' => 2, - 'row_id' => 3, + 'entity_id' => 3, 'all_groups' => 1, 'customer_group_id' => 0, 'qty' => 3.0000, @@ -145,7 +118,7 @@ public function testGet() ] ] ); - $price = $this->getMockBuilder(\Magento\Catalog\Api\Data\TierPriceInterface::class)->getMockForAbstractClass(); + $price = $this->getMockBuilder(TierPriceInterface::class)->getMockForAbstractClass(); $this->tierPriceFactory->expects($this->atLeastOnce())->method('create')->willReturn($price); $prices = $this->tierPriceStorage->get($skus); $this->assertNotEmpty($prices); @@ -183,36 +156,37 @@ public function testGetWithoutTierPrices() */ public function testUpdate() { - $price = $this->getMockBuilder(\Magento\Catalog\Api\Data\TierPriceInterface::class)->getMockForAbstractClass(); - $result = $this->getMockBuilder(\Magento\Catalog\Model\Product\Price\Validation\Result::class) - ->disableOriginalConstructor() - ->getMock(); - $result->expects($this->atLeastOnce())->method('getFailedRowIds')->willReturn([]); + $price = $this->createMock(TierPriceInterface::class); + $result = $this->createMock(PriceValidationResult::class); + $result->expects($this->once()) + ->method('getFailedRowIds') + ->willReturn([]); $this->productIdLocator->expects($this->atLeastOnce()) ->method('retrieveProductIdsBySkus') ->willReturn(['simple' => ['2' => 'simple'], 'virtual' => ['3' => 'virtual']]); - $this->tierPriceValidator - ->expects($this->atLeastOnce()) + $this->tierPriceValidator->expects($this->once()) ->method('retrieveValidationResult') ->willReturn($result); - $this->tierPriceFactory->expects($this->atLeastOnce())->method('createSkeleton')->willReturn( - [ - 'row_id' => 2, - 'all_groups' => 1, - 'customer_group_id' => 0, - 'qty' => 2, - 'value' => 3, - 'percentage_value' => null, - 'website_id' => 0 - ] - ); + $this->tierPriceFactory->expects($this->once()) + ->method('createSkeleton') + ->willReturn( + [ + 'entity_id' => 2, + 'all_groups' => 1, + 'customer_group_id' => 0, + 'qty' => 2, + 'value' => 3, + 'percentage_value' => null, + 'website_id' => 0 + ] + ); $this->tierPricePersistence->expects($this->once()) ->method('get') ->willReturn( [ [ 'value_id' => 1, - 'row_id' => 2, + 'entity_id' => 2, 'all_groups' => 1, 'customer_group_id' => 0, 'qty' => 2.0000, @@ -222,11 +196,15 @@ public function testUpdate() ] ] ); - $this->tierPricePersistence->expects($this->atLeastOnce())->method('update'); - $this->priceIndexer->expects($this->atLeastOnce())->method('execute'); - $this->config->expects($this->atLeastOnce())->method('isEnabled')->willReturn(true); - $this->typeList->expects($this->atLeastOnce())->method('invalidate'); - $price->expects($this->atLeastOnce())->method('getSku')->willReturn('simple'); + $this->tierPricePersistence->expects($this->once()) + ->method('update'); + $this->priceIndexProcessor->expects($this->once()) + ->method('reindexList') + ->with([2, 3]); + $price->expects($this->atLeastOnce()) + ->method('getSku') + ->willReturn('simple'); + $this->assertEmpty($this->tierPriceStorage->update([$price])); } @@ -237,35 +215,41 @@ public function testUpdate() */ public function testReplace() { - $price = $this->getMockBuilder(\Magento\Catalog\Api\Data\TierPriceInterface::class)->getMockForAbstractClass(); - $price->expects($this->atLeastOnce())->method('getSku')->willReturn('virtual'); - $result = $this->getMockBuilder(\Magento\Catalog\Model\Product\Price\Validation\Result::class) - ->disableOriginalConstructor() - ->getMock(); - $result->expects($this->atLeastOnce())->method('getFailedRowIds')->willReturn([]); + $price = $this->createMock(TierPriceInterface::class); + $price->expects($this->atLeastOnce()) + ->method('getSku') + ->willReturn('virtual'); + $result = $this->createMock(PriceValidationResult::class); + $result->expects($this->once()) + ->method('getFailedRowIds') + ->willReturn([]); $this->productIdLocator->expects($this->atLeastOnce()) ->method('retrieveProductIdsBySkus') ->willReturn(['simple' => ['2' => 'simple'], 'virtual' => ['3' => 'virtual']]); $this->tierPriceValidator - ->expects($this->atLeastOnce()) + ->expects($this->once()) ->method('retrieveValidationResult') ->willReturn($result); - $this->tierPriceFactory->expects($this->atLeastOnce())->method('createSkeleton')->willReturn( - [ - 'row_id' => 3, - 'all_groups' => 1, - 'customer_group_id' => 0, - 'qty' => 3, - 'value' => 7, - 'percentage_value' => null, - 'website_id' => 0 - ] - ); - $this->tierPricePersistence->expects($this->atLeastOnce())->method('replace'); - $this->priceIndexer->expects($this->atLeastOnce())->method('execute'); - $this->config->expects($this->atLeastOnce())->method('isEnabled')->willReturn(true); - $this->typeList->expects($this->atLeastOnce())->method('invalidate'); + $this->tierPriceFactory->expects($this->once()) + ->method('createSkeleton') + ->willReturn( + [ + 'entity_id' => 3, + 'all_groups' => 1, + 'customer_group_id' => 0, + 'qty' => 3, + 'value' => 7, + 'percentage_value' => null, + 'website_id' => 0 + ] + ); + $this->tierPricePersistence->expects($this->once()) + ->method('replace'); + $this->priceIndexProcessor->expects($this->once()) + ->method('reindexList') + ->with([2, 3]); + $this->assertEmpty($this->tierPriceStorage->replace([$price])); } @@ -276,13 +260,15 @@ public function testReplace() */ public function testDelete() { - $price = $this->getMockBuilder(\Magento\Catalog\Api\Data\TierPriceInterface::class)->getMockForAbstractClass(); - $price->expects($this->atLeastOnce())->method('getSku')->willReturn('simple'); - $result = $this->getMockBuilder(\Magento\Catalog\Model\Product\Price\Validation\Result::class) - ->disableOriginalConstructor() - ->getMock(); - $result->expects($this->atLeastOnce())->method('getFailedRowIds')->willReturn([]); - $this->tierPriceValidator->expects($this->atLeastOnce()) + $price = $this->createMock(TierPriceInterface::class); + $price->expects($this->atLeastOnce()) + ->method('getSku') + ->willReturn('simple'); + $result = $this->createMock(PriceValidationResult::class); + $result->expects($this->once()) + ->method('getFailedRowIds') + ->willReturn([]); + $this->tierPriceValidator->expects($this->once()) ->method('retrieveValidationResult') ->willReturn($result); $this->productIdLocator->expects($this->atLeastOnce()) @@ -294,7 +280,7 @@ public function testDelete() [ [ 'value_id' => 7, - 'row_id' => 7, + 'entity_id' => 7, 'all_groups' => 1, 'customer_group_id' => 0, 'qty' => 5.0000, @@ -304,21 +290,24 @@ public function testDelete() ] ] ); - $this->tierPriceFactory->expects($this->atLeastOnce())->method('createSkeleton')->willReturn( - [ - 'row_id' => 3, - 'all_groups' => 1, - 'customer_group_id' => 0, - 'qty' => 3, - 'value' => 7, - 'percentage_value' => null, - 'website_id' => 0 - ] - ); - $this->tierPricePersistence->expects($this->atLeastOnce())->method('delete'); - $this->priceIndexer->expects($this->atLeastOnce())->method('execute'); - $this->config->expects($this->atLeastOnce())->method('isEnabled')->willReturn(true); - $this->typeList->expects($this->atLeastOnce())->method('invalidate'); + $this->tierPriceFactory->expects($this->once()) + ->method('createSkeleton')->willReturn( + [ + 'entity_id' => 3, + 'all_groups' => 1, + 'customer_group_id' => 0, + 'qty' => 3, + 'value' => 7, + 'percentage_value' => null, + 'website_id' => 0 + ] + ); + $this->tierPricePersistence->expects($this->once()) + ->method('delete'); + $this->priceIndexProcessor->expects($this->once()) + ->method('reindexList') + ->with([2]); + $this->assertEmpty($this->tierPriceStorage->delete([$price])); } } diff --git a/app/code/Magento/Catalog/Test/Unit/Model/ProductTest.php b/app/code/Magento/Catalog/Test/Unit/Model/ProductTest.php index 8bf8473080c54..5eaf2422e95a9 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/ProductTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/ProductTest.php @@ -13,6 +13,7 @@ use Magento\Framework\Api\ExtensibleDataInterface; use Magento\Framework\Api\ExtensionAttributesFactory; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; +use Magento\Store\Model\StoreManagerInterface; /** * Product Test @@ -207,6 +208,11 @@ class ProductTest extends \PHPUnit\Framework\TestCase */ private $eavConfig; + /** + * @var StoreManagerInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $storeManager; + /** * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ @@ -303,13 +309,13 @@ protected function setUp() ->disableOriginalConstructor() ->getMock(); - $storeManager = $this->getMockBuilder(\Magento\Store\Model\StoreManagerInterface::class) + $this->storeManager = $this->getMockBuilder(\Magento\Store\Model\StoreManagerInterface::class) ->disableOriginalConstructor() ->getMockForAbstractClass(); - $storeManager->expects($this->any()) + $this->storeManager->expects($this->any()) ->method('getStore') ->will($this->returnValue($this->store)); - $storeManager->expects($this->any()) + $this->storeManager->expects($this->any()) ->method('getWebsite') ->will($this->returnValue($this->website)); $this->indexerRegistryMock = $this->createPartialMock( @@ -394,7 +400,7 @@ protected function setUp() 'extensionFactory' => $this->extensionAttributesFactory, 'productPriceIndexerProcessor' => $this->productPriceProcessor, 'catalogProductOptionFactory' => $optionFactory, - 'storeManager' => $storeManager, + 'storeManager' => $this->storeManager, 'resource' => $this->resource, 'registry' => $this->registry, 'moduleManager' => $this->moduleManager, @@ -450,6 +456,48 @@ public function testGetStoreIds() $this->assertEquals($expectedStoreIds, $this->model->getStoreIds()); } + /** + * @dataProvider getSingleStoreIds + * @param bool $isObjectNew + */ + public function testGetStoreSingleSiteModelIds( + bool $isObjectNew + ) { + $websiteIDs = [0 => 2]; + $this->model->setWebsiteIds( + !$isObjectNew ? $websiteIDs : array_flip($websiteIDs) + ); + + $this->model->isObjectNew($isObjectNew); + + $this->storeManager->expects( + $this->exactly( + (int) !$isObjectNew + ) + ) + ->method('isSingleStoreMode') + ->will($this->returnValue(true)); + + $this->website->expects( + $this->once() + )->method('getStoreIds') + ->will($this->returnValue($websiteIDs)); + + $this->assertEquals($websiteIDs, $this->model->getStoreIds()); + } + + public function getSingleStoreIds() + { + return [ + [ + false + ], + [ + true + ], + ]; + } + public function testGetStoreId() { $this->model->setStoreId(3); @@ -1221,8 +1269,7 @@ public function testGetMediaGalleryImagesMerging() { $mediaEntries = [ - 'images' => - [ + 'images' => [ [ 'value_id' => 1, 'file' => 'imageFile.jpg', 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 b823549391257..279c3c3ac8587 100644 --- a/app/code/Magento/Catalog/Test/Unit/Pricing/Price/BasePriceTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Pricing/Price/BasePriceTest.php @@ -39,7 +39,7 @@ class BasePriceTest extends \PHPUnit\Framework\TestCase /** * @var \Magento\Catalog\Pricing\Price\TierPrice|\PHPUnit_Framework_MockObject_MockObject */ - protected $tearPriceMock; + protected $tierPriceMock; /** * @var \Magento\Catalog\Pricing\Price\SpecialPrice|\PHPUnit_Framework_MockObject_MockObject @@ -60,7 +60,7 @@ protected function setUp() $this->saleableItemMock = $this->createMock(\Magento\Catalog\Model\Product::class); $this->priceInfoMock = $this->createMock(\Magento\Framework\Pricing\PriceInfo\Base::class); $this->regularPriceMock = $this->createMock(\Magento\Catalog\Pricing\Price\RegularPrice::class); - $this->tearPriceMock = $this->createMock(\Magento\Catalog\Pricing\Price\TierPrice::class); + $this->tierPriceMock = $this->createMock(\Magento\Catalog\Pricing\Price\TierPrice::class); $this->specialPriceMock = $this->createMock(\Magento\Catalog\Pricing\Price\SpecialPrice::class); $this->calculatorMock = $this->createMock(\Magento\Framework\Pricing\Adjustment\Calculator::class); @@ -69,7 +69,7 @@ protected function setUp() ->will($this->returnValue($this->priceInfoMock)); $this->prices = [ 'regular_price' => $this->regularPriceMock, - 'tear_price' => $this->tearPriceMock, + 'tier_price' => $this->tierPriceMock, 'special_price' => $this->specialPriceMock, ]; @@ -97,7 +97,7 @@ public function testGetValue($specialPriceValue, $expectedResult) $this->regularPriceMock->expects($this->exactly(3)) ->method('getValue') ->will($this->returnValue(100)); - $this->tearPriceMock->expects($this->exactly(2)) + $this->tierPriceMock->expects($this->exactly(2)) ->method('getValue') ->will($this->returnValue(99)); $this->specialPriceMock->expects($this->any()) diff --git a/app/code/Magento/Catalog/composer.json b/app/code/Magento/Catalog/composer.json index fa8daaabe5710..8023634fa074d 100644 --- a/app/code/Magento/Catalog/composer.json +++ b/app/code/Magento/Catalog/composer.json @@ -31,8 +31,7 @@ "magento/module-ui": "*", "magento/module-url-rewrite": "*", "magento/module-widget": "*", - "magento/module-wishlist": "*", - "magento/module-authorization": "*" + "magento/module-wishlist": "*" }, "suggest": { "magento/module-cookie": "*", diff --git a/app/code/Magento/Catalog/etc/config.xml b/app/code/Magento/Catalog/etc/config.xml index 3a842166a3825..20511f4ff2295 100644 --- a/app/code/Magento/Catalog/etc/config.xml +++ b/app/code/Magento/Catalog/etc/config.xml @@ -23,9 +23,9 @@ grid-list - 9,15,30 + 12,24,36 5,10,15,20,25 - 9 + 12 10 0 position diff --git a/app/code/Magento/Catalog/etc/di.xml b/app/code/Magento/Catalog/etc/di.xml index 1c0b2daf4d6f3..d4d20995a48b4 100644 --- a/app/code/Magento/Catalog/etc/di.xml +++ b/app/code/Magento/Catalog/etc/di.xml @@ -76,6 +76,9 @@ + + + diff --git a/app/code/Magento/Catalog/etc/events.xml b/app/code/Magento/Catalog/etc/events.xml index 5bcdc88369064..f4345ce719a19 100644 --- a/app/code/Magento/Catalog/etc/events.xml +++ b/app/code/Magento/Catalog/etc/events.xml @@ -26,6 +26,7 @@ + diff --git a/app/code/Magento/Catalog/i18n/en_US.csv b/app/code/Magento/Catalog/i18n/en_US.csv index fa34b088c0f6b..9b7f8a2b07730 100644 --- a/app/code/Magento/Catalog/i18n/en_US.csv +++ b/app/code/Magento/Catalog/i18n/en_US.csv @@ -813,3 +813,5 @@ Details,Details "Edit Category Design","Edit Category Design" "A total of %1 record(s) haven't been deleted. Please see server logs for more details.","A total of %1 record(s) haven't been deleted. Please see server logs for more details." "Are you sure you want to delete this category?","Are you sure you want to delete this category?" +"Attribute Set Information","Attribute Set Information" + 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 6ce43132ef48a..992093c4a6658 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 @@ -349,6 +349,7 @@ true + true true diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/breadcrumbs.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/breadcrumbs.phtml index c4aa84704b598..fd18db94fdc88 100644 --- a/app/code/Magento/Catalog/view/frontend/templates/product/breadcrumbs.phtml +++ b/app/code/Magento/Catalog/view/frontend/templates/product/breadcrumbs.phtml @@ -8,8 +8,14 @@ $viewModel = $block->getData('viewModel'); ?> +helper(\Magento\Framework\Json\Helper\Data::class)->jsonDecode($viewModel->getJsonConfigurationHtmlEscaped()); +$widgetOptions = $this->helper(\Magento\Framework\Json\Helper\Data::class)->jsonEncode($widget['breadcrumbs']); +?> diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/list/items.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/list/items.phtml index 91e261900aef2..926e7c78a7df0 100644 --- a/app/code/Magento/Catalog/view/frontend/templates/product/list/items.phtml +++ b/app/code/Magento/Catalog/view/frontend/templates/product/list/items.phtml @@ -170,7 +170,7 @@ switch ($type = $block->getType()) { = $block->escapeHtml(__('Check items to add to the cart or')) ?> - = $block->escapeHtml(__('select all')) ?> + = $block->escapeHtml(__('select all')) ?> diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/list/toolbar.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/list/toolbar.phtml index b2ae8b9f7ab13..76ef6baf4993e 100644 --- a/app/code/Magento/Catalog/view/frontend/templates/product/list/toolbar.phtml +++ b/app/code/Magento/Catalog/view/frontend/templates/product/list/toolbar.phtml @@ -15,7 +15,10 @@ // phpcs:disable PSR2.Methods.FunctionCallSignature.SpaceBeforeOpenBracket ?> getCollection()->getSize()) :?> - + helper(\Magento\Framework\Json\Helper\Data::class)->jsonDecode($block->getWidgetOptionsJson()); + $widgetOptions = $this->helper(\Magento\Framework\Json\Helper\Data::class)->jsonEncode($widget['productListToolbarForm']); + ?> + isExpanded()) :?> getTemplateFile('Magento_Catalog::product/list/toolbar/viewmode.phtml')) ?> diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/view/form.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/view/form.phtml index 8d298aec9f1cb..c6d351b2a9571 100644 --- a/app/code/Magento/Catalog/view/frontend/templates/product/view/form.phtml +++ b/app/code/Magento/Catalog/view/frontend/templates/product/view/form.phtml @@ -39,19 +39,13 @@ = $block->getChildHtml('form_bottom') ?> - diff --git a/app/code/Magento/Catalog/view/frontend/web/js/catalog-add-to-cart.js b/app/code/Magento/Catalog/view/frontend/web/js/catalog-add-to-cart.js index d74105fe531e4..382b4ef98532b 100644 --- a/app/code/Magento/Catalog/view/frontend/web/js/catalog-add-to-cart.js +++ b/app/code/Magento/Catalog/view/frontend/web/js/catalog-add-to-cart.js @@ -135,7 +135,9 @@ define([ // trigger global event, so other modules will be able add parameters to redirect url $('body').trigger('catalogCategoryAddToCartRedirect', eventData); - if (eventData.redirectParameters.length > 0) { + if (eventData.redirectParameters.length > 0 && + window.location.href.split(/[?#]/)[0] === res.backUrl + ) { parameters = res.backUrl.split('#'); parameters.push(eventData.redirectParameters.join('&')); res.backUrl = parameters.join('#'); diff --git a/app/code/Magento/Catalog/view/frontend/web/js/related-products.js b/app/code/Magento/Catalog/view/frontend/web/js/related-products.js index d17c25d421e02..822dd5b9a7b13 100644 --- a/app/code/Magento/Catalog/view/frontend/web/js/related-products.js +++ b/app/code/Magento/Catalog/view/frontend/web/js/related-products.js @@ -17,7 +17,7 @@ define([ relatedProductsField: '#related-products-field', // Hidden input field that stores related products. selectAllMessage: $.mage.__('select all'), unselectAllMessage: $.mage.__('unselect all'), - selectAllLink: '[role="button"]', + selectAllLink: '[data-role="select-all"]', elementsSelector: '.item.product' }, diff --git a/app/code/Magento/CatalogGraphQl/DataProvider/AttributeQuery.php b/app/code/Magento/CatalogGraphQl/DataProvider/AttributeQuery.php new file mode 100644 index 0000000000000..b0f085932bb8e --- /dev/null +++ b/app/code/Magento/CatalogGraphQl/DataProvider/AttributeQuery.php @@ -0,0 +1,354 @@ +resourceConnection = $resourceConnection; + $this->metadataPool = $metadataPool; + $this->entityType = $entityType; + $this->linkedAttributes = $linkedAttributes; + $this->eavConfig = $eavConfig; + } + + /** + * Form and return query to get eav entity $attributes for given $entityIds. + * + * If eav entities were not found, then data is fetching from $entityTableName. + * + * @param array $entityIds + * @param array $attributes + * @param int $storeId + * @return Select + * @throws \Zend_Db_Select_Exception + * @throws \Exception + */ + public function getQuery(array $entityIds, array $attributes, int $storeId): Select + { + /** @var \Magento\Framework\EntityManager\EntityMetadataInterface $metadata */ + $metadata = $this->metadataPool->getMetadata($this->entityType); + $entityTableName = $metadata->getEntityTable(); + + /** @var \Magento\Framework\DB\Adapter\AdapterInterface $connection */ + $connection = $this->resourceConnection->getConnection(); + $entityTableAttributes = \array_keys($connection->describeTable($entityTableName)); + + $attributeMetadataTable = $this->resourceConnection->getTableName('eav_attribute'); + $eavAttributes = $this->getEavAttributeCodes($attributes, $entityTableAttributes); + $entityTableAttributes = \array_intersect($attributes, $entityTableAttributes); + + $eavAttributesMetaData = $this->getAttributesMetaData($connection, $attributeMetadataTable, $eavAttributes); + + if ($eavAttributesMetaData) { + $select = $this->getEavAttributes( + $connection, + $metadata, + $entityTableAttributes, + $entityIds, + $eavAttributesMetaData, + $entityTableName, + $storeId + ); + } else { + $select = $this->getAttributesFromEntityTable( + $connection, + $entityTableAttributes, + $entityIds, + $entityTableName + ); + } + + return $select; + } + + /** + * Form and return query to get entity $entityTableAttributes for given $entityIds + * + * @param \Magento\Framework\DB\Adapter\AdapterInterface $connection + * @param array $entityTableAttributes + * @param array $entityIds + * @param string $entityTableName + * @return Select + */ + private function getAttributesFromEntityTable( + \Magento\Framework\DB\Adapter\AdapterInterface $connection, + array $entityTableAttributes, + array $entityIds, + string $entityTableName + ): Select { + $select = $connection->select() + ->from(['e' => $entityTableName], $entityTableAttributes) + ->where('e.entity_id IN (?)', $entityIds); + + return $select; + } + + /** + * Return ids of eav attributes by $eavAttributeCodes. + * + * @param \Magento\Framework\DB\Adapter\AdapterInterface $connection + * @param string $attributeMetadataTable + * @param array $eavAttributeCodes + * @return array + */ + private function getAttributesMetaData( + \Magento\Framework\DB\Adapter\AdapterInterface $connection, + string $attributeMetadataTable, + array $eavAttributeCodes + ): array { + $eavAttributeIdsSelect = $connection->select() + ->from(['a' => $attributeMetadataTable], ['attribute_id', 'backend_type', 'attribute_code']) + ->where('a.attribute_code IN (?)', $eavAttributeCodes) + ->where('a.entity_type_id = ?', $this->getEntityTypeId()); + + return $connection->fetchAssoc($eavAttributeIdsSelect); + } + + /** + * Form and return query to get eav entity $attributes for given $entityIds. + * + * @param \Magento\Framework\DB\Adapter\AdapterInterface $connection + * @param \Magento\Framework\EntityManager\EntityMetadataInterface $metadata + * @param array $entityTableAttributes + * @param array $entityIds + * @param array $eavAttributesMetaData + * @param string $entityTableName + * @param int $storeId + * @return Select + * @throws \Zend_Db_Select_Exception + */ + private function getEavAttributes( + \Magento\Framework\DB\Adapter\AdapterInterface $connection, + \Magento\Framework\EntityManager\EntityMetadataInterface $metadata, + array $entityTableAttributes, + array $entityIds, + array $eavAttributesMetaData, + string $entityTableName, + int $storeId + ): Select { + $selects = []; + $attributeValueExpression = $connection->getCheckSql( + $connection->getIfNullSql('store_eav.value_id', -1) . ' > 0', + 'store_eav.value', + 'eav.value' + ); + $linkField = $metadata->getLinkField(); + $attributesPerTable = $this->getAttributeCodeTables($entityTableName, $eavAttributesMetaData); + foreach ($attributesPerTable as $attributeTable => $eavAttributes) { + $attributeCodeExpression = $this->buildAttributeCodeExpression($eavAttributes); + + $selects[] = $connection->select() + ->from(['e' => $entityTableName], $entityTableAttributes) + ->joinLeft( + ['eav' => $this->resourceConnection->getTableName($attributeTable)], + \sprintf('e.%1$s = eav.%1$s', $linkField) . + $connection->quoteInto(' AND eav.attribute_id IN (?)', \array_keys($eavAttributesMetaData)) . + $connection->quoteInto(' AND eav.store_id = ?', \Magento\Store\Model\Store::DEFAULT_STORE_ID), + [] + ) + ->joinLeft( + ['store_eav' => $this->resourceConnection->getTableName($attributeTable)], + \sprintf( + 'e.%1$s = store_eav.%1$s AND store_eav.attribute_id = ' . + 'eav.attribute_id and store_eav.store_id = %2$d', + $linkField, + $storeId + ), + [] + ) + ->where('e.entity_id IN (?)', $entityIds) + ->columns( + [ + 'attribute_code' => $attributeCodeExpression, + 'value' => $attributeValueExpression + ] + ); + } + + return $connection->select()->union($selects, Select::SQL_UNION_ALL); + } + + /** + * Build expression for attribute code field. + * + * An example: + * + * ``` + * CASE + * WHEN eav.attribute_id = '73' THEN 'name' + * WHEN eav.attribute_id = '121' THEN 'url_key' + * END + * ``` + * + * @param array $eavAttributes + * @return \Zend_Db_Expr + */ + private function buildAttributeCodeExpression(array $eavAttributes): \Zend_Db_Expr + { + $dbConnection = $this->resourceConnection->getConnection(); + $expressionParts = ['CASE']; + + foreach ($eavAttributes as $attribute) { + $expressionParts[]= + $dbConnection->quoteInto('WHEN eav.attribute_id = ?', $attribute['attribute_id'], \Zend_Db::INT_TYPE) . + $dbConnection->quoteInto(' THEN ?', $attribute['attribute_code'], 'string'); + } + + $expressionParts[]= 'END'; + + return new \Zend_Db_Expr(implode(' ', $expressionParts)); + } + + /** + * Get list of attribute tables. + * + * Returns result in the following format: * + * ``` + * $attributeAttributeCodeTables = [ + * 'm2_catalog_product_entity_varchar' => + * '45' => [ + * 'attribute_id' => 45, + * 'backend_type' => 'varchar', + * 'name' => attribute_code, + * ] + * ] + * ]; + * ``` + * + * @param string $entityTable + * @param array $eavAttributesMetaData + * @return array + */ + private function getAttributeCodeTables($entityTable, $eavAttributesMetaData): array + { + $attributeAttributeCodeTables = []; + $metaTypes = \array_unique(\array_column($eavAttributesMetaData, 'backend_type')); + + foreach ($metaTypes as $type) { + if (\in_array($type, self::SUPPORTED_BACKEND_TYPES, true)) { + $tableName = \sprintf('%s_%s', $entityTable, $type); + $attributeAttributeCodeTables[$tableName] = array_filter( + $eavAttributesMetaData, + function ($attribute) use ($type) { + return $attribute['backend_type'] === $type; + } + ); + } + } + + return $attributeAttributeCodeTables; + } + + /** + * Get EAV attribute codes + * Remove attributes from entity table and attributes from exclude list + * Add linked attributes to output + * + * @param array $attributes + * @param array $entityTableAttributes + * @return array + */ + private function getEavAttributeCodes($attributes, $entityTableAttributes): array + { + $attributes = \array_diff($attributes, $entityTableAttributes); + $unusedAttributeList = []; + $newAttributes = []; + foreach ($this->linkedAttributes as $attribute => $linkedAttributes) { + if (null === $linkedAttributes) { + $unusedAttributeList[] = $attribute; + } elseif (\is_array($linkedAttributes) && \in_array($attribute, $attributes, true)) { + $newAttributes[] = $linkedAttributes; + } + } + $attributes = \array_diff($attributes, $unusedAttributeList); + + return \array_unique(\array_merge($attributes, ...$newAttributes)); + } + + /** + * Retrieve entity type id + * + * @return int + * @throws \Exception + */ + private function getEntityTypeId(): int + { + if (!isset($this->entityTypeIdMap[$this->entityType])) { + $this->entityTypeIdMap[$this->entityType] = (int)$this->eavConfig->getEntityType( + $this->metadataPool->getMetadata($this->entityType)->getEavEntityType() + )->getId(); + } + + return $this->entityTypeIdMap[$this->entityType]; + } +} diff --git a/app/code/Magento/CatalogGraphQl/DataProvider/Category/Query/CategoryAttributeQuery.php b/app/code/Magento/CatalogGraphQl/DataProvider/Category/Query/CategoryAttributeQuery.php new file mode 100644 index 0000000000000..e3dfa38c78258 --- /dev/null +++ b/app/code/Magento/CatalogGraphQl/DataProvider/Category/Query/CategoryAttributeQuery.php @@ -0,0 +1,60 @@ +attributeQueryFactory = $attributeQueryFactory; + } + + /** + * Form and return query to get eav attributes for given categories + * + * @param array $categoryIds + * @param array $categoryAttributes + * @param int $storeId + * @return Select + * @throws \Zend_Db_Select_Exception + */ + public function getQuery(array $categoryIds, array $categoryAttributes, int $storeId): Select + { + $categoryAttributes = \array_merge($categoryAttributes, self::$requiredAttributes); + + $attributeQuery = $this->attributeQueryFactory->create( + [ + 'entityType' => CategoryInterface::class + ] + ); + + return $attributeQuery->getQuery($categoryIds, $categoryAttributes, $storeId); + } +} diff --git a/app/code/Magento/CatalogGraphQl/DataProvider/CategoryAttributesMapper.php b/app/code/Magento/CatalogGraphQl/DataProvider/CategoryAttributesMapper.php new file mode 100644 index 0000000000000..ea3c0b608d212 --- /dev/null +++ b/app/code/Magento/CatalogGraphQl/DataProvider/CategoryAttributesMapper.php @@ -0,0 +1,117 @@ +graphqlConfig = $graphqlConfig; + } + + /** + * Returns attribute values for given attribute codes. + * + * @param array $fetchResult + * @return array + */ + public function getAttributesValues(array $fetchResult): array + { + $attributes = []; + + foreach ($fetchResult as $row) { + if (!isset($attributes[$row['entity_id']])) { + $attributes[$row['entity_id']] = $row; + //TODO: do we need to introduce field mapping? + $attributes[$row['entity_id']]['id'] = $row['entity_id']; + } + if (isset($row['attribute_code'])) { + $attributes[$row['entity_id']][$row['attribute_code']] = $row['value']; + } + } + + return $this->formatAttributes($attributes); + } + + /** + * Format attributes that should be converted to array type + * + * @param array $attributes + * @return array + */ + private function formatAttributes(array $attributes): array + { + $arrayTypeAttributes = $this->getFieldsOfArrayType(); + + return $arrayTypeAttributes + ? array_map( + function ($data) use ($arrayTypeAttributes) { + foreach ($arrayTypeAttributes as $attributeCode) { + $data[$attributeCode] = $this->valueToArray($data[$attributeCode] ?? null); + } + return $data; + }, + $attributes + ) + : $attributes; + } + + /** + * Cast string to array + * + * @param string|null $value + * @return array + */ + private function valueToArray($value): array + { + return $value ? \explode(',', $value) : []; + } + + /** + * Get fields that should be converted to array type + * + * @return array + */ + private function getFieldsOfArrayType(): array + { + $categoryTreeSchema = $this->graphqlConfig->getConfigElement('CategoryTree'); + if (!$categoryTreeSchema instanceof Type) { + throw new \LogicException('CategoryTree type not defined in schema.'); + } + + $fields = []; + foreach ($categoryTreeSchema->getInterfaces() as $interface) { + /** @var InterfaceType $configElement */ + $configElement = $this->graphqlConfig->getConfigElement($interface['interface']); + + foreach ($configElement->getFields() as $field) { + if ($field->isList()) { + $fields[] = $field->getName(); + } + } + } + + return $fields; + } +} diff --git a/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/AttributeOptionProvider.php b/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/AttributeOptionProvider.php new file mode 100644 index 0000000000000..7781473128754 --- /dev/null +++ b/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/AttributeOptionProvider.php @@ -0,0 +1,107 @@ + [ + * attribute_code => code, + * attribute_label => attribute label, + * option_label => option label, + * options => [option_id => 'option label', ...], + * ] + * ... + * ] + */ +class AttributeOptionProvider +{ + /** + * @var ResourceConnection + */ + private $resourceConnection; + + /** + * @param ResourceConnection $resourceConnection + */ + public function __construct(ResourceConnection $resourceConnection) + { + $this->resourceConnection = $resourceConnection; + } + + /** + * Get option data. Return list of attributes with option data + * + * @param array $optionIds + * @return array + * @throws \Zend_Db_Statement_Exception + */ + public function getOptions(array $optionIds): array + { + if (!$optionIds) { + return []; + } + + $connection = $this->resourceConnection->getConnection(); + $select = $connection->select() + ->from( + ['a' => $this->resourceConnection->getTableName('eav_attribute')], + [ + 'attribute_id' => 'a.attribute_id', + 'attribute_code' => 'a.attribute_code', + 'attribute_label' => 'a.frontend_label', + ] + ) + ->joinInner( + ['options' => $this->resourceConnection->getTableName('eav_attribute_option')], + 'a.attribute_id = options.attribute_id', + [] + ) + ->joinInner( + ['option_value' => $this->resourceConnection->getTableName('eav_attribute_option_value')], + 'options.option_id = option_value.option_id', + [ + 'option_label' => 'option_value.value', + 'option_id' => 'option_value.option_id', + ] + ) + ->where('option_value.option_id IN (?)', $optionIds); + + return $this->formatResult($select); + } + + /** + * Format result + * + * @param \Magento\Framework\DB\Select $select + * @return array + * @throws \Zend_Db_Statement_Exception + */ + private function formatResult(\Magento\Framework\DB\Select $select): array + { + $statement = $this->resourceConnection->getConnection()->query($select); + + $result = []; + while ($option = $statement->fetch()) { + if (!isset($result[$option['attribute_code']])) { + $result[$option['attribute_code']] = [ + 'attribute_id' => $option['attribute_id'], + 'attribute_code' => $option['attribute_code'], + 'attribute_label' => $option['attribute_label'], + 'options' => [], + ]; + } + $result[$option['attribute_code']]['options'][$option['option_id']] = $option['option_label']; + } + + return $result; + } +} diff --git a/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/Builder/Attribute.php b/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/Builder/Attribute.php new file mode 100644 index 0000000000000..b70c9f6165fc6 --- /dev/null +++ b/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/Builder/Attribute.php @@ -0,0 +1,157 @@ +attributeOptionProvider = $attributeOptionProvider; + $this->layerFormatter = $layerFormatter; + $this->bucketNameFilter = \array_merge($this->bucketNameFilter, $bucketNameFilter); + } + + /** + * @inheritdoc + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + * @throws \Zend_Db_Statement_Exception + */ + public function build(AggregationInterface $aggregation, ?int $storeId): array + { + $attributeOptions = $this->getAttributeOptions($aggregation); + + // build layer per attribute + $result = []; + foreach ($this->getAttributeBuckets($aggregation) as $bucket) { + $bucketName = $bucket->getName(); + $attributeCode = \preg_replace('~_bucket$~', '', $bucketName); + $attribute = $attributeOptions[$attributeCode] ?? []; + + $result[$bucketName] = $this->layerFormatter->buildLayer( + $attribute['attribute_label'] ?? $bucketName, + \count($bucket->getValues()), + $attribute['attribute_code'] ?? $bucketName + ); + + foreach ($bucket->getValues() as $value) { + $metrics = $value->getMetrics(); + $result[$bucketName]['options'][] = $this->layerFormatter->buildItem( + $attribute['options'][$metrics['value']] ?? $metrics['value'], + $metrics['value'], + $metrics['count'] + ); + } + } + + return $result; + } + + /** + * Get attribute buckets excluding specified bucket names + * + * @param AggregationInterface $aggregation + * @return \Generator|BucketInterface[] + */ + private function getAttributeBuckets(AggregationInterface $aggregation) + { + foreach ($aggregation->getBuckets() as $bucket) { + if (\in_array($bucket->getName(), $this->bucketNameFilter, true)) { + continue; + } + if ($this->isBucketEmpty($bucket)) { + continue; + } + yield $bucket; + } + } + + /** + * Check that bucket contains data + * + * @param BucketInterface|null $bucket + * @return bool + */ + private function isBucketEmpty(?BucketInterface $bucket): bool + { + return null === $bucket || !$bucket->getValues(); + } + + /** + * Get list of attributes with options + * + * @param AggregationInterface $aggregation + * @return array + * @throws \Zend_Db_Statement_Exception + */ + private function getAttributeOptions(AggregationInterface $aggregation): array + { + $attributeOptionIds = []; + foreach ($this->getAttributeBuckets($aggregation) as $bucket) { + $attributeOptionIds[] = \array_map( + function (AggregationValueInterface $value) { + return $value->getValue(); + }, + $bucket->getValues() + ); + } + + if (!$attributeOptionIds) { + return []; + } + + return $this->attributeOptionProvider->getOptions(\array_merge(...$attributeOptionIds)); + } +} diff --git a/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/Builder/Category.php b/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/Builder/Category.php new file mode 100644 index 0000000000000..b0e67d72e25ba --- /dev/null +++ b/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/Builder/Category.php @@ -0,0 +1,151 @@ + [ + 'request_name' => 'category_id', + 'label' => 'Category' + ], + ]; + + /** + * @var CategoryAttributeQuery + */ + private $categoryAttributeQuery; + + /** + * @var CategoryAttributesMapper + */ + private $attributesMapper; + + /** + * @var ResourceConnection + */ + private $resourceConnection; + + /** + * @var RootCategoryProvider + */ + private $rootCategoryProvider; + + /** + * @var LayerFormatter + */ + private $layerFormatter; + + /** + * @param CategoryAttributeQuery $categoryAttributeQuery + * @param CategoryAttributesMapper $attributesMapper + * @param RootCategoryProvider $rootCategoryProvider + * @param ResourceConnection $resourceConnection + * @param LayerFormatter $layerFormatter + */ + public function __construct( + CategoryAttributeQuery $categoryAttributeQuery, + CategoryAttributesMapper $attributesMapper, + RootCategoryProvider $rootCategoryProvider, + ResourceConnection $resourceConnection, + LayerFormatter $layerFormatter + ) { + $this->categoryAttributeQuery = $categoryAttributeQuery; + $this->attributesMapper = $attributesMapper; + $this->resourceConnection = $resourceConnection; + $this->rootCategoryProvider = $rootCategoryProvider; + $this->layerFormatter = $layerFormatter; + } + + /** + * @inheritdoc + * @throws \Magento\Framework\Exception\LocalizedException + * @throws \Zend_Db_Select_Exception + */ + public function build(AggregationInterface $aggregation, ?int $storeId): array + { + $bucket = $aggregation->getBucket(self::CATEGORY_BUCKET); + if ($this->isBucketEmpty($bucket)) { + return []; + } + + $categoryIds = \array_map( + function (AggregationValueInterface $value) { + return (int)$value->getValue(); + }, + $bucket->getValues() + ); + + $categoryIds = \array_diff($categoryIds, [$this->rootCategoryProvider->getRootCategory($storeId)]); + $categoryLabels = \array_column( + $this->attributesMapper->getAttributesValues( + $this->resourceConnection->getConnection()->fetchAll( + $this->categoryAttributeQuery->getQuery($categoryIds, ['name'], $storeId) + ) + ), + 'name', + 'entity_id' + ); + + if (!$categoryLabels) { + return []; + } + + $result = $this->layerFormatter->buildLayer( + self::$bucketMap[self::CATEGORY_BUCKET]['label'], + \count($categoryIds), + self::$bucketMap[self::CATEGORY_BUCKET]['request_name'] + ); + + foreach ($bucket->getValues() as $value) { + $categoryId = $value->getValue(); + if (!\in_array($categoryId, $categoryIds, true)) { + continue ; + } + $result['options'][] = $this->layerFormatter->buildItem( + $categoryLabels[$categoryId] ?? $categoryId, + $categoryId, + $value->getMetrics()['count'] + ); + } + + return [$result]; + } + + /** + * Check that bucket contains data + * + * @param BucketInterface|null $bucket + * @return bool + */ + private function isBucketEmpty(?BucketInterface $bucket): bool + { + return null === $bucket || !$bucket->getValues(); + } +} diff --git a/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/Builder/Price.php b/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/Builder/Price.php new file mode 100644 index 0000000000000..02b638edbdce8 --- /dev/null +++ b/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/Builder/Price.php @@ -0,0 +1,88 @@ + [ + 'request_name' => 'price', + 'label' => 'Price' + ], + ]; + + /** + * @param LayerFormatter $layerFormatter + */ + public function __construct( + LayerFormatter $layerFormatter + ) { + $this->layerFormatter = $layerFormatter; + } + + /** + * @inheritdoc + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function build(AggregationInterface $aggregation, ?int $storeId): array + { + $bucket = $aggregation->getBucket(self::PRICE_BUCKET); + if ($this->isBucketEmpty($bucket)) { + return []; + } + + $result = $this->layerFormatter->buildLayer( + self::$bucketMap[self::PRICE_BUCKET]['label'], + \count($bucket->getValues()), + self::$bucketMap[self::PRICE_BUCKET]['request_name'] + ); + + foreach ($bucket->getValues() as $value) { + $metrics = $value->getMetrics(); + $result['options'][] = $this->layerFormatter->buildItem( + \str_replace('_', '-', $metrics['value']), + $metrics['value'], + $metrics['count'] + ); + } + + return [$result]; + } + + /** + * Check that bucket contains data + * + * @param BucketInterface|null $bucket + * @return bool + */ + private function isBucketEmpty(?BucketInterface $bucket): bool + { + return null === $bucket || !$bucket->getValues(); + } +} diff --git a/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/Formatter/LayerFormatter.php b/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/Formatter/LayerFormatter.php new file mode 100644 index 0000000000000..48a1265b10fc3 --- /dev/null +++ b/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/Formatter/LayerFormatter.php @@ -0,0 +1,48 @@ + $layerName, + 'count' => $itemsCount, + 'attribute_code' => $requestName + ]; + } + + /** + * Format layer item data + * + * @param string $label + * @param string|int $value + * @param string|int $count + * @return array + */ + public function buildItem($label, $value, $count): array + { + return [ + 'label' => $label, + 'value' => $value, + 'count' => $count, + ]; + } +} diff --git a/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/LayerBuilder.php b/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/LayerBuilder.php new file mode 100644 index 0000000000000..ff661236be62f --- /dev/null +++ b/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/LayerBuilder.php @@ -0,0 +1,43 @@ +builders = $builders; + } + + /** + * @inheritdoc + */ + public function build(AggregationInterface $aggregation, ?int $storeId): array + { + $layers = []; + foreach ($this->builders as $builder) { + $layers[] = $builder->build($aggregation, $storeId); + } + $layers = \array_merge(...$layers); + + return \array_filter($layers); + } +} diff --git a/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/LayerBuilderInterface.php b/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/LayerBuilderInterface.php new file mode 100644 index 0000000000000..bd55bc6938b39 --- /dev/null +++ b/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/LayerBuilderInterface.php @@ -0,0 +1,40 @@ + 'layer name', + * 'filter_items_count' => 'filter items count', + * 'request_var' => 'filter name in request', + * 'filter_items' => [ + * 'label' => 'item name', + * 'value_string' => 'item value, e.g. category ID', + * 'items_count' => 'product count', + * ], + * ], + * ... + * ]; + */ +interface LayerBuilderInterface +{ + /** + * Build layer data + * + * @param AggregationInterface $aggregation + * @param int|null $storeId + * @return array [[{layer data}], ...] + */ + public function build(AggregationInterface $aggregation, ?int $storeId): array; +} diff --git a/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/RootCategoryProvider.php b/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/RootCategoryProvider.php new file mode 100644 index 0000000000000..4b8a4a31b3c35 --- /dev/null +++ b/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/RootCategoryProvider.php @@ -0,0 +1,55 @@ +resourceConnection = $resourceConnection; + } + + /** + * Get root category for specified store id + * + * @param int $storeId + * @return int + */ + public function getRootCategory(int $storeId): int + { + $connection = $this->resourceConnection->getConnection(); + + $select = $connection->select() + ->from( + ['store' => $this->resourceConnection->getTableName('store')], + [] + ) + ->join( + ['store_group' => $this->resourceConnection->getTableName('store_group')], + 'store.group_id = store_group.group_id', + ['root_category_id' => 'store_group.root_category_id'] + ) + ->where('store.store_id = ?', $storeId); + + return (int)$connection->fetchOne($select); + } +} diff --git a/app/code/Magento/CatalogGraphQl/DataProvider/Product/SearchCriteriaBuilder.php b/app/code/Magento/CatalogGraphQl/DataProvider/Product/SearchCriteriaBuilder.php new file mode 100644 index 0000000000000..0e92bbbab4259 --- /dev/null +++ b/app/code/Magento/CatalogGraphQl/DataProvider/Product/SearchCriteriaBuilder.php @@ -0,0 +1,208 @@ +scopeConfig = $scopeConfig; + $this->filterBuilder = $filterBuilder; + $this->filterGroupBuilder = $filterGroupBuilder; + $this->builder = $builder; + $this->visibility = $visibility; + $this->sortOrderBuilder = $sortOrderBuilder; + } + + /** + * Build search criteria + * + * @param array $args + * @param bool $includeAggregation + * @return SearchCriteriaInterface + */ + public function build(array $args, bool $includeAggregation): SearchCriteriaInterface + { + $searchCriteria = $this->builder->build('products', $args); + $isSearch = !empty($args['search']); + $this->updateRangeFilters($searchCriteria); + + if ($includeAggregation) { + $this->preparePriceAggregation($searchCriteria); + $requestName = 'graphql_product_search_with_aggregation'; + } else { + $requestName = 'graphql_product_search'; + } + $searchCriteria->setRequestName($requestName); + + if ($isSearch) { + $this->addFilter($searchCriteria, 'search_term', $args['search']); + } + + if (!$searchCriteria->getSortOrders()) { + $this->addDefaultSortOrder($searchCriteria, $isSearch); + } + + $this->addVisibilityFilter($searchCriteria, $isSearch, !empty($args['filter'])); + + $searchCriteria->setCurrentPage($args['currentPage']); + $searchCriteria->setPageSize($args['pageSize']); + + return $searchCriteria; + } + + /** + * Add filter by visibility + * + * @param SearchCriteriaInterface $searchCriteria + * @param bool $isSearch + * @param bool $isFilter + */ + private function addVisibilityFilter(SearchCriteriaInterface $searchCriteria, bool $isSearch, bool $isFilter): void + { + if ($isFilter && $isSearch) { + // Index already contains products filtered by visibility: catalog, search, both + return ; + } + $visibilityIds = $isSearch + ? $this->visibility->getVisibleInSearchIds() + : $this->visibility->getVisibleInCatalogIds(); + + $this->addFilter($searchCriteria, 'visibility', $visibilityIds); + } + + /** + * Prepare price aggregation algorithm + * + * @param SearchCriteriaInterface $searchCriteria + * @return void + */ + private function preparePriceAggregation(SearchCriteriaInterface $searchCriteria): void + { + $priceRangeCalculation = $this->scopeConfig->getValue( + \Magento\Catalog\Model\Layer\Filter\Dynamic\AlgorithmFactory::XML_PATH_RANGE_CALCULATION, + \Magento\Store\Model\ScopeInterface::SCOPE_STORE + ); + if ($priceRangeCalculation) { + $this->addFilter($searchCriteria, 'price_dynamic_algorithm', $priceRangeCalculation); + } + } + + /** + * Add filter to search criteria + * + * @param SearchCriteriaInterface $searchCriteria + * @param string $field + * @param mixed $value + */ + private function addFilter(SearchCriteriaInterface $searchCriteria, string $field, $value): void + { + $filter = $this->filterBuilder + ->setField($field) + ->setValue($value) + ->create(); + $this->filterGroupBuilder->addFilter($filter); + $filterGroups = $searchCriteria->getFilterGroups(); + $filterGroups[] = $this->filterGroupBuilder->create(); + $searchCriteria->setFilterGroups($filterGroups); + } + + /** + * Sort by relevance DESC by default + * + * @param SearchCriteriaInterface $searchCriteria + * @param bool $isSearch + */ + private function addDefaultSortOrder(SearchCriteriaInterface $searchCriteria, $isSearch = false): void + { + $sortField = $isSearch ? 'relevance' : EavAttributeInterface::POSITION; + $sortDirection = $isSearch ? SortOrder::SORT_DESC : SortOrder::SORT_ASC; + $defaultSortOrder = $this->sortOrderBuilder + ->setField($sortField) + ->setDirection($sortDirection) + ->create(); + + $searchCriteria->setSortOrders([$defaultSortOrder]); + } + + /** + * Format range filters so replacement works + * + * Range filter fields in search request must replace value like '%field.from%' or '%field.to%' + * + * @param SearchCriteriaInterface $searchCriteria + */ + private function updateRangeFilters(SearchCriteriaInterface $searchCriteria): void + { + $filterGroups = $searchCriteria->getFilterGroups(); + foreach ($filterGroups as $filterGroup) { + $filters = $filterGroup->getFilters(); + foreach ($filters as $filter) { + if (in_array($filter->getConditionType(), ['from', 'to'])) { + $filter->setField($filter->getField() . '.' . $filter->getConditionType()); + } + } + } + } +} diff --git a/app/code/Magento/CatalogGraphQl/Model/AggregationOptionTypeResolver.php b/app/code/Magento/CatalogGraphQl/Model/AggregationOptionTypeResolver.php new file mode 100644 index 0000000000000..3a532a1a6c760 --- /dev/null +++ b/app/code/Magento/CatalogGraphQl/Model/AggregationOptionTypeResolver.php @@ -0,0 +1,29 @@ +typeResolvers = $typeResolvers; + } + + /** + * @inheritdoc + */ + public function resolveType(array $data) : string + { + /** @var TypeResolverInterface $typeResolver */ + foreach ($this->typeResolvers as $typeResolver) { + $resolvedType = $typeResolver->resolveType($data); + if ($resolvedType) { + return $resolvedType; + } + } + throw new GraphQlInputException(__('Cannot resolve aggregation option type')); + } +} diff --git a/app/code/Magento/CatalogGraphQl/Model/Config/FilterAttributeReader.php b/app/code/Magento/CatalogGraphQl/Model/Config/FilterAttributeReader.php new file mode 100644 index 0000000000000..4f3a88cc788df --- /dev/null +++ b/app/code/Magento/CatalogGraphQl/Model/Config/FilterAttributeReader.php @@ -0,0 +1,134 @@ +mapper = $mapper; + $this->collectionFactory = $collectionFactory; + $this->exactMatchAttributes = array_merge($this->exactMatchAttributes, $exactMatchAttributes); + } + + /** + * Read configuration scope + * + * @param string|null $scope + * @return array + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function read($scope = null) : array + { + $typeNames = $this->mapper->getMappedTypes(self::ENTITY_TYPE); + $config = []; + + foreach ($this->getAttributeCollection() as $attribute) { + $attributeCode = $attribute->getAttributeCode(); + + foreach ($typeNames as $typeName) { + $config[$typeName]['fields'][$attributeCode] = [ + 'name' => $attributeCode, + 'type' => $this->getFilterType($attribute), + 'arguments' => [], + 'required' => false, + 'description' => sprintf('Attribute label: %s', $attribute->getDefaultFrontendLabel()) + ]; + } + } + + return $config; + } + + /** + * Map attribute type to filter type + * + * @param Attribute $attribute + * @return string + */ + private function getFilterType(Attribute $attribute): string + { + if (in_array($attribute->getAttributeCode(), $this->exactMatchAttributes)) { + return self::FILTER_EQUAL_TYPE; + } + + $filterTypeMap = [ + 'price' => self::FILTER_RANGE_TYPE, + 'date' => self::FILTER_RANGE_TYPE, + 'select' => self::FILTER_EQUAL_TYPE, + 'multiselect' => self::FILTER_EQUAL_TYPE, + 'boolean' => self::FILTER_EQUAL_TYPE, + 'text' => self::FILTER_MATCH_TYPE, + 'textarea' => self::FILTER_MATCH_TYPE, + ]; + + return $filterTypeMap[$attribute->getFrontendInput()] ?? self::FILTER_MATCH_TYPE; + } + + /** + * Create attribute collection + * + * @return Collection|\Magento\Catalog\Model\ResourceModel\Eav\Attribute[] + */ + private function getAttributeCollection() + { + return $this->collectionFactory->create() + ->addHasOptionsFilter() + ->addIsSearchableFilter() + ->addDisplayInAdvancedSearchFilter(); + } +} diff --git a/app/code/Magento/CatalogGraphQl/Model/Config/SortAttributeReader.php b/app/code/Magento/CatalogGraphQl/Model/Config/SortAttributeReader.php new file mode 100644 index 0000000000000..215b28be0579c --- /dev/null +++ b/app/code/Magento/CatalogGraphQl/Model/Config/SortAttributeReader.php @@ -0,0 +1,79 @@ +mapper = $mapper; + $this->attributesCollection = $attributesCollection; + } + + /** + * Read configuration scope + * + * @param string|null $scope + * @return array + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function read($scope = null) : array + { + $map = $this->mapper->getMappedTypes(self::ENTITY_TYPE); + $config =[]; + $attributes = $this->attributesCollection->addSearchableAttributeFilter()->addFilter('used_for_sort_by', 1); + /** @var \Magento\Catalog\Model\ResourceModel\Eav\Attribute $attribute */ + foreach ($attributes as $attribute) { + $attributeCode = $attribute->getAttributeCode(); + $attributeLabel = $attribute->getDefaultFrontendLabel(); + foreach ($map as $type) { + $config[$type]['fields'][$attributeCode] = [ + 'name' => $attributeCode, + 'type' => self::FIELD_TYPE, + 'arguments' => [], + 'required' => false, + 'description' => __('Attribute label: ') . $attributeLabel + ]; + } + } + + return $config; + } +} diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Aggregations.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Aggregations.php new file mode 100644 index 0000000000000..47a1d1f977f9b --- /dev/null +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Aggregations.php @@ -0,0 +1,68 @@ +filtersDataProvider = $filtersDataProvider; + $this->layerBuilder = $layerBuilder; + } + + /** + * @inheritdoc + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ) { + if (!isset($value['layer_type']) || !isset($value['search_result'])) { + return null; + } + + $aggregations = $value['search_result']->getSearchAggregation(); + + if ($aggregations) { + /** @var StoreInterface $store */ + $store = $context->getExtensionAttributes()->getStore(); + $storeId = (int)$store->getId(); + return $this->layerBuilder->build($aggregations, $storeId); + } else { + return []; + } + } +} diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Category/Products.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Category/Products.php index e0580213ddea7..abc5ae7e1da7f 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Category/Products.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Category/Products.php @@ -8,6 +8,9 @@ namespace Magento\CatalogGraphQl\Model\Resolver\Category; use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\CatalogGraphQl\DataProvider\Product\SearchCriteriaBuilder; +use Magento\CatalogGraphQl\Model\Resolver\Products\Query\Search; +use Magento\Framework\App\ObjectManager; use Magento\Framework\GraphQl\Config\Element\Field; use Magento\Framework\GraphQl\Query\ResolverInterface; use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; @@ -27,27 +30,46 @@ class Products implements ResolverInterface /** * @var Builder + * @deprecated */ private $searchCriteriaBuilder; /** * @var Filter + * @deprecated */ private $filterQuery; + /** + * @var Search + */ + private $searchQuery; + + /** + * @var SearchCriteriaBuilder + */ + private $searchApiCriteriaBuilder; + /** * @param ProductRepositoryInterface $productRepository * @param Builder $searchCriteriaBuilder * @param Filter $filterQuery + * @param Search $searchQuery + * @param SearchCriteriaBuilder $searchApiCriteriaBuilder */ public function __construct( ProductRepositoryInterface $productRepository, Builder $searchCriteriaBuilder, - Filter $filterQuery + Filter $filterQuery, + Search $searchQuery = null, + SearchCriteriaBuilder $searchApiCriteriaBuilder = null ) { $this->productRepository = $productRepository; $this->searchCriteriaBuilder = $searchCriteriaBuilder; $this->filterQuery = $filterQuery; + $this->searchQuery = $searchQuery ?? ObjectManager::getInstance()->get(Search::class); + $this->searchApiCriteriaBuilder = $searchApiCriteriaBuilder ?? + ObjectManager::getInstance()->get(SearchCriteriaBuilder::class); } /** @@ -60,21 +82,20 @@ public function resolve( array $value = null, array $args = null ) { - $args['filter'] = [ - 'category_id' => [ - 'eq' => $value['id'] - ] - ]; - $searchCriteria = $this->searchCriteriaBuilder->build($field->getName(), $args); if ($args['currentPage'] < 1) { throw new GraphQlInputException(__('currentPage value must be greater than 0.')); } if ($args['pageSize'] < 1) { throw new GraphQlInputException(__('pageSize value must be greater than 0.')); } - $searchCriteria->setCurrentPage($args['currentPage']); - $searchCriteria->setPageSize($args['pageSize']); - $searchResult = $this->filterQuery->getResult($searchCriteria, $info); + + $args['filter'] = [ + 'category_id' => [ + 'eq' => $value['id'] + ] + ]; + $searchCriteria = $this->searchApiCriteriaBuilder->build($args, false); + $searchResult = $this->searchQuery->getResult($searchCriteria, $info); //possible division by 0 if ($searchCriteria->getPageSize()) { diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/CategoryTree.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/CategoryTree.php index 89d3805383e1a..4284aed610848 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/CategoryTree.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/CategoryTree.php @@ -10,11 +10,11 @@ use Magento\Catalog\Model\Category; use Magento\CatalogGraphQl\Model\Resolver\Category\CheckCategoryIsActive; use Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider\ExtractDataFromCategoryTree; -use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; use Magento\Framework\GraphQl\Config\Element\Field; -use Magento\Framework\GraphQl\Exception\GraphQlInputException; use Magento\Framework\GraphQl\Exception\GraphQlNoSuchEntityException; use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider\CategoryTree as CategoryTreeDataProvider; /** * Category tree field resolver, used for GraphQL request processing. @@ -27,7 +27,7 @@ class CategoryTree implements ResolverInterface const CATEGORY_INTERFACE = 'CategoryInterface'; /** - * @var \Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider\CategoryTree + * @var CategoryTreeDataProvider */ private $categoryTree; @@ -42,12 +42,12 @@ class CategoryTree implements ResolverInterface private $checkCategoryIsActive; /** - * @param \Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider\CategoryTree $categoryTree + * @param CategoryTreeDataProvider $categoryTree * @param ExtractDataFromCategoryTree $extractDataFromCategoryTree * @param CheckCategoryIsActive $checkCategoryIsActive */ public function __construct( - \Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider\CategoryTree $categoryTree, + CategoryTreeDataProvider $categoryTree, ExtractDataFromCategoryTree $extractDataFromCategoryTree, CheckCategoryIsActive $checkCategoryIsActive ) { @@ -56,22 +56,6 @@ public function __construct( $this->checkCategoryIsActive = $checkCategoryIsActive; } - /** - * Get category id - * - * @param array $args - * @return int - * @throws GraphQlInputException - */ - private function getCategoryId(array $args) : int - { - if (!isset($args['id'])) { - throw new GraphQlInputException(__('"id for category should be specified')); - } - - return (int)$args['id']; - } - /** * @inheritdoc */ @@ -81,7 +65,9 @@ public function resolve(Field $field, $context, ResolveInfo $info, array $value return $value[$field->getName()]; } - $rootCategoryId = $this->getCategoryId($args); + $rootCategoryId = isset($args['id']) ? (int)$args['id'] : + (int)$context->getExtensionAttributes()->getStore()->getRootCategoryId(); + if ($rootCategoryId !== Category::TREE_ROOT_ID) { $this->checkCategoryIsActive->execute($rootCategoryId); } diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products.php index a75a9d2cf50a0..691f93e4148bc 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products.php @@ -16,6 +16,7 @@ use Magento\Framework\GraphQl\Query\Resolver\Argument\SearchCriteria\SearchFilter; use Magento\Framework\GraphQl\Query\ResolverInterface; use Magento\Catalog\Model\Layer\Resolver; +use Magento\CatalogGraphQl\DataProvider\Product\SearchCriteriaBuilder; /** * Products field resolver, used for GraphQL request processing. @@ -24,6 +25,7 @@ class Products implements ResolverInterface { /** * @var Builder + * @deprecated */ private $searchCriteriaBuilder; @@ -34,30 +36,41 @@ class Products implements ResolverInterface /** * @var Filter + * @deprecated */ private $filterQuery; /** * @var SearchFilter + * @deprecated */ private $searchFilter; + /** + * @var SearchCriteriaBuilder + */ + private $searchApiCriteriaBuilder; + /** * @param Builder $searchCriteriaBuilder * @param Search $searchQuery * @param Filter $filterQuery * @param SearchFilter $searchFilter + * @param SearchCriteriaBuilder|null $searchApiCriteriaBuilder */ public function __construct( Builder $searchCriteriaBuilder, Search $searchQuery, Filter $filterQuery, - SearchFilter $searchFilter + SearchFilter $searchFilter, + SearchCriteriaBuilder $searchApiCriteriaBuilder = null ) { $this->searchCriteriaBuilder = $searchCriteriaBuilder; $this->searchQuery = $searchQuery; $this->filterQuery = $filterQuery; $this->searchFilter = $searchFilter; + $this->searchApiCriteriaBuilder = $searchApiCriteriaBuilder ?? + \Magento\Framework\App\ObjectManager::getInstance()->get(SearchCriteriaBuilder::class); } /** @@ -70,40 +83,29 @@ public function resolve( array $value = null, array $args = null ) { - $searchCriteria = $this->searchCriteriaBuilder->build($field->getName(), $args); if ($args['currentPage'] < 1) { throw new GraphQlInputException(__('currentPage value must be greater than 0.')); } if ($args['pageSize'] < 1) { throw new GraphQlInputException(__('pageSize value must be greater than 0.')); } - $searchCriteria->setCurrentPage($args['currentPage']); - $searchCriteria->setPageSize($args['pageSize']); if (!isset($args['search']) && !isset($args['filter'])) { throw new GraphQlInputException( __("'search' or 'filter' input argument is required.") ); - } elseif (isset($args['search'])) { - $layerType = Resolver::CATALOG_LAYER_SEARCH; - $this->searchFilter->add($args['search'], $searchCriteria); - $searchResult = $this->searchQuery->getResult($searchCriteria, $info); - } else { - $layerType = Resolver::CATALOG_LAYER_CATEGORY; - $searchResult = $this->filterQuery->getResult($searchCriteria, $info); - } - //possible division by 0 - if ($searchCriteria->getPageSize()) { - $maxPages = ceil($searchResult->getTotalCount() / $searchCriteria->getPageSize()); - } else { - $maxPages = 0; } - $currentPage = $searchCriteria->getCurrentPage(); - if ($searchCriteria->getCurrentPage() > $maxPages && $searchResult->getTotalCount() > 0) { + //get product children fields queried + $productFields = (array)$info->getFieldSelection(1); + $includeAggregations = isset($productFields['filters']) || isset($productFields['aggregations']); + $searchCriteria = $this->searchApiCriteriaBuilder->build($args, $includeAggregations); + $searchResult = $this->searchQuery->getResult($searchCriteria, $info, $args); + + if ($searchResult->getCurrentPage() > $searchResult->getTotalPages() && $searchResult->getTotalCount() > 0) { throw new GraphQlInputException( __( 'currentPage value %1 specified is greater than the %2 page(s) available.', - [$currentPage, $maxPages] + [$searchResult->getCurrentPage(), $searchResult->getTotalPages()] ) ); } @@ -112,11 +114,12 @@ public function resolve( 'total_count' => $searchResult->getTotalCount(), 'items' => $searchResult->getProductsSearchResult(), 'page_info' => [ - 'page_size' => $searchCriteria->getPageSize(), - 'current_page' => $currentPage, - 'total_pages' => $maxPages + 'page_size' => $searchResult->getPageSize(), + 'current_page' => $searchResult->getCurrentPage(), + 'total_pages' => $searchResult->getTotalPages() ], - 'layer_type' => $layerType + 'search_result' => $searchResult, + 'layer_type' => isset($args['search']) ? Resolver::CATALOG_LAYER_SEARCH : Resolver::CATALOG_LAYER_CATEGORY, ]; return $data; diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/Product.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/Product.php index e5e0d1aea4285..2076ec6726988 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/Product.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/Product.php @@ -8,6 +8,7 @@ namespace Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider; use Magento\Catalog\Model\Product\Visibility; +use Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider\Product\CollectionPostProcessor; use Magento\Framework\Api\SearchCriteriaInterface; use Magento\Catalog\Model\ResourceModel\Product\CollectionFactory; use Magento\Catalog\Api\Data\ProductSearchResultsInterfaceFactory; @@ -32,7 +33,12 @@ class Product /** * @var CollectionProcessorInterface */ - private $collectionProcessor; + private $collectionPreProcessor; + + /** + * @var CollectionPostProcessor + */ + private $collectionPostProcessor; /** * @var Visibility @@ -44,17 +50,20 @@ class Product * @param ProductSearchResultsInterfaceFactory $searchResultsFactory * @param Visibility $visibility * @param CollectionProcessorInterface $collectionProcessor + * @param CollectionPostProcessor $collectionPostProcessor */ public function __construct( CollectionFactory $collectionFactory, ProductSearchResultsInterfaceFactory $searchResultsFactory, Visibility $visibility, - CollectionProcessorInterface $collectionProcessor + CollectionProcessorInterface $collectionProcessor, + CollectionPostProcessor $collectionPostProcessor ) { $this->collectionFactory = $collectionFactory; $this->searchResultsFactory = $searchResultsFactory; $this->visibility = $visibility; - $this->collectionProcessor = $collectionProcessor; + $this->collectionPreProcessor = $collectionProcessor; + $this->collectionPostProcessor = $collectionPostProcessor; } /** @@ -75,7 +84,7 @@ public function getList( /** @var \Magento\Catalog\Model\ResourceModel\Product\Collection $collection */ $collection = $this->collectionFactory->create(); - $this->collectionProcessor->process($collection, $searchCriteria, $attributes); + $this->collectionPreProcessor->process($collection, $searchCriteria, $attributes); if (!$isChildSearch) { $visibilityIds = $isSearch @@ -83,18 +92,9 @@ public function getList( : $this->visibility->getVisibleInCatalogIds(); $collection->setVisibility($visibilityIds); } - $collection->load(); - // Methods that perform extra fetches post-load - if (in_array('media_gallery_entries', $attributes)) { - $collection->addMediaGalleryData(); - } - if (in_array('media_gallery', $attributes)) { - $collection->addMediaGalleryData(); - } - if (in_array('options', $attributes)) { - $collection->addOptionsToResult(); - } + $collection->load(); + $this->collectionPostProcessor->process($collection, $attributes); $searchResult = $this->searchResultsFactory->create(); $searchResult->setSearchCriteria($searchCriteria); diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/Product/CollectionPostProcessor.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/Product/CollectionPostProcessor.php new file mode 100644 index 0000000000000..fadf22e7643af --- /dev/null +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/Product/CollectionPostProcessor.php @@ -0,0 +1,42 @@ +isLoaded()) { + $collection->load(); + } + // Methods that perform extra fetches post-load + if (in_array('media_gallery_entries', $attributeNames)) { + $collection->addMediaGalleryData(); + } + if (in_array('media_gallery', $attributeNames)) { + $collection->addMediaGalleryData(); + } + if (in_array('options', $attributeNames)) { + $collection->addOptionsToResult(); + } + + return $collection; + } +} diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/ProductSearch.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/ProductSearch.php new file mode 100644 index 0000000000000..ff845f4796763 --- /dev/null +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/ProductSearch.php @@ -0,0 +1,144 @@ +collectionFactory = $collectionFactory; + $this->searchResultsFactory = $searchResultsFactory; + $this->collectionPreProcessor = $collectionPreProcessor; + $this->collectionPostProcessor = $collectionPostProcessor; + $this->searchResultApplierFactory = $searchResultsApplierFactory; + } + + /** + * Get list of product data with full data set. Adds eav attributes to result set from passed in array + * + * @param SearchCriteriaInterface $searchCriteria + * @param SearchResultInterface $searchResult + * @param array $attributes + * @return SearchResultsInterface + */ + public function getList( + SearchCriteriaInterface $searchCriteria, + SearchResultInterface $searchResult, + array $attributes = [] + ): SearchResultsInterface { + /** @var Collection $collection */ + $collection = $this->collectionFactory->create(); + + //Join search results + $this->getSearchResultsApplier($searchResult, $collection, $this->getSortOrderArray($searchCriteria))->apply(); + + $this->collectionPreProcessor->process($collection, $searchCriteria, $attributes); + $collection->load(); + $this->collectionPostProcessor->process($collection, $attributes); + + $searchResults = $this->searchResultsFactory->create(); + $searchResults->setSearchCriteria($searchCriteria); + $searchResults->setItems($collection->getItems()); + $searchResults->setTotalCount($searchResult->getTotalCount()); + return $searchResults; + } + + /** + * Create searchResultApplier + * + * @param SearchResultInterface $searchResult + * @param Collection $collection + * @param array $orders + * @return SearchResultApplierInterface + */ + private function getSearchResultsApplier( + SearchResultInterface $searchResult, + Collection $collection, + array $orders + ): SearchResultApplierInterface { + return $this->searchResultApplierFactory->create( + [ + 'collection' => $collection, + 'searchResult' => $searchResult, + 'orders' => $orders + ] + ); + } + + /** + * Format sort orders into associative array + * + * E.g. ['field1' => 'DESC', 'field2' => 'ASC", ...] + * + * @param SearchCriteriaInterface $searchCriteria + * @return array + */ + private function getSortOrderArray(SearchCriteriaInterface $searchCriteria) + { + $ordersArray = []; + $sortOrders = $searchCriteria->getSortOrders(); + if (is_array($sortOrders)) { + foreach ($sortOrders as $sortOrder) { + $ordersArray[$sortOrder->getField()] = $sortOrder->getDirection(); + } + } + + return $ordersArray; + } +} diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/FilterArgument/ProductEntityAttributesForAst.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/FilterArgument/ProductEntityAttributesForAst.php index a547f63b217fe..973b8fbcd6b0f 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/FilterArgument/ProductEntityAttributesForAst.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/FilterArgument/ProductEntityAttributesForAst.php @@ -23,13 +23,15 @@ class ProductEntityAttributesForAst implements FieldEntityAttributesInterface private $config; /** + * Additional attributes that are not retrieved by getting fields from ProductInterface + * * @var array */ private $additionalAttributes = ['min_price', 'max_price', 'category_id']; /** * @param ConfigInterface $config - * @param array $additionalAttributes + * @param string[] $additionalAttributes */ public function __construct( ConfigInterface $config, @@ -40,7 +42,12 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc + * + * Gather all the product entity attributes that can be filtered by search criteria. + * Example format ['attributeNameInGraphQl' => ['type' => 'String'. 'fieldName' => 'attributeNameInSearchCriteria']] + * + * @return array */ public function getEntityAttributes() : array { @@ -55,14 +62,20 @@ public function getEntityAttributes() : array $configElement = $this->config->getConfigElement($interface['interface']); foreach ($configElement->getFields() as $field) { - $fields[$field->getName()] = 'String'; + $fields[$field->getName()] = [ + 'type' => 'String', + 'fieldName' => $field->getName(), + ]; } } - foreach ($this->additionalAttributes as $attribute) { - $fields[$attribute] = 'String'; + foreach ($this->additionalAttributes as $attributeName) { + $fields[$attributeName] = [ + 'type' => 'String', + 'fieldName' => $attributeName, + ]; } - return array_keys($fields); + return $fields; } } diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/Query/FieldSelection.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/Query/FieldSelection.php new file mode 100644 index 0000000000000..3912bab05ebbe --- /dev/null +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/Query/FieldSelection.php @@ -0,0 +1,93 @@ +fieldTranslator = $fieldTranslator; + } + + /** + * Get requested fields from products query + * + * @param ResolveInfo $resolveInfo + * @return string[] + */ + public function getProductsFieldSelection(ResolveInfo $resolveInfo): array + { + return $this->getProductFields($resolveInfo); + } + + /** + * Return field names for all requested product fields. + * + * @param ResolveInfo $info + * @return string[] + */ + private function getProductFields(ResolveInfo $info): array + { + $fieldNames = []; + foreach ($info->fieldNodes as $node) { + if ($node->name->value !== 'products') { + continue; + } + foreach ($node->selectionSet->selections as $selection) { + if ($selection->name->value !== 'items') { + continue; + } + $fieldNames[] = $this->collectProductFieldNames($selection, $fieldNames); + } + } + + $fieldNames = array_merge(...$fieldNames); + + return $fieldNames; + } + + /** + * Collect field names for each node in selection + * + * @param SelectionNode $selection + * @param array $fieldNames + * @return array + */ + private function collectProductFieldNames(SelectionNode $selection, array $fieldNames = []): array + { + foreach ($selection->selectionSet->selections as $itemSelection) { + if ($itemSelection->kind === 'InlineFragment') { + foreach ($itemSelection->selectionSet->selections as $inlineSelection) { + if ($inlineSelection->kind === 'InlineFragment') { + continue; + } + $fieldNames[] = $this->fieldTranslator->translate($inlineSelection->name->value); + } + continue; + } + $fieldNames[] = $this->fieldTranslator->translate($itemSelection->name->value); + } + + return $fieldNames; + } +} diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/Query/Filter.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/Query/Filter.php index 62e2f0c488c6c..cc25af44fdfbe 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/Query/Filter.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/Query/Filter.php @@ -12,7 +12,6 @@ use Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider\Product; use Magento\CatalogGraphQl\Model\Resolver\Products\SearchResult; use Magento\CatalogGraphQl\Model\Resolver\Products\SearchResultFactory; -use Magento\Framework\GraphQl\Query\FieldTranslator; /** * Retrieve filtered product data based off given search criteria in a format that GraphQL can interpret. @@ -30,31 +29,31 @@ class Filter private $productDataProvider; /** - * @var FieldTranslator + * @var \Magento\Catalog\Model\Layer\Resolver */ - private $fieldTranslator; + private $layerResolver; /** - * @var \Magento\Catalog\Model\Layer\Resolver + * FieldSelection */ - private $layerResolver; + private $fieldSelection; /** * @param SearchResultFactory $searchResultFactory * @param Product $productDataProvider * @param \Magento\Catalog\Model\Layer\Resolver $layerResolver - * @param FieldTranslator $fieldTranslator + * @param FieldSelection $fieldSelection */ public function __construct( SearchResultFactory $searchResultFactory, Product $productDataProvider, \Magento\Catalog\Model\Layer\Resolver $layerResolver, - FieldTranslator $fieldTranslator + FieldSelection $fieldSelection ) { $this->searchResultFactory = $searchResultFactory; $this->productDataProvider = $productDataProvider; - $this->fieldTranslator = $fieldTranslator; $this->layerResolver = $layerResolver; + $this->fieldSelection = $fieldSelection; } /** @@ -70,7 +69,7 @@ public function getResult( ResolveInfo $info, bool $isSearch = false ): SearchResult { - $fields = $this->getProductFields($info); + $fields = $this->fieldSelection->getProductsFieldSelection($info); $products = $this->productDataProvider->getList($searchCriteria, $fields, $isSearch); $productArray = []; /** @var \Magento\Catalog\Model\Product $product */ @@ -79,42 +78,11 @@ public function getResult( $productArray[$product->getId()]['model'] = $product; } - return $this->searchResultFactory->create($products->getTotalCount(), $productArray); - } - - /** - * Return field names for all requested product fields. - * - * @param ResolveInfo $info - * @return string[] - */ - private function getProductFields(ResolveInfo $info) : array - { - $fieldNames = []; - foreach ($info->fieldNodes as $node) { - if ($node->name->value !== 'products') { - continue; - } - foreach ($node->selectionSet->selections as $selection) { - if ($selection->name->value !== 'items') { - continue; - } - - foreach ($selection->selectionSet->selections as $itemSelection) { - if ($itemSelection->kind === 'InlineFragment') { - foreach ($itemSelection->selectionSet->selections as $inlineSelection) { - if ($inlineSelection->kind === 'InlineFragment') { - continue; - } - $fieldNames[] = $this->fieldTranslator->translate($inlineSelection->name->value); - } - continue; - } - $fieldNames[] = $this->fieldTranslator->translate($itemSelection->name->value); - } - } - } - - return $fieldNames; + return $this->searchResultFactory->create( + [ + 'totalCount' => $products->getTotalCount(), + 'productsSearchResult' => $productArray + ] + ); } } diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/Query/Search.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/Query/Search.php index bc40c664425ff..ef83cc6132ecc 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/Query/Search.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/Query/Search.php @@ -7,12 +7,13 @@ namespace Magento\CatalogGraphQl\Model\Resolver\Products\Query; +use Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider\ProductSearch; use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; use Magento\Framework\Api\Search\SearchCriteriaInterface; -use Magento\CatalogGraphQl\Model\Resolver\Products\SearchCriteria\Helper\Filter as FilterHelper; use Magento\CatalogGraphQl\Model\Resolver\Products\SearchResult; use Magento\CatalogGraphQl\Model\Resolver\Products\SearchResultFactory; use Magento\Search\Api\SearchInterface; +use Magento\Framework\Api\Search\SearchCriteriaInterfaceFactory; /** * Full text search for catalog using given search criteria. @@ -25,52 +26,52 @@ class Search private $search; /** - * @var FilterHelper + * @var SearchResultFactory */ - private $filterHelper; + private $searchResultFactory; /** - * @var Filter + * @var \Magento\Search\Model\Search\PageSizeProvider */ - private $filterQuery; + private $pageSizeProvider; /** - * @var SearchResultFactory + * @var SearchCriteriaInterfaceFactory */ - private $searchResultFactory; + private $searchCriteriaFactory; /** - * @var \Magento\Framework\EntityManager\MetadataPool + * @var FieldSelection */ - private $metadataPool; + private $fieldSelection; /** - * @var \Magento\Search\Model\Search\PageSizeProvider + * @var ProductSearch */ - private $pageSizeProvider; + private $productsProvider; /** * @param SearchInterface $search - * @param FilterHelper $filterHelper - * @param Filter $filterQuery * @param SearchResultFactory $searchResultFactory - * @param \Magento\Framework\EntityManager\MetadataPool $metadataPool * @param \Magento\Search\Model\Search\PageSizeProvider $pageSize + * @param SearchCriteriaInterfaceFactory $searchCriteriaFactory + * @param FieldSelection $fieldSelection + * @param ProductSearch $productsProvider */ public function __construct( SearchInterface $search, - FilterHelper $filterHelper, - Filter $filterQuery, SearchResultFactory $searchResultFactory, - \Magento\Framework\EntityManager\MetadataPool $metadataPool, - \Magento\Search\Model\Search\PageSizeProvider $pageSize + \Magento\Search\Model\Search\PageSizeProvider $pageSize, + SearchCriteriaInterfaceFactory $searchCriteriaFactory, + FieldSelection $fieldSelection, + ProductSearch $productsProvider ) { $this->search = $search; - $this->filterHelper = $filterHelper; - $this->filterQuery = $filterQuery; $this->searchResultFactory = $searchResultFactory; - $this->metadataPool = $metadataPool; $this->pageSizeProvider = $pageSize; + $this->searchCriteriaFactory = $searchCriteriaFactory; + $this->fieldSelection = $fieldSelection; + $this->productsProvider = $productsProvider; } /** @@ -81,11 +82,12 @@ public function __construct( * @return SearchResult * @throws \Exception */ - public function getResult(SearchCriteriaInterface $searchCriteria, ResolveInfo $info) : SearchResult - { - $idField = $this->metadataPool->getMetadata( - \Magento\Catalog\Api\Data\ProductInterface::class - )->getIdentifierField(); + public function getResult( + SearchCriteriaInterface $searchCriteria, + ResolveInfo $info + ): SearchResult { + $queryFields = $this->fieldSelection->getProductsFieldSelection($info); + $realPageSize = $searchCriteria->getPageSize(); $realCurrentPage = $searchCriteria->getCurrentPage(); // Current page must be set to 0 and page size to max for search to grab all ID's as temporary workaround @@ -94,64 +96,39 @@ public function getResult(SearchCriteriaInterface $searchCriteria, ResolveInfo $ $searchCriteria->setCurrentPage(0); $itemsResults = $this->search->search($searchCriteria); - $ids = []; - $searchIds = []; - foreach ($itemsResults->getItems() as $item) { - $ids[$item->getId()] = null; - $searchIds[] = $item->getId(); - } - - $filter = $this->filterHelper->generate($idField, 'in', $searchIds); - $searchCriteria = $this->filterHelper->remove($searchCriteria, 'search_term'); - $searchCriteria = $this->filterHelper->add($searchCriteria, $filter); - $searchResult = $this->filterQuery->getResult($searchCriteria, $info, true); - - $searchCriteria->setPageSize($realPageSize); - $searchCriteria->setCurrentPage($realCurrentPage); - $paginatedProducts = $this->paginateList($searchResult, $searchCriteria); - - $products = []; - if (!isset($searchCriteria->getSortOrders()[0])) { - foreach ($paginatedProducts as $product) { - if (in_array($product[$idField], $searchIds)) { - $ids[$product[$idField]] = $product; - } - } - $products = array_filter($ids); - } else { - foreach ($paginatedProducts as $product) { - $productId = isset($product['entity_id']) ? $product['entity_id'] : $product[$idField]; - if (in_array($productId, $searchIds)) { - $products[] = $product; - } - } - } + //Create copy of search criteria without conditions (conditions will be applied by joining search result) + $searchCriteriaCopy = $this->searchCriteriaFactory->create() + ->setSortOrders($searchCriteria->getSortOrders()) + ->setPageSize($realPageSize) + ->setCurrentPage($realCurrentPage); - return $this->searchResultFactory->create($searchResult->getTotalCount(), $products); - } + $searchResults = $this->productsProvider->getList($searchCriteriaCopy, $itemsResults, $queryFields); - /** - * Paginate an array of Ids that get pulled back in search based off search criteria and total count. - * - * @param SearchResult $searchResult - * @param SearchCriteriaInterface $searchCriteria - * @return int[] - */ - private function paginateList(SearchResult $searchResult, SearchCriteriaInterface $searchCriteria) : array - { - $length = $searchCriteria->getPageSize(); - // Search starts pages from 0 - $offset = $length * ($searchCriteria->getCurrentPage() - 1); - - if ($searchCriteria->getPageSize()) { - $maxPages = ceil($searchResult->getTotalCount() / $searchCriteria->getPageSize()); + //possible division by 0 + if ($realPageSize) { + $maxPages = (int)ceil($searchResults->getTotalCount() / $realPageSize); } else { $maxPages = 0; } + $searchCriteria->setPageSize($realPageSize); + $searchCriteria->setCurrentPage($realCurrentPage); - if ($searchCriteria->getCurrentPage() > $maxPages && $searchResult->getTotalCount() > 0) { - $offset = (int)$maxPages; + $productArray = []; + /** @var \Magento\Catalog\Model\Product $product */ + foreach ($searchResults->getItems() as $product) { + $productArray[$product->getId()] = $product->getData(); + $productArray[$product->getId()]['model'] = $product; } - return array_slice($searchResult->getProductsSearchResult(), $offset, $length); + + return $this->searchResultFactory->create( + [ + 'totalCount' => $searchResults->getTotalCount(), + 'productsSearchResult' => $productArray, + 'searchAggregation' => $itemsResults->getAggregations(), + 'pageSize' => $realPageSize, + 'currentPage' => $realCurrentPage, + 'totalPages' => $maxPages, + ] + ); } } diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/SearchResult.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/SearchResult.php index 6e229bdc38a31..e4a137413b4c5 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/SearchResult.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/SearchResult.php @@ -7,31 +7,21 @@ namespace Magento\CatalogGraphQl\Model\Resolver\Products; -use Magento\Framework\Api\SearchResultsInterface; +use Magento\Framework\Api\Search\AggregationInterface; /** * Container for a product search holding the item result and the array in the GraphQL-readable product type format. */ class SearchResult { - /** - * @var SearchResultsInterface - */ - private $totalCount; - - /** - * @var array - */ - private $productsSearchResult; + private $data; /** - * @param int $totalCount - * @param array $productsSearchResult + * @param array $data */ - public function __construct(int $totalCount, array $productsSearchResult) + public function __construct(array $data) { - $this->totalCount = $totalCount; - $this->productsSearchResult = $productsSearchResult; + $this->data = $data; } /** @@ -41,7 +31,7 @@ public function __construct(int $totalCount, array $productsSearchResult) */ public function getTotalCount() : int { - return $this->totalCount; + return $this->data['totalCount'] ?? 0; } /** @@ -51,6 +41,46 @@ public function getTotalCount() : int */ public function getProductsSearchResult() : array { - return $this->productsSearchResult; + return $this->data['productsSearchResult'] ?? []; + } + + /** + * Retrieve aggregated search results + * + * @return AggregationInterface|null + */ + public function getSearchAggregation(): ?AggregationInterface + { + return $this->data['searchAggregation'] ?? null; + } + + /** + * Retrieve the page size for the search + * + * @return int + */ + public function getPageSize(): int + { + return $this->data['pageSize'] ?? 0; + } + + /** + * Retrieve the current page for the search + * + * @return int + */ + public function getCurrentPage(): int + { + return $this->data['currentPage'] ?? 0; + } + + /** + * Retrieve total pages for the search + * + * @return int + */ + public function getTotalPages(): int + { + return $this->data['totalPages'] ?? 0; } } diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/SearchResultFactory.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/SearchResultFactory.php index aec9362f47c3a..479e6a3f96235 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/SearchResultFactory.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/SearchResultFactory.php @@ -30,15 +30,15 @@ public function __construct(ObjectManagerInterface $objectManager) /** * Instantiate SearchResult * - * @param int $totalCount - * @param array $productsSearchResult + * @param array $data * @return SearchResult */ - public function create(int $totalCount, array $productsSearchResult) : SearchResult - { + public function create( + array $data + ): SearchResult { return $this->objectManager->create( SearchResult::class, - ['totalCount' => $totalCount, 'productsSearchResult' => $productsSearchResult] + ['data' => $data] ); } } diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/RootCategoryId.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/RootCategoryId.php new file mode 100644 index 0000000000000..4b3e0a1a58dfd --- /dev/null +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/RootCategoryId.php @@ -0,0 +1,26 @@ +getExtensionAttributes()->getStore()->getRootCategoryId(); + } +} diff --git a/app/code/Magento/CatalogGraphQl/Plugin/Search/Request/ConfigReader.php b/app/code/Magento/CatalogGraphQl/Plugin/Search/Request/ConfigReader.php new file mode 100644 index 0000000000000..992ab50467c72 --- /dev/null +++ b/app/code/Magento/CatalogGraphQl/Plugin/Search/Request/ConfigReader.php @@ -0,0 +1,286 @@ +generatorResolver = $generatorResolver; + $this->productAttributeCollectionFactory = $productAttributeCollectionFactory; + $this->exactMatchAttributes = array_merge($this->exactMatchAttributes, $exactMatchAttributes); + } + + /** + * Merge reader's value with generated + * + * @param \Magento\Framework\Config\ReaderInterface $subject + * @param array $result + * @return array + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function afterRead( + \Magento\Framework\Config\ReaderInterface $subject, + array $result + ) { + $searchRequestNameWithAggregation = $this->generateRequest(); + $searchRequest = $searchRequestNameWithAggregation; + $searchRequest['queries'][$this->requestName] = $searchRequest['queries'][$this->requestNameWithAggregation]; + unset($searchRequest['queries'][$this->requestNameWithAggregation], $searchRequest['aggregations']); + + return array_merge_recursive( + $result, + [ + $this->requestNameWithAggregation => $searchRequestNameWithAggregation, + $this->requestName => $searchRequest, + ] + ); + } + + /** + * Retrieve searchable attributes + * + * @return Attribute[] + */ + private function getSearchableAttributes(): array + { + $attributes = []; + /** @var Collection $productAttributes */ + $productAttributes = $this->productAttributeCollectionFactory->create(); + $productAttributes->addFieldToFilter( + ['is_searchable', 'is_visible_in_advanced_search', 'is_filterable', 'is_filterable_in_search'], + [1, 1, [1, 2], 1] + ); + + /** @var Attribute $attribute */ + foreach ($productAttributes->getItems() as $attribute) { + $attributes[$attribute->getAttributeCode()] = $attribute; + } + + return $attributes; + } + + /** + * Generate search request for search products via GraphQL + * + * @return array + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + */ + private function generateRequest() + { + $request = []; + foreach ($this->getSearchableAttributes() as $attribute) { + if (\in_array($attribute->getAttributeCode(), ['price', 'visibility', 'category_ids'])) { + //some fields have special semantics + continue; + } + $queryName = $attribute->getAttributeCode() . '_query'; + $filterName = $attribute->getAttributeCode() . RequestGenerator::FILTER_SUFFIX; + $request['queries'][$this->requestNameWithAggregation]['queryReference'][] = [ + 'clause' => 'must', + 'ref' => $queryName, + ]; + + switch ($attribute->getBackendType()) { + case 'static': + case 'text': + case 'varchar': + if ($this->isExactMatchAttribute($attribute)) { + $request['queries'][$queryName] = $this->generateFilterQuery($queryName, $filterName); + $request['filters'][$filterName] = $this->generateTermFilter($filterName, $attribute); + } else { + $request['queries'][$queryName] = $this->generateMatchQuery($queryName, $attribute); + } + break; + case 'decimal': + case 'datetime': + case 'date': + $request['queries'][$queryName] = $this->generateFilterQuery($queryName, $filterName); + $request['filters'][$filterName] = $this->generateRangeFilter($filterName, $attribute); + break; + default: + $request['queries'][$queryName] = $this->generateFilterQuery($queryName, $filterName); + $request['filters'][$filterName] = $this->generateTermFilter($filterName, $attribute); + } + $generator = $this->generatorResolver->getGeneratorForType($attribute->getBackendType()); + + if ($attribute->getData(EavAttributeInterface::IS_FILTERABLE)) { + $bucketName = $attribute->getAttributeCode() . self::BUCKET_SUFFIX; + $request['aggregations'][$bucketName] = $generator->getAggregationData($attribute, $bucketName); + } + + $this->addSearchAttributeToFullTextSearch($attribute, $request); + } + + return $request; + } + + /** + * Add attribute with specified boost to "search" query used in full text search + * + * @param Attribute $attribute + * @param array $request + * @return void + */ + private function addSearchAttributeToFullTextSearch(Attribute $attribute, &$request): void + { + // Match search by custom price attribute isn't supported + if ($attribute->getFrontendInput() !== 'price') { + $request['queries']['search']['match'][] = [ + 'field' => $attribute->getAttributeCode(), + 'boost' => $attribute->getSearchWeight() ?: 1, + ]; + } + } + + /** + * Return array representation of range filter + * + * @param string $filterName + * @param Attribute $attribute + * @return array + */ + private function generateRangeFilter(string $filterName, Attribute $attribute) + { + return [ + 'field' => $attribute->getAttributeCode(), + 'name' => $filterName, + 'type' => FilterInterface::TYPE_RANGE, + 'from' => '$' . $attribute->getAttributeCode() . '.from$', + 'to' => '$' . $attribute->getAttributeCode() . '.to$', + ]; + } + + /** + * Return array representation of term filter + * + * @param string $filterName + * @param Attribute $attribute + * @return array + */ + private function generateTermFilter(string $filterName, Attribute $attribute) + { + return [ + 'type' => FilterInterface::TYPE_TERM, + 'name' => $filterName, + 'field' => $attribute->getAttributeCode(), + 'value' => '$' . $attribute->getAttributeCode() . '$', + ]; + } + + /** + * Return array representation of query based on filter + * + * @param string $queryName + * @param string $filterName + * @return array + */ + private function generateFilterQuery(string $queryName, string $filterName) + { + return [ + 'name' => $queryName, + 'type' => QueryInterface::TYPE_FILTER, + 'filterReference' => [ + [ + 'ref' => $filterName, + ], + ], + ]; + } + + /** + * Return array representation of match query + * + * @param string $queryName + * @param Attribute $attribute + * @return array + */ + private function generateMatchQuery(string $queryName, Attribute $attribute) + { + return [ + 'name' => $queryName, + 'type' => 'matchQuery', + 'value' => '$' . $attribute->getAttributeCode() . '$', + 'match' => [ + [ + 'field' => $attribute->getAttributeCode(), + 'boost' => $attribute->getSearchWeight() ?: 1, + ], + ], + ]; + } + + /** + * Check if attribute's filter should use exact match + * + * @param Attribute $attribute + * @return bool + */ + private function isExactMatchAttribute(Attribute $attribute) + { + if (in_array($attribute->getFrontendInput(), ['select', 'multiselect'])) { + return true; + } + if (in_array($attribute->getAttributeCode(), $this->exactMatchAttributes)) { + return true; + } + + return false; + } +} diff --git a/app/code/Magento/CatalogGraphQl/composer.json b/app/code/Magento/CatalogGraphQl/composer.json index 13fcbe9a7d357..1582f29c25951 100644 --- a/app/code/Magento/CatalogGraphQl/composer.json +++ b/app/code/Magento/CatalogGraphQl/composer.json @@ -10,6 +10,7 @@ "magento/module-search": "*", "magento/module-store": "*", "magento/module-eav-graph-ql": "*", + "magento/module-catalog-search": "*", "magento/framework": "*" }, "suggest": { diff --git a/app/code/Magento/CatalogGraphQl/etc/di.xml b/app/code/Magento/CatalogGraphQl/etc/di.xml index a5006355ed265..485ae792193e3 100644 --- a/app/code/Magento/CatalogGraphQl/etc/di.xml +++ b/app/code/Magento/CatalogGraphQl/etc/di.xml @@ -19,6 +19,8 @@ Magento\CatalogGraphQl\Model\Config\AttributeReader Magento\CatalogGraphQl\Model\Config\CategoryAttributeReader + Magento\CatalogGraphQl\Model\Config\SortAttributeReader + Magento\CatalogGraphQl\Model\Config\FilterAttributeReader @@ -55,4 +57,16 @@ Magento\Catalog\Model\Api\SearchCriteria\ProductCollectionProcessor + + + + + sku + + + + + + + diff --git a/app/code/Magento/CatalogGraphQl/etc/graphql/di.xml b/app/code/Magento/CatalogGraphQl/etc/graphql/di.xml index 2292004f3cf01..fe3413dc3b218 100644 --- a/app/code/Magento/CatalogGraphQl/etc/graphql/di.xml +++ b/app/code/Magento/CatalogGraphQl/etc/graphql/di.xml @@ -28,6 +28,13 @@ + + + + Magento\CatalogGraphQl\Model\AggregationOptionTypeResolver + + + @@ -48,6 +55,12 @@ CustomizableRadioOption CustomizableCheckboxOption + + ProductAttributeSortInput + + + ProductAttributeFilterInput + @@ -95,4 +108,14 @@ + + + + + Magento\CatalogGraphQl\DataProvider\Product\LayeredNavigation\Builder\Price + Magento\CatalogGraphQl\DataProvider\Product\LayeredNavigation\Builder\Category + Magento\CatalogGraphQl\DataProvider\Product\LayeredNavigation\Builder\Attribute + + + diff --git a/app/code/Magento/CatalogGraphQl/etc/schema.graphqls b/app/code/Magento/CatalogGraphQl/etc/schema.graphqls index ea56faf94408e..76a58857cebc2 100644 --- a/app/code/Magento/CatalogGraphQl/etc/schema.graphqls +++ b/app/code/Magento/CatalogGraphQl/etc/schema.graphqls @@ -4,10 +4,10 @@ type Query { products ( search: String @doc(description: "Performs a full-text search using the specified key words."), - filter: ProductFilterInput @doc(description: "Identifies which product attributes to search for and return."), + filter: ProductAttributeFilterInput @doc(description: "Identifies which product attributes to search for and return."), pageSize: Int = 20 @doc(description: "Specifies the maximum number of results to return at once. This attribute is optional."), currentPage: Int = 1 @doc(description: "Specifies which page of results to return. The default value is 1."), - sort: ProductSortInput @doc(description: "Specifies which attribute to sort on, and whether to return the results in ascending or descending order.") + sort: ProductAttributeSortInput @doc(description: "Specifies which attributes to sort on, and whether to return the results in ascending or descending order.") ): Products @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Products") @doc(description: "The products query searches for products that match the criteria specified in the search and filter attributes.") @cache(cacheIdentity: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\Identity") category ( @@ -221,7 +221,7 @@ interface CategoryInterface @typeResolver(class: "Magento\\CatalogGraphQl\\Model products( pageSize: Int = 20 @doc(description: "Specifies the maximum number of results to return at once. This attribute is optional."), currentPage: Int = 1 @doc(description: "Specifies which page of results to return. The default value is 1."), - sort: ProductSortInput @doc(description: "Specifies which attribute to sort on, and whether to return the results in ascending or descending order.") + sort: ProductAttributeSortInput @doc(description: "Specifies which attributes to sort on, and whether to return the results in ascending or descending order.") ): CategoryProducts @doc(description: "The list of products assigned to the category.") @cache(cacheIdentity: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\Identity") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Category\\Products") breadcrumbs: [Breadcrumb] @doc(description: "Breadcrumbs, parent categories info.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Category\\Breadcrumbs") } @@ -270,7 +270,8 @@ type Products @doc(description: "The Products object is the top-level object ret items: [ProductInterface] @doc(description: "An array of products that match the specified search criteria.") page_info: SearchResultPageInfo @doc(description: "An object that includes the page_info and currentPage values specified in the query.") total_count: Int @doc(description: "The number of products returned.") - filters: [LayerFilter] @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\LayerFilters") @doc(description: "Layered navigation filters array.") + filters: [LayerFilter] @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\LayerFilters") @doc(description: "Layered navigation filters array.") @deprecated(reason: "Use aggregations instead") + aggregations: [Aggregation] @doc(description: "Layered navigation aggregations.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Aggregations") sort_fields: SortFields @doc(description: "An object that includes the default sort field and all available sort fields.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Category\\SortFields") } @@ -280,7 +281,11 @@ type CategoryProducts @doc(description: "The category products object returned i total_count: Int @doc(description: "The number of products returned.") } -input ProductFilterInput @doc(description: "ProductFilterInput defines the filters to be used in the search. A filter contains at least one attribute, a comparison operator, and the value that is being searched for.") { +input ProductAttributeFilterInput @doc(description: "ProductAttributeFilterInput defines the filters to be used in the search. A filter contains at least one attribute, a comparison operator, and the value that is being searched for.") { + category_id: FilterEqualTypeInput @doc(description: "Filter product by category id") +} + +input ProductFilterInput @doc(description: "ProductFilterInput is deprecated, use @ProductAttributeFilterInput instead. ProductFilterInput defines the filters to be used in the search. A filter contains at least one attribute, a comparison operator, and the value that is being searched for.") { name: FilterTypeInput @doc(description: "The product name. Customers use this name to identify the product.") sku: FilterTypeInput @doc(description: "A number or code assigned to a product to identify the product, options, price, and manufacturer.") description: FilterTypeInput @doc(description: "Detailed information about the product. The value can include simple HTML tags.") @@ -333,7 +338,7 @@ type ProductMediaGalleryEntriesVideoContent @doc(description: "ProductMediaGalle video_metadata: String @doc(description: "Optional data about the video.") } -input ProductSortInput @doc(description: "ProductSortInput specifies the attribute to use for sorting search results and indicates whether the results are sorted in ascending or descending order.") { +input ProductSortInput @doc(description: "ProductSortInput is deprecated, use @ProductAttributeSortInput instead. ProductSortInput specifies the attribute to use for sorting search results and indicates whether the results are sorted in ascending or descending order.") { name: SortEnum @doc(description: "The product name. Customers use this name to identify the product.") sku: SortEnum @doc(description: "A number or code assigned to a product to identify the product, options, price, and manufacturer.") description: SortEnum @doc(description: "Detailed information about the product. The value can include simple HTML tags.") @@ -367,6 +372,12 @@ input ProductSortInput @doc(description: "ProductSortInput specifies the attribu gift_message_available: SortEnum @doc(description: "Indicates whether a gift message is available.") } +input ProductAttributeSortInput @doc(description: "ProductAttributeSortInput specifies the attribute to use for sorting search results and indicates whether the results are sorted in ascending or descending order. It's possible to sort products using searchable attributes with enabled 'Use in Filter Options' option") +{ + relevance: SortEnum @doc(description: "Sort by the search relevance score (default).") + position: SortEnum @doc(description: "Sort by the position assigned to each product.") +} + type MediaGalleryEntry @doc(description: "MediaGalleryEntry defines characteristics about images and videos associated with a specific product.") { id: Int @doc(description: "The identifier assigned to the object.") media_type: String @doc(description: "image or video.") @@ -380,22 +391,39 @@ type MediaGalleryEntry @doc(description: "MediaGalleryEntry defines characterist } type LayerFilter { - name: String @doc(description: "Layered navigation filter name.") - request_var: String @doc(description: "Request variable name for filter query.") - filter_items_count: Int @doc(description: "Count of filter items in filter group.") - filter_items: [LayerFilterItemInterface] @doc(description: "Array of filter items.") + name: String @doc(description: "Layered navigation filter name.") @deprecated(reason: "Use Aggregation.label instead.") + request_var: String @doc(description: "Request variable name for filter query.") @deprecated(reason: "Use Aggregation.attribute_code instead.") + filter_items_count: Int @doc(description: "Count of filter items in filter group.") @deprecated(reason: "Use Aggregation.count instead.") + filter_items: [LayerFilterItemInterface] @doc(description: "Array of filter items.") @deprecated(reason: "Use Aggregation.options instead.") } interface LayerFilterItemInterface @typeResolver(class: "Magento\\CatalogGraphQl\\Model\\LayerFilterItemTypeResolverComposite") { - label: String @doc(description: "Filter label.") - value_string: String @doc(description: "Value for filter request variable to be used in query.") - items_count: Int @doc(description: "Count of items by filter.") + label: String @doc(description: "Filter label.") @deprecated(reason: "Use AggregationOption.label instead.") + value_string: String @doc(description: "Value for filter request variable to be used in query.") @deprecated(reason: "Use AggregationOption.value instead.") + items_count: Int @doc(description: "Count of items by filter.") @deprecated(reason: "Use AggregationOption.count instead.") } type LayerFilterItem implements LayerFilterItemInterface { } +type Aggregation @doc(description: "A bucket that contains information for each filterable option (such as price, category ID, and custom attributes).") { + count: Int @doc(description: "The number of options in the aggregation group.") + label: String @doc(description: "The aggregation display name.") + attribute_code: String! @doc(description: "Attribute code of the aggregation group.") + options: [AggregationOption] @doc(description: "Array of options for the aggregation.") +} + +interface AggregationOptionInterface @typeResolver(class: "Magento\\CatalogGraphQl\\Model\\AggregationOptionTypeResolverComposite") { + count: Int @doc(description: "The number of items that match the aggregation option.") + label: String @doc(description: "Aggregation option display label.") + value: String! @doc(description: "The internal ID that represents the value of the option.") +} + +type AggregationOption implements AggregationOptionInterface { + +} + type SortField { value: String @doc(description: "Attribute code of sort field.") label: String @doc(description: "Label of sort field.") @@ -416,6 +444,7 @@ type StoreConfig @doc(description: "The type contains information about a store grid_per_page : Int @doc(description: "Products per Page on Grid Default Value.") list_per_page : Int @doc(description: "Products per Page on List Default Value.") catalog_default_sort_by : String @doc(description: "Default Sort By.") + root_category_id: Int @doc(description: "The ID of the root category") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\RootCategoryId") } type ProductVideo @doc(description: "Contains information about a product video.") implements MediaGalleryInterface { diff --git a/app/code/Magento/CatalogGraphQl/etc/search_request.xml b/app/code/Magento/CatalogGraphQl/etc/search_request.xml new file mode 100644 index 0000000000000..ab1eea9eb6fda --- /dev/null +++ b/app/code/Magento/CatalogGraphQl/etc/search_request.xml @@ -0,0 +1,90 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0 + 10000 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0 + 10000 + + diff --git a/app/code/Magento/CatalogImportExport/Model/Import/Product.php b/app/code/Magento/CatalogImportExport/Model/Import/Product.php index 5c083d421f0e1..4ff995c2a872c 100644 --- a/app/code/Magento/CatalogImportExport/Model/Import/Product.php +++ b/app/code/Magento/CatalogImportExport/Model/Import/Product.php @@ -1192,8 +1192,10 @@ protected function _initTypeModels() if ($model->isSuitable()) { $this->_productTypeModels[$productTypeName] = $model; } + // phpcs:disable Magento2.Performance.ForeachArrayMerge.ForeachArrayMerge $this->_fieldsMap = array_merge($this->_fieldsMap, $model->getCustomFieldsMapping()); $this->_specialAttributes = array_merge($this->_specialAttributes, $model->getParticularAttributes()); + // phpcs:enable } $this->_initErrorTemplates(); // remove doubles @@ -2972,6 +2974,10 @@ private function formatStockDataForRow(array $rowData): array $stockItemDo = $this->stockRegistry->getStockItem($row['product_id'], $row['website_id']); $existStockData = $stockItemDo->getData(); + if (isset($rowData['qty']) && $rowData['qty'] == 0 && !isset($rowData['is_in_stock'])) { + $rowData['is_in_stock'] = 0; + } + $row = array_merge( $this->defaultStockData, array_intersect_key($existStockData, $this->defaultStockData), diff --git a/app/code/Magento/CatalogImportExport/Model/Import/Product/MediaGalleryProcessor.php b/app/code/Magento/CatalogImportExport/Model/Import/Product/MediaGalleryProcessor.php index d43dc11a68fcf..00e6da0ebe077 100644 --- a/app/code/Magento/CatalogImportExport/Model/Import/Product/MediaGalleryProcessor.php +++ b/app/code/Magento/CatalogImportExport/Model/Import/Product/MediaGalleryProcessor.php @@ -284,6 +284,42 @@ private function initMediaGalleryResources() } } + /** + * Get the last media position for each product from the given list + * + * @param int $storeId + * @param array $productIds + * @return array + */ + private function getLastMediaPositionPerProduct(int $storeId, array $productIds): array + { + $result = []; + if ($productIds) { + $productKeyName = $this->getProductEntityLinkField(); + // this result could be achieved by using GROUP BY. But there is no index on position column, therefore + // it can be slower than the implementation below + $positions = $this->connection->fetchAll( + $this->connection + ->select() + ->from($this->mediaGalleryValueTableName, [$productKeyName, 'position']) + ->where("$productKeyName IN (?)", $productIds) + ->where('value_id is not null') + ->where('store_id = ?', $storeId) + ); + // Make sure the result contains all product ids even if the product has no media files + $result = array_fill_keys($productIds, 0); + // Find the largest position for each product + foreach ($positions as $record) { + $productId = $record[$productKeyName]; + $result[$productId] = $result[$productId] < $record['position'] + ? $record['position'] + : $result[$productId]; + } + } + + return $result; + } + /** * Save media gallery data per store. * @@ -301,24 +337,30 @@ private function processMediaPerStore( ) { $multiInsertData = []; $dataForSkinnyTable = []; + $lastMediaPositionPerProduct = $this->getLastMediaPositionPerProduct( + $storeId, + array_unique(array_merge(...array_values($valueToProductId))) + ); + foreach ($mediaGalleryData as $mediaGalleryRows) { foreach ($mediaGalleryRows as $insertValue) { - foreach ($newMediaValues as $value_id => $values) { + foreach ($newMediaValues as $valueId => $values) { if ($values['value'] == $insertValue['value']) { - $insertValue['value_id'] = $value_id; + $insertValue['value_id'] = $valueId; $insertValue[$this->getProductEntityLinkField()] = array_shift($valueToProductId[$values['value']]); - unset($newMediaValues[$value_id]); + unset($newMediaValues[$valueId]); break; } } if (isset($insertValue['value_id'])) { + $productId = $insertValue[$this->getProductEntityLinkField()]; $valueArr = [ 'value_id' => $insertValue['value_id'], 'store_id' => $storeId, - $this->getProductEntityLinkField() => $insertValue[$this->getProductEntityLinkField()], + $this->getProductEntityLinkField() => $productId, 'label' => $insertValue['label'], - 'position' => $insertValue['position'], + 'position' => $lastMediaPositionPerProduct[$productId] + $insertValue['position'], 'disabled' => $insertValue['disabled'], ]; $multiInsertData[] = $valueArr; diff --git a/app/code/Magento/CatalogImportExport/Model/Import/Product/Option.php b/app/code/Magento/CatalogImportExport/Model/Import/Product/Option.php index 6cdafa7fc6f5a..4d8088a235402 100644 --- a/app/code/Magento/CatalogImportExport/Model/Import/Product/Option.php +++ b/app/code/Magento/CatalogImportExport/Model/Import/Product/Option.php @@ -684,7 +684,12 @@ protected function _getNewOptionsWithTheSameTitlesErrorRows(array $sourceProduct ksort($outerTitles); ksort($innerTitles); if ($outerTitles === $innerTitles) { - $errorRows = array_merge($errorRows, $innerData['rows'], $outerData['rows']); + foreach ($innerData['rows'] as $innerDataRow) { + $errorRows[] = $innerDataRow; + } + foreach ($outerData['rows'] as $outerDataRow) { + $errorRows[] = $outerDataRow; + } } } } @@ -719,7 +724,9 @@ protected function _findOldOptionsWithTheSameTitles() } } if ($optionsCount > 1) { - $errorRows = array_merge($errorRows, $outerData['rows']); + foreach ($outerData['rows'] as $dataRow) { + $errorRows[] = $dataRow; + } } } } @@ -747,7 +754,9 @@ protected function _findNewOldOptionsTypeMismatch() ksort($outerTitles); ksort($innerTitles); if ($outerTitles === $innerTitles && $outerData['type'] != $innerData['type']) { - $errorRows = array_merge($errorRows, $outerData['rows']); + foreach ($outerData['rows'] as $dataRow) { + $errorRows[] = $dataRow; + } } } } @@ -959,8 +968,10 @@ public function validateRow(array $rowData, $rowNumber) $multiRowData = $this->_getMultiRowFormat($rowData); - foreach ($multiRowData as $optionData) { - $combinedData = array_merge($rowData, $optionData); + foreach ($multiRowData as $combinedData) { + foreach ($rowData as $key => $field) { + $combinedData[$key] = $field; + } if ($this->_isRowWithCustomOption($combinedData)) { if ($this->_isMainOptionRow($combinedData)) { @@ -1109,15 +1120,15 @@ protected function _getMultiRowFormat($rowData) foreach ($rowData['custom_options'] as $name => $customOption) { $i++; foreach ($customOption as $rowOrder => $optionRow) { - $row = array_merge( - [ - self::COLUMN_STORE => '', - self::COLUMN_TITLE => $name, - self::COLUMN_SORT_ORDER => $i, - self::COLUMN_ROW_SORT => $rowOrder - ], - $this->processOptionRow($name, $optionRow) - ); + $row = [ + self::COLUMN_STORE => '', + self::COLUMN_TITLE => $name, + self::COLUMN_SORT_ORDER => $i, + self::COLUMN_ROW_SORT => $rowOrder + ]; + foreach ($this->processOptionRow($name, $optionRow) as $key => $value) { + $row[$key] = $value; + } $name = ''; $multiRow[] = $row; } @@ -1215,6 +1226,8 @@ private function addFileOptions($result, $optionRow) * * @return boolean * @SuppressWarnings(PHPMD.CyclomaticComplexity) + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + * @SuppressWarnings(PHPMD.NPathComplexity) */ protected function _importData() { @@ -1256,9 +1269,11 @@ protected function _importData() $optionsToRemove[] = $this->_rowProductId; } } - foreach ($multiRowData as $optionData) { - $combinedData = array_merge($rowData, $optionData); + foreach ($multiRowData as $combinedData) { + foreach ($rowData as $key => $field) { + $combinedData[$key] = $field; + } if (!$this->isRowAllowedToImport($combinedData, $rowNumber) || !$this->_parseRequiredData($combinedData) ) { @@ -1441,7 +1456,11 @@ protected function _collectOptionMainData( if (!$this->_isRowHasSpecificType($this->_rowType) && ($priceData = $this->_getPriceData($rowData, $nextOptionId, $this->_rowType)) ) { - $prices[$nextOptionId] = $priceData; + if ($this->_isPriceGlobal) { + $prices[$nextOptionId][Store::DEFAULT_STORE_ID] = $priceData; + } else { + $prices[$nextOptionId][$this->_rowStoreId] = $priceData; + } } if (!isset($products[$this->_rowProductId])) { @@ -1547,6 +1566,7 @@ protected function _collectOptionTitle(array $rowData, $prevOptionId, array &$ti * @param array &$prices * @param array &$typeValues * @return $this + * @SuppressWarnings(PHPMD.UnusedLocalVariable) */ protected function _compareOptionsWithExisting(array &$options, array &$titles, array &$prices, array &$typeValues) { @@ -1557,7 +1577,9 @@ protected function _compareOptionsWithExisting(array &$options, array &$titles, $titles[$optionId] = $titles[$newOptionId]; unset($titles[$newOptionId]); if (isset($prices[$newOptionId])) { - $prices[$newOptionId]['option_id'] = $optionId; + foreach ($prices[$newOptionId] as $storeId => $priceStoreData) { + $prices[$newOptionId][$storeId]['option_id'] = $optionId; + } } if (isset($typeValues[$newOptionId])) { $typeValues[$optionId] = $typeValues[$newOptionId]; @@ -1590,8 +1612,10 @@ private function restoreOriginalOptionTypeIds(array &$typeValues, array &$typePr $optionType['option_type_id'] = $existingTypeId; $typeTitles[$existingTypeId] = $typeTitles[$optionTypeId]; unset($typeTitles[$optionTypeId]); - $typePrices[$existingTypeId] = $typePrices[$optionTypeId]; - unset($typePrices[$optionTypeId]); + if (isset($typePrices[$optionTypeId])) { + $typePrices[$existingTypeId] = $typePrices[$optionTypeId]; + unset($typePrices[$optionTypeId]); + } // If option type titles match at least in one store, consider current option type as existing break; } @@ -1651,7 +1675,7 @@ protected function _parseRequiredData(array $rowData) if (!isset($this->_storeCodeToId[$rowData[self::COLUMN_STORE]])) { return false; } - $this->_rowStoreId = $this->_storeCodeToId[$rowData[self::COLUMN_STORE]]; + $this->_rowStoreId = (int)$this->_storeCodeToId[$rowData[self::COLUMN_STORE]]; } else { $this->_rowStoreId = Store::DEFAULT_STORE_ID; } @@ -1767,7 +1791,7 @@ protected function _getPriceData(array $rowData, $optionId, $type) ) { $priceData = [ 'option_id' => $optionId, - 'store_id' => Store::DEFAULT_STORE_ID, + 'store_id' => $this->_rowStoreId, 'price_type' => 'fixed', ]; @@ -1901,11 +1925,19 @@ protected function _saveTitles(array $titles) protected function _savePrices(array $prices) { if ($prices) { - $this->_connection->insertOnDuplicate( - $this->_tables['catalog_product_option_price'], - $prices, - ['price', 'price_type'] - ); + $optionPriceRows = []; + foreach ($prices as $storesData) { + foreach ($storesData as $row) { + $optionPriceRows[] = $row; + } + } + if ($optionPriceRows) { + $this->_connection->insertOnDuplicate( + $this->_tables['catalog_product_option_price'], + $optionPriceRows, + ['price', 'price_type'] + ); + } } return $this; diff --git a/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportImportConfigurableProductWithImagesTest.xml b/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportImportConfigurableProductWithImagesTest.xml new file mode 100644 index 0000000000000..88f6e6c9f9039 --- /dev/null +++ b/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportImportConfigurableProductWithImagesTest.xml @@ -0,0 +1,216 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/Product/Type/OptionTest.php b/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/Product/Type/OptionTest.php index f0a52a67e0095..9f63decac5ff7 100644 --- a/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/Product/Type/OptionTest.php +++ b/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/Product/Type/OptionTest.php @@ -79,8 +79,8 @@ class OptionTest extends \Magento\ImportExport\Test\Unit\Model\Import\AbstractIm * @var array */ protected $_expectedPrices = [ - 2 => ['option_id' => 2, 'store_id' => 0, 'price_type' => 'fixed', 'price' => 0], - 3 => ['option_id' => 3, 'store_id' => 0, 'price_type' => 'fixed', 'price' => 2] + 0 => ['option_id' => 2, 'store_id' => 0, 'price_type' => 'fixed', 'price' => 0], + 1 => ['option_id' => 3, 'store_id' => 0, 'price_type' => 'fixed', 'price' => 2] ]; /** diff --git a/app/code/Magento/CatalogInventory/Ui/DataProvider/Product/Form/Modifier/AdvancedInventory.php b/app/code/Magento/CatalogInventory/Ui/DataProvider/Product/Form/Modifier/AdvancedInventory.php index 7386f133b569a..da465f5bdd3dc 100644 --- a/app/code/Magento/CatalogInventory/Ui/DataProvider/Product/Form/Modifier/AdvancedInventory.php +++ b/app/code/Magento/CatalogInventory/Ui/DataProvider/Product/Form/Modifier/AdvancedInventory.php @@ -1,4 +1,7 @@ $this->locator->getProduct()->isLockedAttribute($fieldCode), ]; - $qty['arguments']['data']['config'] = [ - 'component' => 'Magento_CatalogInventory/js/components/qty-validator-changer', - 'group' => 'quantity_and_stock_status_qty', - 'dataType' => 'number', - 'formElement' => 'input', - 'componentType' => 'field', - 'visible' => '1', - 'require' => '0', - 'additionalClasses' => 'admin__field-small', - 'label' => __('Quantity'), - 'scopeLabel' => '[GLOBAL]', - 'dataScope' => 'qty', - 'validation' => [ - 'validate-number' => true, - 'less-than-equals-to' => StockDataFilter::MAX_QTY_VALUE, - ], - 'imports' => [ - 'handleChanges' => '${$.provider}:data.product.stock_data.is_qty_decimal', - ], - 'sortOrder' => 10, - ]; - $advancedInventoryButton['arguments']['data']['config'] = [ - 'displayAsLink' => true, - 'formElement' => 'container', - 'componentType' => 'container', - 'component' => 'Magento_Ui/js/form/components/button', - 'template' => 'ui/form/components/button/container', - 'actions' => [ - [ - 'targetName' => 'product_form.product_form.advanced_inventory_modal', - 'actionName' => 'toggleModal', - ], - ], - 'title' => __('Advanced Inventory'), - 'provider' => false, - 'additionalForGroup' => true, - 'source' => 'product_details', - 'sortOrder' => 20, - ]; $container['children'] = [ - 'qty' => $qty, - 'advanced_inventory_button' => $advancedInventoryButton, + 'qty' => $this->getQtyMetaStructure(), + 'advanced_inventory_button' => $this->getAdvancedInventoryButtonMetaStructure(), ]; $this->meta = $this->arrayManager->merge( $fieldsetPath . '/children', $this->meta, - ['quantity_and_stock_status_qty' => $container] + ['container_quantity_and_stock_status_qty' => $container] ); } } + + /** + * Get Qty meta structure + * + * @return array + */ + private function getQtyMetaStructure() + { + return [ + 'arguments' => [ + 'data' => [ + 'config' => [ + 'component' => 'Magento_CatalogInventory/js/components/qty-validator-changer', + 'group' => 'quantity_and_stock_status_qty', + 'dataType' => 'number', + 'formElement' => 'input', + 'componentType' => 'field', + 'visible' => '1', + 'require' => '0', + 'additionalClasses' => 'admin__field-small', + 'label' => __('Quantity'), + 'scopeLabel' => '[GLOBAL]', + 'dataScope' => 'qty', + 'validation' => [ + 'validate-number' => true, + 'less-than-equals-to' => StockDataFilter::MAX_QTY_VALUE, + ], + 'imports' => [ + 'handleChanges' => '${$.provider}:data.product.stock_data.is_qty_decimal', + ], + 'sortOrder' => 10, + 'disabled' => $this->locator->getProduct()->isLockedAttribute('quantity_and_stock_status'), + ] + ] + ] + ]; + } + + /** + * Get advances inventory button meta structure + * + * @return array + */ + private function getAdvancedInventoryButtonMetaStructure() + { + return [ + 'arguments' => [ + 'data' => [ + 'config' => [ + 'displayAsLink' => true, + 'formElement' => 'container', + 'componentType' => 'container', + 'component' => 'Magento_Ui/js/form/components/button', + 'template' => 'ui/form/components/button/container', + 'actions' => [ + [ + 'targetName' => 'product_form.product_form.advanced_inventory_modal', + 'actionName' => 'toggleModal', + ], + ], + 'title' => __('Advanced Inventory'), + 'provider' => false, + 'additionalForGroup' => true, + 'source' => 'product_details', + 'sortOrder' => 20, + ] + ] + ] + ]; + } } diff --git a/app/code/Magento/CatalogInventory/view/adminhtml/ui_component/product_form.xml b/app/code/Magento/CatalogInventory/view/adminhtml/ui_component/product_form.xml index 0a7f0fdc32d40..fc0690157fb37 100644 --- a/app/code/Magento/CatalogInventory/view/adminhtml/ui_component/product_form.xml +++ b/app/code/Magento/CatalogInventory/view/adminhtml/ui_component/product_form.xml @@ -74,8 +74,12 @@ ${$.provider}:data.product.stock_data.manage_stock + ${$.parentName}.manage_stock:disabled ${$.parentName}.manage_stock:disabled + + ns = ${ $.ns }, index = qty, group = quantity_and_stock_status_qty:disabled + @@ -101,6 +105,7 @@ quantity_and_stock_status.qty ns = ${ $.ns }, index = qty, group = quantity_and_stock_status_qty:value + ns = ${ $.ns }, index = qty, group = quantity_and_stock_status_qty:disabled ${$.provider}:data.product.stock_data.is_qty_decimal @@ -149,8 +154,12 @@ ${$.provider}:data.product.stock_data.min_qty + ${$.parentName}.min_qty:disabled ${$.parentName}.min_qty:disabled + + ns = ${ $.ns }, index = qty, group = quantity_and_stock_status_qty:disabled + @@ -211,6 +220,13 @@ true use_config_min_sale_qty + + ${$.parentName}.min_sale_qty:disabled + ${$.parentName}.min_sale_qty:disabled + + + ns = ${ $.ns }, index = qty, group = quantity_and_stock_status_qty:disabled + @@ -308,8 +324,12 @@ ${$.provider}:data.product.stock_data.max_sale_qty + ${$.parentName}.max_sale_qty:disabled ${$.parentName}.max_sale_qty:disabled + + ns = ${ $.ns }, index = qty, group = quantity_and_stock_status_qty:disabled + @@ -337,6 +357,7 @@ stock_data.is_qty_decimal ${$.provider}:data.product.stock_data.manage_stock + ns = ${ $.ns }, index = qty, group = quantity_and_stock_status_qty:disabled @@ -359,6 +380,7 @@ stock_data.is_decimal_divided ${$.provider}:data.product.stock_data.manage_stock + ns = ${ $.ns }, index = qty, group = quantity_and_stock_status_qty:disabled @@ -418,8 +440,12 @@ ${$.provider}:data.product.stock_data.backorders + ${$.parentName}.backorders:disabled ${$.parentName}.backorders:disabled + + ns = ${ $.ns }, index = qty, group = quantity_and_stock_status_qty:disabled + @@ -475,8 +501,12 @@ ${$.provider}:data.product.stock_data.notify_stock_qty + ${$.parentName}.notify_stock_qty:disabled ${$.parentName}.notify_stock_qty:disabled + + ns = ${ $.ns }, index = qty, group = quantity_and_stock_status_qty:disabled + @@ -534,8 +564,12 @@ ${$.provider}:data.product.stock_data.enable_qty_increments + ${$.parentName}.enable_qty_increments:disabled ${$.parentName}.enable_qty_increments:disabled + + ns = ${ $.ns }, index = qty, group = quantity_and_stock_status_qty:disabled + @@ -595,8 +629,12 @@ ${$.provider}:data.product.stock_data.qty_increments + ${$.parentName}.qty_increments:disabled ${$.parentName}.qty_increments:disabled + + ns = ${ $.ns }, index = qty, group = quantity_and_stock_status_qty:disabled + @@ -633,6 +671,9 @@ [GLOBAL] Stock Status is_in_stock + + ns = ${ $.ns }, index = qty, group = quantity_and_stock_status_qty:disabled + diff --git a/app/code/Magento/CatalogRule/Controller/Adminhtml/Promo/Catalog/Save.php b/app/code/Magento/CatalogRule/Controller/Adminhtml/Promo/Catalog/Save.php index 4f58293d53359..6d499b93e411f 100644 --- a/app/code/Magento/CatalogRule/Controller/Adminhtml/Promo/Catalog/Save.php +++ b/app/code/Magento/CatalogRule/Controller/Adminhtml/Promo/Catalog/Save.php @@ -12,6 +12,7 @@ use Magento\Framework\Registry; use Magento\Framework\Stdlib\DateTime\Filter\Date; use Magento\Framework\App\Request\DataPersistorInterface; +use Magento\Framework\Stdlib\DateTime\TimezoneInterface; /** * Save action for catalog rule @@ -25,19 +26,27 @@ class Save extends \Magento\CatalogRule\Controller\Adminhtml\Promo\Catalog imple */ protected $dataPersistor; + /** + * @var TimezoneInterface + */ + private $localeDate; + /** * @param Context $context * @param Registry $coreRegistry * @param Date $dateFilter * @param DataPersistorInterface $dataPersistor + * @param TimezoneInterface $localeDate */ public function __construct( Context $context, Registry $coreRegistry, Date $dateFilter, - DataPersistorInterface $dataPersistor + DataPersistorInterface $dataPersistor, + TimezoneInterface $localeDate ) { $this->dataPersistor = $dataPersistor; + $this->localeDate = $localeDate; parent::__construct($context, $coreRegistry, $dateFilter); } @@ -46,16 +55,15 @@ public function __construct( * * @return \Magento\Framework\App\ResponseInterface|\Magento\Framework\Controller\ResultInterface|void * @SuppressWarnings(PHPMD.CyclomaticComplexity) + * @SuppressWarnings(PHPMD.NPathComplexity) */ public function execute() { if ($this->getRequest()->getPostValue()) { - /** @var \Magento\CatalogRule\Api\CatalogRuleRepositoryInterface $ruleRepository */ $ruleRepository = $this->_objectManager->get( \Magento\CatalogRule\Api\CatalogRuleRepositoryInterface::class ); - /** @var \Magento\CatalogRule\Model\Rule $model */ $model = $this->_objectManager->create(\Magento\CatalogRule\Model\Rule::class); @@ -65,7 +73,9 @@ public function execute() ['request' => $this->getRequest()] ); $data = $this->getRequest()->getPostValue(); - + if (!$this->getRequest()->getParam('from_date')) { + $data['from_date'] = $this->localeDate->formatDate(); + } $filterValues = ['from_date' => $this->_dateFilter]; if ($this->getRequest()->getParam('to_date')) { $filterValues['to_date'] = $this->_dateFilter; diff --git a/app/code/Magento/CatalogRule/Model/Indexer/ReindexRuleProduct.php b/app/code/Magento/CatalogRule/Model/Indexer/ReindexRuleProduct.php index 55a234bb8ae27..944710773123f 100644 --- a/app/code/Magento/CatalogRule/Model/Indexer/ReindexRuleProduct.php +++ b/app/code/Magento/CatalogRule/Model/Indexer/ReindexRuleProduct.php @@ -8,7 +8,10 @@ use Magento\CatalogRule\Model\Indexer\IndexerTableSwapperInterface as TableSwapper; use Magento\Catalog\Model\ResourceModel\Indexer\ActiveTableSwitcher; -use Magento\Framework\App\ObjectManager; +use Magento\CatalogRule\Model\Rule; +use Magento\Framework\App\ResourceConnection; +use Magento\Framework\Stdlib\DateTime\TimezoneInterface; +use Magento\Store\Model\ScopeInterface; /** * Reindex rule relations with products. @@ -16,7 +19,7 @@ class ReindexRuleProduct { /** - * @var \Magento\Framework\App\ResourceConnection + * @var ResourceConnection */ private $resource; @@ -31,36 +34,40 @@ class ReindexRuleProduct private $tableSwapper; /** - * @param \Magento\Framework\App\ResourceConnection $resource + * @var TimezoneInterface + */ + private $localeDate; + + /** + * @param ResourceConnection $resource * @param ActiveTableSwitcher $activeTableSwitcher - * @param TableSwapper|null $tableSwapper + * @param TableSwapper $tableSwapper + * @param TimezoneInterface $localeDate */ public function __construct( - \Magento\Framework\App\ResourceConnection $resource, + ResourceConnection $resource, ActiveTableSwitcher $activeTableSwitcher, - TableSwapper $tableSwapper = null + TableSwapper $tableSwapper, + TimezoneInterface $localeDate ) { $this->resource = $resource; $this->activeTableSwitcher = $activeTableSwitcher; - $this->tableSwapper = $tableSwapper ?? - ObjectManager::getInstance()->get(TableSwapper::class); + $this->tableSwapper = $tableSwapper; + $this->localeDate = $localeDate; } /** * Reindex information about rule relations with products. * - * @param \Magento\CatalogRule\Model\Rule $rule + * @param Rule $rule * @param int $batchCount * @param bool $useAdditionalTable * @return bool * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) */ - public function execute( - \Magento\CatalogRule\Model\Rule $rule, - $batchCount, - $useAdditionalTable = false - ) { + public function execute(Rule $rule, $batchCount, $useAdditionalTable = false) + { if (!$rule->getIsActive() || empty($rule->getWebsiteIds())) { return false; } @@ -84,21 +91,28 @@ public function execute( $ruleId = $rule->getId(); $customerGroupIds = $rule->getCustomerGroupIds(); - $fromTime = strtotime($rule->getFromDate()); - $toTime = strtotime($rule->getToDate()); - $toTime = $toTime ? $toTime + \Magento\CatalogRule\Model\Indexer\IndexBuilder::SECONDS_IN_DAY - 1 : 0; $sortOrder = (int)$rule->getSortOrder(); $actionOperator = $rule->getSimpleAction(); $actionAmount = $rule->getDiscountAmount(); $actionStop = $rule->getStopRulesProcessing(); $rows = []; + foreach ($websiteIds as $websiteId) { + $scopeTz = new \DateTimeZone( + $this->localeDate->getConfigTimezone(ScopeInterface::SCOPE_WEBSITE, $websiteId) + ); + $fromTime = $rule->getFromDate() + ? (new \DateTime($rule->getFromDate(), $scopeTz))->getTimestamp() + : 0; + $toTime = $rule->getToDate() + ? (new \DateTime($rule->getToDate(), $scopeTz))->getTimestamp() + IndexBuilder::SECONDS_IN_DAY - 1 + : 0; - foreach ($productIds as $productId => $validationByWebsite) { - foreach ($websiteIds as $websiteId) { + foreach ($productIds as $productId => $validationByWebsite) { if (empty($validationByWebsite[$websiteId])) { continue; } + foreach ($customerGroupIds as $customerGroupId) { $rows[] = [ 'rule_id' => $ruleId, @@ -123,6 +137,7 @@ public function execute( if (!empty($rows)) { $connection->insertMultiple($indexTable, $rows); } + return true; } } diff --git a/app/code/Magento/CatalogRule/Model/Indexer/ReindexRuleProductPrice.php b/app/code/Magento/CatalogRule/Model/Indexer/ReindexRuleProductPrice.php index 6a87be3c50a64..11ba87730bec1 100644 --- a/app/code/Magento/CatalogRule/Model/Indexer/ReindexRuleProductPrice.php +++ b/app/code/Magento/CatalogRule/Model/Indexer/ReindexRuleProductPrice.php @@ -6,54 +6,58 @@ namespace Magento\CatalogRule\Model\Indexer; +use Magento\Catalog\Model\Product; +use Magento\Framework\Stdlib\DateTime\TimezoneInterface; +use Magento\Store\Model\StoreManagerInterface; + /** * Reindex product prices according rule settings. */ class ReindexRuleProductPrice { /** - * @var \Magento\Store\Model\StoreManagerInterface + * @var StoreManagerInterface */ private $storeManager; /** - * @var \Magento\CatalogRule\Model\Indexer\RuleProductsSelectBuilder + * @var RuleProductsSelectBuilder */ private $ruleProductsSelectBuilder; /** - * @var \Magento\CatalogRule\Model\Indexer\ProductPriceCalculator + * @var ProductPriceCalculator */ private $productPriceCalculator; /** - * @var \Magento\Framework\Stdlib\DateTime\DateTime + * @var TimezoneInterface */ - private $dateTime; + private $localeDate; /** - * @var \Magento\CatalogRule\Model\Indexer\RuleProductPricesPersistor + * @var RuleProductPricesPersistor */ private $pricesPersistor; /** - * @param \Magento\Store\Model\StoreManagerInterface $storeManager + * @param StoreManagerInterface $storeManager * @param RuleProductsSelectBuilder $ruleProductsSelectBuilder * @param ProductPriceCalculator $productPriceCalculator - * @param \Magento\Framework\Stdlib\DateTime\DateTime $dateTime - * @param \Magento\CatalogRule\Model\Indexer\RuleProductPricesPersistor $pricesPersistor + * @param TimezoneInterface $localeDate + * @param RuleProductPricesPersistor $pricesPersistor */ public function __construct( - \Magento\Store\Model\StoreManagerInterface $storeManager, - \Magento\CatalogRule\Model\Indexer\RuleProductsSelectBuilder $ruleProductsSelectBuilder, - \Magento\CatalogRule\Model\Indexer\ProductPriceCalculator $productPriceCalculator, - \Magento\Framework\Stdlib\DateTime\DateTime $dateTime, - \Magento\CatalogRule\Model\Indexer\RuleProductPricesPersistor $pricesPersistor + StoreManagerInterface $storeManager, + RuleProductsSelectBuilder $ruleProductsSelectBuilder, + ProductPriceCalculator $productPriceCalculator, + TimezoneInterface $localeDate, + RuleProductPricesPersistor $pricesPersistor ) { $this->storeManager = $storeManager; $this->ruleProductsSelectBuilder = $ruleProductsSelectBuilder; $this->productPriceCalculator = $productPriceCalculator; - $this->dateTime = $dateTime; + $this->localeDate = $localeDate; $this->pricesPersistor = $pricesPersistor; } @@ -61,22 +65,16 @@ public function __construct( * Reindex product prices. * * @param int $batchCount - * @param \Magento\Catalog\Model\Product|null $product + * @param Product|null $product * @param bool $useAdditionalTable * @return bool * @SuppressWarnings(PHPMD.CyclomaticComplexity) */ - public function execute( - $batchCount, - \Magento\Catalog\Model\Product $product = null, - $useAdditionalTable = false - ) { - $fromDate = mktime(0, 0, 0, date('m'), date('d') - 1); - $toDate = mktime(0, 0, 0, date('m'), date('d') + 1); - + public function execute($batchCount, Product $product = null, $useAdditionalTable = false) + { /** * Update products rules prices per each website separately - * because of max join limit in mysql + * because for each website date in website's timezone should be used */ foreach ($this->storeManager->getWebsites() as $website) { $productsStmt = $this->ruleProductsSelectBuilder->build($website->getId(), $product, $useAdditionalTable); @@ -84,6 +82,13 @@ public function execute( $stopFlags = []; $prevKey = null; + $storeGroup = $this->storeManager->getGroup($website->getDefaultGroupId()); + $currentDate = $this->localeDate->scopeDate($storeGroup->getDefaultStoreId(), null, true); + $previousDate = (clone $currentDate)->modify('-1 day'); + $previousDate->setTime(23, 59, 59); + $nextDate = (clone $currentDate)->modify('+1 day'); + $nextDate->setTime(0, 0, 0); + while ($ruleData = $productsStmt->fetch()) { $ruleProductId = $ruleData['product_id']; $productKey = $ruleProductId . @@ -100,12 +105,11 @@ public function execute( } } - $ruleData['from_time'] = $this->roundTime($ruleData['from_time']); - $ruleData['to_time'] = $this->roundTime($ruleData['to_time']); /** * Build prices for each day */ - for ($time = $fromDate; $time <= $toDate; $time += IndexBuilder::SECONDS_IN_DAY) { + foreach ([$previousDate, $currentDate, $nextDate] as $date) { + $time = $date->getTimestamp(); if (($ruleData['from_time'] == 0 || $time >= $ruleData['from_time']) && ($ruleData['to_time'] == 0 || $time <= $ruleData['to_time']) @@ -118,7 +122,7 @@ public function execute( if (!isset($dayPrices[$priceKey])) { $dayPrices[$priceKey] = [ - 'rule_date' => $time, + 'rule_date' => $date, 'website_id' => $ruleData['website_id'], 'customer_group_id' => $ruleData['customer_group_id'], 'product_id' => $ruleProductId, @@ -151,18 +155,7 @@ public function execute( } $this->pricesPersistor->execute($dayPrices, $useAdditionalTable); } - return true; - } - /** - * @param int $timeStamp - * @return int - */ - private function roundTime($timeStamp) - { - if (is_numeric($timeStamp) && $timeStamp != 0) { - $timeStamp = $this->dateTime->timestamp($this->dateTime->date('Y-m-d 00:00:00', $timeStamp)); - } - return $timeStamp; + return true; } } diff --git a/app/code/Magento/CatalogRule/Model/ResourceModel/Product/CollectionProcessor.php b/app/code/Magento/CatalogRule/Model/ResourceModel/Product/CollectionProcessor.php index 0dee9eda5b6e8..1fd6f0cbc986f 100644 --- a/app/code/Magento/CatalogRule/Model/ResourceModel/Product/CollectionProcessor.php +++ b/app/code/Magento/CatalogRule/Model/ResourceModel/Product/CollectionProcessor.php @@ -90,7 +90,7 @@ public function addPriceData(ProductCollection $productCollection, $joinColumn = ), $connection->quoteInto( 'catalog_rule.rule_date = ?', - $this->dateTime->formatDate($this->localeDate->date(null, null, false), false) + $this->dateTime->formatDate($this->localeDate->scopeDate($store->getId()), false) ), ] ), diff --git a/app/code/Magento/CatalogRule/Model/ResourceModel/Product/LinkedProductSelectBuilderByCatalogRulePrice.php b/app/code/Magento/CatalogRule/Model/ResourceModel/Product/LinkedProductSelectBuilderByCatalogRulePrice.php index 02d2631058a1a..48c463fc18b80 100644 --- a/app/code/Magento/CatalogRule/Model/ResourceModel/Product/LinkedProductSelectBuilderByCatalogRulePrice.php +++ b/app/code/Magento/CatalogRule/Model/ResourceModel/Product/LinkedProductSelectBuilderByCatalogRulePrice.php @@ -88,7 +88,8 @@ public function __construct( */ public function build($productId) { - $currentDate = $this->dateTime->formatDate($this->localeDate->date(null, null, false), false); + $timestamp = $this->localeDate->scopeTimeStamp($this->storeManager->getStore()); + $currentDate = $this->dateTime->formatDate($timestamp, false); $linkField = $this->metadataPool->getMetadata(ProductInterface::class)->getLinkField(); $productTable = $this->resource->getTableName('catalog_product_entity'); diff --git a/app/code/Magento/CatalogRule/Observer/PrepareCatalogProductCollectionPricesObserver.php b/app/code/Magento/CatalogRule/Observer/PrepareCatalogProductCollectionPricesObserver.php index a635c5611eff6..bf0c85e671dd7 100644 --- a/app/code/Magento/CatalogRule/Observer/PrepareCatalogProductCollectionPricesObserver.php +++ b/app/code/Magento/CatalogRule/Observer/PrepareCatalogProductCollectionPricesObserver.php @@ -105,7 +105,7 @@ public function execute(\Magento\Framework\Event\Observer $observer) if ($observer->getEvent()->hasDate()) { $date = new \DateTime($observer->getEvent()->getDate()); } else { - $date = $this->localeDate->date(null, null, false); + $date = (new \DateTime())->setTimestamp($this->localeDate->scopeTimeStamp($store)); } $productIds = []; diff --git a/app/code/Magento/CatalogRule/Observer/ProcessAdminFinalPriceObserver.php b/app/code/Magento/CatalogRule/Observer/ProcessAdminFinalPriceObserver.php index 2fd23ae391474..89ed519cfb8c8 100644 --- a/app/code/Magento/CatalogRule/Observer/ProcessAdminFinalPriceObserver.php +++ b/app/code/Magento/CatalogRule/Observer/ProcessAdminFinalPriceObserver.php @@ -65,7 +65,8 @@ public function __construct( public function execute(\Magento\Framework\Event\Observer $observer) { $product = $observer->getEvent()->getProduct(); - $date = $this->localeDate->date(null, null, false); + $storeId = $product->getStoreId(); + $date = $this->localeDate->scopeDate($storeId); $key = false; $ruleData = $this->coreRegistry->registry('rule_data'); diff --git a/app/code/Magento/CatalogRule/Observer/ProcessFrontFinalPriceObserver.php b/app/code/Magento/CatalogRule/Observer/ProcessFrontFinalPriceObserver.php index b27768ae091ed..075fe9e51f7dc 100644 --- a/app/code/Magento/CatalogRule/Observer/ProcessFrontFinalPriceObserver.php +++ b/app/code/Magento/CatalogRule/Observer/ProcessFrontFinalPriceObserver.php @@ -80,7 +80,7 @@ public function execute(\Magento\Framework\Event\Observer $observer) if ($observer->hasDate()) { $date = new \DateTime($observer->getEvent()->getDate()); } else { - $date = $this->localeDate->date(null, null, false); + $date = $this->localeDate->scopeDate($storeId); } if ($observer->hasWebsiteId()) { diff --git a/app/code/Magento/CatalogRule/Pricing/Price/CatalogRulePrice.php b/app/code/Magento/CatalogRule/Pricing/Price/CatalogRulePrice.php index 8bce5456ffa72..7cbbc547571ab 100644 --- a/app/code/Magento/CatalogRule/Pricing/Price/CatalogRulePrice.php +++ b/app/code/Magento/CatalogRule/Pricing/Price/CatalogRulePrice.php @@ -88,7 +88,7 @@ public function getValue() $this->value = (float)$this->product->getData(self::PRICE_CODE) ?: false; } else { $this->value = $this->ruleResource->getRulePrice( - $this->dateTime->date(null, null, false), + $this->dateTime->scopeDate($this->storeManager->getStore()->getId()), $this->storeManager->getStore()->getWebsiteId(), $this->customerSession->getCustomerGroupId(), $this->product->getId() diff --git a/app/code/Magento/CatalogRule/Test/Unit/Model/Indexer/IndexBuilderTest.php b/app/code/Magento/CatalogRule/Test/Unit/Model/Indexer/IndexBuilderTest.php index 920dcb8e1ede5..78668366bccdc 100644 --- a/app/code/Magento/CatalogRule/Test/Unit/Model/Indexer/IndexBuilderTest.php +++ b/app/code/Magento/CatalogRule/Test/Unit/Model/Indexer/IndexBuilderTest.php @@ -252,7 +252,7 @@ public function testUpdateCatalogRuleGroupWebsiteData() ); $resourceMock->expects($this->any()) ->method('getMainTable') - ->will($this->returnValue('catalog_product_entity_tear_price')); + ->will($this->returnValue('catalog_product_entity_tier_price')); $backendModelMock->expects($this->any()) ->method('getResource') ->will($this->returnValue($resourceMock)); diff --git a/app/code/Magento/CatalogRule/Test/Unit/Model/Indexer/ReindexRuleProductPriceTest.php b/app/code/Magento/CatalogRule/Test/Unit/Model/Indexer/ReindexRuleProductPriceTest.php index 6d7f0673ed281..5f63283df6760 100644 --- a/app/code/Magento/CatalogRule/Test/Unit/Model/Indexer/ReindexRuleProductPriceTest.php +++ b/app/code/Magento/CatalogRule/Test/Unit/Model/Indexer/ReindexRuleProductPriceTest.php @@ -6,65 +6,62 @@ namespace Magento\CatalogRule\Test\Unit\Model\Indexer; -use Magento\CatalogRule\Model\Indexer\IndexBuilder; +use Magento\Catalog\Model\Product; +use Magento\CatalogRule\Model\Indexer\ProductPriceCalculator; +use Magento\CatalogRule\Model\Indexer\ReindexRuleProductPrice; +use Magento\CatalogRule\Model\Indexer\RuleProductPricesPersistor; +use Magento\CatalogRule\Model\Indexer\RuleProductsSelectBuilder; +use Magento\Framework\Stdlib\DateTime\TimezoneInterface; +use Magento\Store\Api\Data\GroupInterface; +use Magento\Store\Api\Data\WebsiteInterface; +use Magento\Store\Model\StoreManagerInterface; +use PHPUnit\Framework\MockObject\MockObject; class ReindexRuleProductPriceTest extends \PHPUnit\Framework\TestCase { /** - * @var \Magento\CatalogRule\Model\Indexer\ReindexRuleProductPrice + * @var ReindexRuleProductPrice */ private $model; /** - * @var \Magento\Store\Model\StoreManagerInterface|\PHPUnit_Framework_MockObject_MockObject + * @var StoreManagerInterface|MockObject */ private $storeManagerMock; /** - * @var \Magento\CatalogRule\Model\Indexer\RuleProductsSelectBuilder|\PHPUnit_Framework_MockObject_MockObject + * @var RuleProductsSelectBuilder|MockObject */ private $ruleProductsSelectBuilderMock; /** - * @var \Magento\CatalogRule\Model\Indexer\ProductPriceCalculator|\PHPUnit_Framework_MockObject_MockObject + * @var ProductPriceCalculator|MockObject */ private $productPriceCalculatorMock; /** - * @var \Magento\Framework\Stdlib\DateTime\DateTime|\PHPUnit_Framework_MockObject_MockObject + * @var TimezoneInterface|MockObject */ - private $dateTimeMock; + private $localeDate; /** - * @var \Magento\CatalogRule\Model\Indexer\RuleProductPricesPersistor|\PHPUnit_Framework_MockObject_MockObject + * @var RuleProductPricesPersistor|MockObject */ private $pricesPersistorMock; protected function setUp() { - $this->storeManagerMock = $this->getMockBuilder(\Magento\Store\Model\StoreManagerInterface::class) - ->disableOriginalConstructor() - ->getMock(); - $this->ruleProductsSelectBuilderMock = - $this->getMockBuilder(\Magento\CatalogRule\Model\Indexer\RuleProductsSelectBuilder::class) - ->disableOriginalConstructor() - ->getMock(); - $this->productPriceCalculatorMock = - $this->getMockBuilder(\Magento\CatalogRule\Model\Indexer\ProductPriceCalculator::class) - ->disableOriginalConstructor() - ->getMock(); - $this->dateTimeMock = $this->getMockBuilder(\Magento\Framework\Stdlib\DateTime\DateTime::class) - ->disableOriginalConstructor() - ->getMock(); - $this->pricesPersistorMock = - $this->getMockBuilder(\Magento\CatalogRule\Model\Indexer\RuleProductPricesPersistor::class) - ->disableOriginalConstructor() - ->getMock(); - $this->model = new \Magento\CatalogRule\Model\Indexer\ReindexRuleProductPrice( + $this->storeManagerMock = $this->createMock(StoreManagerInterface::class); + $this->ruleProductsSelectBuilderMock = $this->createMock(RuleProductsSelectBuilder::class); + $this->productPriceCalculatorMock = $this->createMock(ProductPriceCalculator::class); + $this->localeDate = $this->createMock(TimezoneInterface::class); + $this->pricesPersistorMock = $this->createMock(RuleProductPricesPersistor::class); + + $this->model = new ReindexRuleProductPrice( $this->storeManagerMock, $this->ruleProductsSelectBuilderMock, $this->productPriceCalculatorMock, - $this->dateTimeMock, + $this->localeDate, $this->pricesPersistorMock ); } @@ -72,19 +69,32 @@ protected function setUp() public function testExecute() { $websiteId = 234; - $productMock = $this->getMockBuilder(\Magento\Catalog\Model\Product::class) - ->disableOriginalConstructor() - ->getMock(); - - $websiteMock = $this->getMockBuilder(\Magento\Store\Api\Data\WebsiteInterface::class) - ->disableOriginalConstructor() - ->getMock(); - $websiteMock->expects($this->once())->method('getId')->willReturn($websiteId); - $this->storeManagerMock->expects($this->once())->method('getWebsites')->willReturn([$websiteMock]); - - $statementMock = $this->getMockBuilder(\Zend_Db_Statement_Interface::class) - ->disableOriginalConstructor() - ->getMock(); + $defaultGroupId = 11; + $defaultStoreId = 22; + + $websiteMock = $this->createMock(WebsiteInterface::class); + $websiteMock->expects($this->once()) + ->method('getId') + ->willReturn($websiteId); + $websiteMock->expects($this->once()) + ->method('getDefaultGroupId') + ->willReturn($defaultGroupId); + $this->storeManagerMock->expects($this->once()) + ->method('getWebsites') + ->willReturn([$websiteMock]); + $groupMock = $this->createMock(GroupInterface::class); + $groupMock->method('getId') + ->willReturn($defaultStoreId); + $groupMock->expects($this->once()) + ->method('getDefaultStoreId') + ->willReturn($defaultStoreId); + $this->storeManagerMock->expects($this->once()) + ->method('getGroup') + ->with($defaultGroupId) + ->willReturn($groupMock); + + $productMock = $this->createMock(Product::class); + $statementMock = $this->createMock(\Zend_Db_Statement_Interface::class); $this->ruleProductsSelectBuilderMock->expects($this->once()) ->method('build') ->with($websiteId, $productMock, true) @@ -99,29 +109,22 @@ public function testExecute() 'action_stop' => true ]; - $this->dateTimeMock->expects($this->at(0)) - ->method('date') - ->with('Y-m-d 00:00:00', $ruleData['from_time']) - ->willReturn($ruleData['from_time']); - $this->dateTimeMock->expects($this->at(1)) - ->method('timestamp') - ->with($ruleData['from_time']) - ->willReturn($ruleData['from_time']); - - $this->dateTimeMock->expects($this->at(2)) - ->method('date') - ->with('Y-m-d 00:00:00', $ruleData['to_time']) - ->willReturn($ruleData['to_time']); - $this->dateTimeMock->expects($this->at(3)) - ->method('timestamp') - ->with($ruleData['to_time']) - ->willReturn($ruleData['to_time']); - - $statementMock->expects($this->at(0))->method('fetch')->willReturn($ruleData); - $statementMock->expects($this->at(1))->method('fetch')->willReturn(false); - - $this->productPriceCalculatorMock->expects($this->atLeastOnce())->method('calculate'); - $this->pricesPersistorMock->expects($this->once())->method('execute'); + $this->localeDate->expects($this->once()) + ->method('scopeDate') + ->with($defaultStoreId, null, true) + ->willReturn(new \DateTime()); + + $statementMock->expects($this->at(0)) + ->method('fetch') + ->willReturn($ruleData); + $statementMock->expects($this->at(1)) + ->method('fetch') + ->willReturn(false); + + $this->productPriceCalculatorMock->expects($this->atLeastOnce()) + ->method('calculate'); + $this->pricesPersistorMock->expects($this->once()) + ->method('execute'); $this->assertTrue($this->model->execute(1, $productMock, true)); } diff --git a/app/code/Magento/CatalogRule/Test/Unit/Model/Indexer/ReindexRuleProductTest.php b/app/code/Magento/CatalogRule/Test/Unit/Model/Indexer/ReindexRuleProductTest.php index 0dbbaee8d2871..a86ab736fb289 100644 --- a/app/code/Magento/CatalogRule/Test/Unit/Model/Indexer/ReindexRuleProductTest.php +++ b/app/code/Magento/CatalogRule/Test/Unit/Model/Indexer/ReindexRuleProductTest.php @@ -8,89 +8,96 @@ use Magento\Catalog\Model\ResourceModel\Indexer\ActiveTableSwitcher; use Magento\CatalogRule\Model\Indexer\IndexerTableSwapperInterface; +use Magento\CatalogRule\Model\Indexer\ReindexRuleProduct; +use Magento\CatalogRule\Model\Rule; +use Magento\Framework\App\ResourceConnection; +use Magento\Framework\DB\Adapter\AdapterInterface; +use Magento\Framework\Stdlib\DateTime\TimezoneInterface; +use Magento\Store\Model\ScopeInterface; +use PHPUnit\Framework\MockObject\MockObject; class ReindexRuleProductTest extends \PHPUnit\Framework\TestCase { /** - * @var \Magento\CatalogRule\Model\Indexer\ReindexRuleProduct + * @var ReindexRuleProduct */ private $model; /** - * @var \Magento\Framework\App\ResourceConnection|\PHPUnit_Framework_MockObject_MockObject + * @var ResourceConnection|MockObject */ private $resourceMock; /** - * @var ActiveTableSwitcher|\PHPUnit_Framework_MockObject_MockObject + * @var ActiveTableSwitcher|MockObject */ private $activeTableSwitcherMock; /** - * @var IndexerTableSwapperInterface|\PHPUnit_Framework_MockObject_MockObject + * @var IndexerTableSwapperInterface|MockObject */ private $tableSwapperMock; + /** + * @var TimezoneInterface|MockObject + */ + private $localeDateMock; + protected function setUp() { - $this->resourceMock = $this->getMockBuilder(\Magento\Framework\App\ResourceConnection::class) - ->disableOriginalConstructor() - ->getMock(); - $this->activeTableSwitcherMock = $this->getMockBuilder(ActiveTableSwitcher::class) - ->disableOriginalConstructor() - ->getMock(); - $this->tableSwapperMock = $this->getMockForAbstractClass( - IndexerTableSwapperInterface::class - ); - $this->model = new \Magento\CatalogRule\Model\Indexer\ReindexRuleProduct( + $this->resourceMock = $this->createMock(ResourceConnection::class); + $this->activeTableSwitcherMock = $this->createMock(ActiveTableSwitcher::class); + $this->tableSwapperMock = $this->createMock(IndexerTableSwapperInterface::class); + $this->localeDateMock = $this->createMock(TimezoneInterface::class); + + $this->model = new ReindexRuleProduct( $this->resourceMock, $this->activeTableSwitcherMock, - $this->tableSwapperMock + $this->tableSwapperMock, + $this->localeDateMock ); } public function testExecuteIfRuleInactive() { - $ruleMock = $this->getMockBuilder(\Magento\CatalogRule\Model\Rule::class) - ->disableOriginalConstructor() - ->getMock(); - $ruleMock->expects($this->once())->method('getIsActive')->willReturn(false); + $ruleMock = $this->createMock(Rule::class); + $ruleMock->expects($this->once()) + ->method('getIsActive') + ->willReturn(false); $this->assertFalse($this->model->execute($ruleMock, 100, true)); } public function testExecuteIfRuleWithoutWebsiteIds() { - $ruleMock = $this->getMockBuilder(\Magento\CatalogRule\Model\Rule::class) - ->disableOriginalConstructor() - ->getMock(); - $ruleMock->expects($this->once())->method('getIsActive')->willReturn(true); - $ruleMock->expects($this->once())->method('getWebsiteIds')->willReturn(null); + $ruleMock = $this->createMock(Rule::class); + $ruleMock->expects($this->once()) + ->method('getIsActive') + ->willReturn(true); + $ruleMock->expects($this->once()) + ->method('getWebsiteIds') + ->willReturn(null); $this->assertFalse($this->model->execute($ruleMock, 100, true)); } public function testExecute() { + $websiteId = 3; + $websiteTz = 'America/Los_Angeles'; $productIds = [ - 4 => [1 => 1], - 5 => [1 => 1], - 6 => [1 => 1], + 4 => [$websiteId => 1], + 5 => [$websiteId => 1], + 6 => [$websiteId => 1], ]; - $ruleMock = $this->getMockBuilder(\Magento\CatalogRule\Model\Rule::class) - ->disableOriginalConstructor() - ->getMock(); - $ruleMock->expects($this->once())->method('getIsActive')->willReturn(true); - $ruleMock->expects($this->exactly(2))->method('getWebsiteIds')->willReturn(1); - $ruleMock->expects($this->once())->method('getMatchingProductIds')->willReturn($productIds); $this->tableSwapperMock->expects($this->once()) ->method('getWorkingTableName') ->with('catalogrule_product') ->willReturn('catalogrule_product_replica'); - $connectionMock = $this->getMockBuilder(\Magento\Framework\DB\Adapter\AdapterInterface::class) - ->disableOriginalConstructor() - ->getMock(); - $this->resourceMock->expects($this->at(0))->method('getConnection')->willReturn($connectionMock); + $connectionMock = $this->createMock(AdapterInterface::class); + $this->resourceMock->expects($this->at(0)) + ->method('getConnection') + ->willReturn($connectionMock); $this->resourceMock->expects($this->at(1)) ->method('getTableName') ->with('catalogrule_product') @@ -100,21 +107,30 @@ public function testExecute() ->with('catalogrule_product_replica') ->willReturn('catalogrule_product_replica'); + $ruleMock = $this->createMock(Rule::class); + $ruleMock->expects($this->once())->method('getIsActive')->willReturn(true); + $ruleMock->expects($this->exactly(2))->method('getWebsiteIds')->willReturn([$websiteId]); + $ruleMock->expects($this->once())->method('getMatchingProductIds')->willReturn($productIds); $ruleMock->expects($this->once())->method('getId')->willReturn(100); $ruleMock->expects($this->once())->method('getCustomerGroupIds')->willReturn([10]); - $ruleMock->expects($this->once())->method('getFromDate')->willReturn('2017-06-21'); - $ruleMock->expects($this->once())->method('getToDate')->willReturn('2017-06-30'); + $ruleMock->expects($this->atLeastOnce())->method('getFromDate')->willReturn('2017-06-21'); + $ruleMock->expects($this->atLeastOnce())->method('getToDate')->willReturn('2017-06-30'); $ruleMock->expects($this->once())->method('getSortOrder')->willReturn(1); $ruleMock->expects($this->once())->method('getSimpleAction')->willReturn('simple_action'); $ruleMock->expects($this->once())->method('getDiscountAmount')->willReturn(43); $ruleMock->expects($this->once())->method('getStopRulesProcessing')->willReturn(true); + $this->localeDateMock->expects($this->once()) + ->method('getConfigTimezone') + ->with(ScopeInterface::SCOPE_WEBSITE, $websiteId) + ->willReturn($websiteTz); + $batchRows = [ [ 'rule_id' => 100, 'from_time' => 1498028400, 'to_time' => 1498892399, - 'website_id' => 1, + 'website_id' => $websiteId, 'customer_group_id' => 10, 'product_id' => 4, 'action_operator' => 'simple_action', @@ -126,7 +142,7 @@ public function testExecute() 'rule_id' => 100, 'from_time' => 1498028400, 'to_time' => 1498892399, - 'website_id' => 1, + 'website_id' => $websiteId, 'customer_group_id' => 10, 'product_id' => 5, 'action_operator' => 'simple_action', @@ -141,7 +157,7 @@ public function testExecute() 'rule_id' => 100, 'from_time' => 1498028400, 'to_time' => 1498892399, - 'website_id' => 1, + 'website_id' => $websiteId, 'customer_group_id' => 10, 'product_id' => 6, 'action_operator' => 'simple_action', diff --git a/app/code/Magento/CatalogRule/Test/Unit/Pricing/Price/CatalogRulePriceTest.php b/app/code/Magento/CatalogRule/Test/Unit/Pricing/Price/CatalogRulePriceTest.php index cb1a7f53f752c..7514d2bc4b5c5 100644 --- a/app/code/Magento/CatalogRule/Test/Unit/Pricing/Price/CatalogRulePriceTest.php +++ b/app/code/Magento/CatalogRule/Test/Unit/Pricing/Price/CatalogRulePriceTest.php @@ -112,6 +112,7 @@ protected function setUp() */ public function testGetValue() { + $storeId = 5; $coreWebsiteId = 2; $productId = 4; $customerGroupId = 3; @@ -120,9 +121,12 @@ public function testGetValue() $catalogRulePrice = 55.12; $convertedPrice = 45.34; + $this->coreStoreMock->expects($this->once()) + ->method('getId') + ->willReturn($storeId); $this->dataTimeMock->expects($this->once()) - ->method('date') - ->with(null, null, false) + ->method('scopeDate') + ->with($storeId) ->willReturn($date); $this->coreStoreMock->expects($this->once()) ->method('getWebsiteId') diff --git a/app/code/Magento/CatalogSearch/Model/Adapter/Mysql/Filter/Preprocessor.php b/app/code/Magento/CatalogSearch/Model/Adapter/Mysql/Filter/Preprocessor.php index c758e773f43c1..a97d362c5de7f 100644 --- a/app/code/Magento/CatalogSearch/Model/Adapter/Mysql/Filter/Preprocessor.php +++ b/app/code/Magento/CatalogSearch/Model/Adapter/Mysql/Filter/Preprocessor.php @@ -165,7 +165,10 @@ private function processQueryWithField(FilterInterface $filter, $isNegation, $qu $this->customerSession->getCustomerGroupId() ); } elseif ($filter->getField() === 'category_ids') { - return 'category_ids_index.category_id = ' . (int) $filter->getValue(); + return $this->connection->quoteInto( + 'category_ids_index.category_id in (?)', + $filter->getValue() + ); } elseif ($attribute->isStatic()) { $alias = $this->aliasResolver->getAlias($filter); $resultQuery = str_replace( @@ -198,8 +201,9 @@ private function processQueryWithField(FilterInterface $filter, $isNegation, $qu ) ->joinLeft( ['current_store' => $table], - 'current_store.attribute_id = main_table.attribute_id AND current_store.store_id = ' - . $currentStoreId, + "current_store.{$linkIdField} = main_table.{$linkIdField} AND " + . "current_store.attribute_id = main_table.attribute_id AND current_store.store_id = " + . $currentStoreId, null ) ->columns([$filter->getField() => $ifNullCondition]) diff --git a/app/code/Magento/CatalogSearch/Model/Advanced.php b/app/code/Magento/CatalogSearch/Model/Advanced.php index 5b96a8c21cbea..8ce2e0140f528 100644 --- a/app/code/Magento/CatalogSearch/Model/Advanced.php +++ b/app/code/Magento/CatalogSearch/Model/Advanced.php @@ -181,8 +181,8 @@ public function __construct( /** * Add advanced search filters to product collection * - * @param array $values - * @return $this + * @param array $values + * @return $this * @throws LocalizedException * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) @@ -197,6 +197,11 @@ public function addFilters($values) if (!isset($values[$attribute->getAttributeCode()])) { continue; } + if ($attribute->getFrontendInput() == 'text' || $attribute->getFrontendInput() == 'textarea') { + if (!trim($values[$attribute->getAttributeCode()])) { + continue; + } + } $value = $values[$attribute->getAttributeCode()]; $preparedSearchValue = $this->getPreparedSearchCriteria($attribute, $value); if (false === $preparedSearchValue) { @@ -343,9 +348,9 @@ protected function addSearchCriteria($attribute, $value) * * @todo: Move this code to block * - * @param EntityAttribute $attribute - * @param mixed $value - * @return string|bool + * @param EntityAttribute $attribute + * @param mixed $value + * @return string|bool * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) */ diff --git a/app/code/Magento/CatalogSearch/Model/ResourceModel/Advanced/Collection.php b/app/code/Magento/CatalogSearch/Model/ResourceModel/Advanced/Collection.php index 1946dd35b8d37..595bc12ca956a 100644 --- a/app/code/Magento/CatalogSearch/Model/ResourceModel/Advanced/Collection.php +++ b/app/code/Magento/CatalogSearch/Model/ResourceModel/Advanced/Collection.php @@ -391,7 +391,8 @@ private function getSearchResultApplier(SearchResultInterface $searchResult): Se 'collection' => $this, 'searchResult' => $searchResult, /** This variable sets by serOrder method, but doesn't have a getter method. */ - 'orders' => $this->_orders + 'orders' => $this->_orders, + 'size' => $this->getPageSize(), ] ); } diff --git a/app/code/Magento/CatalogSearch/Model/ResourceModel/Fulltext/Collection.php b/app/code/Magento/CatalogSearch/Model/ResourceModel/Fulltext/Collection.php index 4f84f3868c6a3..14305359a71b3 100644 --- a/app/code/Magento/CatalogSearch/Model/ResourceModel/Fulltext/Collection.php +++ b/app/code/Magento/CatalogSearch/Model/ResourceModel/Fulltext/Collection.php @@ -485,12 +485,12 @@ private function getSearchCriteriaResolver(): SearchCriteriaResolverInterface { return $this->searchCriteriaResolverFactory->create( [ - 'builder' => $this->getSearchCriteriaBuilder(), - 'collection' => $this, - 'searchRequestName' => $this->searchRequestName, - 'currentPage' => $this->_curPage, - 'size' => $this->getPageSize(), - 'orders' => $this->searchOrders, + 'builder' => $this->getSearchCriteriaBuilder(), + 'collection' => $this, + 'searchRequestName' => $this->searchRequestName, + 'currentPage' => (int)$this->_curPage, + 'size' => $this->getPageSize(), + 'orders' => $this->searchOrders, ] ); } @@ -505,10 +505,12 @@ private function getSearchResultApplier(SearchResultInterface $searchResult): Se { return $this->searchResultApplierFactory->create( [ - 'collection' => $this, - 'searchResult' => $searchResult, - /** This variable sets by serOrder method, but doesn't have a getter method. */ - 'orders' => $this->_orders, + 'collection' => $this, + 'searchResult' => $searchResult, + /** This variable sets by serOrder method, but doesn't have a getter method. */ + 'orders' => $this->_orders, + 'size' => $this->getPageSize(), + 'currentPage' => (int)$this->_curPage, ] ); } diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Section/AdminCatalogSearchTermIndexSection.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Section/AdminCatalogSearchTermIndexSection.xml index aa0145b9f96cd..dcaf7fb3a561d 100644 --- a/app/code/Magento/CatalogSearch/Test/Mftf/Section/AdminCatalogSearchTermIndexSection.xml +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Section/AdminCatalogSearchTermIndexSection.xml @@ -21,5 +21,6 @@ + \ No newline at end of file diff --git a/app/code/Magento/CatalogSearch/Test/Unit/Model/Adapter/Mysql/Filter/PreprocessorTest.php b/app/code/Magento/CatalogSearch/Test/Unit/Model/Adapter/Mysql/Filter/PreprocessorTest.php index 7e3de7534e8c4..a79ffcc33cabe 100644 --- a/app/code/Magento/CatalogSearch/Test/Unit/Model/Adapter/Mysql/Filter/PreprocessorTest.php +++ b/app/code/Magento/CatalogSearch/Test/Unit/Model/Adapter/Mysql/Filter/PreprocessorTest.php @@ -129,7 +129,7 @@ protected function setUp() ->getMock(); $this->connection = $this->getMockBuilder(\Magento\Framework\DB\Adapter\AdapterInterface::class) ->disableOriginalConstructor() - ->setMethods(['select', 'getIfNullSql', 'quote']) + ->setMethods(['select', 'getIfNullSql', 'quote', 'quoteInto']) ->getMockForAbstractClass(); $this->select = $this->getMockBuilder(\Magento\Framework\DB\Select::class) ->disableOriginalConstructor() @@ -222,9 +222,10 @@ public function testProcessPrice() public function processCategoryIdsDataProvider() { return [ - ['5', 'category_ids_index.category_id = 5'], - [3, 'category_ids_index.category_id = 3'], - ["' and 1 = 0", 'category_ids_index.category_id = 0'], + ['5', "category_ids_index.category_id in ('5')"], + [3, "category_ids_index.category_id in (3)"], + ["' and 1 = 0", "category_ids_index.category_id in ('\' and 1 = 0')"], + [['5', '10'], "category_ids_index.category_id in ('5', '10')"] ]; } @@ -251,6 +252,12 @@ public function testProcessCategoryIds($categoryId, $expectedResult) ->with(\Magento\Catalog\Model\Product::ENTITY, 'category_ids') ->will($this->returnValue($this->attribute)); + $this->connection + ->expects($this->once()) + ->method('quoteInto') + ->with('category_ids_index.category_id in (?)', $categoryId) + ->willReturn($expectedResult); + $actualResult = $this->target->process($this->filter, $isNegation, $query); $this->assertSame($expectedResult, $this->removeWhitespaces($actualResult)); } diff --git a/app/code/Magento/CatalogSearch/Test/Unit/Model/ResourceModel/Advanced/CollectionTest.php b/app/code/Magento/CatalogSearch/Test/Unit/Model/ResourceModel/Advanced/CollectionTest.php index 683070c286239..f5e5a34047aff 100644 --- a/app/code/Magento/CatalogSearch/Test/Unit/Model/ResourceModel/Advanced/CollectionTest.php +++ b/app/code/Magento/CatalogSearch/Test/Unit/Model/ResourceModel/Advanced/CollectionTest.php @@ -14,6 +14,7 @@ use Magento\CatalogSearch\Test\Unit\Model\ResourceModel\BaseCollection; use Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\SearchResultApplierFactory; use Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\TotalRecordsResolverFactory; +use PHPUnit\Framework\MockObject\MockObject; /** * Tests Magento\CatalogSearch\Model\ResourceModel\Advanced\Collection @@ -35,32 +36,37 @@ class CollectionTest extends BaseCollection private $advancedCollection; /** - * @var \Magento\Framework\Api\FilterBuilder|\PHPUnit_Framework_MockObject_MockObject + * @var \Magento\Framework\Api\FilterBuilder|MockObject */ private $filterBuilder; /** - * @var \Magento\Framework\Api\Search\SearchCriteriaBuilder|\PHPUnit_Framework_MockObject_MockObject + * @var \Magento\Framework\Api\Search\SearchCriteriaBuilder|MockObject */ private $criteriaBuilder; /** - * @var \Magento\Framework\Search\Adapter\Mysql\TemporaryStorageFactory|\PHPUnit_Framework_MockObject_MockObject + * @var \Magento\Framework\Search\Adapter\Mysql\TemporaryStorageFactory|MockObject */ private $temporaryStorageFactory; /** - * @var \Magento\Search\Api\SearchInterface|\PHPUnit_Framework_MockObject_MockObject + * @var \Magento\Search\Api\SearchInterface|MockObject */ private $search; /** - * @var \Magento\Eav\Model\Config|\PHPUnit_Framework_MockObject_MockObject + * @var \Magento\Eav\Model\Config|MockObject */ private $eavConfig; /** - * setUp method for CollectionTest + * @var SearchResultApplierFactory|MockObject + */ + private $searchResultApplierFactory; + + /** + * @inheritdoc */ protected function setUp() { @@ -97,17 +103,10 @@ protected function setUp() ->method('create') ->willReturn($searchCriteriaResolver); - $searchResultApplier = $this->getMockBuilder(SearchResultApplierInterface::class) - ->disableOriginalConstructor() - ->setMethods(['apply']) - ->getMockForAbstractClass(); - $searchResultApplierFactory = $this->getMockBuilder(SearchResultApplierFactory::class) + $this->searchResultApplierFactory = $this->getMockBuilder(SearchResultApplierFactory::class) ->disableOriginalConstructor() ->setMethods(['create']) ->getMock(); - $searchResultApplierFactory->expects($this->any()) - ->method('create') - ->willReturn($searchResultApplier); $totalRecordsResolver = $this->getMockBuilder(TotalRecordsResolverInterface::class) ->disableOriginalConstructor() @@ -134,12 +133,15 @@ protected function setUp() 'productLimitationFactory' => $productLimitationFactoryMock, 'collectionProvider' => null, 'searchCriteriaResolverFactory' => $searchCriteriaResolverFactory, - 'searchResultApplierFactory' => $searchResultApplierFactory, + 'searchResultApplierFactory' => $this->searchResultApplierFactory, 'totalRecordsResolverFactory' => $totalRecordsResolverFactory ] ); } + /** + * Test to Load data with filter in place + */ public function testLoadWithFilterNoFilters() { $this->advancedCollection->loadWithFilter(); @@ -150,6 +152,7 @@ public function testLoadWithFilterNoFilters() */ public function testLike() { + $pageSize = 10; $attributeCode = 'description'; $attributeCodeId = 42; $attribute = $this->createMock(\Magento\Catalog\Model\ResourceModel\Eav\Attribute::class); @@ -168,6 +171,22 @@ public function testLike() $searchResult = $this->createMock(\Magento\Framework\Api\Search\SearchResultInterface::class); $this->search->expects($this->once())->method('search')->willReturn($searchResult); + $this->advancedCollection->setPageSize($pageSize); + $this->advancedCollection->setCurPage(0); + + $searchResultApplier = $this->createMock(SearchResultApplierInterface::class); + $this->searchResultApplierFactory->expects($this->once()) + ->method('create') + ->with( + [ + 'collection' => $this->advancedCollection, + 'searchResult' => $searchResult, + 'orders' => [], + 'size' => $pageSize, + ] + ) + ->willReturn($searchResultApplier); + // addFieldsToFilter will load filters, // then loadWithFilter will trigger _renderFiltersBefore code in Advanced/Collection $this->assertSame( @@ -177,7 +196,7 @@ public function testLike() } /** - * @return \PHPUnit_Framework_MockObject_MockObject + * @return MockObject */ protected function getCriteriaBuilder() { @@ -185,6 +204,7 @@ protected function getCriteriaBuilder() ->setMethods(['addFilter', 'create', 'setRequestName']) ->disableOriginalConstructor() ->getMock(); + return $criteriaBuilder; } } diff --git a/app/code/Magento/CatalogSearch/Test/Unit/Model/ResourceModel/Fulltext/CollectionTest.php b/app/code/Magento/CatalogSearch/Test/Unit/Model/ResourceModel/Fulltext/CollectionTest.php index 9170b81dc3182..9b4010cfae453 100644 --- a/app/code/Magento/CatalogSearch/Test/Unit/Model/ResourceModel/Fulltext/CollectionTest.php +++ b/app/code/Magento/CatalogSearch/Test/Unit/Model/ResourceModel/Fulltext/CollectionTest.php @@ -5,6 +5,7 @@ */ namespace Magento\CatalogSearch\Test\Unit\Model\ResourceModel\Fulltext; +use Magento\Catalog\Model\ResourceModel\Product\Collection\ProductLimitationFactory; use Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\SearchCriteriaResolverFactory; use Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\SearchCriteriaResolverInterface; use Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\SearchResultApplierFactory; @@ -12,11 +13,12 @@ use Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\SearchResultApplierInterface; use Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\TotalRecordsResolverInterface; use Magento\CatalogSearch\Test\Unit\Model\ResourceModel\BaseCollection; +use PHPUnit\Framework\MockObject\MockObject; use Magento\Framework\Search\Adapter\Mysql\TemporaryStorageFactory; -use PHPUnit_Framework_MockObject_MockObject as MockObject; -use Magento\Catalog\Model\ResourceModel\Product\Collection\ProductLimitationFactory; /** + * Test class for Fulltext Collection + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class CollectionTest extends BaseCollection @@ -27,12 +29,12 @@ class CollectionTest extends BaseCollection private $objectManager; /** - * @var \Magento\Framework\Search\Adapter\Mysql\TemporaryStorage|\PHPUnit_Framework_MockObject_MockObject + * @var \Magento\Framework\Search\Adapter\Mysql\TemporaryStorage|MockObject */ private $temporaryStorage; /** - * @var \Magento\Search\Api\SearchInterface|\PHPUnit_Framework_MockObject_MockObject + * @var \Magento\Search\Api\SearchInterface|MockObject */ private $search; @@ -61,6 +63,11 @@ class CollectionTest extends BaseCollection */ private $filterBuilder; + /** + * @var SearchResultApplierFactory|MockObject + */ + private $searchResultApplierFactory; + /** * @var \Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection */ @@ -72,7 +79,7 @@ class CollectionTest extends BaseCollection private $filter; /** - * setUp method for CollectionTest + * @inheritdoc */ protected function setUp() { @@ -115,17 +122,10 @@ protected function setUp() ->method('create') ->willReturn($searchCriteriaResolver); - $searchResultApplier = $this->getMockBuilder(SearchResultApplierInterface::class) - ->disableOriginalConstructor() - ->setMethods(['apply']) - ->getMockForAbstractClass(); - $searchResultApplierFactory = $this->getMockBuilder(SearchResultApplierFactory::class) + $this->searchResultApplierFactory = $this->getMockBuilder(SearchResultApplierFactory::class) ->disableOriginalConstructor() ->setMethods(['create']) ->getMock(); - $searchResultApplierFactory->expects($this->any()) - ->method('create') - ->willReturn($searchResultApplier); $totalRecordsResolver = $this->getMockBuilder(TotalRecordsResolverInterface::class) ->disableOriginalConstructor() @@ -148,7 +148,7 @@ protected function setUp() 'temporaryStorageFactory' => $temporaryStorageFactory, 'productLimitationFactory' => $productLimitationFactoryMock, 'searchCriteriaResolverFactory' => $searchCriteriaResolverFactory, - 'searchResultApplierFactory' => $searchResultApplierFactory, + 'searchResultApplierFactory' => $this->searchResultApplierFactory, 'totalRecordsResolverFactory' => $totalRecordsResolverFactory, ] ); @@ -161,6 +161,9 @@ protected function setUp() $this->model->setFilterBuilder($this->filterBuilder); } + /** + * @inheritdoc + */ protected function tearDown() { $reflectionProperty = new \ReflectionProperty(\Magento\Framework\App\ObjectManager::class, '_instance'); @@ -168,16 +171,49 @@ protected function tearDown() $reflectionProperty->setValue(null); } + /** + * Test to Return field faceted data from faceted search result + */ public function testGetFacetedDataWithEmptyAggregations() { + $pageSize = 10; + $searchResult = $this->getMockBuilder(\Magento\Framework\Api\Search\SearchResultInterface::class) ->getMockForAbstractClass(); $this->search->expects($this->once()) ->method('search') ->willReturn($searchResult); + + $searchResultApplier = $this->getMockBuilder(SearchResultApplierInterface::class) + ->disableOriginalConstructor() + ->setMethods(['apply']) + ->getMockForAbstractClass(); + $this->searchResultApplierFactory->expects($this->any()) + ->method('create') + ->willReturn($searchResultApplier); + + $this->model->setPageSize($pageSize); + $this->model->setCurPage(0); + + $this->searchResultApplierFactory->expects($this->once()) + ->method('create') + ->with( + [ + 'collection' => $this->model, + 'searchResult' => $searchResult, + 'orders' => [], + 'size' => $pageSize, + 'currentPage' => 0, + ] + ) + ->willReturn($searchResultApplier); + $this->model->getFacetedData('field'); } + /** + * Test to Apply attribute filter to facet collection + */ public function testAddFieldToFilter() { $this->filter = $this->createFilter(); @@ -220,6 +256,7 @@ protected function getCriteriaBuilder() protected function getFilterBuilder() { $filterBuilder = $this->createMock(\Magento\Framework\Api\FilterBuilder::class); + return $filterBuilder; } @@ -241,6 +278,7 @@ protected function addFiltersToFilterBuilder(MockObject $filterBuilder, array $f ->with($value) ->willReturnSelf(); } + return $filterBuilder; } @@ -252,6 +290,7 @@ protected function createFilter() $filter = $this->getMockBuilder(\Magento\Framework\Api\Filter::class) ->disableOriginalConstructor() ->getMock(); + return $filter; } } diff --git a/app/code/Magento/CatalogUrlRewrite/Observer/AfterImportDataObserver.php b/app/code/Magento/CatalogUrlRewrite/Observer/AfterImportDataObserver.php index 33c0cafc8f081..704b60a8aaf2a 100644 --- a/app/code/Magento/CatalogUrlRewrite/Observer/AfterImportDataObserver.php +++ b/app/code/Magento/CatalogUrlRewrite/Observer/AfterImportDataObserver.php @@ -299,12 +299,16 @@ protected function _populateForUrlGeneration($rowData) */ private function isNeedToPopulateForUrlGeneration($rowData, $newSku, $oldSku): bool { - if ((empty($newSku) || !isset($newSku['entity_id'])) - || ($this->import->getRowScope($rowData) == ImportProduct::SCOPE_STORE - && empty($rowData[self::URL_KEY_ATTRIBUTE_CODE])) - || (array_key_exists($rowData[ImportProduct::COL_SKU], $oldSku) - && !isset($rowData[self::URL_KEY_ATTRIBUTE_CODE]) - && $this->import->getBehavior() === ImportExport::BEHAVIOR_APPEND)) { + if (( + (empty($newSku) || !isset($newSku['entity_id'])) + || ($this->import->getRowScope($rowData) == ImportProduct::SCOPE_STORE + && empty($rowData[self::URL_KEY_ATTRIBUTE_CODE])) + || (array_key_exists(strtolower($rowData[ImportProduct::COL_SKU]), $oldSku) + && !isset($rowData[self::URL_KEY_ATTRIBUTE_CODE]) + && $this->import->getBehavior() === ImportExport::BEHAVIOR_APPEND) + ) + && !isset($rowData["categories"]) + ) { return false; } return true; @@ -477,7 +481,7 @@ protected function currentUrlRewritesRegenerate() $url = $currentUrlRewrite->getIsAutogenerated() ? $this->generateForAutogenerated($currentUrlRewrite, $category) : $this->generateForCustom($currentUrlRewrite, $category); - $urlRewrites = array_merge($urlRewrites, $url); + $urlRewrites = $url + $urlRewrites; } $this->product = null; diff --git a/app/code/Magento/CatalogUrlRewrite/Observer/CategoryUrlPathAutogeneratorObserver.php b/app/code/Magento/CatalogUrlRewrite/Observer/CategoryUrlPathAutogeneratorObserver.php index 713dd6ac0c736..7f987124040fd 100644 --- a/app/code/Magento/CatalogUrlRewrite/Observer/CategoryUrlPathAutogeneratorObserver.php +++ b/app/code/Magento/CatalogUrlRewrite/Observer/CategoryUrlPathAutogeneratorObserver.php @@ -9,7 +9,6 @@ use Magento\CatalogUrlRewrite\Model\CategoryUrlPathGenerator; use Magento\CatalogUrlRewrite\Service\V1\StoreViewService; use Magento\Catalog\Api\CategoryRepositoryInterface; -use Magento\Framework\Event\Observer; use Magento\CatalogUrlRewrite\Model\Category\ChildrenCategoriesProvider; use Magento\Framework\Event\ObserverInterface; use Magento\Store\Model\Store; @@ -68,13 +67,15 @@ public function execute(\Magento\Framework\Event\Observer $observer) { /** @var Category $category */ $category = $observer->getEvent()->getCategory(); - $useDefaultAttribute = !$category->isObjectNew() && !empty($category->getData('use_default')['url_key']); + $useDefaultAttribute = !empty($category->getData('use_default')['url_key']); if ($category->getUrlKey() !== false && !$useDefaultAttribute) { $resultUrlKey = $this->categoryUrlPathGenerator->getUrlKey($category); $this->updateUrlKey($category, $resultUrlKey); - } else if ($useDefaultAttribute) { - $resultUrlKey = $category->formatUrlKey($category->getOrigData('name')); - $this->updateUrlKey($category, $resultUrlKey); + } elseif ($useDefaultAttribute) { + if (!$category->isObjectNew()) { + $resultUrlKey = $category->formatUrlKey($category->getOrigData('name')); + $this->updateUrlKey($category, $resultUrlKey); + } $category->setUrlKey(null)->setUrlPath(null); } } diff --git a/app/code/Magento/CatalogUrlRewrite/Setup/Patch/Data/UpdateUrlKeySearchable.php b/app/code/Magento/CatalogUrlRewrite/Setup/Patch/Data/UpdateUrlKeySearchable.php new file mode 100644 index 0000000000000..75f88a8573069 --- /dev/null +++ b/app/code/Magento/CatalogUrlRewrite/Setup/Patch/Data/UpdateUrlKeySearchable.php @@ -0,0 +1,79 @@ +moduleDataSetup = $moduleDataSetup; + $this->categorySetupFactory = $categorySetupFactory; + } + + /** + * @inheritdoc + */ + public function apply() + { + /** @var CategorySetup $categorySetup */ + $categorySetup = $this->categorySetupFactory->create(['setup' => $this->moduleDataSetup]); + + $categorySetup->updateAttribute( + \Magento\Catalog\Model\Product::ENTITY, + 'url_key', + 'is_searchable', + true + ); + + $categorySetup->updateAttribute( + \Magento\Catalog\Model\Category::ENTITY, + 'url_key', + 'is_searchable', + true + ); + } + + /** + * @inheritdoc + */ + public static function getDependencies() + { + return [CreateUrlAttributes::class]; + } + + /** + * @inheritdoc + */ + public function getAliases() + { + return []; + } +} diff --git a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Observer/CategoryUrlPathAutogeneratorObserverTest.php b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Observer/CategoryUrlPathAutogeneratorObserverTest.php index 1b4d1e08aa208..0a570adab309a 100644 --- a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Observer/CategoryUrlPathAutogeneratorObserverTest.php +++ b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Observer/CategoryUrlPathAutogeneratorObserverTest.php @@ -3,37 +3,51 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\CatalogUrlRewrite\Test\Unit\Observer; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; class CategoryUrlPathAutogeneratorObserverTest extends \PHPUnit\Framework\TestCase { - /** @var \Magento\CatalogUrlRewrite\Observer\CategoryUrlPathAutogeneratorObserver */ - protected $categoryUrlPathAutogeneratorObserver; + /** + * @var \Magento\CatalogUrlRewrite\Observer\CategoryUrlPathAutogeneratorObserver + */ + private $categoryUrlPathAutogeneratorObserver; - /** @var \PHPUnit_Framework_MockObject_MockObject */ - protected $categoryUrlPathGenerator; + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $categoryUrlPathGenerator; - /** @var \PHPUnit_Framework_MockObject_MockObject */ - protected $childrenCategoriesProvider; + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $childrenCategoriesProvider; - /** @var \PHPUnit_Framework_MockObject_MockObject */ - protected $observer; + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $observer; - /** @var \PHPUnit_Framework_MockObject_MockObject */ - protected $category; + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $category; /** * @var \Magento\CatalogUrlRewrite\Service\V1\StoreViewService|\PHPUnit_Framework_MockObject_MockObject */ - protected $storeViewService; + private $storeViewService; /** * @var \Magento\Catalog\Model\ResourceModel\Category|\PHPUnit_Framework_MockObject_MockObject */ - protected $categoryResource; + private $categoryResource; + /** + * @inheritDoc + */ protected function setUp() { $this->observer = $this->createPartialMock( @@ -41,16 +55,15 @@ protected function setUp() ['getEvent', 'getCategory'] ); $this->categoryResource = $this->createMock(\Magento\Catalog\Model\ResourceModel\Category::class); - $this->category = $this->createPartialMock(\Magento\Catalog\Model\Category::class, [ - 'setUrlKey', - 'setUrlPath', + $this->category = $this->createPartialMock( + \Magento\Catalog\Model\Category::class, + [ 'dataHasChangedFor', - 'isObjectNew', 'getResource', - 'getUrlKey', 'getStoreId', - 'getData' - ]); + 'formatUrlKey' + ] + ); $this->category->expects($this->any())->method('getResource')->willReturn($this->categoryResource); $this->observer->expects($this->any())->method('getEvent')->willReturnSelf(); $this->observer->expects($this->any())->method('getCategory')->willReturn($this->category); @@ -73,106 +86,125 @@ protected function setUp() ); } - public function testSetCategoryUrlAndCategoryPath() + /** + * @param $isObjectNew + * @throws \Magento\Framework\Exception\LocalizedException + * @dataProvider shouldFormatUrlKeyAndGenerateUrlPathIfUrlKeyIsNotUsingDefaultValueDataProvider + */ + public function testShouldFormatUrlKeyAndGenerateUrlPathIfUrlKeyIsNotUsingDefaultValue($isObjectNew) { - $this->category->expects($this->once())->method('getUrlKey')->willReturn('category'); - $this->categoryUrlPathGenerator->expects($this->once())->method('getUrlKey')->willReturn('urk_key'); - $this->category->expects($this->once())->method('setUrlKey')->with('urk_key')->willReturnSelf(); - $this->categoryUrlPathGenerator->expects($this->once())->method('getUrlPath')->willReturn('url_path'); - $this->category->expects($this->once())->method('setUrlPath')->with('url_path')->willReturnSelf(); - $this->category->expects($this->exactly(2))->method('isObjectNew')->willReturn(true); - + $expectedUrlKey = 'formatted_url_key'; + $expectedUrlPath = 'generated_url_path'; + $categoryData = ['use_default' => ['url_key' => 0], 'url_key' => 'some_key', 'url_path' => '']; + $this->category->setData($categoryData); + $this->category->isObjectNew($isObjectNew); + $this->categoryUrlPathGenerator->expects($this->once())->method('getUrlKey')->willReturn($expectedUrlKey); + $this->categoryUrlPathGenerator->expects($this->once())->method('getUrlPath')->willReturn($expectedUrlPath); + $this->assertEquals($categoryData['url_key'], $this->category->getUrlKey()); + $this->assertEquals($categoryData['url_path'], $this->category->getUrlPath()); $this->categoryUrlPathAutogeneratorObserver->execute($this->observer); + $this->assertEquals($expectedUrlKey, $this->category->getUrlKey()); + $this->assertEquals($expectedUrlPath, $this->category->getUrlPath()); + $this->categoryResource->expects($this->never())->method('saveAttribute'); } - public function testExecuteWithoutUrlKeyAndUrlPathUpdating() + /** + * @return array + */ + public function shouldFormatUrlKeyAndGenerateUrlPathIfUrlKeyIsNotUsingDefaultValueDataProvider() { - $this->category->expects($this->once())->method('getUrlKey')->willReturn(false); - $this->category->expects($this->never())->method('setUrlKey'); - $this->category->expects($this->never())->method('setUrlPath'); - $this->categoryUrlPathAutogeneratorObserver->execute($this->observer); + return [ + [true], + [false], + ]; } /** - * @expectedException \Magento\Framework\Exception\LocalizedException - * @expectedExceptionMessage Invalid URL key + * @param $isObjectNew + * @throws \Magento\Framework\Exception\LocalizedException + * @dataProvider shouldResetUrlPathAndUrlKeyIfUrlKeyIsUsingDefaultValueDataProvider */ - public function testExecuteWithException() + public function testShouldResetUrlPathAndUrlKeyIfUrlKeyIsUsingDefaultValue($isObjectNew) { - $categoryName = 'test'; - $categoryData = ['url_key' => 0]; - $this->category->expects($this->once())->method('getUrlKey')->willReturn($categoryName); - $this->category->expects($this->once()) - ->method('getData') - ->with('use_default') - ->willReturn($categoryData); - $this->categoryUrlPathGenerator->expects($this->once()) - ->method('getUrlKey') - ->with($this->category) - ->willReturn(null); + $categoryData = ['use_default' => ['url_key' => 1], 'url_key' => 'some_key', 'url_path' => 'some_path']; + $this->category->setData($categoryData); + $this->category->isObjectNew($isObjectNew); + $this->category->expects($this->any())->method('formatUrlKey')->willReturn('formatted_key'); + $this->assertEquals($categoryData['url_key'], $this->category->getUrlKey()); + $this->assertEquals($categoryData['url_path'], $this->category->getUrlPath()); $this->categoryUrlPathAutogeneratorObserver->execute($this->observer); + $this->assertNull($this->category->getUrlKey()); + $this->assertNull($this->category->getUrlPath()); } - public function testUrlKeyAndUrlPathUpdating() + /** + * @return array + */ + public function shouldResetUrlPathAndUrlKeyIfUrlKeyIsUsingDefaultValueDataProvider() { - $this->categoryUrlPathGenerator->expects($this->once())->method('getUrlKey')->with($this->category) - ->willReturn('url_key'); - $this->categoryUrlPathGenerator->expects($this->once())->method('getUrlPath')->with($this->category) - ->willReturn('url_path'); - - $this->category->expects($this->once())->method('getUrlKey')->willReturn('not_formatted_url_key'); - $this->category->expects($this->once())->method('setUrlKey')->with('url_key')->willReturnSelf(); - $this->category->expects($this->once())->method('setUrlPath')->with('url_path')->willReturnSelf(); - // break code execution - $this->category->expects($this->exactly(2))->method('isObjectNew')->willReturn(true); + return [ + [true], + [false], + ]; + } + /** + * @param $useDefaultUrlKey + * @param $isObjectNew + * @throws \Magento\Framework\Exception\LocalizedException + * @dataProvider shouldThrowExceptionIfUrlKeyIsEmptyDataProvider + */ + public function testShouldThrowExceptionIfUrlKeyIsEmpty($useDefaultUrlKey, $isObjectNew) + { + $this->expectExceptionMessage('Invalid URL key'); + $categoryData = ['use_default' => ['url_key' => $useDefaultUrlKey], 'url_key' => '', 'url_path' => '']; + $this->category->setData($categoryData); + $this->category->isObjectNew($isObjectNew); + $this->assertEquals($isObjectNew, $this->category->isObjectNew()); + $this->assertEquals($categoryData['url_key'], $this->category->getUrlKey()); + $this->assertEquals($categoryData['url_path'], $this->category->getUrlPath()); $this->categoryUrlPathAutogeneratorObserver->execute($this->observer); + $this->assertEquals($categoryData['url_key'], $this->category->getUrlKey()); + $this->assertEquals($categoryData['url_path'], $this->category->getUrlPath()); } - public function testUrlPathAttributeNoUpdatingIfCategoryIsNew() + /** + * @return array + */ + public function shouldThrowExceptionIfUrlKeyIsEmptyDataProvider() { - $this->categoryUrlPathGenerator->expects($this->any())->method('getUrlKey')->willReturn('url_key'); - $this->categoryUrlPathGenerator->expects($this->any())->method('getUrlPath')->willReturn('url_path'); - - $this->category->expects($this->any())->method('getUrlKey')->willReturn('not_formatted_url_key'); - $this->category->expects($this->any())->method('setUrlKey')->willReturnSelf(); - $this->category->expects($this->any())->method('setUrlPath')->willReturnSelf(); - - $this->category->expects($this->exactly(2))->method('isObjectNew')->willReturn(true); - $this->categoryResource->expects($this->never())->method('saveAttribute'); - - $this->categoryUrlPathAutogeneratorObserver->execute($this->observer); + return [ + [0, false], + [0, true], + [1, false], + ]; } public function testUrlPathAttributeUpdating() { - $this->categoryUrlPathGenerator->expects($this->any())->method('getUrlKey')->willReturn('url_key'); - $this->categoryUrlPathGenerator->expects($this->any())->method('getUrlPath')->willReturn('url_path'); - - $this->category->expects($this->any())->method('getUrlKey')->willReturn('not_formatted_url_key'); - $this->category->expects($this->any())->method('setUrlKey')->willReturnSelf(); - $this->category->expects($this->any())->method('setUrlPath')->willReturnSelf(); - $this->category->expects($this->exactly(2))->method('isObjectNew')->willReturn(false); - + $categoryData = ['url_key' => 'some_key', 'url_path' => '']; + $this->category->setData($categoryData); + $this->category->isObjectNew(false); + $expectedUrlKey = 'formatted_url_key'; + $expectedUrlPath = 'generated_url_path'; + $this->categoryUrlPathGenerator->expects($this->any())->method('getUrlKey')->willReturn($expectedUrlKey); + $this->categoryUrlPathGenerator->expects($this->any())->method('getUrlPath')->willReturn($expectedUrlPath); $this->categoryResource->expects($this->once())->method('saveAttribute')->with($this->category, 'url_path'); - - // break code execution $this->category->expects($this->once())->method('dataHasChangedFor')->with('url_path')->willReturn(false); - $this->categoryUrlPathAutogeneratorObserver->execute($this->observer); } public function testChildrenUrlPathAttributeNoUpdatingIfParentUrlPathIsNotChanged() { + $categoryData = ['url_key' => 'some_key', 'url_path' => '']; + $this->category->setData($categoryData); + $this->category->isObjectNew(false); + $this->categoryUrlPathGenerator->expects($this->any())->method('getUrlKey')->willReturn('url_key'); $this->categoryUrlPathGenerator->expects($this->any())->method('getUrlPath')->willReturn('url_path'); $this->categoryResource->expects($this->once())->method('saveAttribute')->with($this->category, 'url_path'); - $this->category->expects($this->any())->method('getUrlKey')->willReturn('not_formatted_url_key'); - $this->category->expects($this->any())->method('setUrlKey')->willReturnSelf(); - $this->category->expects($this->any())->method('setUrlPath')->willReturnSelf(); - $this->category->expects($this->exactly(2))->method('isObjectNew')->willReturn(false); // break code execution $this->category->expects($this->once())->method('dataHasChangedFor')->with('url_path')->willReturn(false); @@ -181,13 +213,12 @@ public function testChildrenUrlPathAttributeNoUpdatingIfParentUrlPathIsNotChange public function testChildrenUrlPathAttributeUpdatingForSpecificStore() { + $categoryData = ['url_key' => 'some_key', 'url_path' => '']; + $this->category->setData($categoryData); + $this->category->isObjectNew(false); + $this->categoryUrlPathGenerator->expects($this->any())->method('getUrlKey')->willReturn('generated_url_key'); $this->categoryUrlPathGenerator->expects($this->any())->method('getUrlPath')->willReturn('generated_url_path'); - - $this->category->expects($this->any())->method('getUrlKey')->willReturn('not_formatted_url_key'); - $this->category->expects($this->any())->method('setUrlKey')->willReturnSelf(); - $this->category->expects($this->any())->method('setUrlPath')->willReturnSelf(); - $this->category->expects($this->exactly(2))->method('isObjectNew')->willReturn(false); $this->category->expects($this->any())->method('dataHasChangedFor')->willReturn(true); // only for specific store $this->category->expects($this->atLeastOnce())->method('getStoreId')->willReturn(1); @@ -195,15 +226,18 @@ public function testChildrenUrlPathAttributeUpdatingForSpecificStore() $childCategoryResource = $this->getMockBuilder(\Magento\Catalog\Model\ResourceModel\Category::class) ->disableOriginalConstructor()->getMock(); $childCategory = $this->getMockBuilder(\Magento\Catalog\Model\Category::class) - ->setMethods([ - 'getUrlPath', - 'setUrlPath', - 'getResource', - 'getStore', - 'getStoreId', - 'setStoreId' - ]) - ->disableOriginalConstructor()->getMock(); + ->setMethods( + [ + 'getUrlPath', + 'setUrlPath', + 'getResource', + 'getStore', + 'getStoreId', + 'setStoreId' + ] + ) + ->disableOriginalConstructor() + ->getMock(); $childCategory->expects($this->any())->method('getResource')->willReturn($childCategoryResource); $childCategory->expects($this->once())->method('setStoreId')->with(1); diff --git a/app/code/Magento/CatalogUrlRewriteGraphQl/etc/di.xml b/app/code/Magento/CatalogUrlRewriteGraphQl/etc/di.xml index 20e6b7e9c0053..e99f89477e807 100644 --- a/app/code/Magento/CatalogUrlRewriteGraphQl/etc/di.xml +++ b/app/code/Magento/CatalogUrlRewriteGraphQl/etc/di.xml @@ -14,4 +14,12 @@ + + + + + url_key + + + diff --git a/app/code/Magento/CatalogUrlRewriteGraphQl/etc/schema.graphqls b/app/code/Magento/CatalogUrlRewriteGraphQl/etc/schema.graphqls index 89108e578d673..4453674de04dd 100644 --- a/app/code/Magento/CatalogUrlRewriteGraphQl/etc/schema.graphqls +++ b/app/code/Magento/CatalogUrlRewriteGraphQl/etc/schema.graphqls @@ -12,6 +12,10 @@ input ProductFilterInput { url_path: FilterTypeInput @deprecated(reason: "Use product's `canonical_url` or url rewrites instead") } +input ProductAttributeFilterInput { + url_key: FilterEqualTypeInput @doc(description: "The part of the URL that identifies the product") +} + input ProductSortInput { url_key: SortEnum @doc(description: "The part of the URL that identifies the product") url_path: SortEnum @deprecated(reason: "Use product's `canonical_url` or url rewrites instead") diff --git a/app/code/Magento/CatalogWidget/composer.json b/app/code/Magento/CatalogWidget/composer.json index 6722d0df93752..8c1bd220a0f32 100644 --- a/app/code/Magento/CatalogWidget/composer.json +++ b/app/code/Magento/CatalogWidget/composer.json @@ -14,7 +14,8 @@ "magento/module-rule": "*", "magento/module-store": "*", "magento/module-widget": "*", - "magento/module-wishlist": "*" + "magento/module-wishlist": "*", + "magento/module-theme": "*" }, "type": "magento2-module", "license": [ diff --git a/app/code/Magento/Checkout/Controller/Cart/UpdateItemQty.php b/app/code/Magento/Checkout/Controller/Cart/UpdateItemQty.php index ac4a93e6066a4..9d17e32b2c93d 100644 --- a/app/code/Magento/Checkout/Controller/Cart/UpdateItemQty.php +++ b/app/code/Magento/Checkout/Controller/Cart/UpdateItemQty.php @@ -8,16 +8,25 @@ namespace Magento\Checkout\Controller\Cart; use Magento\Checkout\Model\Cart\RequestQuantityProcessor; +use Magento\Checkout\Model\Session as CheckoutSession; +use Magento\Framework\App\Action\Action; use Magento\Framework\App\Action\Context; +use Magento\Framework\App\Action\HttpPostActionInterface; +use Magento\Framework\Data\Form\FormKey\Validator as FormKeyValidator; use Magento\Framework\Exception\LocalizedException; -use Magento\Checkout\Model\Session as CheckoutSession; +use Magento\Framework\Exception\NotFoundException; use Magento\Framework\Serialize\Serializer\Json; -use Magento\Framework\Data\Form\FormKey\Validator as FormKeyValidator; use Magento\Quote\Model\Quote\Item; use Psr\Log\LoggerInterface; -class UpdateItemQty extends \Magento\Framework\App\Action\Action +/** + * UpdateItemQty ajax request + * + * @package Magento\Checkout\Controller\Cart + */ +class UpdateItemQty extends Action implements HttpPostActionInterface { + /** * @var RequestQuantityProcessor */ @@ -44,13 +53,16 @@ class UpdateItemQty extends \Magento\Framework\App\Action\Action private $logger; /** - * @param Context $context, + * UpdateItemQty constructor + * + * @param Context $context * @param RequestQuantityProcessor $quantityProcessor * @param FormKeyValidator $formKeyValidator * @param CheckoutSession $checkoutSession * @param Json $json * @param LoggerInterface $logger */ + public function __construct( Context $context, RequestQuantityProcessor $quantityProcessor, @@ -68,30 +80,26 @@ public function __construct( } /** + * Controller execute method + * * @return void */ public function execute() { try { - if (!$this->formKeyValidator->validate($this->getRequest())) { - throw new LocalizedException( - __('Something went wrong while saving the page. Please refresh the page and try again.') - ); - } + $this->validateRequest(); + $this->validateFormKey(); $cartData = $this->getRequest()->getParam('cart'); - if (!is_array($cartData)) { - throw new LocalizedException( - __('Something went wrong while saving the page. Please refresh the page and try again.') - ); - } + + $this->validateCartData($cartData); $cartData = $this->quantityProcessor->process($cartData); $quote = $this->checkoutSession->getQuote(); foreach ($cartData as $itemId => $itemInfo) { $item = $quote->getItemById($itemId); - $qty = isset($itemInfo['qty']) ? (double)$itemInfo['qty'] : 0; + $qty = isset($itemInfo['qty']) ? (double) $itemInfo['qty'] : 0; if ($item) { $this->updateItemQuantity($item, $qty); } @@ -111,11 +119,13 @@ public function execute() * * @param Item $item * @param float $qty + * @return void * @throws LocalizedException */ private function updateItemQuantity(Item $item, float $qty) { if ($qty > 0) { + $item->clearMessage(); $item->setQty($qty); if ($item->getHasError()) { @@ -145,9 +155,7 @@ private function jsonResponse(string $error = '') */ private function getResponseData(string $error = ''): array { - $response = [ - 'success' => true, - ]; + $response = ['success' => true]; if (!empty($error)) { $response = [ @@ -158,4 +166,48 @@ private function getResponseData(string $error = ''): array return $response; } + + /** + * Validates the Request HTTP method + * + * @return void + * @throws NotFoundException + */ + private function validateRequest() + { + if ($this->getRequest()->isPost() === false) { + throw new NotFoundException(__('Page Not Found')); + } + } + + /** + * Validates form key + * + * @return void + * @throws LocalizedException + */ + private function validateFormKey() + { + if (!$this->formKeyValidator->validate($this->getRequest())) { + throw new LocalizedException( + __('Something went wrong while saving the page. Please refresh the page and try again.') + ); + } + } + + /** + * Validates cart data + * + * @param array|null $cartData + * @return void + * @throws LocalizedException + */ + private function validateCartData($cartData = null) + { + if (!is_array($cartData)) { + throw new LocalizedException( + __('Something went wrong while saving the page. Please refresh the page and try again.') + ); + } + } } diff --git a/app/code/Magento/Checkout/Model/Session.php b/app/code/Magento/Checkout/Model/Session.php index 6dfdefb8601aa..a654c78853d7a 100644 --- a/app/code/Magento/Checkout/Model/Session.php +++ b/app/code/Magento/Checkout/Model/Session.php @@ -17,6 +17,7 @@ * @api * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) + * @SuppressWarnings(PHPMD.TooManyFields) */ class Session extends \Magento\Framework\Session\SessionManager { @@ -46,6 +47,15 @@ class Session extends \Magento\Framework\Session\SessionManager */ protected $_loadInactive = false; + /** + * A flag to track when the quote is being loaded and attached to the session object. + * + * Used in trigger_recollect infinite loop detection. + * + * @var bool + */ + private $isLoading = false; + /** * Loaded order instance * @@ -227,6 +237,10 @@ public function getQuote() $this->_eventManager->dispatch('custom_quote_process', ['checkout_session' => $this]); if ($this->_quote === null) { + if ($this->isLoading) { + throw new \LogicException("Infinite loop detected, review the trace for the looping path"); + } + $this->isLoading = true; $quote = $this->quoteFactory->create(); if ($this->getQuoteId()) { try { @@ -289,6 +303,7 @@ public function getQuote() $quote->setStore($this->_storeManager->getStore()); $this->_quote = $quote; + $this->isLoading = false; } if (!$this->isQuoteMasked() && !$this->_customerSession->isLoggedIn() && $this->getQuoteId()) { @@ -370,6 +385,11 @@ public function loadCustomerQuote() $this->quoteRepository->save( $customerQuote->merge($this->getQuote())->collectTotals() ); + $newQuote = $this->quoteRepository->get($customerQuote->getId()); + $this->quoteRepository->save( + $newQuote->collectTotals() + ); + $customerQuote = $newQuote; } $this->setQuoteId($customerQuote->getId()); diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertStorefrontMiniCartSubtotalActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertStorefrontMiniCartSubtotalActionGroup.xml new file mode 100644 index 0000000000000..eba82860e8164 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertStorefrontMiniCartSubtotalActionGroup.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/NoErrorCartCheckoutForProductsDeletedFromMiniCartTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/NoErrorCartCheckoutForProductsDeletedFromMiniCartTest.xml index 1f3d9db5ca524..3c98f9177f4a7 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/NoErrorCartCheckoutForProductsDeletedFromMiniCartTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/NoErrorCartCheckoutForProductsDeletedFromMiniCartTest.xml @@ -47,7 +47,8 @@ - + + diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddBundleDynamicProductToShoppingCartWithDisableMiniCartSidebarTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddBundleDynamicProductToShoppingCartWithDisableMiniCartSidebarTest.xml index 9c00f2be1d60b..d67800e21afc2 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddBundleDynamicProductToShoppingCartWithDisableMiniCartSidebarTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddBundleDynamicProductToShoppingCartWithDisableMiniCartSidebarTest.xml @@ -109,7 +109,7 @@ - + diff --git a/app/code/Magento/Checkout/etc/adminhtml/system.xml b/app/code/Magento/Checkout/etc/adminhtml/system.xml index 11e3ba5f3ed9a..399474a36bfc7 100644 --- a/app/code/Magento/Checkout/etc/adminhtml/system.xml +++ b/app/code/Magento/Checkout/etc/adminhtml/system.xml @@ -27,12 +27,14 @@ Maximum Number of Items to Display in Order Summary + validate-zero-or-greater validate-digits Shopping Cart Quote Lifetime (days) + validate-zero-or-greater validate-digits After Adding a Product Redirect to Shopping Cart @@ -40,6 +42,7 @@ Number of Items to Display Pager + validate-zero-or-greater validate-digits Show Cross-sell Items in the Shopping Cart @@ -54,16 +57,18 @@ - Shopping Cart Sidebar + Mini Cart - Display Shopping Cart Sidebar + Display Mini Cart Magento\Config\Model\Config\Source\Yesno Number of Items to Display Scrollbar + validate-zero-or-greater validate-digits Maximum Number of Items to Display + validate-zero-or-greater validate-digits @@ -83,6 +88,7 @@ Send Payment Failed Email Copy To + validate-emails Separate by ",". diff --git a/app/code/Magento/Checkout/etc/frontend/sections.xml b/app/code/Magento/Checkout/etc/frontend/sections.xml index 90c2878f501cf..46dd8d9106545 100644 --- a/app/code/Magento/Checkout/etc/frontend/sections.xml +++ b/app/code/Magento/Checkout/etc/frontend/sections.xml @@ -41,7 +41,6 @@ - diff --git a/app/code/Magento/Checkout/i18n/en_US.csv b/app/code/Magento/Checkout/i18n/en_US.csv index 7f2f0b4390321..251985faf6cc4 100644 --- a/app/code/Magento/Checkout/i18n/en_US.csv +++ b/app/code/Magento/Checkout/i18n/en_US.csv @@ -156,8 +156,8 @@ Shipping,Shipping "Number of Items to Display Pager","Number of Items to Display Pager" "My Cart Link","My Cart Link" "Display Cart Summary","Display Cart Summary" -"Shopping Cart Sidebar","Shopping Cart Sidebar" -"Display Shopping Cart Sidebar","Display Shopping Cart Sidebar" +"Mini Cart","Mini Cart" +"Display Mini Cart","Display Mini Cart" "Number of Items to Display Scrollbar","Number of Items to Display Scrollbar" "Maximum Number of Items to Display","Maximum Number of Items to Display" "Payment Failed Emails","Payment Failed Emails" diff --git a/app/code/Magento/Checkout/view/frontend/templates/cart/coupon.phtml b/app/code/Magento/Checkout/view/frontend/templates/cart/coupon.phtml index bf8490affea0c..65dc514e476ff 100644 --- a/app/code/Magento/Checkout/view/frontend/templates/cart/coupon.phtml +++ b/app/code/Magento/Checkout/view/frontend/templates/cart/coupon.phtml @@ -7,10 +7,13 @@ /** * @var \Magento\Framework\View\Element\AbstractBlock $block */ + +// We should use strlen function because coupon code could be "0", converted to bool will lead to false +$hasCouponCode = (bool) strlen($block->getCouponCode()); ?> , "openedState": "active", "saveState": false}}' > = $block->escapeHtml(__('Apply Discount Code')) ?> @@ -23,7 +26,7 @@ "removeCouponSelector": "#remove-coupon", "applyButton": "button.action.apply", "cancelButton": "button.action.cancel"}}'> - + = $block->escapeHtml(__('Enter discount code')) ?> @@ -34,14 +37,14 @@ name="coupon_code" value="= $block->escapeHtmlAttr($block->getCouponCode()) ?>" placeholder="= $block->escapeHtmlAttr(__('Enter discount code')) ?>" - getCouponCode())) :?> + disabled="disabled" /> - getCouponCode())) :?> + = $block->escapeHtml(__('Apply Discount')) ?> @@ -54,7 +57,7 @@ - getCouponCode())) : ?> + = /* @noEscape */ $block->getChildHtml('captcha') ?> diff --git a/app/code/Magento/Checkout/view/frontend/templates/cart/form.phtml b/app/code/Magento/Checkout/view/frontend/templates/cart/form.phtml index e1ab036c7d889..370d70c44d886 100644 --- a/app/code/Magento/Checkout/view/frontend/templates/cart/form.phtml +++ b/app/code/Magento/Checkout/view/frontend/templates/cart/form.phtml @@ -56,7 +56,7 @@ = $block->escapeHtml(__('Continue Shopping')) ?> - isShowBeforeOrderConfirm($product) && $helper->isMinima = $block->escapeHtml($_formatedOptionValue['full_view']) ?> - = $block->escapeHtml($_formatedOptionValue['value'], ['span']) ?> + = $block->escapeHtml($_formatedOptionValue['value'], ['span', 'a']) ?> @@ -104,6 +104,7 @@ $canApplyMsrp = $helper->isShowBeforeOrderConfirm($product) && $helper->isMinima value="= $block->escapeHtmlAttr($block->getQty()) ?>" type="number" size="4" + step="any" title="= $block->escapeHtmlAttr(__('Qty')) ?>" class="input-text qty" data-validate="{required:true,'validate-greater-than-zero':true}" diff --git a/app/code/Magento/Checkout/view/frontend/web/js/shopping-cart.js b/app/code/Magento/Checkout/view/frontend/web/js/shopping-cart.js index 39bd07f0c73a0..b15599673095f 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/shopping-cart.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/shopping-cart.js @@ -14,14 +14,14 @@ define([ _create: function () { var items, i, reload; - $(this.options.emptyCartButton).on('click', $.proxy(function (event) { - if (event.detail === 0) { - return; - } - + $(this.options.emptyCartButton).on('click', $.proxy(function () { $(this.options.emptyCartButton).attr('name', 'update_cart_action_temp'); $(this.options.updateCartActionContainer) .attr('name', 'update_cart_action').attr('value', 'empty_cart'); + + if ($(this.options.emptyCartButton).parents('form').length > 0) { + $(this.options.emptyCartButton).parents('form').submit(); + } }, this)); items = $.find('[data-role="cart-item-qty"]'); diff --git a/app/code/Magento/Checkout/view/frontend/web/js/sidebar.js b/app/code/Magento/Checkout/view/frontend/web/js/sidebar.js index e67b04e6104cc..f58a560a6b3ca 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/sidebar.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/sidebar.js @@ -261,6 +261,10 @@ define([ $(document).trigger('ajax:removeFromCart', { productIds: [productData['product_id']] }); + + if (window.location.href.indexOf(this.shoppingCartUrl) === 0) { + window.location.reload(); + } } }, @@ -343,7 +347,7 @@ define([ if ($(this).find('.options').length > 0) { $(this).collapsible(); } - outerHeight = $(this).outerHeight(); + outerHeight = $(this).outerHeight(true); if (counter-- > 0) { height += outerHeight; diff --git a/app/code/Magento/Checkout/view/frontend/web/js/view/shipping-information.js b/app/code/Magento/Checkout/view/frontend/web/js/view/shipping-information.js index b2367fbda5412..2158873842687 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/view/shipping-information.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/view/shipping-information.js @@ -28,9 +28,16 @@ define([ * @return {String} */ getShippingMethodTitle: function () { - var shippingMethod = quote.shippingMethod(); + var shippingMethod = quote.shippingMethod(), + shippingMethodTitle = ''; - return shippingMethod ? shippingMethod['carrier_title'] + ' - ' + shippingMethod['method_title'] : ''; + if (typeof shippingMethod['method_title'] !== 'undefined') { + shippingMethodTitle = ' - ' + shippingMethod['method_title']; + } + + return shippingMethod ? + shippingMethod['carrier_title'] + shippingMethodTitle : + shippingMethod['carrier_title']; }, /** diff --git a/app/code/Magento/Checkout/view/frontend/web/template/minicart/item/default.html b/app/code/Magento/Checkout/view/frontend/web/template/minicart/item/default.html index 41d442a76d510..32bbd66d13e68 100644 --- a/app/code/Magento/Checkout/view/frontend/web/template/minicart/item/default.html +++ b/app/code/Magento/Checkout/view/frontend/web/template/minicart/item/default.html @@ -44,7 +44,10 @@ - + + + + diff --git a/app/code/Magento/Cms/Model/ResourceModel/Page/Collection.php b/app/code/Magento/Cms/Model/ResourceModel/Page/Collection.php index 4ccc2c8f6e77d..96886a995b1c9 100644 --- a/app/code/Magento/Cms/Model/ResourceModel/Page/Collection.php +++ b/app/code/Magento/Cms/Model/ResourceModel/Page/Collection.php @@ -74,7 +74,9 @@ public function addStoreFilter($store, $withAdmin = true) { if (!$this->getFlag('store_filter_added')) { $this->performAddStoreFilter($store, $withAdmin); + $this->setFlag('store_filter_added', true); } + return $this; } diff --git a/app/code/Magento/Cms/Model/Wysiwyg/Images/Storage.php b/app/code/Magento/Cms/Model/Wysiwyg/Images/Storage.php index 953b4d455f52a..e02d2b461a94e 100644 --- a/app/code/Magento/Cms/Model/Wysiwyg/Images/Storage.php +++ b/app/code/Magento/Cms/Model/Wysiwyg/Images/Storage.php @@ -447,7 +447,6 @@ public function createDirectory($name, $path) 'id' => $this->_cmsWysiwygImages->convertPathToId($newPath), ]; return $result; - // phpcs:ignore Magento2.Exceptions.ThrowCatch } catch (\Magento\Framework\Exception\FileSystemException $e) { throw new \Magento\Framework\Exception\LocalizedException(__('We cannot create a new directory.')); } @@ -474,7 +473,6 @@ public function deleteDirectory($path) $this->_deleteByPath($path); $path = $this->getThumbnailRoot() . $this->_getRelativePathToRoot($path); $this->_deleteByPath($path); - // phpcs:ignore Magento2.Exceptions.ThrowCatch } catch (\Magento\Framework\Exception\FileSystemException $e) { throw new \Magento\Framework\Exception\LocalizedException( __('We cannot delete directory %1.', $this->_getRelativePathToRoot($path)) diff --git a/app/code/Magento/Cms/Test/Mftf/Test/AdminAddImageToWYSIWYGBlockTest.xml b/app/code/Magento/Cms/Test/Mftf/Test/AdminAddImageToWYSIWYGBlockTest.xml index 5baf75d43c53f..03edc69e6d625 100644 --- a/app/code/Magento/Cms/Test/Mftf/Test/AdminAddImageToWYSIWYGBlockTest.xml +++ b/app/code/Magento/Cms/Test/Mftf/Test/AdminAddImageToWYSIWYGBlockTest.xml @@ -16,9 +16,6 @@ - - - diff --git a/app/code/Magento/Cms/Test/Mftf/Test/AdminAddImageToWYSIWYGCMSTest.xml b/app/code/Magento/Cms/Test/Mftf/Test/AdminAddImageToWYSIWYGCMSTest.xml index e63a6be51bcc0..205850f888797 100644 --- a/app/code/Magento/Cms/Test/Mftf/Test/AdminAddImageToWYSIWYGCMSTest.xml +++ b/app/code/Magento/Cms/Test/Mftf/Test/AdminAddImageToWYSIWYGCMSTest.xml @@ -16,9 +16,6 @@ - - - diff --git a/app/code/Magento/Cms/ViewModel/Page/Grid/UrlBuilder.php b/app/code/Magento/Cms/ViewModel/Page/Grid/UrlBuilder.php index 0faf62607f5c4..15b9fe408d228 100644 --- a/app/code/Magento/Cms/ViewModel/Page/Grid/UrlBuilder.php +++ b/app/code/Magento/Cms/ViewModel/Page/Grid/UrlBuilder.php @@ -103,7 +103,7 @@ private function prepareRequestQuery(string $store, string $href) : array StoreManagerInterface::PARAM_NAME => $store, ActionInterface::PARAM_NAME_URL_ENCODED => $this->urlEncoder->encode($href) ]; - if ($storeView->getCode() !== $store) { + if (null !== $storeView && $storeView->getCode() !== $store) { $query['___from_store'] = $storeView->getCode(); } diff --git a/app/code/Magento/Cms/view/adminhtml/ui_component/cms_block_form.xml b/app/code/Magento/Cms/view/adminhtml/ui_component/cms_block_form.xml index adee89d011460..ad0f33df59d4e 100644 --- a/app/code/Magento/Cms/view/adminhtml/ui_component/cms_block_form.xml +++ b/app/code/Magento/Cms/view/adminhtml/ui_component/cms_block_form.xml @@ -111,7 +111,7 @@ identifier - + block diff --git a/app/code/Magento/CmsUrlRewrite/Plugin/Cms/Model/Store/View.php b/app/code/Magento/CmsUrlRewrite/Plugin/Cms/Model/Store/View.php new file mode 100644 index 0000000000000..e56225cbe2548 --- /dev/null +++ b/app/code/Magento/CmsUrlRewrite/Plugin/Cms/Model/Store/View.php @@ -0,0 +1,102 @@ +urlPersist = $urlPersist; + $this->searchCriteriaBuilder = $searchCriteriaBuilder; + $this->pageRepository = $pageRepository; + $this->cmsPageUrlRewriteGenerator = $cmsPageUrlRewriteGenerator; + } + + /** + * Replace cms page url rewrites on store view save + * + * @param ResourceStore $object + * @param ResourceStore $result + * @param ResourceStore $store + * @return void + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function afterSave(ResourceStore $object, ResourceStore $result, AbstractModel $store): void + { + if ($store->isObjectNew() || $store->dataHasChangedFor('group_id')) { + $this->urlPersist->replace( + $this->generateCmsPagesUrls((int)$store->getId()) + ); + } + } + + /** + * Generate url rewrites for cms pages to store view + * + * @param int $storeId + * @return array + */ + private function generateCmsPagesUrls(int $storeId): array + { + $rewrites = []; + $urls = []; + $searchCriteria = $this->searchCriteriaBuilder->create(); + $cmsPagesCollection = $this->pageRepository->getList($searchCriteria)->getItems(); + foreach ($cmsPagesCollection as $page) { + $page->setStoreId($storeId); + $rewrites[] = $this->cmsPageUrlRewriteGenerator->generate($page); + } + $urls = array_merge($urls, ...$rewrites); + + return $urls; + } +} diff --git a/app/code/Magento/CmsUrlRewrite/etc/adminhtml/di.xml b/app/code/Magento/CmsUrlRewrite/etc/adminhtml/di.xml new file mode 100644 index 0000000000000..c6b0e4b05f16b --- /dev/null +++ b/app/code/Magento/CmsUrlRewrite/etc/adminhtml/di.xml @@ -0,0 +1,12 @@ + + + + + + + diff --git a/app/code/Magento/Config/README.md b/app/code/Magento/Config/README.md index 6214cc94b04a0..83f49a851c662 100644 --- a/app/code/Magento/Config/README.md +++ b/app/code/Magento/Config/README.md @@ -1,8 +1,6 @@ -#Config +# Magento_Config module + The Config module is designed to implement system configuration functionality. -It provides mechanisms to add, edit, store and retrieve the configuration data -for each scope (there can be a default scope as well as scopes for each website and store). +It provides mechanisms to add, edit, store and retrieve the configuration data for each scope (there can be a default scope as well as scopes for each website and store). -Modules can add items to be configured on the system configuration page by creating -system.xml files in their etc/adminhtml directories. These system.xml files get merged -to populate the forms in the config page. +Modules can add items to be configured on the system configuration page by creating system.xml files in their etc/adminhtml directories. These system.xml files get merged to populate the forms in the config page. diff --git a/app/code/Magento/Config/Test/Mftf/Test/CheckingCountryDropDownWithOneAllowedCountryTest.xml b/app/code/Magento/Config/Test/Mftf/Test/CheckingCountryDropDownWithOneAllowedCountryTest.xml index b0a7ee07ddad0..1d54f2ef52c02 100644 --- a/app/code/Magento/Config/Test/Mftf/Test/CheckingCountryDropDownWithOneAllowedCountryTest.xml +++ b/app/code/Magento/Config/Test/Mftf/Test/CheckingCountryDropDownWithOneAllowedCountryTest.xml @@ -22,6 +22,7 @@ + diff --git a/app/code/Magento/Config/etc/acl.xml b/app/code/Magento/Config/etc/acl.xml index 3c6d3faa4242f..ff6ac2f50253a 100644 --- a/app/code/Magento/Config/etc/acl.xml +++ b/app/code/Magento/Config/etc/acl.xml @@ -12,7 +12,10 @@ - + + diff --git a/app/code/Magento/Config/view/adminhtml/templates/system/config/edit.phtml b/app/code/Magento/Config/view/adminhtml/templates/system/config/edit.phtml index 4b8ebb39beb92..7e7a540e88b2e 100644 --- a/app/code/Magento/Config/view/adminhtml/templates/system/config/edit.phtml +++ b/app/code/Magento/Config/view/adminhtml/templates/system/config/edit.phtml @@ -58,10 +58,16 @@ require([ var parentSection = groupElement.parents('.section-config'); parentSection.addClass('highlighted'); + parentSection.addClass('active'); setTimeout(function() { parentSection.removeClass('highlighted', 2000, "easeInBack"); }, 3000); if (!parentSection.hasClass('active')) { + if(section == 'payment') { + var openSection = jQuery('.open').first().attr('id'); + var splitIdArray = openSection.split('_'); + section = section + '_' + splitIdArray[1]; + } Fieldset.toggleCollapse(section + '_' + group); } } diff --git a/app/code/Magento/ConfigurableProduct/Block/Product/View/Type/Configurable.php b/app/code/Magento/ConfigurableProduct/Block/Product/View/Type/Configurable.php index e07879e93a6b4..71db9d32aa593 100644 --- a/app/code/Magento/ConfigurableProduct/Block/Product/View/Type/Configurable.php +++ b/app/code/Magento/ConfigurableProduct/Block/Product/View/Type/Configurable.php @@ -7,6 +7,7 @@ */ namespace Magento\ConfigurableProduct\Block\Product\View\Type; +use Magento\Catalog\Model\Product\Attribute\Source\Status; use Magento\ConfigurableProduct\Model\ConfigurableAttributeData; use Magento\Customer\Helper\Session\CurrentCustomer; use Magento\Customer\Model\Session; @@ -183,8 +184,9 @@ public function getAllowProducts() $products = []; $skipSaleableCheck = $this->catalogProduct->getSkipSaleableCheck(); $allProducts = $this->getProduct()->getTypeInstance()->getUsedProducts($this->getProduct(), null); + /** @var $product \Magento\Catalog\Model\Product */ foreach ($allProducts as $product) { - if ($product->isSaleable() || $skipSaleableCheck) { + if ($skipSaleableCheck || ((int) $product->getStatus()) === Status::STATUS_ENABLED) { $products[] = $product; } } diff --git a/app/code/Magento/ConfigurableProduct/Helper/Data.php b/app/code/Magento/ConfigurableProduct/Helper/Data.php index 674bd3703fa82..a5fdcd62c7aa1 100644 --- a/app/code/Magento/ConfigurableProduct/Helper/Data.php +++ b/app/code/Magento/ConfigurableProduct/Helper/Data.php @@ -14,6 +14,7 @@ /** * Class Data + * * Helper class for getting options * @api * @since 100.0.2 @@ -87,8 +88,9 @@ public function getOptions($currentProduct, $allowedProducts) $productAttribute = $attribute->getProductAttribute(); $productAttributeId = $productAttribute->getId(); $attributeValue = $product->getData($productAttribute->getAttributeCode()); - - $options[$productAttributeId][$attributeValue][] = $productId; + if ($product->isSalable()) { + $options[$productAttributeId][$attributeValue][] = $productId; + } $options['index'][$productId][$productAttributeId] = $attributeValue; } } diff --git a/app/code/Magento/ConfigurableProduct/Model/Product/VariationHandler.php b/app/code/Magento/ConfigurableProduct/Model/Product/VariationHandler.php index 09d251519269f..df2a9707f18d5 100644 --- a/app/code/Magento/ConfigurableProduct/Model/Product/VariationHandler.php +++ b/app/code/Magento/ConfigurableProduct/Model/Product/VariationHandler.php @@ -106,6 +106,7 @@ public function generateSimpleProducts($parentProduct, $productsData) $this->fillSimpleProductData( $newSimpleProduct, $parentProduct, + // phpcs:ignore Magento2.Performance.ForeachArrayMerge array_merge($simpleProductData, $configurableAttribute) ); $newSimpleProduct->save(); @@ -238,10 +239,6 @@ public function duplicateImagesForVariations($productsData) foreach ($simpleProductData['media_gallery']['images'] as $imageId => $image) { $image['variation_id'] = $variationId; - if (isset($imagesForCopy[$imageId][0])) { - // skip duplicate image for first product - unset($imagesForCopy[$imageId][0]); - } $imagesForCopy[$imageId][] = $image; } } diff --git a/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Product/LinkedProductSelectBuilderComposite.php b/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Product/LinkedProductSelectBuilderComposite.php deleted file mode 100644 index 59a7b81e068a5..0000000000000 --- a/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Product/LinkedProductSelectBuilderComposite.php +++ /dev/null @@ -1,45 +0,0 @@ -linkedProductSelectBuilder = $linkedProductSelectBuilder; - } - - /** - * {@inheritdoc} - */ - public function build($productId) - { - $selects = []; - foreach ($this->linkedProductSelectBuilder as $productSelectBuilder) { - $selects = array_merge($selects, $productSelectBuilder->build($productId)); - } - - return $selects; - } -} diff --git a/app/code/Magento/ConfigurableProduct/Plugin/Model/ResourceModel/Attribute/InStockOptionSelectBuilder.php b/app/code/Magento/ConfigurableProduct/Plugin/Model/ResourceModel/Attribute/InStockOptionSelectBuilder.php index 0afde6144bf88..4c4bd01950300 100644 --- a/app/code/Magento/ConfigurableProduct/Plugin/Model/ResourceModel/Attribute/InStockOptionSelectBuilder.php +++ b/app/code/Magento/ConfigurableProduct/Plugin/Model/ResourceModel/Attribute/InStockOptionSelectBuilder.php @@ -5,6 +5,7 @@ */ namespace Magento\ConfigurableProduct\Plugin\Model\ResourceModel\Attribute; +use Magento\CatalogInventory\Api\StockConfigurationInterface; use Magento\CatalogInventory\Model\ResourceModel\Stock\Status; use Magento\ConfigurableProduct\Model\ResourceModel\Attribute\OptionSelectBuilderInterface; use Magento\Framework\DB\Select; @@ -20,13 +21,21 @@ class InStockOptionSelectBuilder * @var Status */ private $stockStatusResource; - + /** + * @var StockConfigurationInterface + */ + private $stockConfig; + /** * @param Status $stockStatusResource + * @param StockConfigurationInterface $stockConfig */ - public function __construct(Status $stockStatusResource) - { + public function __construct( + Status $stockStatusResource, + StockConfigurationInterface $stockConfig + ) { $this->stockStatusResource = $stockStatusResource; + $this->stockConfig = $stockConfig; } /** @@ -40,14 +49,16 @@ public function __construct(Status $stockStatusResource) */ public function afterGetSelect(OptionSelectBuilderInterface $subject, Select $select) { - $select->joinInner( - ['stock' => $this->stockStatusResource->getMainTable()], - 'stock.product_id = entity.entity_id', - [] - )->where( - 'stock.stock_status = ?', - \Magento\CatalogInventory\Model\Stock\Status::STATUS_IN_STOCK - ); + if (!$this->stockConfig->isShowOutOfStock()) { + $select->joinInner( + ['stock' => $this->stockStatusResource->getMainTable()], + 'stock.product_id = entity.entity_id', + [] + )->where( + 'stock.stock_status = ?', + \Magento\CatalogInventory\Model\Stock\Status::STATUS_IN_STOCK + ); + } return $select; } diff --git a/app/code/Magento/ConfigurableProduct/Plugin/Product/Initialization/CleanConfigurationTmpImages.php b/app/code/Magento/ConfigurableProduct/Plugin/Product/Initialization/CleanConfigurationTmpImages.php new file mode 100644 index 0000000000000..b3959d794bbd3 --- /dev/null +++ b/app/code/Magento/ConfigurableProduct/Plugin/Product/Initialization/CleanConfigurationTmpImages.php @@ -0,0 +1,139 @@ +request = $request; + $this->fileStorageDb = $fileStorageDb; + $this->mediaConfig = $mediaConfig; + $this->serialize = $serialize; + $this->mediaDirectory = $filesystem->getDirectoryWrite(DirectoryList::MEDIA); + } + + /** + * Clean Tmp configurable images + * + * @param \Magento\Catalog\Controller\Adminhtml\Product\Initialization\Helper $subject + * @param \Magento\Catalog\Model\Product $configurableProduct + * + * @return \Magento\Catalog\Model\Product + * @throws \Magento\Framework\Exception\FileSystemException + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function afterInitialize( + \Magento\Catalog\Controller\Adminhtml\Product\Initialization\Helper $subject, + \Magento\Catalog\Model\Product $configurableProduct + ) { + + $configurations = $this->getConfigurations(); + foreach ($configurations as $simpleProductData) { + if (!isset($simpleProductData['media_gallery']['images'])) { + continue; + } + + foreach ($simpleProductData['media_gallery']['images'] as $image) { + $file = $this->getFilenameFromTmp($image['file']); + if ($this->fileStorageDb->checkDbUsage()) { + $filename = $this->mediaDirectory->getAbsolutePath($this->mediaConfig->getTmpMediaShortUrl($file)); + $this->fileStorageDb->deleteFile($filename); + } else { + $filename = $this->mediaConfig->getTmpMediaPath($file); + $this->mediaDirectory->delete($filename); + } + } + } + + return $configurableProduct; + } + + /** + * Trim .tmp ending from filename + * + * @param string $file + * @return string + */ + private function getFilenameFromTmp($file) + { + return strrpos($file, '.tmp') === strlen($file) - 4 ? substr($file, 0, strlen($file) - 4) : $file; + } + + /** + * Get configurations from request + * + * @return array + */ + private function getConfigurations() + { + $result = []; + $configurableMatrix = $this->request->getParam('configurable-matrix-serialized', "[]"); + if (isset($configurableMatrix) && $configurableMatrix !== "") { + $configurableMatrix = $this->serialize->unserialize($configurableMatrix) ?? []; + + foreach ($configurableMatrix as $item) { + if (empty($item['was_changed']) && empty($item['newProduct'])) { + continue; + } + + $result[] = $item; + } + } + + return $result; + } +} diff --git a/app/code/Magento/ConfigurableProduct/Pricing/Price/ConfigurableOptionsProvider.php b/app/code/Magento/ConfigurableProduct/Pricing/Price/ConfigurableOptionsProvider.php index 6cc5625df5b18..9a19f9338593c 100644 --- a/app/code/Magento/ConfigurableProduct/Pricing/Price/ConfigurableOptionsProvider.php +++ b/app/code/Magento/ConfigurableProduct/Pricing/Price/ConfigurableOptionsProvider.php @@ -7,16 +7,15 @@ namespace Magento\ConfigurableProduct\Pricing\Price; use Magento\Catalog\Api\Data\ProductInterface; -use Magento\Catalog\Model\ResourceModel\Product\LinkedProductSelectBuilderInterface; -use Magento\Framework\App\ResourceConnection; use Magento\ConfigurableProduct\Model\Product\Type\Configurable; -use Magento\Catalog\Model\ResourceModel\Product\CollectionFactory; -use Magento\Framework\App\RequestSafetyInterface; +/** + * Provide configurable child products for price calculation + */ class ConfigurableOptionsProvider implements ConfigurableOptionsProviderInterface { /** - * @var \Magento\ConfigurableProduct\Model\Product\Type\Configurable + * @var Configurable */ private $configurable; @@ -27,24 +26,15 @@ class ConfigurableOptionsProvider implements ConfigurableOptionsProviderInterfac /** * @param Configurable $configurable - * @param ResourceConnection $resourceConnection - * @param LinkedProductSelectBuilderInterface $linkedProductSelectBuilder - * @param CollectionFactory $collectionFactory - * @param RequestSafetyInterface $requestSafety - * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function __construct( - Configurable $configurable, - ResourceConnection $resourceConnection, - LinkedProductSelectBuilderInterface $linkedProductSelectBuilder, - CollectionFactory $collectionFactory, - RequestSafetyInterface $requestSafety + Configurable $configurable ) { $this->configurable = $configurable; } /** - * {@inheritdoc} + * @inheritdoc */ public function getProducts(ProductInterface $product) { diff --git a/app/code/Magento/ConfigurableProduct/Pricing/Render/FinalPriceBox.php b/app/code/Magento/ConfigurableProduct/Pricing/Render/FinalPriceBox.php index 9689352ab888e..b48a987c23526 100644 --- a/app/code/Magento/ConfigurableProduct/Pricing/Render/FinalPriceBox.php +++ b/app/code/Magento/ConfigurableProduct/Pricing/Render/FinalPriceBox.php @@ -10,42 +10,40 @@ use Magento\Catalog\Pricing\Price\MinimalPriceCalculatorInterface; use Magento\Catalog\Pricing\Price\RegularPrice; use Magento\ConfigurableProduct\Pricing\Price\ConfigurableOptionsProviderInterface; -use Magento\ConfigurableProduct\Pricing\Price\LowestPriceOptionsProviderInterface; -use Magento\Framework\App\ObjectManager; use Magento\Framework\Pricing\Price\PriceInterface; use Magento\Framework\Pricing\Render\RendererPool; use Magento\Framework\Pricing\SaleableInterface; use Magento\Framework\View\Element\Template\Context; +/** + * Class for final_price box rendering + */ class FinalPriceBox extends \Magento\Catalog\Pricing\Render\FinalPriceBox { /** - * @var LowestPriceOptionsProviderInterface + * @var ConfigurableOptionsProviderInterface */ - private $lowestPriceOptionsProvider; + private $configurableOptionsProvider; /** * @param Context $context * @param SaleableInterface $saleableItem * @param PriceInterface $price * @param RendererPool $rendererPool + * @param SalableResolverInterface $salableResolver + * @param MinimalPriceCalculatorInterface $minimalPriceCalculator * @param ConfigurableOptionsProviderInterface $configurableOptionsProvider * @param array $data - * @param LowestPriceOptionsProviderInterface $lowestPriceOptionsProvider - * @param SalableResolverInterface|null $salableResolver - * @param MinimalPriceCalculatorInterface|null $minimalPriceCalculator - * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function __construct( Context $context, SaleableInterface $saleableItem, PriceInterface $price, RendererPool $rendererPool, + SalableResolverInterface $salableResolver, + MinimalPriceCalculatorInterface $minimalPriceCalculator, ConfigurableOptionsProviderInterface $configurableOptionsProvider, - array $data = [], - LowestPriceOptionsProviderInterface $lowestPriceOptionsProvider = null, - SalableResolverInterface $salableResolver = null, - MinimalPriceCalculatorInterface $minimalPriceCalculator = null + array $data = [] ) { parent::__construct( $context, @@ -56,8 +54,8 @@ public function __construct( $salableResolver, $minimalPriceCalculator ); - $this->lowestPriceOptionsProvider = $lowestPriceOptionsProvider ?: - ObjectManager::getInstance()->get(LowestPriceOptionsProviderInterface::class); + + $this->configurableOptionsProvider = $configurableOptionsProvider; } /** @@ -68,7 +66,7 @@ public function __construct( public function hasSpecialPrice() { $product = $this->getSaleableItem(); - foreach ($this->lowestPriceOptionsProvider->getProducts($product) as $subProduct) { + foreach ($this->configurableOptionsProvider->getProducts($product) as $subProduct) { $regularPrice = $subProduct->getPriceInfo()->getPrice(RegularPrice::PRICE_CODE)->getValue(); $finalPrice = $subProduct->getPriceInfo()->getPrice(FinalPrice::PRICE_CODE)->getValue(); if ($finalPrice < $regularPrice) { diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminConfigurableProductActionGroup.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminConfigurableProductActionGroup.xml index a0a3a551c3d93..488667a4585bb 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminConfigurableProductActionGroup.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminConfigurableProductActionGroup.xml @@ -16,7 +16,7 @@ - + @@ -108,6 +108,65 @@ + + + Goes to the Admin Product grid page. Creates a Configurable Product with 2 product attributes. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Save configurable product + + + + + + + + + + Generates the Product Configurations for the provided Attribute Code on the Configurable Product creation/edit page. @@ -296,12 +355,21 @@ - + + + Change the price of a configurable child product in the grid under configurations. + + + + + + + EXTENDS: changeProductConfigurationsInGrid. Removes 'fillFieldSkuForFirstAttributeOption' and 'fillFieldSkuForSecondAttributeOption'. - + diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Data/ConfigurableProductData.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Data/ConfigurableProductData.xml index de6714a9b959e..3f21c98068d8a 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Data/ConfigurableProductData.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Data/ConfigurableProductData.xml @@ -78,4 +78,10 @@ EavStockItem CustomAttributeCategoryIds + + + api-configurable-export-import-product + API Configurable Export Import Product + api-configurable-export-import-product + diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/AdminProductFormConfigurationsSection.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/AdminProductFormConfigurationsSection.xml index 1defecbc7c285..336f95aa55576 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/AdminProductFormConfigurationsSection.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/AdminProductFormConfigurationsSection.xml @@ -39,6 +39,9 @@ + + + diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductWithThreeProductDisplayOutOfStockProductsTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductWithThreeProductDisplayOutOfStockProductsTest.xml index 0b83fdc1788d3..308e256543736 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductWithThreeProductDisplayOutOfStockProductsTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductWithThreeProductDisplayOutOfStockProductsTest.xml @@ -135,6 +135,6 @@ - + \ No newline at end of file diff --git a/app/code/Magento/ConfigurableProduct/Test/Unit/Controller/Adminhtml/Product/Initialization/Helper/Plugin/UpdateConfigurationsTest.php b/app/code/Magento/ConfigurableProduct/Test/Unit/Controller/Adminhtml/Product/Initialization/Helper/Plugin/UpdateConfigurationsTest.php index cc645c22c8acf..69de6c973cd70 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Unit/Controller/Adminhtml/Product/Initialization/Helper/Plugin/UpdateConfigurationsTest.php +++ b/app/code/Magento/ConfigurableProduct/Test/Unit/Controller/Adminhtml/Product/Initialization/Helper/Plugin/UpdateConfigurationsTest.php @@ -187,6 +187,7 @@ public function testAfterInitialize() * * @param array $expectedData * @param bool $hasDataChanges + * @param bool $wasChanged * @return Product|\PHPUnit_Framework_MockObject_MockObject */ protected function getProductMock(array $expectedData = null, $hasDataChanges = false, $wasChanged = false) diff --git a/app/code/Magento/ConfigurableProduct/Test/Unit/Helper/DataTest.php b/app/code/Magento/ConfigurableProduct/Test/Unit/Helper/DataTest.php index cd9fb419981a1..3da5595574116 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Unit/Helper/DataTest.php +++ b/app/code/Magento/ConfigurableProduct/Test/Unit/Helper/DataTest.php @@ -87,11 +87,13 @@ public function testGetOptions(array $expected, array $data) $this->_imageHelperMock->expects($this->any()) ->method('init') - ->willReturnMap([ - [$data['current_product_mock'], 'product_page_image_large', [], $imageHelper1], - [$data['allowed_products'][0], 'product_page_image_large', [], $imageHelper1], - [$data['allowed_products'][1], 'product_page_image_large', [], $imageHelper2], - ]); + ->willReturnMap( + [ + [$data['current_product_mock'], 'product_page_image_large', [], $imageHelper1], + [$data['allowed_products'][0], 'product_page_image_large', [], $imageHelper1], + [$data['allowed_products'][1], 'product_page_image_large', [], $imageHelper2], + ] + ); } $this->assertEquals( @@ -148,7 +150,7 @@ public function getOptionsDataProvider() for ($i = 1; $i <= 2; $i++) { $productMock = $this->createPartialMock( \Magento\Catalog\Model\Product::class, - ['getData', 'getImage', 'getId', '__wakeup', 'getMediaGalleryImages'] + ['getData', 'getImage', 'getId', '__wakeup', 'getMediaGalleryImages', 'isSalable'] ); $productMock->expects($this->any()) ->method('getData') @@ -156,6 +158,10 @@ public function getOptionsDataProvider() $productMock->expects($this->any()) ->method('getId') ->will($this->returnValue('product_id_' . $i)); + $productMock + ->expects($this->any()) + ->method('isSalable') + ->will($this->returnValue(true)); if ($i == 2) { $productMock->expects($this->any()) ->method('getImage') @@ -230,11 +236,13 @@ public function testGetGalleryImages() self::identicalTo('product_page_image_large') ] ) - ->will(self::onConsecutiveCalls( - 'testSmallImageUrl', - 'testMediumImageUrl', - 'testLargeImageUrl' - )); + ->will( + self::onConsecutiveCalls( + 'testSmallImageUrl', + 'testMediumImageUrl', + 'testLargeImageUrl' + ) + ); $this->_imageHelperMock->expects(self::never()) ->method('setImageFile') ->with('test_file') @@ -265,9 +273,9 @@ private function getImagesCollection() ->getMock(); $items = [ - new \Magento\Framework\DataObject([ - 'file' => 'test_file' - ]), + new \Magento\Framework\DataObject( + ['file' => 'test_file'] + ), ]; $collectionMock->expects($this->any()) diff --git a/app/code/Magento/ConfigurableProduct/Test/Unit/Plugin/Product/Initialization/CleanConfigurationTmpImagesTest.php b/app/code/Magento/ConfigurableProduct/Test/Unit/Plugin/Product/Initialization/CleanConfigurationTmpImagesTest.php new file mode 100644 index 0000000000000..7f18da7e314e8 --- /dev/null +++ b/app/code/Magento/ConfigurableProduct/Test/Unit/Plugin/Product/Initialization/CleanConfigurationTmpImagesTest.php @@ -0,0 +1,234 @@ +requestMock = $this->getMockBuilder(RequestInterface::class) + ->getMockForAbstractClass(); + $this->fileStorageDb = $this->getMockBuilder(FileStorage::class) + ->disableOriginalConstructor() + ->getMock(); + $this->mediaConfig = $this->getMockBuilder(MediaConfig::class) + ->disableOriginalConstructor() + ->getMock(); + $this->filesystem = $this->getMockBuilder(Filesystem::class) + ->disableOriginalConstructor() + ->getMock(); + $this->writeFolder = $this->getMockBuilder(Write::class) + ->disableOriginalConstructor() + ->getMock(); + $this->seralizer = $this->getMockBuilder(Json::class) + ->disableOriginalConstructor() + ->getMock(); + $this->subjectMock = $this->getMockBuilder(ProductInitializationHelper::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->filesystem->expects($this->once()) + ->method('getDirectoryWrite') + ->willReturn($this->writeFolder); + + $this->objectManagerHelper = new ObjectManagerHelper($this); + $this->cleanConfigurationTmpImages = $this->objectManagerHelper->getObject( + CleanConfigurationTmpImages::class, + [ + 'request' => $this->requestMock, + 'fileStorageDb' => $this->fileStorageDb, + 'mediaConfig' => $this->mediaConfig, + 'filesystem' => $this->filesystem, + 'seralizer' => $this->seralizer + ] + ); + } + + /** + * Prepare configurable matrix + * + * @return array + */ + private function getConfigurableMatrix() + { + return [ + [ + 'newProduct' => true, + 'id' => 'product1' + ], + [ + 'newProduct' => false, + 'id' => 'product2', + 'status' => 'simple2_status', + 'sku' => 'simple2_sku', + 'name' => 'simple2_name', + 'price' => '3.33', + 'configurable_attribute' => 'simple2_configurable_attribute', + 'weight' => '5.55', + 'media_gallery' => [ + 'images' => [ + ['file' => 'test'] + ], + ], + 'swatch_image' => 'simple2_swatch_image', + 'small_image' => 'simple2_small_image', + 'thumbnail' => 'simple2_thumbnail', + 'image' => 'simple2_image', + 'was_changed' => true, + ], + [ + 'newProduct' => false, + 'id' => 'product3', + 'qty' => '3', + 'was_changed' => true, + ], + [ + 'newProduct' => false, + 'id' => 'product4', + 'status' => 'simple4_status', + 'sku' => 'simple2_sku', + 'name' => 'simple2_name', + 'price' => '3.33', + 'weight' => '5.55', + ], + ]; + } + + public function testAfterInitialize() + { + $productMock = $this->getProductMock(); + $configurableMatrix = $this->getConfigurableMatrix(); + + $this->requestMock->expects(static::any()) + ->method('getParam') + ->willReturnMap( + [ + ['store', 0, 0], + ['configurable-matrix-serialized', "[]", json_encode($configurableMatrix)] + ] + ); + + $this->assertSame( + $productMock, + $this->cleanConfigurationTmpImages->afterInitialize($this->subjectMock, $productMock) + ); + } + + /** + * Get product mock + * + * @param array $expectedData + * @param bool $hasDataChanges + * @param bool $wasChanged + * @return Product|\PHPUnit_Framework_MockObject_MockObject + */ + protected function getProductMock(array $expectedData = null, $hasDataChanges = false, $wasChanged = false) + { + $productMock = $this->getMockBuilder(Product::class) + ->disableOriginalConstructor() + ->getMock(); + + if ($wasChanged !== false) { + if ($expectedData !== null) { + $productMock->expects(static::once()) + ->method('addData') + ->with($expectedData) + ->willReturnSelf(); + } + + $productMock->expects(static::any()) + ->method('hasDataChanges') + ->willReturn($hasDataChanges); + $productMock->expects($hasDataChanges ? static::once() : static::never()) + ->method('save') + ->willReturnSelf(); + } + return $productMock; + } + + /** + * Test for no exceptions if configurable matrix is empty string. + */ + public function testAfterInitializeEmptyMatrix() + { + $productMock = $this->getProductMock(); + + $this->requestMock->expects(static::any()) + ->method('getParam') + ->willReturnMap( + [ + ['store', 0, 0], + ['configurable-matrix-serialized', null, ''], + ] + ); + + $this->cleanConfigurationTmpImages->afterInitialize($this->subjectMock, $productMock); + + $this->assertEmpty($productMock->getData()); + } +} diff --git a/app/code/Magento/ConfigurableProduct/Test/Unit/Pricing/Render/FinalPriceBoxTest.php b/app/code/Magento/ConfigurableProduct/Test/Unit/Pricing/Render/FinalPriceBoxTest.php index 3c4b9b4392ad7..776986a761cf8 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Unit/Pricing/Render/FinalPriceBoxTest.php +++ b/app/code/Magento/ConfigurableProduct/Test/Unit/Pricing/Render/FinalPriceBoxTest.php @@ -5,63 +5,74 @@ */ namespace Magento\ConfigurableProduct\Test\Unit\Pricing\Render; +use Magento\Catalog\Model\Product; +use Magento\Catalog\Model\Product\Pricing\Renderer\SalableResolverInterface; use Magento\Catalog\Pricing\Price\FinalPrice; +use Magento\Catalog\Pricing\Price\MinimalPriceCalculatorInterface; use Magento\Catalog\Pricing\Price\RegularPrice; -use Magento\ConfigurableProduct\Pricing\Price\LowestPriceOptionsProviderInterface; +use Magento\ConfigurableProduct\Pricing\Price\ConfigurableOptionsProviderInterface; use Magento\ConfigurableProduct\Pricing\Render\FinalPriceBox; +use Magento\Framework\Pricing\Price\PriceInterface; +use Magento\Framework\Pricing\PriceInfoInterface; +use Magento\Framework\Pricing\Render\RendererPool; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\Framework\View\Element\Template\Context; +use PHPUnit\Framework\MockObject\MockObject; class FinalPriceBoxTest extends \PHPUnit\Framework\TestCase { /** - * @var \Magento\Framework\View\Element\Template\Context|\PHPUnit_Framework_MockObject_MockObject + * @var Context|MockObject */ private $context; /** - * @var \Magento\Catalog\Model\Product|\PHPUnit_Framework_MockObject_MockObject + * @var Product|MockObject */ private $saleableItem; /** - * @var \Magento\Framework\Pricing\Price\PriceInterface|\PHPUnit_Framework_MockObject_MockObject + * @var PriceInterface|MockObject */ private $price; /** - * @var \Magento\Framework\Pricing\Render\RendererPool|\PHPUnit_Framework_MockObject_MockObject + * @var RendererPool|MockObject */ private $rendererPool; /** - * @var LowestPriceOptionsProviderInterface|\PHPUnit_Framework_MockObject_MockObject + * @var SalableResolverInterface|MockObject */ - private $lowestPriceOptionsProvider; + private $salableResolver; + + /** + * @var MinimalPriceCalculatorInterface|MockObject + */ + private $minimalPriceCalculator; + + /** + * @var ConfigurableOptionsProviderInterface|MockObject + */ + private $configurableOptionsProvider; /** * @var FinalPriceBox */ private $model; + /** + * @inheritDoc + */ protected function setUp() { - $this->context = $this->getMockBuilder(\Magento\Framework\View\Element\Template\Context::class) - ->disableOriginalConstructor() - ->getMock(); - - $this->saleableItem = $this->getMockBuilder(\Magento\Catalog\Model\Product::class) - ->disableOriginalConstructor() - ->getMock(); - - $this->price = $this->getMockBuilder(\Magento\Framework\Pricing\Price\PriceInterface::class) - ->getMockForAbstractClass(); - - $this->rendererPool = $this->getMockBuilder(\Magento\Framework\Pricing\Render\RendererPool::class) - ->disableOriginalConstructor() - ->getMock(); - - $this->lowestPriceOptionsProvider = $this->getMockBuilder(LowestPriceOptionsProviderInterface::class) - ->getMockForAbstractClass(); + $this->context = $this->createMock(Context::class); + $this->saleableItem = $this->createMock(Product::class); + $this->price = $this->createMock(PriceInterface::class); + $this->rendererPool = $this->createMock(RendererPool::class); + $this->salableResolver = $this->createMock(SalableResolverInterface::class); + $this->minimalPriceCalculator = $this->createMock(MinimalPriceCalculatorInterface::class); + $this->configurableOptionsProvider = $this->createMock(ConfigurableOptionsProviderInterface::class); $this->model = (new ObjectManager($this))->getObject( FinalPriceBox::class, @@ -70,7 +81,9 @@ protected function setUp() 'saleableItem' => $this->saleableItem, 'price' => $this->price, 'rendererPool' => $this->rendererPool, - 'lowestPriceOptionsProvider' => $this->lowestPriceOptionsProvider, + 'salableResolver' => $this->salableResolver, + 'minimalPriceCalculator' => $this->minimalPriceCalculator, + 'configurableOptionsProvider' => $this->configurableOptionsProvider, ] ); } @@ -82,44 +95,33 @@ protected function setUp() * @dataProvider hasSpecialPriceDataProvider */ public function testHasSpecialPrice( - $regularPrice, - $finalPrice, - $expected + float $regularPrice, + float $finalPrice, + bool $expected ) { - $priceMockOne = $this->getMockBuilder(\Magento\Framework\Pricing\Price\PriceInterface::class) - ->getMockForAbstractClass(); - + $priceMockOne = $this->createMock(PriceInterface::class); $priceMockOne->expects($this->once()) ->method('getValue') ->willReturn($regularPrice); - - $priceMockTwo = $this->getMockBuilder(\Magento\Framework\Pricing\Price\PriceInterface::class) - ->getMockForAbstractClass(); - + $priceMockTwo = $this->createMock(PriceInterface::class); $priceMockTwo->expects($this->once()) ->method('getValue') ->willReturn($finalPrice); - - $priceInfoMock = $this->getMockBuilder(\Magento\Framework\Pricing\PriceInfo\Base::class) - ->disableOriginalConstructor() - ->getMock(); - + $priceInfoMock = $this->createMock(PriceInfoInterface::class); $priceInfoMock->expects($this->exactly(2)) ->method('getPrice') - ->willReturnMap([ - [RegularPrice::PRICE_CODE, $priceMockOne], - [FinalPrice::PRICE_CODE, $priceMockTwo], - ]); - - $productMock = $this->getMockBuilder(\Magento\Catalog\Api\Data\ProductInterface::class) - ->setMethods(['getPriceInfo']) - ->getMockForAbstractClass(); - + ->willReturnMap( + [ + [RegularPrice::PRICE_CODE, $priceMockOne], + [FinalPrice::PRICE_CODE, $priceMockTwo], + ] + ); + + $productMock = $this->createMock(Product::class); $productMock->expects($this->exactly(2)) ->method('getPriceInfo') ->willReturn($priceInfoMock); - - $this->lowestPriceOptionsProvider->expects($this->once()) + $this->configurableOptionsProvider->expects($this->once()) ->method('getProducts') ->with($this->saleableItem) ->willReturn([$productMock]); @@ -130,7 +132,7 @@ public function testHasSpecialPrice( /** * @return array */ - public function hasSpecialPriceDataProvider() + public function hasSpecialPriceDataProvider(): array { return [ [10., 20., false], diff --git a/app/code/Magento/ConfigurableProduct/etc/adminhtml/di.xml b/app/code/Magento/ConfigurableProduct/etc/adminhtml/di.xml index a401dceaf5b99..de6765138fce6 100644 --- a/app/code/Magento/ConfigurableProduct/etc/adminhtml/di.xml +++ b/app/code/Magento/ConfigurableProduct/etc/adminhtml/di.xml @@ -9,6 +9,7 @@ + diff --git a/app/code/Magento/ConfigurableProduct/etc/di.xml b/app/code/Magento/ConfigurableProduct/etc/di.xml index 498591fc31569..b8f7ed67a9868 100644 --- a/app/code/Magento/ConfigurableProduct/etc/di.xml +++ b/app/code/Magento/ConfigurableProduct/etc/di.xml @@ -205,13 +205,6 @@ Magento\Catalog\Model\Indexer\Product\Full - - - - Magento\Catalog\Model\ResourceModel\Product\Indexer\LinkedProductSelectBuilderByIndexPrice - - - Magento\ConfigurableProduct\Model\ResourceModel\Product\LinkedProductSelectBuilder @@ -220,7 +213,7 @@ Magento\ConfigurableProduct\Model\ResourceModel\Product\StockStatusBaseSelectProcessor - LinkedProductSelectBuilderByIndexMinPrice + Magento\Catalog\Model\ResourceModel\Product\Indexer\LinkedProductSelectBuilderByIndexPrice diff --git a/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/edit/super/config.phtml b/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/edit/super/config.phtml index c11a1adc19896..240c5e65c79c3 100644 --- a/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/edit/super/config.phtml +++ b/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/edit/super/config.phtml @@ -64,7 +64,7 @@ "productsProvider": "configurable_associated_product_listing.data_source", "productsMassAction": "configurable_associated_product_listing.configurable_associated_product_listing.product_columns.ids", "productsColumns": "configurable_associated_product_listing.configurable_associated_product_listing.product_columns", - "productsGridUrl": "= /* @noEscape */ $block->getUrl('catalog/product/associated_grid', ['componentJson' => true]) ?>", + "productsGridUrl": "= /* @noEscape */ $block->getUrl('catalog/product_associated/grid', ['componentJson' => true]) ?>", "configurableVariations": "configurableVariations" } } diff --git a/app/code/Magento/ConfigurableProduct/view/frontend/web/js/configurable.js b/app/code/Magento/ConfigurableProduct/view/frontend/web/js/configurable.js index c0128dffe7045..ae564610e4b0b 100644 --- a/app/code/Magento/ConfigurableProduct/view/frontend/web/js/configurable.js +++ b/app/code/Magento/ConfigurableProduct/view/frontend/web/js/configurable.js @@ -123,6 +123,8 @@ define([ if (this.options.spConfig.inputsInitialized) { this._setValuesByAttribute(); } + + this._setInitialOptionsLabels(); }, /** @@ -158,6 +160,18 @@ define([ }, this)); }, + /** + * Set additional field with initial label to be used when switching between options with different prices. + * @private + */ + _setInitialOptionsLabels: function () { + $.each(this.options.spConfig.attributes, $.proxy(function (index, element) { + $.each(element.options, $.proxy(function (optIndex, optElement) { + this.options.spConfig.attributes[index].options[optIndex].initialLabel = optElement.label; + }, this)); + }, this)); + }, + /** * Set up .on('change') events for each option element to configure the option. * @private @@ -371,13 +385,18 @@ define([ prevConfig, index = 1, allowedProducts, + allowedProductsByOption, + allowedProductsAll, i, j, finalPrice = parseFloat(this.options.spConfig.prices.finalPrice.amount), optionFinalPrice, optionPriceDiff, optionPrices = this.options.spConfig.optionPrices, - allowedProductMinPrice; + allowedOptions = [], + indexKey, + allowedProductMinPrice, + allowedProductsAllMinPrice; this._clearSelect(element); element.options[0] = new Option('', ''); @@ -389,39 +408,61 @@ define([ } if (options) { - for (i = 0; i < options.length; i++) { - allowedProducts = []; - optionPriceDiff = 0; - + for (indexKey in this.options.spConfig.index) { /* eslint-disable max-depth */ - if (prevConfig) { + if (this.options.spConfig.index.hasOwnProperty(indexKey)) { + allowedOptions = allowedOptions.concat(_.values(this.options.spConfig.index[indexKey])); + } + } + + if (prevConfig) { + allowedProductsByOption = {}; + allowedProductsAll = []; + + for (i = 0; i < options.length; i++) { + /* eslint-disable max-depth */ for (j = 0; j < options[i].products.length; j++) { // prevConfig.config can be undefined if (prevConfig.config && prevConfig.config.allowedProducts && prevConfig.config.allowedProducts.indexOf(options[i].products[j]) > -1) { - allowedProducts.push(options[i].products[j]); + if (!allowedProductsByOption[i]) { + allowedProductsByOption[i] = []; + } + allowedProductsByOption[i].push(options[i].products[j]); + allowedProductsAll.push(options[i].products[j]); } } - } else { - allowedProducts = options[i].products.slice(0); - - if (typeof allowedProducts[0] !== 'undefined' && - typeof optionPrices[allowedProducts[0]] !== 'undefined') { - allowedProductMinPrice = this._getAllowedProductWithMinPrice(allowedProducts); - optionFinalPrice = parseFloat(optionPrices[allowedProductMinPrice].finalPrice.amount); - optionPriceDiff = optionFinalPrice - finalPrice; - - if (optionPriceDiff !== 0) { - options[i].label = options[i].label + ' ' + priceUtils.formatPrice( - optionPriceDiff, - this.options.priceFormat, - true); - } + } + + if (typeof allowedProductsAll[0] !== 'undefined' && + typeof optionPrices[allowedProductsAll[0]] !== 'undefined') { + allowedProductsAllMinPrice = this._getAllowedProductWithMinPrice(allowedProductsAll); + finalPrice = parseFloat(optionPrices[allowedProductsAllMinPrice].finalPrice.amount); + } + } + + for (i = 0; i < options.length; i++) { + allowedProducts = prevConfig ? allowedProductsByOption[i] : options[i].products.slice(0); + optionPriceDiff = 0; + + if (typeof allowedProducts[0] !== 'undefined' && + typeof optionPrices[allowedProducts[0]] !== 'undefined') { + allowedProductMinPrice = this._getAllowedProductWithMinPrice(allowedProducts); + optionFinalPrice = parseFloat(optionPrices[allowedProductMinPrice].finalPrice.amount); + optionPriceDiff = optionFinalPrice - finalPrice; + options[i].label = options[i].initialLabel; + + if (optionPriceDiff !== 0) { + options[i].label += ' ' + priceUtils.formatPrice( + optionPriceDiff, + this.options.priceFormat, + true + ); } } - if (allowedProducts.length > 0) { + if (allowedProducts.length > 0 || _.include(allowedOptions, options[i].id)) { options[i].allowedProducts = allowedProducts; element.options[index] = new Option(this._getOptionLabel(options[i]), options[i].id); @@ -429,6 +470,10 @@ define([ element.options[index].setAttribute('price', options[i].price); } + if (allowedProducts.length === 0) { + element.options[index].disabled = true; + } + element.options[index].config = options[i]; index++; } diff --git a/app/code/Magento/ConfigurableProductGraphQl/etc/module.xml b/app/code/Magento/ConfigurableProductGraphQl/etc/module.xml index 98e7957d0af8e..f249a417f1046 100644 --- a/app/code/Magento/ConfigurableProductGraphQl/etc/module.xml +++ b/app/code/Magento/ConfigurableProductGraphQl/etc/module.xml @@ -12,6 +12,7 @@ + diff --git a/app/code/Magento/Cron/Console/Command/CronInstallCommand.php b/app/code/Magento/Cron/Console/Command/CronInstallCommand.php index a3087b4c3d730..5e30076c21e76 100644 --- a/app/code/Magento/Cron/Console/Command/CronInstallCommand.php +++ b/app/code/Magento/Cron/Console/Command/CronInstallCommand.php @@ -3,6 +3,8 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Cron\Console\Command; use Magento\Framework\Crontab\CrontabManagerInterface; @@ -19,6 +21,9 @@ */ class CronInstallCommand extends Command { + private const COMMAND_OPTION_FORCE = 'force'; + private const COMMAND_OPTION_NON_OPTIONAL = 'non-optional'; + /** * @var CrontabManagerInterface */ @@ -44,19 +49,27 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc */ protected function configure() { $this->setName('cron:install') ->setDescription('Generates and installs crontab for current user') - ->addOption('force', 'f', InputOption::VALUE_NONE, 'Force install tasks'); + ->addOption(self::COMMAND_OPTION_FORCE, 'f', InputOption::VALUE_NONE, 'Force install tasks') + // @codingStandardsIgnoreStart + ->addOption(self::COMMAND_OPTION_NON_OPTIONAL, 'd', InputOption::VALUE_NONE, 'Install only the non-optional (default) tasks'); + // @codingStandardsIgnoreEnd parent::configure(); } /** - * {@inheritdoc} + * Executes "cron:install" command. + * + * @param InputInterface $input + * @param OutputInterface $output + * @return int|null + * @throws LocalizedException */ protected function execute(InputInterface $input, OutputInterface $output) { @@ -65,8 +78,13 @@ protected function execute(InputInterface $input, OutputInterface $output) return Cli::RETURN_FAILURE; } + $tasks = $this->tasksProvider->getTasks(); + if ($input->getOption(self::COMMAND_OPTION_NON_OPTIONAL)) { + $tasks = $this->extractNonOptionalTasks($tasks); + } + try { - $this->crontabManager->saveTasks($this->tasksProvider->getTasks()); + $this->crontabManager->saveTasks($tasks); } catch (LocalizedException $e) { $output->writeln('' . $e->getMessage() . ''); return Cli::RETURN_FAILURE; @@ -76,4 +94,23 @@ protected function execute(InputInterface $input, OutputInterface $output) return Cli::RETURN_SUCCESS; } + + /** + * Returns an array of non-optional tasks + * + * @param array $tasks + * @return array + */ + private function extractNonOptionalTasks(array $tasks = []): array + { + $defaultTasks = []; + + foreach ($tasks as $taskCode => $taskParams) { + if (!$taskParams['optional']) { + $defaultTasks[$taskCode] = $taskParams; + } + } + + return $defaultTasks; + } } diff --git a/app/code/Magento/Cron/etc/adminhtml/system.xml b/app/code/Magento/Cron/etc/adminhtml/system.xml index 95d8d4c8a6966..c8753f1b0b56f 100644 --- a/app/code/Magento/Cron/etc/adminhtml/system.xml +++ b/app/code/Magento/Cron/etc/adminhtml/system.xml @@ -15,21 +15,27 @@ Cron configuration options for group: Generate Schedules Every + validate-zero-or-greater validate-digits Schedule Ahead for + validate-zero-or-greater validate-digits Missed if Not Run Within + validate-zero-or-greater validate-digits History Cleanup Every + validate-zero-or-greater validate-digits Success History Lifetime + validate-zero-or-greater validate-digits Failure History Lifetime + validate-zero-or-greater validate-digits Use Separate Process diff --git a/app/code/Magento/Cron/etc/di.xml b/app/code/Magento/Cron/etc/di.xml index 3e3bdc2053576..eadfa15d49414 100644 --- a/app/code/Magento/Cron/etc/di.xml +++ b/app/code/Magento/Cron/etc/di.xml @@ -66,12 +66,15 @@ {magentoRoot}bin/magento cron:run | grep -v "Ran jobs by schedule" >> {magentoLog}magento.cron.log + false {magentoRoot}update/cron.php >> {magentoLog}update.cron.log + true {magentoRoot}bin/magento setup:cron:run >> {magentoLog}setup.cron.log + true diff --git a/app/code/Magento/Customer/Block/Form/Register.php b/app/code/Magento/Customer/Block/Form/Register.php index a190ccde50b5a..be16046d69075 100644 --- a/app/code/Magento/Customer/Block/Form/Register.php +++ b/app/code/Magento/Customer/Block/Form/Register.php @@ -6,7 +6,8 @@ namespace Magento\Customer\Block\Form; use Magento\Customer\Model\AccountManagement; -use Magento\Newsletter\Observer\PredispatchNewsletterObserver; +use Magento\Framework\App\ObjectManager; +use Magento\Newsletter\Model\Config; /** * Customer register form block @@ -32,6 +33,11 @@ class Register extends \Magento\Directory\Block\Data */ protected $_customerUrl; + /** + * @var Config + */ + private $newsLetterConfig; + /** * Constructor * @@ -45,6 +51,7 @@ class Register extends \Magento\Directory\Block\Data * @param \Magento\Customer\Model\Session $customerSession * @param \Magento\Customer\Model\Url $customerUrl * @param array $data + * @param Config $newsLetterConfig * * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ @@ -58,11 +65,13 @@ public function __construct( \Magento\Framework\Module\ModuleManagerInterface $moduleManager, \Magento\Customer\Model\Session $customerSession, \Magento\Customer\Model\Url $customerUrl, - array $data = [] + array $data = [], + Config $newsLetterConfig = null ) { $this->_customerUrl = $customerUrl; $this->_moduleManager = $moduleManager; $this->_customerSession = $customerSession; + $this->newsLetterConfig = $newsLetterConfig ?: ObjectManager::getInstance()->get(Config::class); parent::__construct( $context, $directoryHelper, @@ -170,7 +179,7 @@ public function getRegion() public function isNewsletterEnabled() { return $this->_moduleManager->isOutputEnabled('Magento_Newsletter') - && $this->getConfig(PredispatchNewsletterObserver::XML_PATH_NEWSLETTER_ACTIVE); + && $this->newsLetterConfig->isActive(); } /** diff --git a/app/code/Magento/Customer/Block/Widget/Dob.php b/app/code/Magento/Customer/Block/Widget/Dob.php index 55101fb82afd0..d874729d9132e 100644 --- a/app/code/Magento/Customer/Block/Widget/Dob.php +++ b/app/code/Magento/Customer/Block/Widget/Dob.php @@ -99,11 +99,34 @@ public function isRequired() */ public function setDate($date) { - $this->setTime($date ? strtotime($date) : false); + $this->setTime($this->filterTime($date)); $this->setValue($this->applyOutputFilter($date)); return $this; } + /** + * Sanitizes time + * + * @param mixed $value + * @return bool|int + */ + private function filterTime($value) + { + $time = false; + if ($value) { + if ($value instanceof \DateTimeInterface) { + $time = $value->getTimestamp(); + } elseif (is_numeric($value)) { + $time = $value; + } elseif (is_string($value)) { + $time = strtotime($value); + $time = $time === false ? $this->_localeDate->date($value, null, false, false)->getTimestamp() : $time; + } + } + + return $time; + } + /** * Return Data Form Filter or false * @@ -200,21 +223,23 @@ public function getStoreLabel($attributeCode) */ public function getFieldHtml() { - $this->dateElement->setData([ - 'extra_params' => $this->getHtmlExtraParams(), - 'name' => $this->getHtmlId(), - 'id' => $this->getHtmlId(), - 'class' => $this->getHtmlClass(), - 'value' => $this->getValue(), - 'date_format' => $this->getDateFormat(), - 'image' => $this->getViewFileUrl('Magento_Theme::calendar.png'), - 'years_range' => '-120y:c+nn', - 'max_date' => '-1d', - 'change_month' => 'true', - 'change_year' => 'true', - 'show_on' => 'both', - 'first_day' => $this->getFirstDay() - ]); + $this->dateElement->setData( + [ + 'extra_params' => $this->getHtmlExtraParams(), + 'name' => $this->getHtmlId(), + 'id' => $this->getHtmlId(), + 'class' => $this->getHtmlClass(), + 'value' => $this->getValue(), + 'date_format' => $this->getDateFormat(), + 'image' => $this->getViewFileUrl('Magento_Theme::calendar.png'), + 'years_range' => '-120y:c+nn', + 'max_date' => '-1d', + 'change_month' => 'true', + 'change_year' => 'true', + 'show_on' => 'both', + 'first_day' => $this->getFirstDay() + ] + ); return $this->dateElement->getHtml(); } diff --git a/app/code/Magento/Customer/Controller/Account/Confirm.php b/app/code/Magento/Customer/Controller/Account/Confirm.php index 2b3cb9aa61ab5..adca90c5e5f24 100644 --- a/app/code/Magento/Customer/Controller/Account/Confirm.php +++ b/app/code/Magento/Customer/Controller/Account/Confirm.php @@ -6,25 +6,27 @@ */ namespace Magento\Customer\Controller\Account; -use Magento\Customer\Model\Url; -use Magento\Framework\App\Action\Context; -use Magento\Customer\Model\Session; -use Magento\Framework\App\Config\ScopeConfigInterface; -use Magento\Store\Model\StoreManagerInterface; use Magento\Customer\Api\AccountManagementInterface; use Magento\Customer\Api\CustomerRepositoryInterface; +use Magento\Customer\Controller\AbstractAccount; use Magento\Customer\Helper\Address; +use Magento\Customer\Model\Session; +use Magento\Customer\Model\Url; +use Magento\Framework\App\Action\Context; +use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\Controller\ResultFactory; use Magento\Framework\UrlFactory; use Magento\Framework\Exception\StateException; use Magento\Store\Model\ScopeInterface; -use Magento\Framework\Controller\ResultFactory; +use Magento\Store\Model\StoreManagerInterface; /** * Class Confirm * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class Confirm extends \Magento\Customer\Controller\AbstractAccount +class Confirm extends AbstractAccount implements HttpGetActionInterface { /** * @var \Magento\Framework\App\Config\ScopeConfigInterface @@ -147,13 +149,16 @@ public function execute() $resultRedirect->setPath('*/*/'); return $resultRedirect; } - try { - $customerId = $this->getRequest()->getParam('id', false); - $key = $this->getRequest()->getParam('key', false); - if (empty($customerId) || empty($key)) { - throw new \Exception(__('Bad request.')); - } + $customerId = $this->getRequest()->getParam('id', false); + $key = $this->getRequest()->getParam('key', false); + if (empty($customerId) || empty($key)) { + $this->messageManager->addErrorMessage(__('Bad request.')); + $url = $this->urlModel->getUrl('*/*/index', ['_secure' => true]); + return $resultRedirect->setUrl($this->_redirect->error($url)); + } + + try { // log in and send greeting email $customerEmail = $this->customerRepository->getById($customerId)->getEmail(); $customer = $this->customerAccountManagement->activate($customerEmail, $key); diff --git a/app/code/Magento/Customer/Controller/Account/Confirmation.php b/app/code/Magento/Customer/Controller/Account/Confirmation.php index a3e2db0207630..59def8640328c 100644 --- a/app/code/Magento/Customer/Controller/Account/Confirmation.php +++ b/app/code/Magento/Customer/Controller/Account/Confirmation.php @@ -1,21 +1,26 @@ storeManager->getStore()->getWebsiteId() ); - $this->messageManager->addSuccess(__('Please check your email for confirmation key.')); + $this->messageManager->addSuccessMessage(__('Please check your email for confirmation key.')); } catch (InvalidTransitionException $e) { - $this->messageManager->addSuccess(__('This email does not require confirmation.')); + $this->messageManager->addSuccessMessage(__('This email does not require confirmation.')); } catch (\Exception $e) { - $this->messageManager->addException($e, __('Wrong email.')); + $this->messageManager->addExceptionMessage($e, __('Wrong email.')); $resultRedirect->setPath('*/*/*', ['email' => $email, '_secure' => true]); return $resultRedirect; } diff --git a/app/code/Magento/Customer/Controller/Account/CreatePost.php b/app/code/Magento/Customer/Controller/Account/CreatePost.php index 4c9c25b5f33d9..a2be0f68b56cb 100644 --- a/app/code/Magento/Customer/Controller/Account/CreatePost.php +++ b/app/code/Magento/Customer/Controller/Account/CreatePost.php @@ -3,6 +3,8 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Customer\Controller\Account; use Magento\Customer\Api\CustomerRepositoryInterface as CustomerRepository; @@ -349,15 +351,14 @@ public function execute() $confirmation = $this->getRequest()->getParam('password_confirmation'); $redirectUrl = $this->session->getBeforeAuthUrl(); $this->checkPasswordConfirmation($password, $confirmation); + + $extensionAttributes = $customer->getExtensionAttributes(); + $extensionAttributes->setIsSubscribed($this->getRequest()->getParam('is_subscribed', false)); + $customer->setExtensionAttributes($extensionAttributes); + $customer = $this->accountManagement ->createAccount($customer, $password, $redirectUrl); - if ($this->getRequest()->getParam('is_subscribed', false)) { - $extensionAttributes = $customer->getExtensionAttributes(); - $extensionAttributes->setIsSubscribed(true); - $customer->setExtensionAttributes($extensionAttributes); - $this->customerRepository->save($customer); - } $this->_eventManager->dispatch( 'customer_register_success', ['account_controller' => $this, 'customer' => $customer] diff --git a/app/code/Magento/Customer/Controller/Adminhtml/Index.php b/app/code/Magento/Customer/Controller/Adminhtml/Index.php index a0317a51260da..ffae1e9f8bf1e 100644 --- a/app/code/Magento/Customer/Controller/Adminhtml/Index.php +++ b/app/code/Magento/Customer/Controller/Adminhtml/Index.php @@ -311,7 +311,7 @@ protected function _addSessionErrorMessages($messages) protected function actUponMultipleCustomers(callable $singleAction, $customerIds) { if (!is_array($customerIds)) { - $this->messageManager->addError(__('Please select customer(s).')); + $this->messageManager->addErrorMessage(__('Please select customer(s).')); return 0; } $customersUpdated = 0; @@ -320,7 +320,7 @@ protected function actUponMultipleCustomers(callable $singleAction, $customerIds $singleAction($customerId); $customersUpdated++; } catch (\Exception $exception) { - $this->messageManager->addError($exception->getMessage()); + $this->messageManager->addErrorMessage($exception->getMessage()); } } return $customersUpdated; diff --git a/app/code/Magento/Customer/Controller/Adminhtml/Index/MassAssignGroup.php b/app/code/Magento/Customer/Controller/Adminhtml/Index/MassAssignGroup.php index 5a9c52bf9b1c0..f55c81da7e0b9 100644 --- a/app/code/Magento/Customer/Controller/Adminhtml/Index/MassAssignGroup.php +++ b/app/code/Magento/Customer/Controller/Adminhtml/Index/MassAssignGroup.php @@ -60,7 +60,7 @@ protected function massAction(AbstractCollection $collection) } if ($customersUpdated) { - $this->messageManager->addSuccess(__('A total of %1 record(s) were updated.', $customersUpdated)); + $this->messageManager->addSuccessMessage(__('A total of %1 record(s) were updated.', $customersUpdated)); } /** @var \Magento\Backend\Model\View\Result\Redirect $resultRedirect */ $resultRedirect = $this->resultFactory->create(ResultFactory::TYPE_REDIRECT); diff --git a/app/code/Magento/Customer/Controller/Adminhtml/Index/MassDelete.php b/app/code/Magento/Customer/Controller/Adminhtml/Index/MassDelete.php index edaeea6a15eb2..85286573bc5e7 100644 --- a/app/code/Magento/Customer/Controller/Adminhtml/Index/MassDelete.php +++ b/app/code/Magento/Customer/Controller/Adminhtml/Index/MassDelete.php @@ -58,7 +58,7 @@ protected function massAction(AbstractCollection $collection) } if ($customersDeleted) { - $this->messageManager->addSuccess(__('A total of %1 record(s) were deleted.', $customersDeleted)); + $this->messageManager->addSuccessMessage(__('A total of %1 record(s) were deleted.', $customersDeleted)); } /** @var \Magento\Backend\Model\View\Result\Redirect $resultRedirect */ $resultRedirect = $this->resultFactory->create(ResultFactory::TYPE_REDIRECT); diff --git a/app/code/Magento/Customer/Controller/Adminhtml/Index/MassSubscribe.php b/app/code/Magento/Customer/Controller/Adminhtml/Index/MassSubscribe.php index 25c56ac60c14b..29a66bf1ff933 100644 --- a/app/code/Magento/Customer/Controller/Adminhtml/Index/MassSubscribe.php +++ b/app/code/Magento/Customer/Controller/Adminhtml/Index/MassSubscribe.php @@ -6,6 +6,7 @@ namespace Magento\Customer\Controller\Adminhtml\Index; use Magento\Backend\App\Action\Context; +use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; use Magento\Ui\Component\MassAction\Filter; use Magento\Customer\Model\ResourceModel\Customer\CollectionFactory; use Magento\Customer\Api\CustomerRepositoryInterface; @@ -16,7 +17,7 @@ /** * Class MassSubscribe */ -class MassSubscribe extends AbstractMassAction +class MassSubscribe extends AbstractMassAction implements HttpPostActionInterface { /** * @var CustomerRepositoryInterface @@ -64,7 +65,7 @@ protected function massAction(AbstractCollection $collection) } if ($customersUpdated) { - $this->messageManager->addSuccess(__('A total of %1 record(s) were updated.', $customersUpdated)); + $this->messageManager->addSuccessMessage(__('A total of %1 record(s) were updated.', $customersUpdated)); } /** @var \Magento\Backend\Model\View\Result\Redirect $resultRedirect */ $resultRedirect = $this->resultFactory->create(ResultFactory::TYPE_REDIRECT); diff --git a/app/code/Magento/Customer/Controller/Adminhtml/Index/MassUnsubscribe.php b/app/code/Magento/Customer/Controller/Adminhtml/Index/MassUnsubscribe.php index 4b40722ba9ab2..fddf18489b9a5 100644 --- a/app/code/Magento/Customer/Controller/Adminhtml/Index/MassUnsubscribe.php +++ b/app/code/Magento/Customer/Controller/Adminhtml/Index/MassUnsubscribe.php @@ -6,6 +6,7 @@ namespace Magento\Customer\Controller\Adminhtml\Index; use Magento\Customer\Api\CustomerRepositoryInterface; +use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; use Magento\Framework\Controller\ResultFactory; use Magento\Backend\App\Action\Context; use Magento\Newsletter\Model\SubscriberFactory; @@ -16,7 +17,7 @@ /** * Class MassUnsubscribe */ -class MassUnsubscribe extends AbstractMassAction +class MassUnsubscribe extends AbstractMassAction implements HttpPostActionInterface { /** * @var CustomerRepositoryInterface @@ -64,7 +65,7 @@ protected function massAction(AbstractCollection $collection) } if ($customersUpdated) { - $this->messageManager->addSuccess(__('A total of %1 record(s) were updated.', $customersUpdated)); + $this->messageManager->addSuccessMessage(__('A total of %1 record(s) were updated.', $customersUpdated)); } /** @var \Magento\Backend\Model\View\Result\Redirect $resultRedirect */ $resultRedirect = $this->resultFactory->create(ResultFactory::TYPE_REDIRECT); diff --git a/app/code/Magento/Customer/Controller/Adminhtml/Index/ResetPassword.php b/app/code/Magento/Customer/Controller/Adminhtml/Index/ResetPassword.php index 1e4fa91cbf899..3b9370c32bf6d 100644 --- a/app/code/Magento/Customer/Controller/Adminhtml/Index/ResetPassword.php +++ b/app/code/Magento/Customer/Controller/Adminhtml/Index/ResetPassword.php @@ -44,7 +44,9 @@ public function execute() \Magento\Customer\Model\AccountManagement::EMAIL_REMINDER, $customer->getWebsiteId() ); - $this->messageManager->addSuccess(__('The customer will receive an email with a link to reset password.')); + $this->messageManager->addSuccessMessage( + __('The customer will receive an email with a link to reset password.') + ); } catch (NoSuchEntityException $exception) { $resultRedirect->setPath('customer/index'); return $resultRedirect; @@ -57,7 +59,7 @@ public function execute() } catch (SecurityViolationException $exception) { $this->messageManager->addErrorMessage($exception->getMessage()); } catch (\Exception $exception) { - $this->messageManager->addException( + $this->messageManager->addExceptionMessage( $exception, __('Something went wrong while resetting customer password.') ); diff --git a/app/code/Magento/Customer/Controller/Adminhtml/Index/Save.php b/app/code/Magento/Customer/Controller/Adminhtml/Index/Save.php index 38ed688a835bc..3ee33af9ec073 100644 --- a/app/code/Magento/Customer/Controller/Adminhtml/Index/Save.php +++ b/app/code/Magento/Customer/Controller/Adminhtml/Index/Save.php @@ -354,7 +354,7 @@ public function execute() $this->_getSession()->unsCustomerFormData(); // Done Saving customer, finish save action $this->_coreRegistry->register(RegistryConstants::CURRENT_CUSTOMER_ID, $customerId); - $this->messageManager->addSuccess(__('You saved the customer.')); + $this->messageManager->addSuccessMessage(__('You saved the customer.')); $returnToEdit = (bool)$this->getRequest()->getParam('back', false); } catch (\Magento\Framework\Validator\Exception $exception) { $messages = $exception->getMessages(); @@ -378,7 +378,10 @@ public function execute() $this->_getSession()->setCustomerFormData($this->retrieveFormattedFormData()); $returnToEdit = true; } catch (\Exception $exception) { - $this->messageManager->addException($exception, __('Something went wrong while saving the customer.')); + $this->messageManager->addExceptionMessage( + $exception, + __('Something went wrong while saving the customer.') + ); $this->_getSession()->setCustomerFormData($this->retrieveFormattedFormData()); $returnToEdit = true; } diff --git a/app/code/Magento/Customer/Controller/Adminhtml/Locks/Unlock.php b/app/code/Magento/Customer/Controller/Adminhtml/Locks/Unlock.php index 1fd06a3182948..32299de777c31 100644 --- a/app/code/Magento/Customer/Controller/Adminhtml/Locks/Unlock.php +++ b/app/code/Magento/Customer/Controller/Adminhtml/Locks/Unlock.php @@ -7,13 +7,14 @@ namespace Magento\Customer\Controller\Adminhtml\Locks; use Magento\Customer\Model\AuthenticationInterface; +use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; use Magento\Framework\Controller\ResultFactory; use Magento\Backend\App\Action; /** * Unlock Customer Controller */ -class Unlock extends \Magento\Backend\App\Action +class Unlock extends \Magento\Backend\App\Action implements HttpGetActionInterface { /** * Authorization level of a basic admin session @@ -55,10 +56,10 @@ public function execute() // unlock customer if ($customerId) { $this->authentication->unlock($customerId); - $this->getMessageManager()->addSuccess(__('Customer has been unlocked successfully.')); + $this->getMessageManager()->addSuccessMessage(__('Customer has been unlocked successfully.')); } } catch (\Exception $e) { - $this->messageManager->addError($e->getMessage()); + $this->messageManager->addErrorMessage($e->getMessage()); } /** @var \Magento\Backend\Model\View\Result\Redirect $resultRedirect */ diff --git a/app/code/Magento/Customer/Model/AccountManagement.php b/app/code/Magento/Customer/Model/AccountManagement.php index 24899a1c5979c..7be8699495bf5 100644 --- a/app/code/Magento/Customer/Model/AccountManagement.php +++ b/app/code/Magento/Customer/Model/AccountManagement.php @@ -66,49 +66,65 @@ class AccountManagement implements AccountManagementInterface { /** - * Configuration paths for email templates and identities + * Configuration paths for create account email template * * @deprecated */ const XML_PATH_REGISTER_EMAIL_TEMPLATE = 'customer/create_account/email_template'; /** + * Configuration paths for register no password email template + * * @deprecated */ const XML_PATH_REGISTER_NO_PASSWORD_EMAIL_TEMPLATE = 'customer/create_account/email_no_password_template'; /** + * Configuration paths for remind email identity + * * @deprecated */ const XML_PATH_REGISTER_EMAIL_IDENTITY = 'customer/create_account/email_identity'; /** + * Configuration paths for remind email template + * * @deprecated */ const XML_PATH_REMIND_EMAIL_TEMPLATE = 'customer/password/remind_email_template'; /** + * Configuration paths for forgot email email template + * * @deprecated */ const XML_PATH_FORGOT_EMAIL_TEMPLATE = 'customer/password/forgot_email_template'; /** + * Configuration paths for forgot email identity + * * @deprecated */ const XML_PATH_FORGOT_EMAIL_IDENTITY = 'customer/password/forgot_email_identity'; /** + * Configuration paths for account confirmation required + * * @deprecated * @see AccountConfirmation::XML_PATH_IS_CONFIRM */ const XML_PATH_IS_CONFIRM = 'customer/create_account/confirm'; /** + * Configuration paths for account confirmation email template + * * @deprecated */ const XML_PATH_CONFIRM_EMAIL_TEMPLATE = 'customer/create_account/email_confirmation_template'; /** + * Configuration paths for confirmation confirmed email template + * * @deprecated */ const XML_PATH_CONFIRMED_EMAIL_TEMPLATE = 'customer/create_account/email_confirmed_template'; @@ -161,11 +177,15 @@ class AccountManagement implements AccountManagementInterface const XML_PATH_REQUIRED_CHARACTER_CLASSES_NUMBER = 'customer/password/required_character_classes_number'; /** + * Configuration path to customer reset password email template + * * @deprecated */ const XML_PATH_RESET_PASSWORD_TEMPLATE = 'customer/password/reset_password_template'; /** + * Minimum password length + * * @deprecated */ const MIN_PASSWORD_LENGTH = 6; @@ -579,7 +599,6 @@ public function authenticate($username, $password) } try { $this->getAuthentication()->authenticate($customerId, $password); - // phpcs:ignore Magento2.Exceptions.ThrowCatch } catch (InvalidEmailOrPasswordException $e) { throw new InvalidEmailOrPasswordException(__('Invalid login or password.')); } @@ -890,7 +909,6 @@ public function createAccountWithPasswordHash(CustomerInterface $customer, $hash throw new InputMismatchException( __('A customer with the same email address already exists in an associated website.') ); - // phpcs:ignore Magento2.Exceptions.ThrowCatch } catch (LocalizedException $e) { throw $e; } @@ -910,7 +928,6 @@ public function createAccountWithPasswordHash(CustomerInterface $customer, $hash } } $this->customerRegistry->remove($customer->getId()); - // phpcs:ignore Magento2.Exceptions.ThrowCatch } catch (InputException $e) { $this->customerRepository->delete($customer); throw $e; @@ -1017,7 +1034,6 @@ private function changePasswordForCustomer($customer, $currentPassword, $newPass { try { $this->getAuthentication()->authenticate($customer->getId(), $currentPassword); - // phpcs:ignore Magento2.Exceptions.ThrowCatch } catch (InvalidEmailOrPasswordException $e) { throw new InvalidEmailOrPasswordException( __("The password doesn't match this account. Verify the password and try again.") diff --git a/app/code/Magento/Customer/Model/Address/CustomerAddressDataFormatter.php b/app/code/Magento/Customer/Model/Address/CustomerAddressDataFormatter.php index 9202d7492040c..dfda2bd324828 100644 --- a/app/code/Magento/Customer/Model/Address/CustomerAddressDataFormatter.php +++ b/app/code/Magento/Customer/Model/Address/CustomerAddressDataFormatter.php @@ -81,6 +81,7 @@ public function prepareAddress(AddressInterface $customerAddress) 'inline' => $this->getCustomerAddressInline($customerAddress), 'custom_attributes' => [], 'extension_attributes' => $customerAddress->getExtensionAttributes(), + 'vat_id' => $customerAddress->getVatId() ]; if ($customerAddress->getCustomAttributes()) { diff --git a/app/code/Magento/Customer/Model/AttributeMetadataConverter.php b/app/code/Magento/Customer/Model/AttributeMetadataConverter.php index 44d104659e069..0407aebf9d670 100644 --- a/app/code/Magento/Customer/Model/AttributeMetadataConverter.php +++ b/app/code/Magento/Customer/Model/AttributeMetadataConverter.php @@ -3,18 +3,36 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Customer\Model; use Magento\Customer\Api\Data\OptionInterfaceFactory; use Magento\Customer\Api\Data\ValidationRuleInterfaceFactory; use Magento\Customer\Api\Data\AttributeMetadataInterfaceFactory; use Magento\Eav\Api\Data\AttributeDefaultValueInterface; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\App\ObjectManager; /** * Converter for AttributeMetadata */ class AttributeMetadataConverter { + /** + * Attribute Code get options from system config + * + * @var array + */ + private const ATTRIBUTE_CODE_LIST_FROM_SYSTEM_CONFIG = ['prefix', 'suffix']; + + /** + * XML Path to get address config + * + * @var string + */ + private const XML_CUSTOMER_ADDRESS = 'customer/address/'; + /** * @var OptionInterfaceFactory */ @@ -35,6 +53,11 @@ class AttributeMetadataConverter */ protected $dataObjectHelper; + /** + * @var ScopeConfigInterface + */ + private $scopeConfig; + /** * Initialize the Converter * @@ -42,17 +65,20 @@ class AttributeMetadataConverter * @param ValidationRuleInterfaceFactory $validationRuleFactory * @param AttributeMetadataInterfaceFactory $attributeMetadataFactory * @param \Magento\Framework\Api\DataObjectHelper $dataObjectHelper + * @param ScopeConfigInterface $scopeConfig */ public function __construct( OptionInterfaceFactory $optionFactory, ValidationRuleInterfaceFactory $validationRuleFactory, AttributeMetadataInterfaceFactory $attributeMetadataFactory, - \Magento\Framework\Api\DataObjectHelper $dataObjectHelper + \Magento\Framework\Api\DataObjectHelper $dataObjectHelper, + ScopeConfigInterface $scopeConfig = null ) { $this->optionFactory = $optionFactory; $this->validationRuleFactory = $validationRuleFactory; $this->attributeMetadataFactory = $attributeMetadataFactory; $this->dataObjectHelper = $dataObjectHelper; + $this->scopeConfig = $scopeConfig ?? ObjectManager::getInstance()->get(ScopeConfigInterface::class); } /** @@ -64,28 +90,34 @@ public function __construct( public function createMetadataAttribute($attribute) { $options = []; - if ($attribute->usesSource()) { - foreach ($attribute->getSource()->getAllOptions() as $option) { - $optionDataObject = $this->optionFactory->create(); - if (!is_array($option['value'])) { - $optionDataObject->setValue($option['value']); - } else { - $optionArray = []; - foreach ($option['value'] as $optionArrayValues) { - $optionObject = $this->optionFactory->create(); - $this->dataObjectHelper->populateWithArray( - $optionObject, - $optionArrayValues, - \Magento\Customer\Api\Data\OptionInterface::class - ); - $optionArray[] = $optionObject; + + if (in_array($attribute->getAttributeCode(), self::ATTRIBUTE_CODE_LIST_FROM_SYSTEM_CONFIG)) { + $options = $this->getOptionFromConfig($attribute->getAttributeCode()); + } else { + if ($attribute->usesSource()) { + foreach ($attribute->getSource()->getAllOptions() as $option) { + $optionDataObject = $this->optionFactory->create(); + if (!is_array($option['value'])) { + $optionDataObject->setValue($option['value']); + } else { + $optionArray = []; + foreach ($option['value'] as $optionArrayValues) { + $optionObject = $this->optionFactory->create(); + $this->dataObjectHelper->populateWithArray( + $optionObject, + $optionArrayValues, + \Magento\Customer\Api\Data\OptionInterface::class + ); + $optionArray[] = $optionObject; + } + $optionDataObject->setOptions($optionArray); } - $optionDataObject->setOptions($optionArray); + $optionDataObject->setLabel($option['label']); + $options[] = $optionDataObject; } - $optionDataObject->setLabel($option['label']); - $options[] = $optionDataObject; } } + $validationRules = []; foreach ((array)$attribute->getValidateRules() as $name => $value) { $validationRule = $this->validationRuleFactory->create() @@ -122,4 +154,26 @@ public function createMetadataAttribute($attribute) ->setIsFilterableInGrid($attribute->getIsFilterableInGrid()) ->setIsSearchableInGrid($attribute->getIsSearchableInGrid()); } + + /** + * Get option from System Config instead of Use Source (Prefix, Suffix) + * + * @param string $attributeCode + * @return \Magento\Customer\Api\Data\OptionInterface[] + */ + private function getOptionFromConfig($attributeCode) + { + $result = []; + $value = $this->scopeConfig->getValue(self::XML_CUSTOMER_ADDRESS . $attributeCode . '_options'); + if ($value) { + $optionArray = explode(';', $value); + foreach ($optionArray as $value) { + $optionObject = $this->optionFactory->create(); + $optionObject->setLabel($value); + $optionObject->setValue($value); + $result[] = $optionObject; + } + } + return $result; + } } diff --git a/app/code/Magento/Customer/Model/Customer.php b/app/code/Magento/Customer/Model/Customer.php index 1287dbe5df708..1f8f7d90f6d0d 100644 --- a/app/code/Magento/Customer/Model/Customer.php +++ b/app/code/Magento/Customer/Model/Customer.php @@ -394,7 +394,9 @@ public function getSharingConfig() public function authenticate($login, $password) { $this->loadByEmail($login); - if ($this->getConfirmation() && $this->isConfirmationRequired()) { + if ($this->getConfirmation() && + $this->accountConfirmation->isConfirmationRequired($this->getWebsiteId(), $this->getId(), $this->getEmail()) + ) { throw new EmailNotConfirmedException( __("This account isn't confirmed. Verify and try again.") ); @@ -415,8 +417,9 @@ public function authenticate($login, $password) /** * Load customer by email * - * @param string $customerEmail - * @return $this + * @param string $customerEmail + * @return $this + * @throws \Magento\Framework\Exception\LocalizedException */ public function loadByEmail($customerEmail) { @@ -427,8 +430,9 @@ public function loadByEmail($customerEmail) /** * Change customer password * - * @param string $newPassword - * @return $this + * @param string $newPassword + * @return $this + * @throws \Magento\Framework\Exception\LocalizedException */ public function changePassword($newPassword) { @@ -440,6 +444,7 @@ public function changePassword($newPassword) * Get full customer name * * @return string + * @throws \Magento\Framework\Exception\LocalizedException */ public function getName() { @@ -462,8 +467,9 @@ public function getName() /** * Add address to address collection * - * @param Address $address - * @return $this + * @param Address $address + * @return $this + * @throws \Magento\Framework\Exception\LocalizedException */ public function addAddress(Address $address) { @@ -487,6 +493,7 @@ public function getAddressById($addressId) * * @param int $addressId * @return Address + * @throws \Magento\Framework\Exception\LocalizedException */ public function getAddressItemById($addressId) { @@ -507,6 +514,7 @@ public function getAddressCollection() * Customer addresses collection * * @return \Magento\Customer\Model\ResourceModel\Address\Collection + * @throws \Magento\Framework\Exception\LocalizedException */ public function getAddressesCollection() { @@ -538,6 +546,7 @@ public function getAddresses() * Retrieve all customer attributes * * @return Attribute[] + * @throws \Magento\Framework\Exception\LocalizedException */ public function getAttributes() { @@ -592,6 +601,7 @@ public function hashPassword($password, $salt = true) * * @param string $password * @return boolean + * @throws \Exception */ public function validatePassword($password) { @@ -805,6 +815,7 @@ public function isConfirmationRequired() */ public function getRandomConfirmationKey() { + // phpcs:ignore Magento2.Security.InsecureFunction return md5(uniqid()); } diff --git a/app/code/Magento/Customer/Model/CustomerExtractor.php b/app/code/Magento/Customer/Model/CustomerExtractor.php index be30b97994787..5d6f3245a0439 100644 --- a/app/code/Magento/Customer/Model/CustomerExtractor.php +++ b/app/code/Magento/Customer/Model/CustomerExtractor.php @@ -1,9 +1,9 @@ compactData($customerData); $allowedAttributes = $customerForm->getAllowedAttributes(); - $isGroupIdEmpty = isset($allowedAttributes['group_id']); + $isGroupIdEmpty = !isset($allowedAttributes['group_id']); $customerDataObject = $this->customerFactory->create(); $this->dataObjectHelper->populateWithArray( @@ -88,15 +93,18 @@ public function extract( $customerData, \Magento\Customer\Api\Data\CustomerInterface::class ); + $store = $this->storeManager->getStore(); + $storeId = $store->getId(); + if ($isGroupIdEmpty) { $customerDataObject->setGroupId( - $this->customerGroupManagement->getDefaultGroup($store->getId())->getId() + $this->customerGroupManagement->getDefaultGroup($storeId)->getId() ); } $customerDataObject->setWebsiteId($store->getWebsiteId()); - $customerDataObject->setStoreId($store->getId()); + $customerDataObject->setStoreId($storeId); return $customerDataObject; } diff --git a/app/code/Magento/Customer/Model/ResourceModel/Customer.php b/app/code/Magento/Customer/Model/ResourceModel/Customer.php index 94196df6fe093..1477287f79f4b 100644 --- a/app/code/Magento/Customer/Model/ResourceModel/Customer.php +++ b/app/code/Magento/Customer/Model/ResourceModel/Customer.php @@ -6,6 +6,7 @@ namespace Magento\Customer\Model\ResourceModel; +use Magento\Customer\Model\AccountConfirmation; use Magento\Customer\Model\Customer\NotificationStorage; use Magento\Framework\App\ObjectManager; use Magento\Framework\Validator\Exception as ValidatorException; @@ -42,12 +43,19 @@ class Customer extends \Magento\Eav\Model\Entity\VersionControl\AbstractEntity */ protected $storeManager; + /** + * @var AccountConfirmation + */ + private $accountConfirmation; + /** * @var NotificationStorage */ private $notificationStorage; /** + * Customer constructor. + * * @param \Magento\Eav\Model\Entity\Context $context * @param \Magento\Framework\Model\ResourceModel\Db\VersionControl\Snapshot $entitySnapshot * @param \Magento\Framework\Model\ResourceModel\Db\VersionControl\RelationComposite $entityRelationComposite @@ -56,6 +64,7 @@ class Customer extends \Magento\Eav\Model\Entity\VersionControl\AbstractEntity * @param \Magento\Framework\Stdlib\DateTime $dateTime * @param \Magento\Store\Model\StoreManagerInterface $storeManager * @param array $data + * @param AccountConfirmation $accountConfirmation */ public function __construct( \Magento\Eav\Model\Entity\Context $context, @@ -65,15 +74,19 @@ public function __construct( \Magento\Framework\Validator\Factory $validatorFactory, \Magento\Framework\Stdlib\DateTime $dateTime, \Magento\Store\Model\StoreManagerInterface $storeManager, - $data = [] + $data = [], + AccountConfirmation $accountConfirmation = null ) { parent::__construct($context, $entitySnapshot, $entityRelationComposite, $data); + $this->_scopeConfig = $scopeConfig; $this->_validatorFactory = $validatorFactory; $this->dateTime = $dateTime; - $this->storeManager = $storeManager; + $this->accountConfirmation = $accountConfirmation ?: ObjectManager::getInstance() + ->get(AccountConfirmation::class); $this->setType('customer'); $this->setConnection('customer_read', 'customer_write'); + $this->storeManager = $storeManager; } /** @@ -144,7 +157,13 @@ protected function _beforeSave(\Magento\Framework\DataObject $customer) } // set confirmation key logic - if (!$customer->getId() && $customer->isConfirmationRequired()) { + if (!$customer->getId() && + $this->accountConfirmation->isConfirmationRequired( + $customer->getWebsiteId(), + $customer->getId(), + $customer->getEmail() + ) + ) { $customer->setConfirmation($customer->getRandomConfirmationKey()); } // remove customer confirmation key from database, if empty diff --git a/app/code/Magento/Customer/Model/Session.php b/app/code/Magento/Customer/Model/Session.php index 5900fed218edf..047327a0b6c29 100644 --- a/app/code/Magento/Customer/Model/Session.php +++ b/app/code/Magento/Customer/Model/Session.php @@ -10,6 +10,7 @@ use Magento\Customer\Api\GroupManagementInterface; use Magento\Customer\Model\Config\Share; use Magento\Customer\Model\ResourceModel\Customer as ResourceCustomer; +use Magento\Framework\App\ObjectManager; /** * Customer session model @@ -17,6 +18,7 @@ * @api * @method string getNoReferer() * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) * @since 100.0.2 */ class Session extends \Magento\Framework\Session\SessionManager @@ -107,6 +109,8 @@ class Session extends \Magento\Framework\Session\SessionManager protected $response; /** + * Session constructor. + * * @param \Magento\Framework\App\Request\Http $request * @param \Magento\Framework\Session\SidResolverInterface $sidResolver * @param \Magento\Framework\Session\Config\ConfigInterface $sessionConfig @@ -118,7 +122,7 @@ class Session extends \Magento\Framework\Session\SessionManager * @param \Magento\Framework\App\State $appState * @param Share $configShare * @param \Magento\Framework\Url\Helper\Data $coreUrl - * @param \Magento\Customer\Model\Url $customerUrl + * @param Url $customerUrl * @param ResourceCustomer $customerResource * @param CustomerFactory $customerFactory * @param \Magento\Framework\UrlFactory $urlFactory @@ -128,6 +132,7 @@ class Session extends \Magento\Framework\Session\SessionManager * @param CustomerRepositoryInterface $customerRepository * @param GroupManagementInterface $groupManagement * @param \Magento\Framework\App\Response\Http $response + * @param AccountConfirmation $accountConfirmation * @throws \Magento\Framework\Exception\SessionException * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ @@ -152,7 +157,8 @@ public function __construct( \Magento\Framework\App\Http\Context $httpContext, CustomerRepositoryInterface $customerRepository, GroupManagementInterface $groupManagement, - \Magento\Framework\App\Response\Http $response + \Magento\Framework\App\Response\Http $response, + AccountConfirmation $accountConfirmation = null ) { $this->_coreUrl = $coreUrl; $this->_customerUrl = $customerUrl; @@ -177,6 +183,8 @@ public function __construct( ); $this->groupManagement = $groupManagement; $this->response = $response; + $this->accountConfirmation = $accountConfirmation ?: ObjectManager::getInstance() + ->get(AccountConfirmation::class); $this->_eventManager->dispatch('customer_session_init', ['customer_session' => $this]); } @@ -216,6 +224,8 @@ public function setCustomerData(CustomerData $customer) * Retrieve customer model object * * @return CustomerData + * @throws \Magento\Framework\Exception\LocalizedException + * @throws \Magento\Framework\Exception\NoSuchEntityException */ public function getCustomerData() { @@ -266,8 +276,14 @@ public function setCustomer(Customer $customerModel) \Magento\Customer\Model\Group::NOT_LOGGED_IN_ID ); $this->setCustomerId($customerModel->getId()); - if (!$customerModel->isConfirmationRequired() && $customerModel->getConfirmation()) { - $customerModel->setConfirmation(null)->save(); + $accountConfirmationRequired = $this->accountConfirmation->isConfirmationRequired( + $customerModel->getWebsiteId(), + $customerModel->getId(), + $customerModel->getEmail() + ); + if (!$accountConfirmationRequired && $customerModel->getConfirmation() && $customerModel->getId()) { + $customerModel->setConfirmation(null); + $this->_customerResource->save($customerModel); } /** @@ -354,10 +370,11 @@ public function setCustomerGroupId($id) } /** - * Get customer group id - * If customer is not logged in system, 'not logged in' group id will be returned + * Get customer group id. If customer is not logged in system, 'not logged in' group id will be returned. * * @return int + * @throws \Magento\Framework\Exception\LocalizedException + * @throws \Magento\Framework\Exception\NoSuchEntityException */ public function getCustomerGroupId() { @@ -407,6 +424,8 @@ public function checkCustomerId($customerId) } /** + * Sets customer as logged in + * * @param Customer $customer * @return $this */ @@ -420,6 +439,8 @@ public function setCustomerAsLoggedIn($customer) } /** + * Sets customer data as logged in + * * @param CustomerData $customer * @return $this */ @@ -521,6 +542,8 @@ protected function _setAuthUrl($key, $url) * Logout without dispatching event * * @return $this + * @throws \Magento\Framework\Exception\LocalizedException + * @throws \Magento\Framework\Exception\NoSuchEntityException */ protected function _logout() { @@ -567,6 +590,8 @@ public function regenerateId() } /** + * Creates URL factory + * * @return \Magento\Framework\UrlInterface */ protected function _createUrl() diff --git a/app/code/Magento/Customer/Observer/UpgradeQuoteCustomerEmailObserver.php b/app/code/Magento/Customer/Observer/UpgradeQuoteCustomerEmailObserver.php new file mode 100644 index 0000000000000..e0c9552b9f508 --- /dev/null +++ b/app/code/Magento/Customer/Observer/UpgradeQuoteCustomerEmailObserver.php @@ -0,0 +1,66 @@ +quoteRepository = $quoteRepository; + } + + /** + * Upgrade quote customer email when customer has changed email + * + * @param Observer $observer + * @return void + */ + public function execute(Observer $observer): void + { + /** @var \Magento\Customer\Model\Data\Customer $customerOrig */ + $customerOrig = $observer->getEvent()->getOrigCustomerDataObject(); + if (!$customerOrig) { + return; + } + + $emailOrig = $customerOrig->getEmail(); + + /** @var \Magento\Customer\Model\Data\Customer $customer */ + $customer = $observer->getEvent()->getCustomerDataObject(); + $email = $customer->getEmail(); + + if ($email == $emailOrig) { + return; + } + + try { + $quote = $this->quoteRepository->getForCustomer($customer->getId()); + $quote->setCustomerEmail($email); + $this->quoteRepository->save($quote); + } catch (NoSuchEntityException $e) { + return; + } + } +} diff --git a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontClearAllCompareProductsTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontClearAllCompareProductsTest.xml index d7372b07de14b..ada3adbfeb83b 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontClearAllCompareProductsTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontClearAllCompareProductsTest.xml @@ -116,6 +116,7 @@ + diff --git a/app/code/Magento/Customer/Test/Unit/Block/Form/RegisterTest.php b/app/code/Magento/Customer/Test/Unit/Block/Form/RegisterTest.php index b93b9f40d75b2..3022177ffb9e1 100644 --- a/app/code/Magento/Customer/Test/Unit/Block/Form/RegisterTest.php +++ b/app/code/Magento/Customer/Test/Unit/Block/Form/RegisterTest.php @@ -7,7 +7,6 @@ use Magento\Customer\Block\Form\Register; use Magento\Customer\Model\AccountManagement; -use Magento\Newsletter\Observer\PredispatchNewsletterObserver; /** * Test class for \Magento\Customer\Block\Form\Register. @@ -49,6 +48,9 @@ class RegisterTest extends \PHPUnit\Framework\TestCase /** @var Register */ private $_block; + /** @var \PHPUnit_Framework_MockObject_MockObject | \Magento\Newsletter\Model\Config */ + private $newsletterConfig; + protected function setUp() { $this->_scopeConfig = $this->createMock(\Magento\Framework\App\Config\ScopeConfigInterface::class); @@ -59,6 +61,7 @@ protected function setUp() \Magento\Customer\Model\Session::class, ['getCustomerFormData'] ); + $this->newsletterConfig = $this->createMock(\Magento\Newsletter\Model\Config::class); $context = $this->createMock(\Magento\Framework\View\Element\Template\Context::class); $context->expects($this->any())->method('getScopeConfig')->will($this->returnValue($this->_scopeConfig)); @@ -71,7 +74,9 @@ protected function setUp() $this->createMock(\Magento\Directory\Model\ResourceModel\Country\CollectionFactory::class), $this->_moduleManager, $this->_customerSession, - $this->_customerUrl + $this->_customerUrl, + [], + $this->newsletterConfig ); } @@ -293,12 +298,10 @@ public function testIsNewsletterEnabled($isNewsletterEnabled, $isNewsletterActiv $this->returnValue($isNewsletterEnabled) ); - $this->_scopeConfig->expects( + $this->newsletterConfig->expects( $this->any() )->method( - 'getValue' - )->with( - PredispatchNewsletterObserver::XML_PATH_NEWSLETTER_ACTIVE + 'isActive' )->will( $this->returnValue($isNewsletterActive) ); diff --git a/app/code/Magento/Customer/Test/Unit/Block/Widget/DobTest.php b/app/code/Magento/Customer/Test/Unit/Block/Widget/DobTest.php index 6926eee4f28f2..8bfddac3cef8f 100644 --- a/app/code/Magento/Customer/Test/Unit/Block/Widget/DobTest.php +++ b/app/code/Magento/Customer/Test/Unit/Block/Widget/DobTest.php @@ -6,14 +6,31 @@ namespace Magento\Customer\Test\Unit\Block\Widget; +use Magento\Customer\Api\CustomerMetadataInterface; +use Magento\Customer\Api\Data\AttributeMetadataInterface; +use Magento\Customer\Api\Data\ValidationRuleInterface; +use Magento\Customer\Helper\Address; +use Magento\Framework\App\CacheInterface; +use Magento\Framework\Cache\FrontendInterface; +use Magento\Framework\Data\Form\FilterFactory; +use Magento\Framework\Escaper; use Magento\Framework\Exception\NoSuchEntityException; use Magento\Customer\Block\Widget\Dob; use Magento\Framework\Locale\Resolver; +use Magento\Framework\Locale\ResolverInterface; +use Magento\Framework\Stdlib\DateTime\Timezone; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\Framework\View\Element\Html\Date; +use Magento\Framework\View\Element\Template\Context; +use PHPUnit\Framework\TestCase; +use PHPUnit_Framework_MockObject_MockObject; +use Zend_Cache_Backend_BlackHole; +use Zend_Cache_Core; /** * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class DobTest extends \PHPUnit\Framework\TestCase +class DobTest extends TestCase { /** Constants used in the unit tests */ const MIN_DATE = '01/01/2010'; @@ -43,82 +60,105 @@ class DobTest extends \PHPUnit\Framework\TestCase const YEAR_HTML = 'yy'; - /** @var \PHPUnit_Framework_MockObject_MockObject|\Magento\Customer\Api\Data\AttributeMetadataInterface */ + /** @var PHPUnit_Framework_MockObject_MockObject|AttributeMetadataInterface */ protected $attribute; /** @var Dob */ protected $_block; - /** @var \PHPUnit_Framework_MockObject_MockObject|\Magento\Customer\Api\CustomerMetadataInterface */ + /** @var PHPUnit_Framework_MockObject_MockObject|CustomerMetadataInterface */ protected $customerMetadata; /** - * @var \Magento\Framework\Data\Form\FilterFactory|\PHPUnit_Framework_MockObject_MockObject + * @var FilterFactory|PHPUnit_Framework_MockObject_MockObject */ protected $filterFactory; /** - * @var \Magento\Framework\Escaper + * @var Escaper */ private $escaper; /** - * @var \Magento\Framework\View\Element\Template\Context + * @var Context */ private $context; + /** + * @var string + */ + private $_locale; + /** + * @inheritDoc + */ protected function setUp() { - $zendCacheCore = new \Zend_Cache_Core(); - $zendCacheCore->setBackend(new \Zend_Cache_Backend_BlackHole()); + $zendCacheCore = new Zend_Cache_Core(); + $zendCacheCore->setBackend(new Zend_Cache_Backend_BlackHole()); $frontendCache = $this->getMockForAbstractClass( - \Magento\Framework\Cache\FrontendInterface::class, + FrontendInterface::class, [], '', false ); $frontendCache->expects($this->any())->method('getLowLevelFrontend')->will($this->returnValue($zendCacheCore)); - $cache = $this->createMock(\Magento\Framework\App\CacheInterface::class); + $cache = $this->createMock(CacheInterface::class); $cache->expects($this->any())->method('getFrontend')->will($this->returnValue($frontendCache)); - $objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); - $localeResolver = $this->createMock(\Magento\Framework\Locale\ResolverInterface::class); + $objectManager = new ObjectManager($this); + $localeResolver = $this->createMock(ResolverInterface::class); $localeResolver->expects($this->any()) ->method('getLocale') - ->willReturn(Resolver::DEFAULT_LOCALE); + ->willReturnCallback( + function () { + return $this->_locale; + } + ); $timezone = $objectManager->getObject( - \Magento\Framework\Stdlib\DateTime\Timezone::class, + Timezone::class, ['localeResolver' => $localeResolver] ); - $this->context = $this->createMock(\Magento\Framework\View\Element\Template\Context::class); + $this->_locale = Resolver::DEFAULT_LOCALE; + $this->context = $this->createMock(Context::class); $this->context->expects($this->any())->method('getLocaleDate')->will($this->returnValue($timezone)); - $this->escaper = $this->getMockBuilder(\Magento\Framework\Escaper::class) + $this->escaper = $this->getMockBuilder(Escaper::class) ->disableOriginalConstructor() ->setMethods(['escapeHtml']) ->getMock(); $this->context->expects($this->any())->method('getEscaper')->will($this->returnValue($this->escaper)); - $this->attribute = $this->getMockBuilder(\Magento\Customer\Api\Data\AttributeMetadataInterface::class) + $this->attribute = $this->getMockBuilder(AttributeMetadataInterface::class) ->getMockForAbstractClass(); - $this->customerMetadata = $this->getMockBuilder(\Magento\Customer\Api\CustomerMetadataInterface::class) + $this->attribute + ->expects($this->any()) + ->method('getInputFilter') + ->willReturn('date'); + $this->customerMetadata = $this->getMockBuilder(CustomerMetadataInterface::class) ->getMockForAbstractClass(); $this->customerMetadata->expects($this->any()) ->method('getAttributeMetadata') ->will($this->returnValue($this->attribute)); - date_default_timezone_set('America/Los_Angeles'); - - $this->filterFactory = $this->getMockBuilder(\Magento\Framework\Data\Form\FilterFactory::class) - ->disableOriginalConstructor() - ->getMock(); + $this->filterFactory = $this->createMock(FilterFactory::class); + $this->filterFactory + ->expects($this->any()) + ->method('create') + ->willReturnCallback( + function () use ($timezone, $localeResolver) { + return new \Magento\Framework\Data\Form\Filter\Date( + $timezone->getDateFormatWithLongYear(), + $localeResolver + ); + } + ); - $this->_block = new \Magento\Customer\Block\Widget\Dob( + $this->_block = new Dob( $this->context, - $this->createMock(\Magento\Customer\Helper\Address::class), + $this->createMock(Address::class), $this->customerMetadata, - $this->createMock(\Magento\Framework\View\Element\Html\Date::class), + $this->createMock(Date::class), $this->filterFactory ); } @@ -143,17 +183,22 @@ public function isEnabledDataProvider() return [[true, true], [false, false]]; } + /** + * Tests isEnabled() + */ public function testIsEnabledWithException() { $this->customerMetadata->expects($this->any()) ->method('getAttributeMetadata') ->will( - $this->throwException(new NoSuchEntityException( - __( - 'No such entity with %fieldName = %fieldValue', - ['fieldName' => 'field', 'fieldValue' => 'value'] + $this->throwException( + new NoSuchEntityException( + __( + 'No such entity with %fieldName = %fieldValue', + ['fieldName' => 'field', 'fieldValue' => 'value'] + ) ) - )) + ) ); $this->assertSame(false, $this->_block->isEnabled()); } @@ -175,12 +220,14 @@ public function testIsRequiredWithException() $this->customerMetadata->expects($this->any()) ->method('getAttributeMetadata') ->will( - $this->throwException(new NoSuchEntityException( - __( - 'No such entity with %fieldName = %fieldValue', - ['fieldName' => 'field', 'fieldValue' => 'value'] + $this->throwException( + new NoSuchEntityException( + __( + 'No such entity with %fieldName = %fieldValue', + ['fieldName' => 'field', 'fieldValue' => 'value'] + ) ) - )) + ) ); $this->assertSame(false, $this->_block->isRequired()); } @@ -197,14 +244,15 @@ public function isRequiredDataProvider() * @param string|bool $date Date (e.g. '01/01/2020' or false for no date) * @param int|bool $expectedTime The value we expect from Dob::getTime() * @param string|bool $expectedDate The value we expect from Dob::getData('date') - * + * @param string $locale * @dataProvider setDateDataProvider */ - public function testSetDate($date, $expectedTime, $expectedDate) + public function testSetDate($date, $expectedTime, $expectedDate, $locale = Resolver::DEFAULT_LOCALE) { + $this->_locale = $locale; $this->assertSame($this->_block, $this->_block->setDate($date)); - $this->assertEquals($expectedTime, $this->_block->getTime()); - $this->assertEquals($expectedDate, $this->_block->getValue()); + $this->assertSame($expectedTime, $this->_block->getTime()); + $this->assertSame($expectedDate, $this->_block->getValue()); } /** @@ -212,32 +260,19 @@ public function testSetDate($date, $expectedTime, $expectedDate) */ public function setDateDataProvider() { - return [[self::DATE, strtotime(self::DATE), self::DATE], [false, false, false]]; - } - - public function testSetDateWithFilter() - { - $date = '2014-01-01'; - $filterCode = 'date'; - - $this->attribute->expects($this->once()) - ->method('getInputFilter') - ->willReturn($filterCode); - - $filterMock = $this->getMockBuilder(\Magento\Framework\Data\Form\Filter\Date::class) - ->disableOriginalConstructor() - ->getMock(); - $filterMock->expects($this->once()) - ->method('outputFilter') - ->with($date) - ->willReturn(self::DATE); - - $this->filterFactory->expects($this->once()) - ->method('create') - ->with($filterCode, ['format' => self::DATE_FORMAT]) - ->willReturn($filterMock); - - $this->_block->setDate($date); + return [ + [false, false, false], + ['', false, ''], + ['12/31/1999', strtotime('1999-12-31'), '12/31/1999', 'en_US'], + ['31-12-1999', strtotime('1999-12-31'), '12/31/1999', 'en_US'], + ['1999-12-31', strtotime('1999-12-31'), '12/31/1999', 'en_US'], + ['31 December 1999', strtotime('1999-12-31'), '12/31/1999', 'en_US'], + ['12/31/1999', strtotime('1999-12-31'), '31/12/1999', 'fr_FR'], + ['31-12-1999', strtotime('1999-12-31'), '31/12/1999', 'fr_FR'], + ['31/12/1999', strtotime('1999-12-31'), '31/12/1999', 'fr_FR'], + ['1999-12-31', strtotime('1999-12-31'), '31/12/1999', 'fr_FR'], + ['31 Décembre 1999', strtotime('1999-12-31'), '31/12/1999', 'fr_FR'], + ]; } /** @@ -301,7 +336,6 @@ public function getYearDataProvider() } /** - * The \Magento\Framework\Locale\ResolverInterface::DEFAULT_LOCALE * is used to derive the Locale that is used to determine the * value of Dob::getDateFormat() for that Locale. */ @@ -358,12 +392,12 @@ public function testGetMinDateRange($validationRules, $expectedValue) */ public function getMinDateRangeDataProvider() { - $emptyValidationRule = $this->getMockBuilder(\Magento\Customer\Api\Data\ValidationRuleInterface::class) + $emptyValidationRule = $this->getMockBuilder(ValidationRuleInterface::class) ->disableOriginalConstructor() ->setMethods(['getName', 'getValue']) ->getMockForAbstractClass(); - $validationRule = $this->getMockBuilder(\Magento\Customer\Api\Data\ValidationRuleInterface::class) + $validationRule = $this->getMockBuilder(ValidationRuleInterface::class) ->disableOriginalConstructor() ->setMethods(['getName', 'getValue']) ->getMockForAbstractClass(); @@ -390,17 +424,22 @@ public function getMinDateRangeDataProvider() ]; } + /** + * Tests getMinDateRange() + */ public function testGetMinDateRangeWithException() { $this->customerMetadata->expects($this->any()) ->method('getAttributeMetadata') ->will( - $this->throwException(new NoSuchEntityException( - __( - 'No such entity with %fieldName = %fieldValue', - ['fieldName' => 'field', 'fieldValue' => 'value'] + $this->throwException( + new NoSuchEntityException( + __( + 'No such entity with %fieldName = %fieldValue', + ['fieldName' => 'field', 'fieldValue' => 'value'] + ) ) - )) + ) ); $this->assertNull($this->_block->getMinDateRange()); } @@ -424,12 +463,12 @@ public function testGetMaxDateRange($validationRules, $expectedValue) */ public function getMaxDateRangeDataProvider() { - $emptyValidationRule = $this->getMockBuilder(\Magento\Customer\Api\Data\ValidationRuleInterface::class) + $emptyValidationRule = $this->getMockBuilder(ValidationRuleInterface::class) ->disableOriginalConstructor() ->setMethods(['getName', 'getValue']) ->getMockForAbstractClass(); - $validationRule = $this->getMockBuilder(\Magento\Customer\Api\Data\ValidationRuleInterface::class) + $validationRule = $this->getMockBuilder(ValidationRuleInterface::class) ->disableOriginalConstructor() ->setMethods(['getName', 'getValue']) ->getMockForAbstractClass(); @@ -455,21 +494,29 @@ public function getMaxDateRangeDataProvider() ]; } + /** + * Tests getMaxDateRange() + */ public function testGetMaxDateRangeWithException() { $this->customerMetadata->expects($this->any()) ->method('getAttributeMetadata') ->will( - $this->throwException(new NoSuchEntityException( - __( - 'No such entity with %fieldName = %fieldValue', - ['fieldName' => 'field', 'fieldValue' => 'value'] + $this->throwException( + new NoSuchEntityException( + __( + 'No such entity with %fieldName = %fieldValue', + ['fieldName' => 'field', 'fieldValue' => 'value'] + ) ) - )) + ) ); $this->assertNull($this->_block->getMaxDateRange()); } + /** + * Tests getHtmlExtraParams() without required options + */ public function testGetHtmlExtraParamsWithoutRequiredOption() { $this->escaper->expects($this->any()) @@ -487,6 +534,9 @@ public function testGetHtmlExtraParamsWithoutRequiredOption() ); } + /** + * Tests getHtmlExtraParams() with required options + */ public function testGetHtmlExtraParamsWithRequiredOption() { $this->attribute->expects($this->once()) diff --git a/app/code/Magento/Customer/Test/Unit/Controller/Account/ConfirmTest.php b/app/code/Magento/Customer/Test/Unit/Controller/Account/ConfirmTest.php index 01fc465d4ae84..28f897adf9176 100644 --- a/app/code/Magento/Customer/Test/Unit/Controller/Account/ConfirmTest.php +++ b/app/code/Magento/Customer/Test/Unit/Controller/Account/ConfirmTest.php @@ -203,10 +203,9 @@ public function testNoCustomerIdInRequest($customerId, $key) ->with($this->equalTo('key'), false) ->will($this->returnValue($key)); - $exception = new \Exception('Bad request.'); $this->messageManagerMock->expects($this->once()) - ->method('addException') - ->with($this->equalTo($exception), $this->equalTo('There was an error confirming the account')); + ->method('addErrorMessage') + ->with(__('Bad request.')); $testUrl = 'http://example.com'; $this->urlMock->expects($this->once()) @@ -255,10 +254,12 @@ public function testSuccessMessage($customerId, $key, $vatValidationEnabled, $ad $this->requestMock->expects($this->any()) ->method('getParam') - ->willReturnMap([ - ['id', false, $customerId], - ['key', false, $key], - ]); + ->willReturnMap( + [ + ['id', false, $customerId], + ['key', false, $key], + ] + ); $this->customerRepositoryMock->expects($this->any()) ->method('getById') @@ -372,11 +373,13 @@ public function testSuccessRedirect( $this->requestMock->expects($this->any()) ->method('getParam') - ->willReturnMap([ - ['id', false, $customerId], - ['key', false, $key], - ['back_url', false, $backUrl], - ]); + ->willReturnMap( + [ + ['id', false, $customerId], + ['key', false, $key], + ['back_url', false, $backUrl], + ] + ); $this->customerRepositoryMock->expects($this->any()) ->method('getById') diff --git a/app/code/Magento/Customer/Test/Unit/Controller/Adminhtml/Index/MassAssignGroupTest.php b/app/code/Magento/Customer/Test/Unit/Controller/Adminhtml/Index/MassAssignGroupTest.php index 10144bdc318c1..cb5ff88ab704a 100644 --- a/app/code/Magento/Customer/Test/Unit/Controller/Adminhtml/Index/MassAssignGroupTest.php +++ b/app/code/Magento/Customer/Test/Unit/Controller/Adminhtml/Index/MassAssignGroupTest.php @@ -170,7 +170,7 @@ public function testExecute() ->willReturnMap([[10, $customerMock], [11, $customerMock], [12, $customerMock]]); $this->messageManagerMock->expects($this->once()) - ->method('addSuccess') + ->method('addSuccessMessage') ->with(__('A total of %1 record(s) were updated.', count($customersIds))); $this->resultRedirectMock->expects($this->any()) diff --git a/app/code/Magento/Customer/Test/Unit/Controller/Adminhtml/Index/MassDeleteTest.php b/app/code/Magento/Customer/Test/Unit/Controller/Adminhtml/Index/MassDeleteTest.php index 190ff2c06618f..1f39e6306b996 100644 --- a/app/code/Magento/Customer/Test/Unit/Controller/Adminhtml/Index/MassDeleteTest.php +++ b/app/code/Magento/Customer/Test/Unit/Controller/Adminhtml/Index/MassDeleteTest.php @@ -155,7 +155,7 @@ public function testExecute() ->willReturnMap([[10, true], [11, true], [12, true]]); $this->messageManagerMock->expects($this->once()) - ->method('addSuccess') + ->method('addSuccessMessage') ->with(__('A total of %1 record(s) were deleted.', count($customersIds))); $this->resultRedirectMock->expects($this->any()) diff --git a/app/code/Magento/Customer/Test/Unit/Controller/Adminhtml/Index/MassSubscribeTest.php b/app/code/Magento/Customer/Test/Unit/Controller/Adminhtml/Index/MassSubscribeTest.php index daf9c64fe7b7b..90bff0b61bcbf 100644 --- a/app/code/Magento/Customer/Test/Unit/Controller/Adminhtml/Index/MassSubscribeTest.php +++ b/app/code/Magento/Customer/Test/Unit/Controller/Adminhtml/Index/MassSubscribeTest.php @@ -171,7 +171,7 @@ public function testExecute() ->willReturnMap([[10, true], [11, true], [12, true]]); $this->messageManagerMock->expects($this->once()) - ->method('addSuccess') + ->method('addSuccessMessage') ->with(__('A total of %1 record(s) were updated.', count($customersIds))); $this->resultRedirectMock->expects($this->any()) diff --git a/app/code/Magento/Customer/Test/Unit/Controller/Adminhtml/Index/MassUnsubscribeTest.php b/app/code/Magento/Customer/Test/Unit/Controller/Adminhtml/Index/MassUnsubscribeTest.php index 05624661a2de4..1bffa836f5034 100644 --- a/app/code/Magento/Customer/Test/Unit/Controller/Adminhtml/Index/MassUnsubscribeTest.php +++ b/app/code/Magento/Customer/Test/Unit/Controller/Adminhtml/Index/MassUnsubscribeTest.php @@ -171,7 +171,7 @@ public function testExecute() ->willReturnMap([[10, true], [11, true], [12, true]]); $this->messageManagerMock->expects($this->once()) - ->method('addSuccess') + ->method('addSuccessMessage') ->with(__('A total of %1 record(s) were updated.', count($customersIds))); $this->resultRedirectMock->expects($this->any()) diff --git a/app/code/Magento/Customer/Test/Unit/Controller/Adminhtml/Index/ResetPasswordTest.php b/app/code/Magento/Customer/Test/Unit/Controller/Adminhtml/Index/ResetPasswordTest.php index 66e5b57eaa424..67ac60e6b9057 100644 --- a/app/code/Magento/Customer/Test/Unit/Controller/Adminhtml/Index/ResetPasswordTest.php +++ b/app/code/Magento/Customer/Test/Unit/Controller/Adminhtml/Index/ResetPasswordTest.php @@ -141,7 +141,7 @@ protected function setUp() $this->messageManager = $this->getMockBuilder( \Magento\Framework\Message\Manager::class )->disableOriginalConstructor()->setMethods( - ['addSuccess', 'addMessage', 'addException', 'addErrorMessage'] + ['addSuccessMessage', 'addMessage', 'addExceptionMessage', 'addErrorMessage'] )->getMock(); $this->resultRedirectFactoryMock = $this->getMockBuilder( @@ -442,7 +442,7 @@ public function testResetPasswordActionException() $this->messageManager->expects( $this->once() )->method( - 'addException' + 'addExceptionMessage' )->with( $this->equalTo($exception), $this->equalTo('Something went wrong while resetting customer password.') @@ -502,7 +502,7 @@ public function testResetPasswordActionSendEmail() $this->messageManager->expects( $this->once() )->method( - 'addSuccess' + 'addSuccessMessage' )->with( $this->equalTo('The customer will receive an email with a link to reset password.') ); diff --git a/app/code/Magento/Customer/Test/Unit/Controller/Adminhtml/Index/SaveTest.php b/app/code/Magento/Customer/Test/Unit/Controller/Adminhtml/Index/SaveTest.php index 57f384d32d980..9724ac13dde8c 100644 --- a/app/code/Magento/Customer/Test/Unit/Controller/Adminhtml/Index/SaveTest.php +++ b/app/code/Magento/Customer/Test/Unit/Controller/Adminhtml/Index/SaveTest.php @@ -338,10 +338,12 @@ public function testExecuteWithExistentCustomer() $this->requestMock->expects($this->atLeastOnce()) ->method('getPostValue') - ->willReturnMap([ - [null, null, $postValue], - [CustomerMetadataInterface::ENTITY_TYPE_CUSTOMER, null, $postValue['customer']], - ]); + ->willReturnMap( + [ + [null, null, $postValue], + [CustomerMetadataInterface::ENTITY_TYPE_CUSTOMER, null, $postValue['customer']], + ] + ); $this->requestMock->expects($this->atLeastOnce()) ->method('getPost') ->willReturnMap( @@ -475,7 +477,7 @@ public function testExecuteWithExistentCustomer() ->with(RegistryConstants::CURRENT_CUSTOMER_ID, $customerId); $this->messageManagerMock->expects($this->once()) - ->method('addSuccess') + ->method('addSuccessMessage') ->with(__('You saved the customer.')) ->willReturnSelf(); @@ -542,10 +544,12 @@ public function testExecuteWithNewCustomer() $this->requestMock->expects($this->any()) ->method('getPostValue') - ->willReturnMap([ - [null, null, $postValue], - [CustomerMetadataInterface::ENTITY_TYPE_CUSTOMER, null, $postValue['customer']], - ]); + ->willReturnMap( + [ + [null, null, $postValue], + [CustomerMetadataInterface::ENTITY_TYPE_CUSTOMER, null, $postValue['customer']], + ] + ); $this->requestMock->expects($this->atLeastOnce()) ->method('getPost') ->willReturnMap( @@ -662,7 +666,7 @@ public function testExecuteWithNewCustomer() ->with(RegistryConstants::CURRENT_CUSTOMER_ID, $customerId); $this->messageManagerMock->expects($this->once()) - ->method('addSuccess') + ->method('addSuccessMessage') ->with(__('You saved the customer.')) ->willReturnSelf(); @@ -723,10 +727,12 @@ public function testExecuteWithNewCustomerAndValidationException() $this->requestMock->expects($this->any()) ->method('getPostValue') - ->willReturnMap([ - [null, null, $postValue], - [CustomerMetadataInterface::ENTITY_TYPE_CUSTOMER, null, $postValue['customer']], - ]); + ->willReturnMap( + [ + [null, null, $postValue], + [CustomerMetadataInterface::ENTITY_TYPE_CUSTOMER, null, $postValue['customer']], + ] + ); $this->requestMock->expects($this->atLeastOnce()) ->method('getPost') ->willReturnMap( @@ -804,7 +810,7 @@ public function testExecuteWithNewCustomerAndValidationException() ->method('register'); $this->messageManagerMock->expects($this->never()) - ->method('addSuccess'); + ->method('addSuccessMessage'); $this->messageManagerMock->expects($this->once()) ->method('addMessage') @@ -812,10 +818,12 @@ public function testExecuteWithNewCustomerAndValidationException() $this->sessionMock->expects($this->once()) ->method('setCustomerFormData') - ->with([ - 'customer' => $extractedData, - 'subscription' => $subscription, - ]); + ->with( + [ + 'customer' => $extractedData, + 'subscription' => $subscription, + ] + ); /** @var Redirect|\PHPUnit_Framework_MockObject_MockObject $redirectMock */ $redirectMock = $this->getMockBuilder(\Magento\Framework\Controller\Result\Redirect::class) @@ -870,10 +878,12 @@ public function testExecuteWithNewCustomerAndLocalizedException() $this->requestMock->expects($this->any()) ->method('getPostValue') - ->willReturnMap([ - [null, null, $postValue], - [CustomerMetadataInterface::ENTITY_TYPE_CUSTOMER, null, $postValue['customer']], - ]); + ->willReturnMap( + [ + [null, null, $postValue], + [CustomerMetadataInterface::ENTITY_TYPE_CUSTOMER, null, $postValue['customer']], + ] + ); $this->requestMock->expects($this->atLeastOnce()) ->method('getPost') ->willReturnMap( @@ -951,7 +961,7 @@ public function testExecuteWithNewCustomerAndLocalizedException() ->method('register'); $this->messageManagerMock->expects($this->never()) - ->method('addSuccess'); + ->method('addSuccessMessage'); $this->messageManagerMock->expects($this->once()) ->method('addMessage') @@ -959,10 +969,12 @@ public function testExecuteWithNewCustomerAndLocalizedException() $this->sessionMock->expects($this->once()) ->method('setCustomerFormData') - ->with([ - 'customer' => $extractedData, - 'subscription' => $subscription, - ]); + ->with( + [ + 'customer' => $extractedData, + 'subscription' => $subscription, + ] + ); /** @var Redirect|\PHPUnit_Framework_MockObject_MockObject $redirectMock */ $redirectMock = $this->getMockBuilder(\Magento\Framework\Controller\Result\Redirect::class) @@ -1017,10 +1029,12 @@ public function testExecuteWithNewCustomerAndException() $this->requestMock->expects($this->any()) ->method('getPostValue') - ->willReturnMap([ - [null, null, $postValue], - [CustomerMetadataInterface::ENTITY_TYPE_CUSTOMER, null, $postValue['customer']], - ]); + ->willReturnMap( + [ + [null, null, $postValue], + [CustomerMetadataInterface::ENTITY_TYPE_CUSTOMER, null, $postValue['customer']], + ] + ); $this->requestMock->expects($this->atLeastOnce()) ->method('getPost') ->willReturnMap( @@ -1099,18 +1113,20 @@ public function testExecuteWithNewCustomerAndException() ->method('register'); $this->messageManagerMock->expects($this->never()) - ->method('addSuccess'); + ->method('addSuccessMessage'); $this->messageManagerMock->expects($this->once()) - ->method('addException') + ->method('addExceptionMessage') ->with($exception, __('Something went wrong while saving the customer.')); $this->sessionMock->expects($this->once()) ->method('setCustomerFormData') - ->with([ - 'customer' => $extractedData, - 'subscription' => $subscription, - ]); + ->with( + [ + 'customer' => $extractedData, + 'subscription' => $subscription, + ] + ); /** @var Redirect|\PHPUnit_Framework_MockObject_MockObject $redirectMock */ $redirectMock = $this->getMockBuilder(\Magento\Framework\Controller\Result\Redirect::class) diff --git a/app/code/Magento/Customer/Test/Unit/Controller/Adminhtml/Locks/UnlockTest.php b/app/code/Magento/Customer/Test/Unit/Controller/Adminhtml/Locks/UnlockTest.php index c92d4ed7812ba..55b4092af7141 100644 --- a/app/code/Magento/Customer/Test/Unit/Controller/Adminhtml/Locks/UnlockTest.php +++ b/app/code/Magento/Customer/Test/Unit/Controller/Adminhtml/Locks/UnlockTest.php @@ -118,7 +118,7 @@ public function testExecute() ->with($this->equalTo('customer_id')) ->will($this->returnValue($customerId)); $this->authenticationMock->expects($this->once())->method('unlock')->with($customerId); - $this->messageManagerMock->expects($this->once())->method('addSuccess'); + $this->messageManagerMock->expects($this->once())->method('addSuccessMessage'); $this->redirectMock->expects($this->once()) ->method('setPath') ->with($this->equalTo('customer/index/edit')) @@ -141,7 +141,7 @@ public function testExecuteWithException() ->method('unlock') ->with($customerId) ->willThrowException(new \Exception($phrase)); - $this->messageManagerMock->expects($this->once())->method('addError'); + $this->messageManagerMock->expects($this->once())->method('addErrorMessage'); $this->controller->execute(); } } diff --git a/app/code/Magento/Customer/Test/Unit/Model/CustomerExtractorTest.php b/app/code/Magento/Customer/Test/Unit/Model/CustomerExtractorTest.php index 351c3435c73fc..6fd5c76da81c0 100644 --- a/app/code/Magento/Customer/Test/Unit/Model/CustomerExtractorTest.php +++ b/app/code/Magento/Customer/Test/Unit/Model/CustomerExtractorTest.php @@ -3,10 +3,14 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Customer\Test\Unit\Model; use Magento\Customer\Model\CustomerExtractor; +/** + * Unit test CustomerExtractorTest + */ class CustomerExtractorTest extends \PHPUnit\Framework\TestCase { /** @var CustomerExtractor */ @@ -137,19 +141,9 @@ public function testExtract() $this->storeManager->expects($this->once()) ->method('getStore') ->willReturn($this->store); - $this->store->expects($this->exactly(2)) - ->method('getId') - ->willReturn(1); - $this->customerGroupManagement->expects($this->once()) - ->method('getDefaultGroup') - ->with(1) - ->willReturn($this->customerGroup); - $this->customerGroup->expects($this->once()) + $this->store->expects($this->once()) ->method('getId') ->willReturn(1); - $this->customerData->expects($this->once()) - ->method('setGroupId') - ->with(1); $this->store->expects($this->once()) ->method('getWebsiteId') ->willReturn(1); diff --git a/app/code/Magento/Customer/Test/Unit/Model/SessionTest.php b/app/code/Magento/Customer/Test/Unit/Model/SessionTest.php index 7efc61af800d3..8565790990df1 100644 --- a/app/code/Magento/Customer/Test/Unit/Model/SessionTest.php +++ b/app/code/Magento/Customer/Test/Unit/Model/SessionTest.php @@ -66,7 +66,7 @@ protected function setUp() $this->urlFactoryMock = $this->createMock(\Magento\Framework\UrlFactory::class); $this->customerFactoryMock = $this->getMockBuilder(\Magento\Customer\Model\CustomerFactory::class) ->disableOriginalConstructor() - ->setMethods(['create']) + ->setMethods(['create', 'save']) ->getMock(); $this->customerRepositoryMock = $this->createMock(\Magento\Customer\Api\CustomerRepositoryInterface::class); $helper = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); @@ -192,15 +192,12 @@ protected function prepareLoginDataMock($customerId) $customerMock = $this->createPartialMock( \Magento\Customer\Model\Customer::class, - ['getId', 'isConfirmationRequired', 'getConfirmation', 'updateData', 'getGroupId'] + ['getId', 'getConfirmation', 'updateData', 'getGroupId'] ); - $customerMock->expects($this->once()) + $customerMock->expects($this->exactly(3)) ->method('getId') ->will($this->returnValue($customerId)); $customerMock->expects($this->once()) - ->method('isConfirmationRequired') - ->will($this->returnValue(true)); - $customerMock->expects($this->never()) ->method('getConfirmation') ->will($this->returnValue($customerId)); diff --git a/app/code/Magento/Customer/Test/Unit/Observer/UpgradeQuoteCustomerEmailObserverTest.php b/app/code/Magento/Customer/Test/Unit/Observer/UpgradeQuoteCustomerEmailObserverTest.php new file mode 100644 index 0000000000000..f41c0ed9f0fb4 --- /dev/null +++ b/app/code/Magento/Customer/Test/Unit/Observer/UpgradeQuoteCustomerEmailObserverTest.php @@ -0,0 +1,107 @@ +observerMock = $this->getMockBuilder(\Magento\Framework\Event\Observer::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->eventMock = $this->getMockBuilder(\Magento\Framework\Event::class) + ->disableOriginalConstructor() + ->setMethods(['getCustomerDataObject', 'getOrigCustomerDataObject']) + ->getMock(); + + $this->observerMock->expects($this->any())->method('getEvent')->will($this->returnValue($this->eventMock)); + + $this->quoteRepositoryMock = $this + ->getMockBuilder(\Magento\Quote\Api\CartRepositoryInterface::class) + ->getMockForAbstractClass(); + $this->model = new UpgradeQuoteCustomerEmailObserver($this->quoteRepositoryMock); + } + + /** + * Unit test for verifying quote customers email upgrade observer + */ + public function testUpgradeQuoteCustomerEmail() + { + $email = "test@test.com"; + $origEmail = "origtest@test.com"; + + $customer = $this->getMockBuilder(\Magento\Customer\Api\Data\CustomerInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $customerOrig = $this->getMockBuilder(\Magento\Customer\Api\Data\CustomerInterface::class) + ->disableOriginalConstructor() + ->getMock(); + + $quoteMock = $this->getMockBuilder(\Magento\Quote\Model\Quote::class) + ->setMethods(['setCustomerEmail']) + ->disableOriginalConstructor() + ->getMock(); + + $this->eventMock->expects($this->any()) + ->method('getCustomerDataObject') + ->will($this->returnValue($customer)); + $this->eventMock->expects($this->any()) + ->method('getOrigCustomerDataObject') + ->will($this->returnValue($customerOrig)); + + $customerOrig->expects($this->any()) + ->method('getEmail') + ->willReturn($this->returnValue($origEmail)); + + $customer->expects($this->any()) + ->method('getEmail') + ->willReturn($this->returnValue($email)); + + $this->quoteRepositoryMock->expects($this->once()) + ->method('getForCustomer') + ->willReturn($quoteMock); + + $quoteMock->expects($this->once()) + ->method('setCustomerEmail'); + + $this->quoteRepositoryMock->expects($this->once()) + ->method('save') + ->with($quoteMock); + + $this->model->execute($this->observerMock); + } +} diff --git a/app/code/Magento/Customer/etc/events.xml b/app/code/Magento/Customer/etc/events.xml index d841d8faa9c38..2a724498a0359 100644 --- a/app/code/Magento/Customer/etc/events.xml +++ b/app/code/Magento/Customer/etc/events.xml @@ -15,4 +15,7 @@ + + + diff --git a/app/code/Magento/Customer/view/adminhtml/ui_component/customer_address_form.xml b/app/code/Magento/Customer/view/adminhtml/ui_component/customer_address_form.xml index 692cb2ecb964d..3af0172b3fca8 100644 --- a/app/code/Magento/Customer/view/adminhtml/ui_component/customer_address_form.xml +++ b/app/code/Magento/Customer/view/adminhtml/ui_component/customer_address_form.xml @@ -191,13 +191,6 @@ text - - - - - - - diff --git a/app/code/Magento/Customer/view/frontend/templates/js/customer-data.phtml b/app/code/Magento/Customer/view/frontend/templates/js/customer-data.phtml index 0373b7be4c71b..a1a784076bac3 100644 --- a/app/code/Magento/Customer/view/frontend/templates/js/customer-data.phtml +++ b/app/code/Magento/Customer/view/frontend/templates/js/customer-data.phtml @@ -7,14 +7,15 @@ /** @var \Magento\Customer\Block\CustomerData $block */ ?> diff --git a/app/code/Magento/Customer/view/frontend/templates/js/customer-data/invalidation-rules.phtml b/app/code/Magento/Customer/view/frontend/templates/js/customer-data/invalidation-rules.phtml index 15d0f52265770..4f8a917d1ed62 100644 --- a/app/code/Magento/Customer/view/frontend/templates/js/customer-data/invalidation-rules.phtml +++ b/app/code/Magento/Customer/view/frontend/templates/js/customer-data/invalidation-rules.phtml @@ -7,18 +7,19 @@ /* @var $block \Magento\Customer\Block\CustomerScopeData */ ?> diff --git a/app/code/Magento/Customer/view/frontend/templates/js/section-config.phtml b/app/code/Magento/Customer/view/frontend/templates/js/section-config.phtml index 94ab4757898cf..ebbd16164d7e8 100644 --- a/app/code/Magento/Customer/view/frontend/templates/js/section-config.phtml +++ b/app/code/Magento/Customer/view/frontend/templates/js/section-config.phtml @@ -7,15 +7,16 @@ /** @var \Magento\Customer\Block\SectionConfig $block */ ?> diff --git a/app/code/Magento/DownloadableGraphQl/Resolver/Customer/DownloadableProducts.php b/app/code/Magento/CustomerDownloadableGraphQl/Model/Resolver/CustomerDownloadableProducts.php similarity index 95% rename from app/code/Magento/DownloadableGraphQl/Resolver/Customer/DownloadableProducts.php rename to app/code/Magento/CustomerDownloadableGraphQl/Model/Resolver/CustomerDownloadableProducts.php index 0d99b64d7ba4d..da5c6cf794bf5 100644 --- a/app/code/Magento/DownloadableGraphQl/Resolver/Customer/DownloadableProducts.php +++ b/app/code/Magento/CustomerDownloadableGraphQl/Model/Resolver/CustomerDownloadableProducts.php @@ -5,7 +5,7 @@ */ declare(strict_types=1); -namespace Magento\DownloadableGraphQl\Resolver\Customer; +namespace Magento\CustomerDownloadableGraphQl\Model\Resolver; use Magento\DownloadableGraphQl\Model\ResourceModel\GetPurchasedDownloadableProducts; use Magento\Framework\GraphQl\Exception\GraphQlAuthorizationException; @@ -20,7 +20,7 @@ * * Returns available downloadable products for customer */ -class DownloadableProducts implements ResolverInterface +class CustomerDownloadableProducts implements ResolverInterface { /** * @var GetPurchasedDownloadableProducts diff --git a/app/code/Magento/CustomerDownloadableGraphQl/README.md b/app/code/Magento/CustomerDownloadableGraphQl/README.md new file mode 100644 index 0000000000000..044eeaf768a92 --- /dev/null +++ b/app/code/Magento/CustomerDownloadableGraphQl/README.md @@ -0,0 +1,4 @@ +# DownloadableGraphQl + +**CustomerDownloadableGraphQl** provides type and resolver information for the GraphQl module +to generate downloadable product information. diff --git a/app/code/Magento/CustomerDownloadableGraphQl/Test/Mftf/README.md b/app/code/Magento/CustomerDownloadableGraphQl/Test/Mftf/README.md new file mode 100644 index 0000000000000..51a682e004f58 --- /dev/null +++ b/app/code/Magento/CustomerDownloadableGraphQl/Test/Mftf/README.md @@ -0,0 +1,3 @@ +# Downloadable Graph Ql Functional Tests + +The Functional Test Module for **Magento Customer Downloadable Graph Ql** module. diff --git a/app/code/Magento/CustomerDownloadableGraphQl/composer.json b/app/code/Magento/CustomerDownloadableGraphQl/composer.json new file mode 100644 index 0000000000000..418d2b57b8b42 --- /dev/null +++ b/app/code/Magento/CustomerDownloadableGraphQl/composer.json @@ -0,0 +1,26 @@ +{ + "name": "magento/module-customer-downloadable-graph-ql", + "description": "N/A", + "type": "magento2-module", + "require": { + "php": "~7.1.3||~7.2.0||~7.3.0", + "magento/module-downloadable-graph-ql": "*", + "magento/module-graph-ql": "*", + "magento/framework": "*" + }, + "suggest": { + "magento/module-catalog-graph-ql": "*" + }, + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Magento\\CustomerDownloadableGraphQl\\": "" + } + } +} diff --git a/app/code/Magento/CustomerDownloadableGraphQl/etc/module.xml b/app/code/Magento/CustomerDownloadableGraphQl/etc/module.xml new file mode 100644 index 0000000000000..934bc81ae629a --- /dev/null +++ b/app/code/Magento/CustomerDownloadableGraphQl/etc/module.xml @@ -0,0 +1,15 @@ + + + + + + + + + + diff --git a/app/code/Magento/CustomerDownloadableGraphQl/etc/schema.graphqls b/app/code/Magento/CustomerDownloadableGraphQl/etc/schema.graphqls new file mode 100644 index 0000000000000..5ff0dc664ff74 --- /dev/null +++ b/app/code/Magento/CustomerDownloadableGraphQl/etc/schema.graphqls @@ -0,0 +1,18 @@ +# Copyright © Magento, Inc. All rights reserved. +# See COPYING.txt for license details. + +type Query { + customerDownloadableProducts: CustomerDownloadableProducts @resolver(class: "Magento\\CustomerDownloadableGraphQl\\Model\\Resolver\\CustomerDownloadableProducts") @doc(description: "The query returns the contents of a customer's downloadable products") @cache(cacheable: false) +} + +type CustomerDownloadableProducts { + items: [CustomerDownloadableProduct] @doc(description: "List of purchased downloadable items") +} + +type CustomerDownloadableProduct { + order_increment_id: String + date: String + status: String + download_url: String + remaining_downloads: String +} diff --git a/app/code/Magento/CustomerDownloadableGraphQl/registration.php b/app/code/Magento/CustomerDownloadableGraphQl/registration.php new file mode 100644 index 0000000000000..c029446bb5651 --- /dev/null +++ b/app/code/Magento/CustomerDownloadableGraphQl/registration.php @@ -0,0 +1,9 @@ +dataObjectHelper = $dataObjectHelper; $this->customerFactory = $customerFactory; $this->accountManagement = $accountManagement; $this->changeSubscriptionStatus = $changeSubscriptionStatus; + $this->validateCustomerData = $validateCustomerData; + $this->dataObjectProcessor = $dataObjectProcessor; } /** @@ -91,6 +110,15 @@ public function execute(array $data, StoreInterface $store): CustomerInterface private function createAccount(array $data, StoreInterface $store): CustomerInterface { $customerDataObject = $this->customerFactory->create(); + /** + * Add required attributes for customer entity + */ + $requiredDataAttributes = $this->dataObjectProcessor->buildOutputDataArray( + $customerDataObject, + CustomerInterface::class + ); + $data = array_merge($requiredDataAttributes, $data); + $this->validateCustomerData->execute($data); $this->dataObjectHelper->populateWithArray( $customerDataObject, $data, diff --git a/app/code/Magento/CustomerGraphQl/Model/Customer/GetAllowedCustomerAttributes.php b/app/code/Magento/CustomerGraphQl/Model/Customer/GetAllowedCustomerAttributes.php new file mode 100644 index 0000000000000..aada79aa016b3 --- /dev/null +++ b/app/code/Magento/CustomerGraphQl/Model/Customer/GetAllowedCustomerAttributes.php @@ -0,0 +1,98 @@ +attributeRepository = $attributeRepository; + $this->customerDataFactory = $customerDataFactory; + $this->dataObjectProcessor = $dataObjectProcessor; + $this->searchCriteriaBuilder = $searchCriteriaBuilder; + } + + /** + * Get allowed customer attributes + * + * @param array $attributeKeys + * + * @throws GraphQlInputException + * + * @return AbstractAttribute[] + */ + public function execute($attributeKeys): array + { + $this->searchCriteriaBuilder->addFilter('attribute_code', $attributeKeys, 'in'); + $searchCriteria = $this->searchCriteriaBuilder->create(); + try { + $attributesSearchResult = $this->attributeRepository->getList( + CustomerMetadataManagementInterface::ENTITY_TYPE_CUSTOMER, + $searchCriteria + ); + } catch (InputException $exception) { + throw new GraphQlInputException(__($exception->getMessage())); + } + + /** @var AbstractAttribute[] $attributes */ + $attributes = $attributesSearchResult->getItems(); + + foreach ($attributes as $index => $attribute) { + if (false === $attribute->getIsVisibleOnFront()) { + unset($attributes[$index]); + } + } + + return $attributes; + } +} diff --git a/app/code/Magento/CustomerGraphQl/Model/Customer/UpdateCustomerAccount.php b/app/code/Magento/CustomerGraphQl/Model/Customer/UpdateCustomerAccount.php index 51d47eaf0d048..f7bf26513dc2e 100644 --- a/app/code/Magento/CustomerGraphQl/Model/Customer/UpdateCustomerAccount.php +++ b/app/code/Magento/CustomerGraphQl/Model/Customer/UpdateCustomerAccount.php @@ -7,10 +7,12 @@ namespace Magento\CustomerGraphQl\Model\Customer; +use Magento\Customer\Api\Data\CustomerInterface; +use Magento\Framework\Exception\NoSuchEntityException; use Magento\Framework\GraphQl\Exception\GraphQlAlreadyExistsException; use Magento\Framework\GraphQl\Exception\GraphQlAuthenticationException; use Magento\Framework\GraphQl\Exception\GraphQlInputException; -use Magento\Customer\Api\Data\CustomerInterface; +use Magento\Framework\GraphQl\Exception\GraphQlNoSuchEntityException; use Magento\Framework\Api\DataObjectHelper; use Magento\Store\Api\Data\StoreInterface; @@ -41,6 +43,11 @@ class UpdateCustomerAccount */ private $changeSubscriptionStatus; + /** + * @var ValidateCustomerData + */ + private $validateCustomerData; + /** * @var array */ @@ -51,6 +58,7 @@ class UpdateCustomerAccount * @param CheckCustomerPassword $checkCustomerPassword * @param DataObjectHelper $dataObjectHelper * @param ChangeSubscriptionStatus $changeSubscriptionStatus + * @param ValidateCustomerData $validateCustomerData * @param array $restrictedKeys */ public function __construct( @@ -58,6 +66,7 @@ public function __construct( CheckCustomerPassword $checkCustomerPassword, DataObjectHelper $dataObjectHelper, ChangeSubscriptionStatus $changeSubscriptionStatus, + ValidateCustomerData $validateCustomerData, array $restrictedKeys = [] ) { $this->saveCustomer = $saveCustomer; @@ -65,10 +74,11 @@ public function __construct( $this->dataObjectHelper = $dataObjectHelper; $this->restrictedKeys = $restrictedKeys; $this->changeSubscriptionStatus = $changeSubscriptionStatus; + $this->validateCustomerData = $validateCustomerData; } /** - * Update customer account data + * Update customer account * * @param CustomerInterface $customer * @param array $data @@ -77,7 +87,7 @@ public function __construct( * @throws GraphQlAlreadyExistsException * @throws GraphQlAuthenticationException * @throws GraphQlInputException - * @throws \Magento\Framework\GraphQl\Exception\GraphQlNoSuchEntityException + * @throws GraphQlNoSuchEntityException */ public function execute(CustomerInterface $customer, array $data, StoreInterface $store): void { @@ -89,11 +99,15 @@ public function execute(CustomerInterface $customer, array $data, StoreInterface $this->checkCustomerPassword->execute($data['password'], (int)$customer->getId()); $customer->setEmail($data['email']); } - + $this->validateCustomerData->execute($data); $filteredData = array_diff_key($data, array_flip($this->restrictedKeys)); $this->dataObjectHelper->populateWithArray($customer, $filteredData, CustomerInterface::class); - $customer->setStoreId($store->getId()); + try { + $customer->setStoreId($store->getId()); + } catch (NoSuchEntityException $exception) { + throw new GraphQlNoSuchEntityException(__($exception->getMessage()), $exception); + } $this->saveCustomer->execute($customer); diff --git a/app/code/Magento/CustomerGraphQl/Model/Customer/ValidateCustomerData.php b/app/code/Magento/CustomerGraphQl/Model/Customer/ValidateCustomerData.php new file mode 100644 index 0000000000000..794cb0048592d --- /dev/null +++ b/app/code/Magento/CustomerGraphQl/Model/Customer/ValidateCustomerData.php @@ -0,0 +1,63 @@ +getAllowedCustomerAttributes = $getAllowedCustomerAttributes; + } + + /** + * Validate customer data + * + * @param array $customerData + * + * @return void + * + * @throws GraphQlInputException + */ + public function execute(array $customerData): void + { + $attributes = $this->getAllowedCustomerAttributes->execute(array_keys($customerData)); + $errorInput = []; + + foreach ($attributes as $attributeInfo) { + if ($attributeInfo->getIsRequired() + && (!isset($customerData[$attributeInfo->getAttributeCode()]) + || $customerData[$attributeInfo->getAttributeCode()] == '') + ) { + $errorInput[] = $attributeInfo->getDefaultFrontendLabel(); + } + } + + if ($errorInput) { + throw new GraphQlInputException( + __('Required parameters are missing: %1', [implode(', ', $errorInput)]) + ); + } + } +} diff --git a/app/code/Magento/CustomerGraphQl/Model/Resolver/CreateCustomer.php b/app/code/Magento/CustomerGraphQl/Model/Resolver/CreateCustomer.php index 1f730f2a5c7e6..6d33dea35835f 100644 --- a/app/code/Magento/CustomerGraphQl/Model/Resolver/CreateCustomer.php +++ b/app/code/Magento/CustomerGraphQl/Model/Resolver/CreateCustomer.php @@ -13,6 +13,8 @@ use Magento\Framework\GraphQl\Exception\GraphQlInputException; use Magento\Framework\GraphQl\Query\ResolverInterface; use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\Newsletter\Model\Config; +use Magento\Store\Model\ScopeInterface; /** * Create customer account resolver @@ -30,13 +32,23 @@ class CreateCustomer implements ResolverInterface private $createCustomerAccount; /** + * @var Config + */ + private $newsLetterConfig; + + /** + * CreateCustomer constructor. + * * @param ExtractCustomerData $extractCustomerData * @param CreateCustomerAccount $createCustomerAccount + * @param Config $newsLetterConfig */ public function __construct( ExtractCustomerData $extractCustomerData, - CreateCustomerAccount $createCustomerAccount + CreateCustomerAccount $createCustomerAccount, + Config $newsLetterConfig ) { + $this->newsLetterConfig = $newsLetterConfig; $this->extractCustomerData = $extractCustomerData; $this->createCustomerAccount = $createCustomerAccount; } @@ -55,6 +67,10 @@ public function resolve( throw new GraphQlInputException(__('"input" value should be specified')); } + if (!$this->newsLetterConfig->isActive(ScopeInterface::SCOPE_STORE)) { + $args['input']['is_subscribed'] = false; + } + $customer = $this->createCustomerAccount->execute( $args['input'], $context->getExtensionAttributes()->getStore() diff --git a/app/code/Magento/CustomerImportExport/Model/Import/Address.php b/app/code/Magento/CustomerImportExport/Model/Import/Address.php index 7a1a09efaa7b6..1a8859d5bd7bf 100644 --- a/app/code/Magento/CustomerImportExport/Model/Import/Address.php +++ b/app/code/Magento/CustomerImportExport/Model/Import/Address.php @@ -165,6 +165,7 @@ class Address extends AbstractCustomer * Array of region parameters * * @var array + * @deprecated field not in use */ protected $_regionParameters; @@ -194,16 +195,19 @@ class Address extends AbstractCustomer /** * @var \Magento\Eav\Model\Config + * @deprecated field not-in use */ protected $_eavConfig; /** * @var \Magento\Customer\Model\AddressFactory + * @deprecated not utilized anymore */ protected $_addressFactory; /** * @var \Magento\Framework\Stdlib\DateTime + * @deprecated the property isn't used */ protected $dateTime; @@ -419,10 +423,7 @@ protected function _getCustomerEntity() protected function _getNextEntityId() { if (!$this->_nextEntityId) { - /** @var $addressResource \Magento\Customer\Model\ResourceModel\Address */ - $addressResource = $this->_addressFactory->create()->getResource(); - $addressTable = $addressResource->getEntityTable(); - $this->_nextEntityId = $this->_resourceHelper->getNextAutoincrement($addressTable); + $this->_nextEntityId = $this->_resourceHelper->getNextAutoincrement($this->_entityTable); } return $this->_nextEntityId++; } @@ -587,7 +588,6 @@ protected function _mergeEntityAttributes(array $newAttributes, array $attribute */ protected function _prepareDataForUpdate(array $rowData):array { - $multiSeparator = $this->getMultipleValueSeparator(); $email = strtolower($rowData[self::COLUMN_EMAIL]); $customerId = $this->_getCustomerId($email, $rowData[self::COLUMN_WEBSITE]); // entity table data @@ -621,27 +621,18 @@ protected function _prepareDataForUpdate(array $rowData):array if (array_key_exists($attributeAlias, $rowData)) { $attributeParams = $this->adjustAttributeDataForWebsite($attributeParams, $websiteId); + $value = $rowData[$attributeAlias]; + if (!strlen($rowData[$attributeAlias])) { - if ($newAddress) { - $value = null; - } else { + if (!$newAddress) { continue; } - } elseif ($newAddress && !strlen($rowData[$attributeAlias])) { - } elseif (in_array($attributeParams['type'], ['select', 'boolean'])) { - $value = $this->getSelectAttrIdByValue($attributeParams, mb_strtolower($rowData[$attributeAlias])); - } elseif ('datetime' == $attributeParams['type']) { - $value = (new \DateTime())->setTimestamp(strtotime($rowData[$attributeAlias])); - $value = $value->format(\Magento\Framework\Stdlib\DateTime::DATETIME_PHP_FORMAT); - } elseif ('multiselect' == $attributeParams['type']) { - $ids = []; - foreach (explode($multiSeparator, mb_strtolower($rowData[$attributeAlias])) as $subValue) { - $ids[] = $this->getSelectAttrIdByValue($attributeParams, $subValue); - } - $value = implode(',', $ids); - } else { - $value = $rowData[$attributeAlias]; + + $value = null; + } elseif (in_array($attributeParams['type'], ['select', 'boolean', 'datetime', 'multiselect'])) { + $value = $this->getValueByAttributeType($rowData[$attributeAlias], $attributeParams); } + if ($attributeParams['is_static']) { $entityRow[$attributeAlias] = $value; } else { @@ -651,22 +642,18 @@ protected function _prepareDataForUpdate(array $rowData):array } foreach (self::getDefaultAddressAttributeMapping() as $columnName => $attributeCode) { if (!empty($rowData[$columnName])) { - /** @var $attribute \Magento\Eav\Model\Entity\Attribute\AbstractAttribute */ $table = $this->_getCustomerEntity()->getResource()->getTable('customer_entity'); $defaults[$table][$customerId][$attributeCode] = $addressId; } } // let's try to find region ID $entityRow['region_id'] = null; - if (!empty($rowData[self::COLUMN_REGION])) { - $countryNormalized = strtolower($rowData[self::COLUMN_COUNTRY_ID]); - $regionNormalized = strtolower($rowData[self::COLUMN_REGION]); - - if (isset($this->_countryRegions[$countryNormalized][$regionNormalized])) { - $regionId = $this->_countryRegions[$countryNormalized][$regionNormalized]; - $entityRow[self::COLUMN_REGION] = $this->_regions[$regionId]; - $entityRow['region_id'] = $regionId; - } + + if (!empty($rowData[self::COLUMN_REGION]) + && $this->getCountryRegionId($rowData[self::COLUMN_COUNTRY_ID], $rowData[self::COLUMN_REGION]) !== false) { + $regionId = $this->getCountryRegionId($rowData[self::COLUMN_COUNTRY_ID], $rowData[self::COLUMN_REGION]); + $entityRow[self::COLUMN_REGION] = $this->_regions[$regionId]; + $entityRow['region_id'] = $regionId; } if ($newAddress) { $entityRowNew = $entityRow; @@ -684,6 +671,39 @@ protected function _prepareDataForUpdate(array $rowData):array ]; } + /** + * Process row data, based on attirbute type + * + * @param string $rowAttributeData + * @param array $attributeParams + * @return \DateTime|int|string + * @throws \Exception + */ + protected function getValueByAttributeType(string $rowAttributeData, array $attributeParams) + { + $multiSeparator = $this->getMultipleValueSeparator(); + $value = $rowAttributeData; + switch ($attributeParams['type']) { + case 'select': + case 'boolean': + $value = $this->getSelectAttrIdByValue($attributeParams, mb_strtolower($rowAttributeData)); + break; + case 'datetime': + $value = (new \DateTime())->setTimestamp(strtotime($rowAttributeData)); + $value = $value->format(\Magento\Framework\Stdlib\DateTime::DATETIME_PHP_FORMAT); + break; + case 'multiselect': + $ids = []; + foreach (explode($multiSeparator, mb_strtolower($rowAttributeData)) as $subValue) { + $ids[] = $this->getSelectAttrIdByValue($attributeParams, $subValue); + } + $value = implode(',', $ids); + break; + } + + return $value; + } + /** * Update and insert data in entity table * @@ -741,6 +761,7 @@ protected function _saveCustomerDefaults(array $defaults) { foreach ($defaults as $tableName => $data) { foreach ($data as $customerId => $defaultsData) { + // phpcs:ignore Magento2.Performance.ForeachArrayMerge $data = array_merge( ['entity_id' => $customerId], $defaultsData @@ -781,11 +802,13 @@ public function getEntityTypeCode() * * @static * @return array + * phpcs:disable Magento2.Functions.StaticFunction */ public static function getDefaultAddressAttributeMapping() { return self::$_defaultAddressAttributeMapping; } + // phpcs:enable /** * Check if address for import is empty (for customer composite mode) @@ -820,7 +843,6 @@ protected function _isOptionalAddressEmpty(array $rowData) * @param int $rowNumber * @return void * @SuppressWarnings(PHPMD.CyclomaticComplexity) - * @SuppressWarnings(PHPMD.NPathComplexity) */ protected function _validateRowForUpdate(array $rowData, $rowNumber) { @@ -833,38 +855,36 @@ protected function _validateRowForUpdate(array $rowData, $rowNumber) if ($customerId === false) { $this->addRowError(self::ERROR_CUSTOMER_NOT_FOUND, $rowNumber); + } elseif ($this->_checkRowDuplicate($customerId, $addressId)) { + $this->addRowError(self::ERROR_DUPLICATE_PK, $rowNumber); } else { - if ($this->_checkRowDuplicate($customerId, $addressId)) { - $this->addRowError(self::ERROR_DUPLICATE_PK, $rowNumber); - } else { - // check simple attributes - foreach ($this->_attributes as $attributeCode => $attributeParams) { - $websiteId = $this->_websiteCodeToId[$website]; - $attributeParams = $this->adjustAttributeDataForWebsite($attributeParams, $websiteId); + // check simple attributes + foreach ($this->_attributes as $attributeCode => $attributeParams) { + $websiteId = $this->_websiteCodeToId[$website]; + $attributeParams = $this->adjustAttributeDataForWebsite($attributeParams, $websiteId); - if (in_array($attributeCode, $this->_ignoredAttributes)) { - continue; - } - if (isset($rowData[$attributeCode]) && strlen($rowData[$attributeCode])) { - $this->isAttributeValid( - $attributeCode, - $attributeParams, - $rowData, - $rowNumber, - $multiSeparator - ); - } elseif ($attributeParams['is_required'] - && !$this->addressStorage->doesExist( - (string)$addressId, - (string)$customerId - ) - ) { - $this->addRowError(self::ERROR_VALUE_IS_REQUIRED, $rowNumber, $attributeCode); - } + if (in_array($attributeCode, $this->_ignoredAttributes)) { + continue; + } elseif (isset($rowData[$attributeCode]) && strlen($rowData[$attributeCode])) { + $this->isAttributeValid( + $attributeCode, + $attributeParams, + $rowData, + $rowNumber, + $multiSeparator + ); + } elseif ($attributeParams['is_required'] + && !$this->addressStorage->doesExist( + (string)$addressId, + (string)$customerId + ) + ) { + $this->addRowError(self::ERROR_VALUE_IS_REQUIRED, $rowNumber, $attributeCode); } + } + if (isset($rowData[self::COLUMN_COUNTRY_ID])) { if (isset($rowData[self::COLUMN_POSTCODE]) - && isset($rowData[self::COLUMN_COUNTRY_ID]) && !$this->postcodeValidator->isValid( $rowData[self::COLUMN_COUNTRY_ID], $rowData[self::COLUMN_POSTCODE] @@ -873,19 +893,14 @@ protected function _validateRowForUpdate(array $rowData, $rowNumber) $this->addRowError(self::ERROR_VALUE_IS_REQUIRED, $rowNumber, self::COLUMN_POSTCODE); } - if (isset($rowData[self::COLUMN_COUNTRY_ID]) && isset($rowData[self::COLUMN_REGION])) { - $countryRegions = isset( - $this->_countryRegions[strtolower($rowData[self::COLUMN_COUNTRY_ID])] - ) ? $this->_countryRegions[strtolower( - $rowData[self::COLUMN_COUNTRY_ID] - )] : []; - - if (!empty($rowData[self::COLUMN_REGION]) && !empty($countryRegions) && !isset( - $countryRegions[strtolower($rowData[self::COLUMN_REGION])] + if (isset($rowData[self::COLUMN_REGION]) + && !empty($rowData[self::COLUMN_REGION]) + && false === $this->getCountryRegionId( + $rowData[self::COLUMN_COUNTRY_ID], + $rowData[self::COLUMN_REGION] ) - ) { - $this->addRowError(self::ERROR_INVALID_REGION, $rowNumber, self::COLUMN_REGION); - } + ) { + $this->addRowError(self::ERROR_INVALID_REGION, $rowNumber, self::COLUMN_REGION); } } } @@ -909,15 +924,13 @@ protected function _validateRowForDelete(array $rowData, $rowNumber) $customerId = $this->_getCustomerId($email, $website); if ($customerId === false) { $this->addRowError(self::ERROR_CUSTOMER_NOT_FOUND, $rowNumber); - } else { - if (!strlen($addressId)) { - $this->addRowError(self::ERROR_ADDRESS_ID_IS_EMPTY, $rowNumber); - } elseif (!$this->addressStorage->doesExist( - (string)$addressId, - (string)$customerId - )) { - $this->addRowError(self::ERROR_ADDRESS_NOT_FOUND, $rowNumber); - } + } elseif (!strlen($addressId)) { + $this->addRowError(self::ERROR_ADDRESS_ID_IS_EMPTY, $rowNumber); + } elseif (!$this->addressStorage->doesExist( + (string)$addressId, + (string)$customerId + )) { + $this->addRowError(self::ERROR_ADDRESS_NOT_FOUND, $rowNumber); } } } @@ -931,19 +944,18 @@ protected function _validateRowForDelete(array $rowData, $rowNumber) */ protected function _checkRowDuplicate($customerId, $addressId) { - if ($this->addressStorage->doesExist( + $isAddressExists = $this->addressStorage->doesExist( (string)$addressId, (string)$customerId - )) { - if (!isset($this->_importedRowPks[$customerId][$addressId])) { - $this->_importedRowPks[$customerId][$addressId] = true; - return false; - } else { - return true; - } - } else { - return false; + ); + + $isPkRowSet = isset($this->_importedRowPks[$customerId][$addressId]); + + if ($isAddressExists && !$isPkRowSet) { + $this->_importedRowPks[$customerId][$addressId] = true; } + + return $isAddressExists && $isPkRowSet; } /** @@ -957,4 +969,24 @@ public function setCustomerAttributes($customerAttributes) $this->_customerAttributes = $customerAttributes; return $this; } + + /** + * Get RegionID from the initialized data + * + * @param string $countryId + * @param string $region + * @return bool|int + */ + private function getCountryRegionId(string $countryId, string $region) + { + $countryNormalized = strtolower($countryId); + $regionNormalized = strtolower($region); + + if (isset($this->_countryRegions[$countryNormalized]) + && isset($this->_countryRegions[$countryNormalized][$regionNormalized])) { + return $this->_countryRegions[$countryNormalized][$regionNormalized]; + } + + return false; + } } diff --git a/app/code/Magento/Deploy/Package/Package.php b/app/code/Magento/Deploy/Package/Package.php index ef50a7f47073d..2e924d41a1b83 100644 --- a/app/code/Magento/Deploy/Package/Package.php +++ b/app/code/Magento/Deploy/Package/Package.php @@ -150,6 +150,8 @@ public function __construct( } /** + * Get area. + * * @return string */ public function getArea() @@ -158,6 +160,8 @@ public function getArea() } /** + * Get parent. + * * @return Package */ public function getParent() @@ -166,6 +170,8 @@ public function getParent() } /** + * Get theme. + * * @return string */ public function getTheme() @@ -174,6 +180,8 @@ public function getTheme() } /** + * Get locale. + * * @return string */ public function getLocale() @@ -204,6 +212,8 @@ public function isVirtual() } /** + * Get param. + * * @param string $name * @return mixed|null */ @@ -213,6 +223,8 @@ public function getParam($name) } /** + * Set param. + * * @param string $name * @param mixed $value * @return bool @@ -320,7 +332,10 @@ public function getFilesByType($type) */ public function deleteFile($fileId) { + $file = $this->files[$fileId]; + $deployedFileId = $file->getDeployedFileId(); unset($this->files[$fileId]); + unset($this->map[$deployedFileId]); } /** @@ -348,6 +363,8 @@ public function aggregate(Package $parentPackage = null) } /** + * Set parent. + * * @param Package $parent * @return bool */ @@ -368,6 +385,8 @@ public function getMap() } /** + * Get state. + * * @return int */ public function getState() @@ -376,6 +395,8 @@ public function getState() } /** + * Set state. + * * @param int $state * @return bool */ @@ -386,6 +407,8 @@ public function setState($state) } /** + * Get inheritance level. + * * @return int */ public function getInheritanceLevel() @@ -422,6 +445,7 @@ public function getParentMap() { $map = []; foreach ($this->getParentPackages() as $parentPackage) { + // phpcs:ignore Magento2.Performance.ForeachArrayMerge.ForeachArrayMerge $map = array_merge($map, $parentPackage->getMap()); } return $map; @@ -438,8 +462,10 @@ public function getParentFiles($type = null) $files = []; foreach ($this->getParentPackages() as $parentPackage) { if ($type === null) { + // phpcs:ignore Magento2.Performance.ForeachArrayMerge.ForeachArrayMerge $files = array_merge($files, $parentPackage->getFiles()); } else { + // phpcs:ignore Magento2.Performance.ForeachArrayMerge.ForeachArrayMerge $files = array_merge($files, $parentPackage->getFilesByType($type)); } } @@ -477,6 +503,8 @@ public function getParentPackages() } /** + * Get pre processors. + * * @return Processor\ProcessorInterface[] */ public function getPreProcessors() @@ -485,6 +513,8 @@ public function getPreProcessors() } /** + * Get post processors. + * * @return Processor\ProcessorInterface[] */ public function getPostProcessors() diff --git a/app/code/Magento/Deploy/Service/Bundle.php b/app/code/Magento/Deploy/Service/Bundle.php index f16b93a185595..26e61624c219e 100644 --- a/app/code/Magento/Deploy/Service/Bundle.php +++ b/app/code/Magento/Deploy/Service/Bundle.php @@ -216,7 +216,7 @@ private function isExcluded($filePath, $area, $theme) $excludedFiles = $this->bundleConfig->getExcludedFiles($area, $theme); foreach ($excludedFiles as $excludedFileId) { $excludedFilePath = $this->prepareExcludePath($excludedFileId); - if ($excludedFilePath === $filePath) { + if ($excludedFilePath === $filePath || $excludedFilePath === str_replace('.min.js', '.js', $filePath)) { return true; } } diff --git a/app/code/Magento/Deploy/Service/DeployPackage.php b/app/code/Magento/Deploy/Service/DeployPackage.php index 52cb6c6075749..34a6b147a0551 100644 --- a/app/code/Magento/Deploy/Service/DeployPackage.php +++ b/app/code/Magento/Deploy/Service/DeployPackage.php @@ -134,9 +134,10 @@ public function deployEmulated(Package $package, array $options, $skipLogging = } catch (ContentProcessorException $exception) { $errorMessage = __('Compilation from source: ') . $file->getSourcePath() - . PHP_EOL . $exception->getMessage(); + . PHP_EOL . $exception->getMessage() . PHP_EOL; $this->errorsCount++; $this->logger->critical($errorMessage); + $package->deleteFile($file->getFileId()); } catch (\Exception $exception) { $this->logger->critical( 'Compilation from source ' . $file->getSourcePath() . ' failed' . PHP_EOL . (string)$exception diff --git a/app/code/Magento/Developer/Console/Command/GeneratePatchCommand.php b/app/code/Magento/Developer/Console/Command/GeneratePatchCommand.php index d8893222e8a0e..78531c7e6c22c 100644 --- a/app/code/Magento/Developer/Console/Command/GeneratePatchCommand.php +++ b/app/code/Magento/Developer/Console/Command/GeneratePatchCommand.php @@ -7,16 +7,18 @@ namespace Magento\Developer\Console\Command; -use Magento\Developer\Model\Di\Information; +use Magento\Framework\App\ObjectManager; use Magento\Framework\Component\ComponentRegistrar; use Magento\Framework\Console\Cli; +use Magento\Framework\Exception\FileSystemException; +use Magento\Framework\Filesystem\DirectoryList; +use Magento\Framework\Filesystem\Directory\ReadFactory; +use Magento\Framework\Filesystem\Directory\WriteFactory; use Symfony\Component\Console\Command\Command; -use Symfony\Component\Console\Exception\InvalidArgumentException; +use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; -use Symfony\Component\Console\Input\InputArgument; -use Symfony\Component\Console\Helper\Table; /** * Allows to generate setup patches @@ -37,20 +39,45 @@ class GeneratePatchCommand extends Command */ private $componentRegistrar; + /** + * @var DirectoryList + */ + private $directoryList; + + /** + * @var ReadFactory + */ + private $readFactory; + + /** + * @var WriteFactory + */ + private $writeFactory; + /** * GeneratePatchCommand constructor. + * * @param ComponentRegistrar $componentRegistrar + * @param DirectoryList|null $directoryList + * @param ReadFactory|null $readFactory + * @param WriteFactory|null $writeFactory */ - public function __construct(ComponentRegistrar $componentRegistrar) - { + public function __construct( + ComponentRegistrar $componentRegistrar, + DirectoryList $directoryList = null, + ReadFactory $readFactory = null, + WriteFactory $writeFactory = null + ) { $this->componentRegistrar = $componentRegistrar; + $this->directoryList = $directoryList ?: ObjectManager::getInstance()->get(DirectoryList::class); + $this->readFactory = $readFactory ?: ObjectManager::getInstance()->get(ReadFactory::class); + $this->writeFactory = $writeFactory ?: ObjectManager::getInstance()->get(WriteFactory::class); + parent::__construct(); } /** - * @inheritdoc - * - * @throws InvalidArgumentException + * Configures the current command. */ protected function configure() { @@ -89,24 +116,21 @@ protected function configure() } /** - * Patch template + * Execute command * - * @return string - */ - private function getPatchTemplate(): string - { - // phpcs:ignore Magento2.Functions.DiscouragedFunction - return file_get_contents(__DIR__ . '/patch_template.php.dist'); - } - - /** - * @inheritdoc - * @throws \InvalidArgumentException + * @param InputInterface $input + * @param OutputInterface $output + * @return int + * @throws FileSystemException */ protected function execute(InputInterface $input, OutputInterface $output): int { $moduleName = $input->getArgument(self::MODULE_NAME); $patchName = $input->getArgument(self::INPUT_KEY_PATCH_NAME); + $includeRevertMethod = false; + if ($input->getOption(self::INPUT_KEY_IS_REVERTABLE)) { + $includeRevertMethod = true; + } $type = $input->getOption(self::INPUT_KEY_PATCH_TYPE); $modulePath = $this->componentRegistrar->getPath(ComponentRegistrar::MODULE, $moduleName); $preparedModuleName = str_replace('_', '\\', $moduleName); @@ -115,18 +139,75 @@ protected function execute(InputInterface $input, OutputInterface $output): int $patchTemplateData = $this->getPatchTemplate(); $patchTemplateData = str_replace('%moduleName%', $preparedModuleName, $patchTemplateData); $patchTemplateData = str_replace('%patchType%', $preparedType, $patchTemplateData); - $patchTemplateData = str_replace('%patchInterface%', $patchInterface, $patchTemplateData); $patchTemplateData = str_replace('%class%', $patchName, $patchTemplateData); - $patchDir = $patchToFile = $modulePath . '/Setup/Patch/' . $preparedType; - // phpcs:ignore Magento2.Functions.DiscouragedFunction - if (!is_dir($patchDir)) { - // phpcs:ignore Magento2.Functions.DiscouragedFunction - mkdir($patchDir, 0777, true); + $tplUseSchemaPatchInt = '%SchemaPatchInterface%'; + $tplUseDataPatchInt = '%useDataPatchInterface%'; + $valUseSchemaPatchInt = 'use Magento\Framework\Setup\Patch\SchemaPatchInterface;' . "\n"; + $valUseDataPatchInt = 'use Magento\Framework\Setup\Patch\DataPatchInterface;' . "\n"; + if ($type === 'schema') { + $patchTemplateData = str_replace($tplUseSchemaPatchInt, $valUseSchemaPatchInt, $patchTemplateData); + $patchTemplateData = str_replace($tplUseDataPatchInt, '', $patchTemplateData); + } else { + $patchTemplateData = str_replace($tplUseDataPatchInt, $valUseDataPatchInt, $patchTemplateData); + $patchTemplateData = str_replace($tplUseSchemaPatchInt, '', $patchTemplateData); } - $patchToFile = $patchDir . '/' . $patchName . '.php'; - // phpcs:ignore Magento2.Functions.DiscouragedFunction - file_put_contents($patchToFile, $patchTemplateData); + + $tplUsePatchRevertInt = '%usePatchRevertableInterface%'; + $tplImplementsInt = '%implementsInterfaces%'; + $tplRevertFunction = '%revertFunction%'; + $valUsePatchRevertInt = 'use Magento\Framework\Setup\Patch\PatchRevertableInterface;' . "\n"; + + if ($includeRevertMethod) { + $valImplementsInt = <<getRevertMethodTemplate(), $patchTemplateData); + } else { + $patchTemplateData = str_replace($tplUsePatchRevertInt, '', $patchTemplateData); + $patchTemplateData = str_replace($tplImplementsInt, $patchInterface, $patchTemplateData); + $patchTemplateData = str_replace($tplRevertFunction, '', $patchTemplateData); + } + + $patchDir = $modulePath . '/Setup/Patch/' . $preparedType; + $patchFile = $patchName . '.php'; + + $fileWriter = $this->writeFactory->create($patchDir); + $fileWriter->writeFile($patchFile, $patchTemplateData); + + $outputPatchFile = str_replace($this->directoryList->getRoot() . '/', '', $patchDir . '/' . $patchFile); + $output->writeln(__('Patch %1 has been successfully generated.', $outputPatchFile)); + return Cli::RETURN_SUCCESS; } + + /** + * Returns patch template + * + * @return string + * @throws FileSystemException + */ + private function getPatchTemplate(): string + { + $read = $this->readFactory->create(__DIR__ . '/'); + $content = $read->readFile('patch_template.php.dist'); + return $content; + } + + /** + * Returns template of revert() function + * + * @return string + * @throws FileSystemException + */ + private function getRevertMethodTemplate(): string + { + $read = $this->readFactory->create(__DIR__ . '/'); + $content = $read->readFile('template_revert_function.php.dist'); + return $content; + } } diff --git a/app/code/Magento/Developer/Console/Command/patch_template.php.dist b/app/code/Magento/Developer/Console/Command/patch_template.php.dist index c1b07246e644f..f4fc25abcb29a 100644 --- a/app/code/Magento/Developer/Console/Command/patch_template.php.dist +++ b/app/code/Magento/Developer/Console/Command/patch_template.php.dist @@ -6,16 +6,12 @@ namespace %moduleName%\Setup\Patch\%patchType%; -use Magento\Framework\Setup\Patch\DataPatchInterface; -use Magento\Framework\Setup\Patch\SchemaPatchInterface; -use Magento\Framework\Setup\Patch\PatchRevertableInterface; use Magento\Framework\Setup\ModuleDataSetupInterface; - +%useDataPatchInterface%%usePatchRevertableInterface%%SchemaPatchInterface% /** * Patch is mechanism, that allows to do atomic upgrade data changes */ -class %class% implements - %patchInterface% +class %class% implements %implementsInterfaces% { /** * @var ModuleDataSetupInterface $moduleDataSetup @@ -38,7 +34,7 @@ class %class% implements public function apply() { } - +%revertFunction% /** * {@inheritdoc} */ diff --git a/app/code/Magento/Developer/Console/Command/template_revert_function.php.dist b/app/code/Magento/Developer/Console/Command/template_revert_function.php.dist new file mode 100644 index 0000000000000..0e5a3f20c419e --- /dev/null +++ b/app/code/Magento/Developer/Console/Command/template_revert_function.php.dist @@ -0,0 +1,7 @@ + + /** + * @inheritdoc + */ + public function revert() + { + } diff --git a/app/code/Magento/Dhl/etc/adminhtml/system.xml b/app/code/Magento/Dhl/etc/adminhtml/system.xml index 597f33b579282..ea3da2ca031a5 100644 --- a/app/code/Magento/Dhl/etc/adminhtml/system.xml +++ b/app/code/Magento/Dhl/etc/adminhtml/system.xml @@ -137,6 +137,7 @@ Sort Order + validate-number validate-zero-or-greater Debug diff --git a/app/code/Magento/Directory/Model/Currency/Import/CurrencyConverterApi.php b/app/code/Magento/Directory/Model/Currency/Import/CurrencyConverterApi.php index f52886a14264d..8b9e0b1be7df2 100644 --- a/app/code/Magento/Directory/Model/Currency/Import/CurrencyConverterApi.php +++ b/app/code/Magento/Directory/Model/Currency/Import/CurrencyConverterApi.php @@ -7,12 +7,17 @@ namespace Magento\Directory\Model\Currency\Import; +/** + * Currency rate import model (From http://free.currencyconverterapi.com/) + * + * Class \Magento\Directory\Model\Currency\Import\CurrencyConverterApi + */ class CurrencyConverterApi extends AbstractImport { /** * @var string */ - const CURRENCY_CONVERTER_URL = 'http://free.currencyconverterapi.com/api/v3/convert?q={{CURRENCY_FROM}}_{{CURRENCY_TO}}&compact=ultra'; //@codingStandardsIgnoreLine + const CURRENCY_CONVERTER_URL = 'http://free.currencyconverterapi.com/api/v3/convert?q={{CURRENCY_FROM}}_{{CURRENCY_TO}}&compact=ultra&apiKey={{API_KEY}}'; //@codingStandardsIgnoreLine /** * Http Client Factory @@ -46,7 +51,7 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc */ public function fetchRates() { @@ -74,23 +79,25 @@ public function fetchRates() */ private function convertBatch($data, $currencyFrom, $currenciesTo) { + $apiKey = $this->scopeConfig->getValue( + 'currency/currencyconverterapi/api_key', + \Magento\Store\Model\ScopeInterface::SCOPE_STORE + ); + if (!$apiKey) { + $this->_messages[] = __('No API Key was specified.'); + return $data; + } foreach ($currenciesTo as $to) { + //phpcs:ignore Magento2.Functions.DiscouragedFunction set_time_limit(0); try { $url = str_replace('{{CURRENCY_FROM}}', $currencyFrom, self::CURRENCY_CONVERTER_URL); $url = str_replace('{{CURRENCY_TO}}', $to, $url); - $response = $this->getServiceResponse($url); + $url = str_replace('{{API_KEY}}', $apiKey, $url); if ($currencyFrom == $to) { $data[$currencyFrom][$to] = $this->_numberFormat(1); } else { - if (empty($response)) { - $this->_messages[] = __('We can\'t retrieve a rate from %1 for %2.', $url, $to); - $data[$currencyFrom][$to] = null; - } else { - $data[$currencyFrom][$to] = $this->_numberFormat( - (double)$response[$currencyFrom . '_' . $to] - ); - } + $data[$currencyFrom][$to] = $this->getCurrencyRate($currencyFrom, $to, $url); } } finally { ini_restore('max_execution_time'); @@ -100,6 +107,36 @@ private function convertBatch($data, $currencyFrom, $currenciesTo) return $data; } + /** + * Get currency rate from api + * + * @param string $currencyFrom + * @param string $to + * @param string $url + * @return double + */ + private function getCurrencyRate($currencyFrom, $to, $url) + { + $rate = null; + $response = $this->getServiceResponse($url); + if (empty($response)) { + $this->_messages[] = __('We can\'t retrieve a rate from %1 for %2.', $url, $to); + $rate = null; + } else { + if (isset($response['error']) && $response['error']) { + if (!in_array($response['error'], $this->_messages)) { + $this->_messages[] = $response['error']; + } + $rate = null; + } else { + $rate = $this->_numberFormat( + (double)$response[$currencyFrom . '_' . $to] + ); + } + } + return $rate; + } + /** * Get Fixer.io service response * @@ -137,7 +174,7 @@ private function getServiceResponse($url, $retry = 0) } /** - * {@inheritdoc} + * @inheritdoc */ protected function _convert($currencyFrom, $currencyTo) { diff --git a/app/code/Magento/Directory/Model/Data/ExchangeRate.php b/app/code/Magento/Directory/Model/Data/ExchangeRate.php index 35aee75f10481..500244304b7b8 100644 --- a/app/code/Magento/Directory/Model/Data/ExchangeRate.php +++ b/app/code/Magento/Directory/Model/Data/ExchangeRate.php @@ -5,6 +5,9 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +declare(strict_types=1); + namespace Magento\Directory\Model\Data; /** @@ -17,6 +20,7 @@ class ExchangeRate extends \Magento\Framework\Api\AbstractExtensibleObject imple { const KEY_CURRENCY_TO = 'currency_to'; const KEY_RATE = 'rate'; + private const KEY_EXCHANGE_RATES = 'exchange_rates'; /** * @inheritDoc diff --git a/app/code/Magento/Directory/etc/adminhtml/system.xml b/app/code/Magento/Directory/etc/adminhtml/system.xml index ec5fa35b6a152..7d650b14b3d97 100644 --- a/app/code/Magento/Directory/etc/adminhtml/system.xml +++ b/app/code/Magento/Directory/etc/adminhtml/system.xml @@ -43,12 +43,19 @@ Connection Timeout in Seconds + validate-zero-or-greater validate-number Currency Converter API - + + API Key + currency/currencyconverterapi/api_key + Magento\Config\Model\Config\Backend\Encrypted + + Connection Timeout in Seconds + validate-zero-or-greater validate-number diff --git a/app/code/Magento/Directory/etc/config.xml b/app/code/Magento/Directory/etc/config.xml index 276d7088cc2ea..c18c4f29d5822 100644 --- a/app/code/Magento/Directory/etc/config.xml +++ b/app/code/Magento/Directory/etc/config.xml @@ -24,6 +24,7 @@ 100 + 0 diff --git a/app/code/Magento/Directory/i18n/en_US.csv b/app/code/Magento/Directory/i18n/en_US.csv index 3dcd2ceebf134..9bd059c752064 100644 --- a/app/code/Magento/Directory/i18n/en_US.csv +++ b/app/code/Magento/Directory/i18n/en_US.csv @@ -52,3 +52,4 @@ Service,Service "The """%1"" is not allowed as base currency for your subscription plan.","The """%1"" is not allowed as base currency for your subscription plan." "An invalid base currency has been entered.","An invalid base currency has been entered." "Currency rates can't be retrieved.","Currency rates can't be retrieved." +"No API Key was specified.","No API Key was specified." diff --git a/app/code/Magento/Downloadable/Test/Mftf/ActionGroup/StorefrontOpenDownloadableLinkActionGroup.xml b/app/code/Magento/Downloadable/Test/Mftf/ActionGroup/StorefrontOpenDownloadableLinkActionGroup.xml new file mode 100644 index 0000000000000..565439655138e --- /dev/null +++ b/app/code/Magento/Downloadable/Test/Mftf/ActionGroup/StorefrontOpenDownloadableLinkActionGroup.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + diff --git a/app/code/Magento/Downloadable/Test/Mftf/ActionGroup/StorefrontOpenDownloadableSampleActionGroup.xml b/app/code/Magento/Downloadable/Test/Mftf/ActionGroup/StorefrontOpenDownloadableSampleActionGroup.xml new file mode 100644 index 0000000000000..25ac45317fe42 --- /dev/null +++ b/app/code/Magento/Downloadable/Test/Mftf/ActionGroup/StorefrontOpenDownloadableSampleActionGroup.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + diff --git a/app/code/Magento/Downloadable/Test/Mftf/Data/LinkData.xml b/app/code/Magento/Downloadable/Test/Mftf/Data/LinkData.xml index 08f1c2349357d..eb3ad674a0fdf 100644 --- a/app/code/Magento/Downloadable/Test/Mftf/Data/LinkData.xml +++ b/app/code/Magento/Downloadable/Test/Mftf/Data/LinkData.xml @@ -63,6 +63,12 @@ URL https://static.magento.com/sites/all/themes/mag_redesign/images/magento-logo.svg + + downloadableSampleUrl + 1 + url + http://example.com + Api Downloadable Link 2.00 @@ -72,4 +78,4 @@ 0 https://static.magento.com/sites/all/themes/mag_redesign/images/magento-logo.svg - \ No newline at end of file + diff --git a/app/code/Magento/Downloadable/Test/Mftf/Metadata/downloadable_link_sample-meta.xml b/app/code/Magento/Downloadable/Test/Mftf/Metadata/downloadable_link_sample-meta.xml new file mode 100644 index 0000000000000..b26bbb7af5a35 --- /dev/null +++ b/app/code/Magento/Downloadable/Test/Mftf/Metadata/downloadable_link_sample-meta.xml @@ -0,0 +1,23 @@ + + + + + + application/json + + string + integer + string + string + sample_file_content + string + + boolean + + diff --git a/app/code/Magento/Downloadable/Test/Mftf/Page/StorefrontDownloadableLinkPage.xml b/app/code/Magento/Downloadable/Test/Mftf/Page/StorefrontDownloadableLinkPage.xml new file mode 100644 index 0000000000000..7ab6b211d7441 --- /dev/null +++ b/app/code/Magento/Downloadable/Test/Mftf/Page/StorefrontDownloadableLinkPage.xml @@ -0,0 +1,13 @@ + + + + + + + diff --git a/app/code/Magento/Downloadable/Test/Mftf/Page/StorefrontDownloadableSamplePage.xml b/app/code/Magento/Downloadable/Test/Mftf/Page/StorefrontDownloadableSamplePage.xml new file mode 100644 index 0000000000000..0d588faa777c0 --- /dev/null +++ b/app/code/Magento/Downloadable/Test/Mftf/Page/StorefrontDownloadableSamplePage.xml @@ -0,0 +1,13 @@ + + + + + + + diff --git a/app/code/Magento/Downloadable/Test/Mftf/Page/StorefrontProductPage.xml b/app/code/Magento/Downloadable/Test/Mftf/Page/StorefrontProductPage.xml new file mode 100644 index 0000000000000..7b9d205d19dc5 --- /dev/null +++ b/app/code/Magento/Downloadable/Test/Mftf/Page/StorefrontProductPage.xml @@ -0,0 +1,14 @@ + + + + + + + + diff --git a/app/code/Magento/Downloadable/Test/Mftf/Section/StorefrontDownloadableProductSection.xml b/app/code/Magento/Downloadable/Test/Mftf/Section/StorefrontDownloadableProductSection.xml index a1db2d4d94941..543aea7d8297f 100644 --- a/app/code/Magento/Downloadable/Test/Mftf/Section/StorefrontDownloadableProductSection.xml +++ b/app/code/Magento/Downloadable/Test/Mftf/Section/StorefrontDownloadableProductSection.xml @@ -12,5 +12,7 @@ + + diff --git a/app/code/Magento/Downloadable/Test/Mftf/Test/AdminAddDefaultImageDownloadableProductTest.xml b/app/code/Magento/Downloadable/Test/Mftf/Test/AdminAddDefaultImageDownloadableProductTest.xml index 3d779740849c5..64f33b01e668f 100644 --- a/app/code/Magento/Downloadable/Test/Mftf/Test/AdminAddDefaultImageDownloadableProductTest.xml +++ b/app/code/Magento/Downloadable/Test/Mftf/Test/AdminAddDefaultImageDownloadableProductTest.xml @@ -22,6 +22,9 @@ + + + diff --git a/app/code/Magento/Downloadable/Test/Mftf/Test/VerifyDisableDownloadableProductSamplesAreNotAccessibleTest.xml b/app/code/Magento/Downloadable/Test/Mftf/Test/VerifyDisableDownloadableProductSamplesAreNotAccessibleTest.xml new file mode 100644 index 0000000000000..f29bbcf925e26 --- /dev/null +++ b/app/code/Magento/Downloadable/Test/Mftf/Test/VerifyDisableDownloadableProductSamplesAreNotAccessibleTest.xml @@ -0,0 +1,114 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/code/Magento/DownloadableGraphQl/composer.json b/app/code/Magento/DownloadableGraphQl/composer.json index 947b4001a5da5..e2e1098031766 100644 --- a/app/code/Magento/DownloadableGraphQl/composer.json +++ b/app/code/Magento/DownloadableGraphQl/composer.json @@ -6,7 +6,6 @@ "php": "~7.1.3||~7.2.0||~7.3.0", "magento/module-catalog": "*", "magento/module-downloadable": "*", - "magento/module-graph-ql": "*", "magento/module-quote": "*", "magento/module-quote-graph-ql": "*", "magento/framework": "*" diff --git a/app/code/Magento/DownloadableGraphQl/etc/schema.graphqls b/app/code/Magento/DownloadableGraphQl/etc/schema.graphqls index 2b986694e2996..db452d1e5ace1 100644 --- a/app/code/Magento/DownloadableGraphQl/etc/schema.graphqls +++ b/app/code/Magento/DownloadableGraphQl/etc/schema.graphqls @@ -1,10 +1,6 @@ # Copyright © Magento, Inc. All rights reserved. # See COPYING.txt for license details. -type Query { - customerDownloadableProducts: CustomerDownloadableProducts @resolver(class: "Magento\\DownloadableGraphQl\\Resolver\\Customer\\DownloadableProducts") @doc(description: "The query returns the contents of a customer's downloadable products") @cache(cacheable: false) -} - type Mutation { addDownloadableProductsToCart(input: AddDownloadableProductsToCartInput): AddDownloadableProductsToCartOutput @resolver(class: "Magento\\QuoteGraphQl\\Model\\Resolver\\AddSimpleProductsToCart") } @@ -34,18 +30,6 @@ type DownloadableCartItem implements CartItemInterface @doc(description: "Downlo samples: [DownloadableProductSamples] @resolver(class: "Magento\\DownloadableGraphQl\\Resolver\\DownloadableCartItem\\Samples") @doc(description: "DownloadableProductSamples defines characteristics of a downloadable product") } -type CustomerDownloadableProducts { - items: [CustomerDownloadableProduct] @doc(description: "List of purchased downloadable items") -} - -type CustomerDownloadableProduct { - order_increment_id: String - date: String - status: String - download_url: String - remaining_downloads: String -} - type DownloadableProduct implements ProductInterface, CustomizableProductInterface @doc(description: "DownloadableProduct defines a product that the customer downloads") { downloadable_product_samples: [DownloadableProductSamples] @resolver(class: "Magento\\DownloadableGraphQl\\Resolver\\Product\\Samples") @doc(description: "An array containing information about samples of this downloadable product.") downloadable_product_links: [DownloadableProductLinks] @resolver(class: "Magento\\DownloadableGraphQl\\Resolver\\Product\\Links") @doc(description: "An array containing information about the links for this downloadable product") @@ -54,8 +38,8 @@ type DownloadableProduct implements ProductInterface, CustomizableProductInterfa } enum DownloadableFileTypeEnum @deprecated(reason: "`sample_url` serves to get the downloadable sample") { - FILE - URL + FILE @deprecated(reason: "`sample_url` serves to get the downloadable sample") + URL @deprecated(reason: "`sample_url` serves to get the downloadable sample") } type DownloadableProductLinks @doc(description: "DownloadableProductLinks defines characteristics of a downloadable product") { diff --git a/app/code/Magento/Eav/Block/Adminhtml/Attribute/Edit/Options/Options.php b/app/code/Magento/Eav/Block/Adminhtml/Attribute/Edit/Options/Options.php index 72f4086c1c56b..7af7bf447c45a 100644 --- a/app/code/Magento/Eav/Block/Adminhtml/Attribute/Edit/Options/Options.php +++ b/app/code/Magento/Eav/Block/Adminhtml/Attribute/Edit/Options/Options.php @@ -4,16 +4,14 @@ * See COPYING.txt for license details. */ -/** - * Attribute add/edit form options tab - * - * @author Magento Core Team - */ namespace Magento\Eav\Block\Adminhtml\Attribute\Edit\Options; use Magento\Store\Model\ResourceModel\Store\Collection; +use Magento\Eav\Model\Entity\Attribute\AbstractAttribute; /** + * Attribute add/edit form options tab + * * @api * @since 100.0.2 */ @@ -61,6 +59,7 @@ public function __construct( /** * Is true only for system attributes which use source model + * * Option labels and position for such attributes are kept in source model and thus cannot be overridden * * @return bool @@ -96,12 +95,16 @@ public function getStoresSortedBySortOrder() { $stores = $this->getStores(); if (is_array($stores)) { - usort($stores, function ($storeA, $storeB) { - if ($storeA->getSortOrder() == $storeB->getSortOrder()) { - return $storeA->getId() < $storeB->getId() ? -1 : 1; + usort( + $stores, + function ($storeA, $storeB) { + if ($storeA->getSortOrder() == $storeB->getSortOrder()) { + return $storeA->getId() < $storeB->getId() ? -1 : 1; + } + + return ($storeA->getSortOrder() < $storeB->getSortOrder()) ? -1 : 1; } - return ($storeA->getSortOrder() < $storeB->getSortOrder()) ? -1 : 1; - }); + ); } return $stores; } @@ -130,12 +133,14 @@ public function getOptionValues() } /** - * @param \Magento\Eav\Model\Entity\Attribute\AbstractAttribute $attribute + * Preparing values of attribute options + * + * @param AbstractAttribute $attribute * @param array|\Magento\Eav\Model\ResourceModel\Entity\Attribute\Option\Collection $optionCollection * @return array */ protected function _prepareOptionValues( - \Magento\Eav\Model\Entity\Attribute\AbstractAttribute $attribute, + AbstractAttribute $attribute, $optionCollection ) { $type = $attribute->getFrontendInput(); @@ -149,6 +154,41 @@ protected function _prepareOptionValues( $values = []; $isSystemAttribute = is_array($optionCollection); + if ($isSystemAttribute) { + $values = $this->getPreparedValues($optionCollection, $isSystemAttribute, $inputType, $defaultValues); + } else { + $optionCollection->setPageSize(200); + $pageCount = $optionCollection->getLastPageNumber(); + $currentPage = 1; + while ($currentPage <= $pageCount) { + $optionCollection->clear(); + $optionCollection->setCurPage($currentPage); + $values = array_merge( + $values, + $this->getPreparedValues($optionCollection, $isSystemAttribute, $inputType, $defaultValues) + ); + $currentPage++; + } + } + + return $values; + } + + /** + * Return prepared values of system or user defined attribute options + * + * @param array|\Magento\Eav\Model\ResourceModel\Entity\Attribute\Option\Collection $optionCollection + * @param bool $isSystemAttribute + * @param string $inputType + * @param array $defaultValues + */ + private function getPreparedValues( + $optionCollection, + bool $isSystemAttribute, + string $inputType, + array $defaultValues + ) { + $values = []; foreach ($optionCollection as $option) { $bunch = $isSystemAttribute ? $this->_prepareSystemAttributeOptionValues( $option, @@ -169,12 +209,13 @@ protected function _prepareOptionValues( /** * Retrieve option values collection + * * It is represented by an array in case of system attribute * - * @param \Magento\Eav\Model\Entity\Attribute\AbstractAttribute $attribute + * @param AbstractAttribute $attribute * @return array|\Magento\Eav\Model\ResourceModel\Entity\Attribute\Option\Collection */ - protected function _getOptionValuesCollection(\Magento\Eav\Model\Entity\Attribute\AbstractAttribute $attribute) + protected function _getOptionValuesCollection(AbstractAttribute $attribute) { if ($this->canManageOptionDefaultOnly()) { $options = $this->_universalFactory->create( @@ -226,7 +267,7 @@ protected function _prepareSystemAttributeOptionValues($option, $inputType, $def foreach ($this->getStores() as $store) { $storeId = $store->getId(); $value['store' . $storeId] = $storeId == - \Magento\Store\Model\Store::DEFAULT_STORE_ID ? $valuePrefix . $this->escapeHtml($option['label']) : ''; + \Magento\Store\Model\Store::DEFAULT_STORE_ID ? $valuePrefix . $this->escapeHtml($option['label']) : ''; } return [$value]; diff --git a/app/code/Magento/Eav/Model/Attribute/Data/Text.php b/app/code/Magento/Eav/Model/Attribute/Data/Text.php index 548efb230fb0b..22cac884491ae 100644 --- a/app/code/Magento/Eav/Model/Attribute/Data/Text.php +++ b/app/code/Magento/Eav/Model/Attribute/Data/Text.php @@ -75,6 +75,8 @@ public function validateValue($value) if (empty($value) && $value !== '0' && $attribute->getDefaultValue() === null) { $label = __($attribute->getStoreLabel()); $errors[] = __('"%1" is a required value.', $label); + + return $errors; } $validateLengthResult = $this->validateLength($attribute, $value); diff --git a/app/code/Magento/Eav/Model/AttributeProvider.php b/app/code/Magento/Eav/Model/AttributeProvider.php index 7f9d0620b6096..419a33664e0eb 100644 --- a/app/code/Magento/Eav/Model/AttributeProvider.php +++ b/app/code/Magento/Eav/Model/AttributeProvider.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); namespace Magento\Eav\Model; @@ -52,7 +53,7 @@ public function __construct( * Returns array of fields * * @param string $entityType - * @return array + * @return string[] * @throws \Exception */ public function getAttributes($entityType) @@ -66,6 +67,7 @@ public function getAttributes($entityType) foreach ($searchResult->getItems() as $attribute) { $attributes[] = $attribute->getAttributeCode(); } + return $attributes; } } diff --git a/app/code/Magento/Eav/Model/Entity/Attribute/OptionManagement.php b/app/code/Magento/Eav/Model/Entity/Attribute/OptionManagement.php index 3c3bc083fdf8f..0ea4c324fe5c9 100644 --- a/app/code/Magento/Eav/Model/Entity/Attribute/OptionManagement.php +++ b/app/code/Magento/Eav/Model/Entity/Attribute/OptionManagement.php @@ -3,9 +3,11 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); namespace Magento\Eav\Model\Entity\Attribute; +use Magento\Eav\Api\Data\AttributeInterface as EavAttributeInterface; use Magento\Framework\Exception\InputException; use Magento\Framework\Exception\NoSuchEntityException; use Magento\Framework\Exception\StateException; @@ -39,7 +41,16 @@ public function __construct( } /** - * @inheritdoc + * Add option to attribute. + * + * @param int $entityType + * @param string $attributeCode + * @param \Magento\Eav\Api\Data\AttributeOptionInterface $option + * @return string + * @throws InputException + * @throws NoSuchEntityException + * @throws StateException + * @SuppressWarnings(PHPMD.CyclomaticComplexity) */ public function add($entityType, $attributeCode, $option) { @@ -64,6 +75,15 @@ public function add($entityType, $attributeCode, $option) } } + if (!$this->isAttributeOptionLabelExists($attribute, (string) $options['value'][$optionId][0])) { + throw new InputException( + __( + 'Admin store attribute option label "%1" is already exists.', + $options['value'][$optionId][0] + ) + ); + } + if ($option->getIsDefault()) { $attribute->setDefault([$optionId]); } @@ -134,10 +154,10 @@ public function getItems($entityType, $attributeCode) /** * Validate option * - * @param \Magento\Eav\Api\Data\AttributeInterface $attribute + * @param EavAttributeInterface $attribute * @param int $optionId - * @throws NoSuchEntityException * @return void + * @throws NoSuchEntityException */ protected function validateOption($attribute, $optionId) { @@ -167,13 +187,13 @@ private function getOptionId(\Magento\Eav\Api\Data\AttributeOptionInterface $opt * Set option value * * @param \Magento\Eav\Api\Data\AttributeOptionInterface $option - * @param \Magento\Eav\Api\Data\AttributeInterface $attribute + * @param EavAttributeInterface $attribute * @param string $optionLabel * @return void */ private function setOptionValue( \Magento\Eav\Api\Data\AttributeOptionInterface $option, - \Magento\Eav\Api\Data\AttributeInterface $attribute, + EavAttributeInterface $attribute, string $optionLabel ) { $optionId = $attribute->getSource()->getOptionId($optionLabel); @@ -188,4 +208,28 @@ private function setOptionValue( } } } + + /** + * Checks if the incoming attribute option label for admin store is already exists. + * + * @param EavAttributeInterface $attribute + * @param string $adminStoreLabel + * @param int $storeId + * @return bool + */ + private function isAttributeOptionLabelExists( + EavAttributeInterface $attribute, + string $adminStoreLabel, + int $storeId = 0 + ) :bool { + $attribute->setStoreId($storeId); + + foreach ($attribute->getSource()->toOptionArray() as $existingAttributeOption) { + if ($existingAttributeOption['label'] === $adminStoreLabel) { + return false; + } + } + + return true; + } } diff --git a/app/code/Magento/Eav/Model/ResourceModel/Entity/Attribute.php b/app/code/Magento/Eav/Model/ResourceModel/Entity/Attribute.php index 0e7a46125d872..d05a7e1e2baa4 100644 --- a/app/code/Magento/Eav/Model/ResourceModel/Entity/Attribute.php +++ b/app/code/Magento/Eav/Model/ResourceModel/Entity/Attribute.php @@ -457,6 +457,7 @@ protected function _updateAttributeOption($object, $optionId, $option) if (!empty($option['delete'][$optionId])) { if ($intOptionId) { $connection->delete($table, ['option_id = ?' => $intOptionId]); + $this->clearSelectedOptionInEntities($object, $intOptionId); } return false; } @@ -475,6 +476,41 @@ protected function _updateAttributeOption($object, $optionId, $option) return $intOptionId; } + /** + * Clear selected option in entities + * + * @param EntityAttribute|AbstractModel $object + * @param int $optionId + * @return void + */ + private function clearSelectedOptionInEntities(AbstractModel $object, int $optionId) + { + $backendTable = $object->getBackendTable(); + $attributeId = $object->getAttributeId(); + if (!$backendTable || !$attributeId) { + return; + } + + $connection = $this->getConnection(); + $where = $connection->quoteInto('attribute_id = ?', $attributeId); + $update = []; + + if ($object->getBackendType() === 'varchar') { + $where.= ' AND ' . $connection->prepareSqlCondition('value', ['finset' => $optionId]); + $concat = $connection->getConcatSql(["','", 'value', "','"]); + $expr = $connection->quoteInto( + "TRIM(BOTH ',' FROM REPLACE($concat,',?,',','))", + $optionId + ); + $update['value'] = new \Zend_Db_Expr($expr); + } else { + $where.= $connection->quoteInto(' AND value = ?', $optionId); + $update['value'] = null; + } + + $connection->update($backendTable, $update, $where); + } + /** * Save option values records per store * diff --git a/app/code/Magento/Eav/Model/ResourceModel/ReadHandler.php b/app/code/Magento/Eav/Model/ResourceModel/ReadHandler.php index 7f6dfa2a5e9ab..bf1405fa64122 100644 --- a/app/code/Magento/Eav/Model/ResourceModel/ReadHandler.php +++ b/app/code/Magento/Eav/Model/ResourceModel/ReadHandler.php @@ -3,20 +3,27 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Eav\Model\ResourceModel; +use Exception; use Magento\Eav\Model\Config; +use Magento\Eav\Model\Entity\Attribute\AbstractAttribute; use Magento\Framework\DataObject; use Magento\Framework\DB\Select; use Magento\Framework\DB\Sql\UnionExpression; use Magento\Framework\EntityManager\MetadataPool; use Magento\Framework\EntityManager\Operation\AttributeInterface; +use Magento\Framework\Exception\ConfigurationMismatchException; +use Magento\Framework\Exception\LocalizedException; use Magento\Framework\Model\Entity\ScopeInterface; use Magento\Framework\Model\Entity\ScopeResolver; use Psr\Log\LoggerInterface; /** * EAV read handler + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class ReadHandler implements AttributeInterface { @@ -63,7 +70,7 @@ public function __construct( * * @param string $entityType * @return \Magento\Eav\Api\Data\AttributeInterface[] - * @throws \Exception if for unknown entity type + * @throws Exception if for unknown entity type * @deprecated Not used anymore * @see ReadHandler::getEntityAttributes */ @@ -80,7 +87,7 @@ protected function getAttributes($entityType) * @param string $entityType * @param DataObject $entity * @return \Magento\Eav\Api\Data\AttributeInterface[] - * @throws \Exception if for unknown entity type + * @throws Exception if for unknown entity type */ private function getEntityAttributes(string $entityType, DataObject $entity): array { @@ -111,9 +118,9 @@ protected function getContextVariables(ScopeInterface $scope) * @param array $entityData * @param array $arguments * @return array - * @throws \Exception - * @throws \Magento\Framework\Exception\ConfigurationMismatchException - * @throws \Magento\Framework\Exception\LocalizedException + * @throws Exception + * @throws ConfigurationMismatchException + * @throws LocalizedException * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function execute($entityType, $entityData, $arguments = []) @@ -129,7 +136,7 @@ public function execute($entityType, $entityData, $arguments = []) $attributesMap = []; $selects = []; - /** @var \Magento\Eav\Model\Entity\Attribute\AbstractAttribute $attribute */ + /** @var AbstractAttribute $attribute */ foreach ($this->getEntityAttributes($entityType, new DataObject($entityData)) as $attribute) { if (!$attribute->isStatic()) { $attributeTables[$attribute->getBackend()->getTable()][] = $attribute->getAttributeId(); @@ -170,8 +177,11 @@ public function execute($entityType, $entityData, $arguments = []) $entityData[$attributesMap[$attributeValue['attribute_id']]] = $attributeValue['value']; } else { $this->logger->warning( - "Attempt to load value of nonexistent EAV attribute '{$attributeValue['attribute_id']}' - for entity type '$entityType'." + "Attempt to load value of nonexistent EAV attribute", + [ + 'attribute_id' => $attributeValue['attribute_id'], + 'entity_type' => $entityType + ] ); } } @@ -184,8 +194,9 @@ public function execute($entityType, $entityData, $arguments = []) * * @param Select[] $selects * @param array $identifiers + * @return void */ - private function applyIdentifierForSelects(array $selects, array $identifiers) + private function applyIdentifierForSelects(array $selects, array $identifiers): void { foreach ($selects as $select) { foreach ($identifiers as $identifier) { diff --git a/app/code/Magento/Eav/Setup/AddOptionToAttribute.php b/app/code/Magento/Eav/Setup/AddOptionToAttribute.php new file mode 100644 index 0000000000000..c6b13f8a6e3ec --- /dev/null +++ b/app/code/Magento/Eav/Setup/AddOptionToAttribute.php @@ -0,0 +1,210 @@ +setup = $setup; + } + + /** + * Add Attribute Option + * + * @param array $option + * + * @return void + * @throws LocalizedException + */ + public function execute(array $option): void + { + $optionTable = $this->setup->getTable('eav_attribute_option'); + $optionValueTable = $this->setup->getTable('eav_attribute_option_value'); + + if (isset($option['value'])) { + $this->addValue($option, $optionTable, $optionValueTable); + } elseif (isset($option['values'])) { + $this->addValues($option, $optionTable, $optionValueTable); + } + } + + /** + * Add option value + * + * @param array $option + * @param string $optionTable + * @param string $optionValueTable + * + * @return void + * @throws LocalizedException + */ + private function addValue(array $option, string $optionTable, string $optionValueTable): void + { + $value = $option['value']; + foreach ($value as $optionId => $values) { + $intOptionId = (int)$optionId; + if (!empty($option['delete'][$optionId])) { + if ($intOptionId) { + $condition = ['option_id =?' => $intOptionId]; + $this->setup->getConnection()->delete($optionTable, $condition); + } + continue; + } + + if (!$intOptionId) { + $data = [ + 'attribute_id' => $option['attribute_id'], + 'sort_order' => isset($option['order'][$optionId]) ? $option['order'][$optionId] : 0, + ]; + $this->setup->getConnection()->insert($optionTable, $data); + $intOptionId = $this->setup->getConnection()->lastInsertId($optionTable); + } else { + $data = [ + 'sort_order' => isset($option['order'][$optionId]) ? $option['order'][$optionId] : 0, + ]; + $this->setup->getConnection()->update( + $optionTable, + $data, + ['option_id=?' => $intOptionId] + ); + } + + // Default value + if (!isset($values[0])) { + throw new LocalizedException( + __("The default option isn't defined. Set the option and try again.") + ); + } + $condition = ['option_id =?' => $intOptionId]; + $this->setup->getConnection()->delete($optionValueTable, $condition); + foreach ($values as $storeId => $value) { + $data = ['option_id' => $intOptionId, 'store_id' => $storeId, 'value' => $value]; + $this->setup->getConnection()->insert($optionValueTable, $data); + } + } + } + + /** + * Add option values + * + * @param array $option + * @param string $optionTable + * @param string $optionValueTable + * + * @return void + */ + private function addValues(array $option, string $optionTable, string $optionValueTable): void + { + $values = $option['values']; + $attributeId = (int)$option['attribute_id']; + $existingOptions = $this->getExistingAttributeOptions($attributeId, $optionTable, $optionValueTable); + foreach ($values as $sortOrder => $value) { + // add option + $data = ['attribute_id' => $attributeId, 'sort_order' => $sortOrder]; + if (!$this->isExistingOptionValue($value, $existingOptions)) { + $this->setup->getConnection()->insert($optionTable, $data); + + //add option value + $intOptionId = $this->setup->getConnection()->lastInsertId($optionTable); + $data = ['option_id' => $intOptionId, 'store_id' => 0, 'value' => $value]; + $this->setup->getConnection()->insert($optionValueTable, $data); + } elseif ($optionId = $this->getExistingOptionIdWithDiffSortOrder( + $sortOrder, + $value, + $existingOptions + ) + ) { + $this->setup->getConnection()->update( + $optionTable, + ['sort_order' => $sortOrder], + ['option_id = ?' => $optionId] + ); + } + } + } + + /** + * Check if option value already exists + * + * @param string $value + * @param array $existingOptions + * + * @return bool + */ + private function isExistingOptionValue(string $value, array $existingOptions): bool + { + foreach ($existingOptions as $option) { + if ($option['value'] == $value) { + return true; + } + } + + return false; + } + + /** + * Get existing attribute options + * + * @param int $attributeId + * @param string $optionTable + * @param string $optionValueTable + * + * @return array + */ + private function getExistingAttributeOptions(int $attributeId, string $optionTable, string $optionValueTable): array + { + $select = $this->setup + ->getConnection() + ->select() + ->from(['o' => $optionTable]) + ->reset('columns') + ->columns(['option_id', 'sort_order']) + ->join(['ov' => $optionValueTable], 'o.option_id = ov.option_id', 'value') + ->where(AttributeInterface::ATTRIBUTE_ID . ' = ?', $attributeId) + ->where('store_id = 0'); + + return $this->setup->getConnection()->fetchAll($select); + } + + /** + * Check if option already exists, but sort_order differs + * + * @param int $sortOrder + * @param string $value + * @param array $existingOptions + * + * @return int|null + */ + private function getExistingOptionIdWithDiffSortOrder(int $sortOrder, string $value, array $existingOptions): ?int + { + foreach ($existingOptions as $option) { + if ($option['value'] == $value && $option['sort_order'] != $sortOrder) { + return (int)$option['option_id']; + } + } + + return null; + } +} diff --git a/app/code/Magento/Eav/Setup/EavSetup.php b/app/code/Magento/Eav/Setup/EavSetup.php index de285e81b1d03..d440a84fc8e65 100644 --- a/app/code/Magento/Eav/Setup/EavSetup.php +++ b/app/code/Magento/Eav/Setup/EavSetup.php @@ -82,6 +82,11 @@ class EavSetup */ private $_defaultAttributeSetName = 'Default'; + /** + * @var AddOptionToAttribute + */ + private $addAttributeOption; + /** * @var Code */ @@ -95,18 +100,23 @@ class EavSetup * @param CacheInterface $cache * @param CollectionFactory $attrGroupCollectionFactory * @param Code|null $attributeCodeValidator + * @param AddOptionToAttribute|null $addAttributeOption + * @SuppressWarnings(PHPMD.LongVariable) */ public function __construct( ModuleDataSetupInterface $setup, Context $context, CacheInterface $cache, CollectionFactory $attrGroupCollectionFactory, - Code $attributeCodeValidator = null + Code $attributeCodeValidator = null, + AddOptionToAttribute $addAttributeOption = null ) { $this->cache = $cache; $this->attrGroupCollectionFactory = $attrGroupCollectionFactory; $this->attributeMapper = $context->getAttributeMapper(); $this->setup = $setup; + $this->addAttributeOption = $addAttributeOption + ?? ObjectManager::getInstance()->get(AddOptionToAttribute::class); $this->attributeCodeValidator = $attributeCodeValidator ?: ObjectManager::getInstance()->get( Code::class ); @@ -567,6 +577,7 @@ public function addAttributeGroup($entityTypeId, $setId, $name, $sortOrder = nul if (empty($data['attribute_group_code'])) { if (empty($attributeGroupCode)) { // in the following code md5 is not used for security purposes + // phpcs:disable Magento2.Security.InsecureFunction $attributeGroupCode = md5($name); } $data['attribute_group_code'] = $attributeGroupCode; @@ -868,62 +879,7 @@ public function addAttribute($entityTypeId, $code, array $attr) */ public function addAttributeOption($option) { - $optionTable = $this->setup->getTable('eav_attribute_option'); - $optionValueTable = $this->setup->getTable('eav_attribute_option_value'); - - if (isset($option['value'])) { - foreach ($option['value'] as $optionId => $values) { - $intOptionId = (int)$optionId; - if (!empty($option['delete'][$optionId])) { - if ($intOptionId) { - $condition = ['option_id =?' => $intOptionId]; - $this->setup->getConnection()->delete($optionTable, $condition); - } - continue; - } - - if (!$intOptionId) { - $data = [ - 'attribute_id' => $option['attribute_id'], - 'sort_order' => isset($option['order'][$optionId]) ? $option['order'][$optionId] : 0, - ]; - $this->setup->getConnection()->insert($optionTable, $data); - $intOptionId = $this->setup->getConnection()->lastInsertId($optionTable); - } else { - $data = [ - 'sort_order' => isset($option['order'][$optionId]) ? $option['order'][$optionId] : 0, - ]; - $this->setup->getConnection()->update( - $optionTable, - $data, - ['option_id=?' => $intOptionId] - ); - } - - // Default value - if (!isset($values[0])) { - throw new \Magento\Framework\Exception\LocalizedException( - __("The default option isn't defined. Set the option and try again.") - ); - } - $condition = ['option_id =?' => $intOptionId]; - $this->setup->getConnection()->delete($optionValueTable, $condition); - foreach ($values as $storeId => $value) { - $data = ['option_id' => $intOptionId, 'store_id' => $storeId, 'value' => $value]; - $this->setup->getConnection()->insert($optionValueTable, $data); - } - } - } elseif (isset($option['values'])) { - foreach ($option['values'] as $sortOrder => $label) { - // add option - $data = ['attribute_id' => $option['attribute_id'], 'sort_order' => $sortOrder]; - $this->setup->getConnection()->insert($optionTable, $data); - $intOptionId = $this->setup->getConnection()->lastInsertId($optionTable); - - $data = ['option_id' => $intOptionId, 'store_id' => 0, 'value' => $label]; - $this->setup->getConnection()->insert($optionValueTable, $data); - } - } + $this->addAttributeOption->execute($option); } /** diff --git a/app/code/Magento/Eav/Test/Unit/Model/Entity/Attribute/OptionManagementTest.php b/app/code/Magento/Eav/Test/Unit/Model/Entity/Attribute/OptionManagementTest.php index b63a4dd2c9ae6..f23814e0de0c4 100644 --- a/app/code/Magento/Eav/Test/Unit/Model/Entity/Attribute/OptionManagementTest.php +++ b/app/code/Magento/Eav/Test/Unit/Model/Entity/Attribute/OptionManagementTest.php @@ -6,6 +6,12 @@ namespace Magento\Eav\Test\Unit\Model\Entity\Attribute; +use Magento\Eav\Api\Data\AttributeOptionInterface as EavAttributeOptionInterface; +use Magento\Eav\Api\Data\AttributeOptionLabelInterface as EavAttributeOptionLabelInterface; +use Magento\Eav\Model\Entity\Attribute\AbstractAttribute as EavAbstractAttribute; +use Magento\Eav\Model\Entity\Attribute\Source\Table as EavAttributeSource; +use PHPUnit\Framework\MockObject\MockObject as MockObject; + class OptionManagementTest extends \PHPUnit\Framework\TestCase { /** @@ -38,25 +44,9 @@ public function testAdd() { $entityType = 42; $attributeCode = 'atrCde'; - $optionMock = $this->getMockForAbstractClass( - \Magento\Eav\Api\Data\AttributeOptionInterface::class, - [], - '', - false, - false, - true, - ['getSourceLabels'] - ); - $attributeMock = $this->getMockForAbstractClass( - \Magento\Framework\Model\AbstractModel::class, - [], - '', - false, - false, - true, - ['usesSource', 'setDefault', 'setOption'] - ); - $labelMock = $this->createMock(\Magento\Eav\Api\Data\AttributeOptionLabelInterface::class); + $attributeMock = $this->getAttribute(); + $optionMock = $this->getAttributeOption(); + $labelMock = $this->getAttributeOptionLabel(); $option = ['value' => [ 'id_new_option' => [ @@ -92,15 +82,7 @@ public function testAddWithEmptyAttributeCode() { $entityType = 42; $attributeCode = ''; - $optionMock = $this->getMockForAbstractClass( - \Magento\Eav\Api\Data\AttributeOptionInterface::class, - [], - '', - false, - false, - true, - ['getSourceLabels'] - ); + $optionMock = $this->getAttributeOption(); $this->resourceModelMock->expects($this->never())->method('save'); $this->model->add($entityType, $attributeCode, $optionMock); } @@ -113,24 +95,8 @@ public function testAddWithWrongOptions() { $entityType = 42; $attributeCode = 'testAttribute'; - $optionMock = $this->getMockForAbstractClass( - \Magento\Eav\Api\Data\AttributeOptionInterface::class, - [], - '', - false, - false, - true, - ['getSourceLabels'] - ); - $attributeMock = $this->getMockForAbstractClass( - \Magento\Framework\Model\AbstractModel::class, - [], - '', - false, - false, - true, - ['usesSource', 'setDefault', 'setOption'] - ); + $attributeMock = $this->getAttribute(); + $optionMock = $this->getAttributeOption(); $this->attributeRepositoryMock->expects($this->once())->method('get')->with($entityType, $attributeCode) ->willReturn($attributeMock); $attributeMock->expects($this->once())->method('usesSource')->willReturn(false); @@ -146,25 +112,9 @@ public function testAddWithCannotSaveException() { $entityType = 42; $attributeCode = 'atrCde'; - $optionMock = $this->getMockForAbstractClass( - \Magento\Eav\Api\Data\AttributeOptionInterface::class, - [], - '', - false, - false, - true, - ['getSourceLabels'] - ); - $attributeMock = $this->getMockForAbstractClass( - \Magento\Framework\Model\AbstractModel::class, - [], - '', - false, - false, - true, - ['usesSource', 'setDefault', 'setOption'] - ); - $labelMock = $this->createMock(\Magento\Eav\Api\Data\AttributeOptionLabelInterface::class); + $optionMock = $this->getAttributeOption(); + $attributeMock = $this->getAttribute(); + $labelMock = $this->getAttributeOptionLabel(); $option = ['value' => [ 'id_new_option' => [ @@ -340,7 +290,7 @@ public function testGetItems() true, ['getOptions'] ); - $optionsMock = [$this->createMock(\Magento\Eav\Api\Data\AttributeOptionInterface::class)]; + $optionsMock = [$this->createMock(EavAttributeOptionInterface::class)]; $this->attributeRepositoryMock->expects($this->once())->method('get')->with($entityType, $attributeCode) ->willReturn($attributeMock); $attributeMock->expects($this->once())->method('getOptions')->willReturn($optionsMock); @@ -380,4 +330,55 @@ public function testGetItemsWithEmptyAttributeCode() $attributeCode = ''; $this->model->getItems($entityType, $attributeCode); } + + /** + * Returns attribute entity mock. + * + * @param array $attributeOptions attribute options for return + * @return MockObject|EavAbstractAttribute + */ + private function getAttribute(array $attributeOptions = []) + { + $attribute = $this->getMockBuilder(EavAbstractAttribute::class) + ->disableOriginalConstructor() + ->setMethods( + [ + 'usesSource', + 'setDefault', + 'setOption', + 'setStoreId', + 'getSource', + ] + ) + ->getMock(); + $source = $this->getMockBuilder(EavAttributeSource::class) + ->disableOriginalConstructor() + ->getMock(); + + $attribute->method('getSource')->willReturn($source); + $source->method('toOptionArray')->willReturn($attributeOptions); + + return $attribute; + } + + /** + * Return attribute option entity mock. + * + * @return MockObject|EavAttributeOptionInterface + */ + private function getAttributeOption() + { + return $this->getMockBuilder(EavAttributeOptionInterface::class) + ->setMethods(['getSourceLabels']) + ->getMockForAbstractClass(); + } + + /** + * @return MockObject|EavAttributeOptionLabelInterface + */ + private function getAttributeOptionLabel() + { + return $this->getMockBuilder(EavAttributeOptionLabelInterface::class) + ->getMockForAbstractClass(); + } } diff --git a/app/code/Magento/Eav/Test/Unit/Setup/AddAttributeOptionTest.php b/app/code/Magento/Eav/Test/Unit/Setup/AddAttributeOptionTest.php new file mode 100644 index 0000000000000..17376ceebbcb4 --- /dev/null +++ b/app/code/Magento/Eav/Test/Unit/Setup/AddAttributeOptionTest.php @@ -0,0 +1,200 @@ +createMock(ModuleDataSetupInterface::class); + $this->connectionMock = $this->createMock(Mysql::class); + $this->connectionMock->method('select') + ->willReturn($objectManager->getObject(Select::class)); + + $setupMock->method('getTable')->willReturn('some_table'); + $setupMock->method('getConnection')->willReturn($this->connectionMock); + + $this->operation = new AddOptionToAttribute($setupMock); + } + + /** + * @throws LocalizedException + */ + public function testAddNewOptions() + { + $this->connectionMock->method('fetchAll')->willReturn([]); + $this->connectionMock->expects($this->exactly(4))->method('insert'); + + $this->operation->execute( + [ + 'values' => ['Black', 'White'], + 'attribute_id' => 4 + ] + ); + } + + /** + * @throws LocalizedException + */ + public function testAddExistingOptionsWithTheSameSortOrder() + { + $this->connectionMock->method('fetchAll')->willReturn( + [ + ['option_id' => 1, 'sort_order' => 0, 'value' => 'Black'], + ['option_id' => 2, 'sort_order' => 1, 'value' => 'White'], + ] + ); + + $this->connectionMock->expects($this->never())->method('insert'); + $this->connectionMock->expects($this->never())->method('update'); + + $this->operation->execute( + [ + 'values' => ['Black', 'White'], + 'attribute_id' => 4 + ] + ); + } + + /** + * @throws LocalizedException + */ + public function testAddExistingOptionsWithDifferentSortOrder() + { + $this->connectionMock->method('fetchAll')->willReturn( + [ + ['option_id' => 1, 'sort_order' => 13, 'value' => 'Black'], + ['option_id' => 2, 'sort_order' => 666, 'value' => 'White'], + ] + ); + + $this->connectionMock->expects($this->never())->method('insert'); + $this->connectionMock->expects($this->exactly(2))->method('update'); + + $this->operation->execute( + [ + 'values' => ['Black', 'White'], + 'attribute_id' => 4 + ] + ); + } + + /** + * @throws LocalizedException + */ + public function testAddMixedOptions() + { + $this->connectionMock->method('fetchAll')->willReturn( + [ + ['option_id' => 1, 'sort_order' => 13, 'value' => 'Black'], + ] + ); + + $this->connectionMock->expects($this->exactly(2))->method('insert'); + $this->connectionMock->expects($this->once())->method('update'); + + $this->operation->execute( + [ + 'values' => ['Black', 'White'], + 'attribute_id' => 4 + ] + ); + } + + /** + * @throws LocalizedException + */ + public function testAddNewOption() + { + $this->connectionMock->expects($this->exactly(2))->method('insert'); + $this->connectionMock->expects($this->once())->method('delete'); + + $this->operation->execute( + [ + 'attribute_id' => 1, + 'order' => [0 => 13], + 'value' => [ + [ + 0 => 'zzz', + ], + ], + ] + ); + } + + /** + * @expectedException \Magento\Framework\Exception\LocalizedException + * @expectedExceptionMessage The default option isn't defined. Set the option and try again. + */ + public function testAddNewOptionWithoutDefaultValue() + { + $this->operation->execute( + [ + 'attribute_id' => 1, + 'order' => [0 => 13], + 'value' => [[]], + ] + ); + } + + public function testDeleteOption() + { + $this->connectionMock->expects($this->never())->method('insert'); + $this->connectionMock->expects($this->never())->method('update'); + $this->connectionMock->expects($this->once())->method('delete'); + + $this->operation->execute( + [ + 'attribute_id' => 1, + 'delete' => [13 => true], + 'value' => [ + 13 => null, + ], + ] + ); + } + + public function testUpdateOption() + { + $this->connectionMock->expects($this->once())->method('insert'); + $this->connectionMock->expects($this->once())->method('update'); + $this->connectionMock->expects($this->once())->method('delete'); + + $this->operation->execute( + [ + 'attribute_id' => 1, + 'value' => [ + 13 => ['zzz'], + ], + ] + ); + } +} diff --git a/app/code/Magento/Eav/i18n/en_US.csv b/app/code/Magento/Eav/i18n/en_US.csv index 73f8b359d1c1b..fa4b026501d1b 100644 --- a/app/code/Magento/Eav/i18n/en_US.csv +++ b/app/code/Magento/Eav/i18n/en_US.csv @@ -143,3 +143,4 @@ hello,hello "The value of attribute not valid","The value of attribute not valid" "EAV types and attributes","EAV types and attributes" "Entity types declaration cache","Entity types declaration cache" +"Admin store attribute option label ""%1"" is already exists.","Admin store attribute option label ""%1"" is already exists." diff --git a/app/code/Magento/EavGraphQl/Model/Resolver/AttributeOptions.php b/app/code/Magento/EavGraphQl/Model/Resolver/AttributeOptions.php index e4c27adc60247..7361d52372cd6 100644 --- a/app/code/Magento/EavGraphQl/Model/Resolver/AttributeOptions.php +++ b/app/code/Magento/EavGraphQl/Model/Resolver/AttributeOptions.php @@ -57,29 +57,31 @@ public function resolve( array $args = null ) : Value { - return $this->valueFactory->create(function () use ($value) { - $entityType = $this->getEntityType($value); - $attributeCode = $this->getAttributeCode($value); + return $this->valueFactory->create( + function () use ($value) { + $entityType = $this->getEntityType($value); + $attributeCode = $this->getAttributeCode($value); - $optionsData = $this->getAttributeOptionsData($entityType, $attributeCode); - return $optionsData; - }); + $optionsData = $this->getAttributeOptionsData($entityType, $attributeCode); + return $optionsData; + } + ); } /** * Get entity type * * @param array $value - * @return int + * @return string * @throws LocalizedException */ - private function getEntityType(array $value): int + private function getEntityType(array $value): string { if (!isset($value['entity_type'])) { throw new LocalizedException(__('"Entity type should be specified')); } - return (int)$value['entity_type']; + return $value['entity_type']; } /** @@ -101,13 +103,13 @@ private function getAttributeCode(array $value): string /** * Get attribute options data * - * @param int $entityType + * @param string $entityType * @param string $attributeCode * @return array * @throws GraphQlInputException * @throws GraphQlNoSuchEntityException */ - private function getAttributeOptionsData(int $entityType, string $attributeCode): array + private function getAttributeOptionsData(string $entityType, string $attributeCode): array { try { $optionsData = $this->attributeOptionsDataProvider->getData($entityType, $attributeCode); diff --git a/app/code/Magento/EavGraphQl/Model/Resolver/CustomAttributeMetadata.php b/app/code/Magento/EavGraphQl/Model/Resolver/CustomAttributeMetadata.php index 62e3f01836619..85445580bb1fb 100644 --- a/app/code/Magento/EavGraphQl/Model/Resolver/CustomAttributeMetadata.php +++ b/app/code/Magento/EavGraphQl/Model/Resolver/CustomAttributeMetadata.php @@ -9,6 +9,7 @@ use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; use Magento\EavGraphQl\Model\Resolver\Query\Type; +use Magento\EavGraphQl\Model\Resolver\Query\FrontendType; use Magento\Framework\Exception\InputException; use Magento\Framework\Exception\LocalizedException; use Magento\Framework\GraphQl\Config\Element\Field; @@ -26,12 +27,19 @@ class CustomAttributeMetadata implements ResolverInterface */ private $type; + /** + * @var FrontendType + */ + private $frontendType; + /** * @param Type $type + * @param FrontendType $frontendType */ - public function __construct(Type $type) + public function __construct(Type $type, FrontendType $frontendType) { $this->type = $type; + $this->frontendType = $frontendType; } /** @@ -52,6 +60,7 @@ public function resolve( continue; } try { + $frontendType = $this->frontendType->getType($attribute['attribute_code'], $attribute['entity_type']); $type = $this->type->getType($attribute['attribute_code'], $attribute['entity_type']); } catch (InputException $exception) { $attributes['items'][] = new GraphQlNoSuchEntityException( @@ -78,7 +87,8 @@ public function resolve( $attributes['items'][] = [ 'attribute_code' => $attribute['attribute_code'], 'entity_type' => $attribute['entity_type'], - 'attribute_type' => ucfirst($type) + 'attribute_type' => ucfirst($type), + 'input_type' => $frontendType ]; } diff --git a/app/code/Magento/EavGraphQl/Model/Resolver/DataProvider/AttributeOptions.php b/app/code/Magento/EavGraphQl/Model/Resolver/DataProvider/AttributeOptions.php index 900a31c1093ed..3371fbe658c9c 100644 --- a/app/code/Magento/EavGraphQl/Model/Resolver/DataProvider/AttributeOptions.php +++ b/app/code/Magento/EavGraphQl/Model/Resolver/DataProvider/AttributeOptions.php @@ -29,11 +29,13 @@ public function __construct( } /** - * @param int $entityType + * Get attribute options data + * + * @param string $entityType * @param string $attributeCode * @return array */ - public function getData(int $entityType, string $attributeCode): array + public function getData(string $entityType, string $attributeCode): array { $options = $this->optionManager->getItems($entityType, $attributeCode); diff --git a/app/code/Magento/EavGraphQl/Model/Resolver/Query/FrontendType.php b/app/code/Magento/EavGraphQl/Model/Resolver/Query/FrontendType.php new file mode 100644 index 0000000000000..c76f19e6dfeb4 --- /dev/null +++ b/app/code/Magento/EavGraphQl/Model/Resolver/Query/FrontendType.php @@ -0,0 +1,61 @@ +attributeRepository = $attributeRepository; + $this->serviceTypeMap = $serviceTypeMap; + } + + /** + * Return frontend type for attribute + * + * @param string $attributeCode + * @param string $entityType + * @return null|string + */ + public function getType(string $attributeCode, string $entityType): ?string + { + $mappedEntityType = $this->serviceTypeMap->getEntityType($entityType); + if ($mappedEntityType) { + $entityType = $mappedEntityType; + } + try { + $attribute = $this->attributeRepository->get($entityType, $attributeCode); + } catch (NoSuchEntityException $e) { + return null; + } + return $attribute->getFrontendInput(); + } +} diff --git a/app/code/Magento/EavGraphQl/etc/schema.graphqls b/app/code/Magento/EavGraphQl/etc/schema.graphqls index 0b174fbc4d84d..21aa7001fab2b 100644 --- a/app/code/Magento/EavGraphQl/etc/schema.graphqls +++ b/app/code/Magento/EavGraphQl/etc/schema.graphqls @@ -13,6 +13,7 @@ type Attribute @doc(description: "Attribute contains the attribute_type of the s attribute_code: String @doc(description: "The unique identifier for an attribute code. This value should be in lowercase letters without spaces.") entity_type: String @doc(description: "The type of entity that defines the attribute") attribute_type: String @doc(description: "The data type of the attribute") + input_type: String @doc(description: "The frontend input type of the attribute") attribute_options: [AttributeOption] @resolver(class: "Magento\\EavGraphQl\\Model\\Resolver\\AttributeOptions") @doc(description: "Attribute options list.") } diff --git a/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/AttributeAdapter.php b/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/AttributeAdapter.php index 165f7e78eb65f..41a50961ae4bc 100644 --- a/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/AttributeAdapter.php +++ b/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/AttributeAdapter.php @@ -115,6 +115,18 @@ public function isBooleanType(): bool && $this->getAttribute()->getBackendType() !== 'varchar'; } + /** + * Check if attribute is text type + * + * @return bool + */ + public function isTextType(): bool + { + return in_array($this->getAttribute()->getBackendType(), ['varchar', 'static'], true) + && in_array($this->getFrontendInput(), ['text'], true) + && $this->getAttribute()->getIsVisible(); + } + /** * Check if attribute has boolean type. * diff --git a/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/StaticField.php b/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/StaticField.php index 6876b23bbb156..0f3020974d08a 100644 --- a/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/StaticField.php +++ b/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/StaticField.php @@ -130,6 +130,15 @@ public function getFields(array $context = []): array ]; } + if ($attributeAdapter->isTextType()) { + $keywordFieldName = FieldTypeConverterInterface::INTERNAL_DATA_TYPE_KEYWORD; + $allAttributes[$fieldName]['fields'][$keywordFieldName] = [ + 'type' => $this->fieldTypeConverter->convert( + FieldTypeConverterInterface::INTERNAL_DATA_TYPE_KEYWORD + ) + ]; + } + if ($attributeAdapter->isComplexType()) { $childFieldName = $this->fieldNameResolver->getFieldName( $attributeAdapter, diff --git a/app/code/Magento/Elasticsearch/Model/ResourceModel/Fulltext/Collection/SearchCriteriaResolver.php b/app/code/Magento/Elasticsearch/Model/ResourceModel/Fulltext/Collection/SearchCriteriaResolver.php index 255c7885e84b9..ce88fc290e23c 100644 --- a/app/code/Magento/Elasticsearch/Model/ResourceModel/Fulltext/Collection/SearchCriteriaResolver.php +++ b/app/code/Magento/Elasticsearch/Model/ResourceModel/Fulltext/Collection/SearchCriteriaResolver.php @@ -76,7 +76,6 @@ public function __construct( */ public function resolve(): SearchCriteria { - $this->builder->setPageSize($this->size); $searchCriteria = $this->builder->create(); $searchCriteria->setRequestName($this->searchRequestName); $searchCriteria->setSortOrders($this->orders); diff --git a/app/code/Magento/Elasticsearch/Model/ResourceModel/Fulltext/Collection/SearchResultApplier.php b/app/code/Magento/Elasticsearch/Model/ResourceModel/Fulltext/Collection/SearchResultApplier.php index 3ae2d384782c3..aac396f238358 100644 --- a/app/code/Magento/Elasticsearch/Model/ResourceModel/Fulltext/Collection/SearchResultApplier.php +++ b/app/code/Magento/Elasticsearch/Model/ResourceModel/Fulltext/Collection/SearchResultApplier.php @@ -25,16 +25,32 @@ class SearchResultApplier implements SearchResultApplierInterface */ private $searchResult; + /** + * @var int + */ + private $size; + + /** + * @var int + */ + private $currentPage; + /** * @param Collection $collection * @param SearchResultInterface $searchResult + * @param int $size + * @param int $currentPage */ public function __construct( Collection $collection, - SearchResultInterface $searchResult + SearchResultInterface $searchResult, + int $size, + int $currentPage ) { $this->collection = $collection; $this->searchResult = $searchResult; + $this->size = $size; + $this->currentPage = $currentPage; } /** @@ -46,8 +62,10 @@ public function apply() $this->collection->getSelect()->where('NULL'); return; } + + $items = $this->sliceItems($this->searchResult->getItems(), $this->size, $this->currentPage); $ids = []; - foreach ($this->searchResult->getItems() as $item) { + foreach ($items as $item) { $ids[] = (int)$item->getId(); } $this->collection->setPageSize(null); @@ -56,4 +74,25 @@ public function apply() $this->collection->getSelect()->reset(\Magento\Framework\DB\Select::ORDER); $this->collection->getSelect()->order("FIELD(e.entity_id,$orderList)"); } + + /** + * Slice current items + * + * @param array $items + * @param int $size + * @param int $currentPage + * @return array + */ + private function sliceItems(array $items, int $size, int $currentPage): array + { + if ($size !== 0) { + $offset = ($currentPage - 1) * $size; + if ($offset < 0) { + $offset = 0; + } + $items = array_slice($items, $offset, $this->size); + } + + return $items; + } } diff --git a/app/code/Magento/Elasticsearch/SearchAdapter/Filter/Builder/Term.php b/app/code/Magento/Elasticsearch/SearchAdapter/Filter/Builder/Term.php index ed8cd049d2915..d88c7e53d813a 100644 --- a/app/code/Magento/Elasticsearch/SearchAdapter/Filter/Builder/Term.php +++ b/app/code/Magento/Elasticsearch/SearchAdapter/Filter/Builder/Term.php @@ -5,10 +5,17 @@ */ namespace Magento\Elasticsearch\SearchAdapter\Filter\Builder; +use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\AttributeProvider; +use Magento\Framework\App\ObjectManager; use Magento\Framework\Search\Request\Filter\Term as TermFilterRequest; use Magento\Framework\Search\Request\FilterInterface as RequestFilterInterface; use Magento\Elasticsearch\Model\Adapter\FieldMapperInterface; +use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType\ConverterInterface + as FieldTypeConverterInterface; +/** + * Term filter builder + */ class Term implements FilterInterface { /** @@ -16,26 +23,56 @@ class Term implements FilterInterface */ protected $fieldMapper; + /** + * @var AttributeProvider + */ + private $attributeAdapterProvider; + + /** + * @var array + * @see \Magento\Elasticsearch\Elasticsearch5\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType\Resolver\IntegerType::$integerTypeAttributes + */ + private $integerTypeAttributes = ['category_ids']; + /** * @param FieldMapperInterface $fieldMapper + * @param AttributeProvider $attributeAdapterProvider + * @param array $integerTypeAttributes */ - public function __construct(FieldMapperInterface $fieldMapper) - { + public function __construct( + FieldMapperInterface $fieldMapper, + AttributeProvider $attributeAdapterProvider = null, + array $integerTypeAttributes = [] + ) { $this->fieldMapper = $fieldMapper; + $this->attributeAdapterProvider = $attributeAdapterProvider + ?? ObjectManager::getInstance()->get(AttributeProvider::class); + $this->integerTypeAttributes = array_merge($this->integerTypeAttributes, $integerTypeAttributes); } /** + * Build term filter request + * * @param RequestFilterInterface|TermFilterRequest $filter * @return array */ public function buildFilter(RequestFilterInterface $filter) { $filterQuery = []; + + $attribute = $this->attributeAdapterProvider->getByAttributeCode($filter->getField()); + $fieldName = $this->fieldMapper->getFieldName($filter->getField()); + + if ($attribute->isTextType() && !in_array($attribute->getAttributeCode(), $this->integerTypeAttributes)) { + $suffix = FieldTypeConverterInterface::INTERNAL_DATA_TYPE_KEYWORD; + $fieldName .= '.' . $suffix; + } + if ($filter->getValue()) { $operator = is_array($filter->getValue()) ? 'terms' : 'term'; $filterQuery []= [ $operator => [ - $this->fieldMapper->getFieldName($filter->getField()) => $filter->getValue(), + $fieldName => $filter->getValue(), ], ]; } diff --git a/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/FieldMapper/Product/FieldProvider/StaticFieldTest.php b/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/FieldMapper/Product/FieldProvider/StaticFieldTest.php index de85b8b6602b8..f90c13c9bfb65 100644 --- a/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/FieldMapper/Product/FieldProvider/StaticFieldTest.php +++ b/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/FieldMapper/Product/FieldProvider/StaticFieldTest.php @@ -139,6 +139,7 @@ public function testGetAllAttributesTypes( $isComplexType, $complexType, $isSortable, + $isTextType, $fieldName, $compositeFieldName, $sortFieldName, @@ -153,29 +154,33 @@ public function testGetAllAttributesTypes( $this->indexTypeConverter->expects($this->any()) ->method('convert') ->with($this->anything()) - ->will($this->returnCallback( - function ($type) { - if ($type === 'no_index') { - return 'no'; - } elseif ($type === 'no_analyze') { - return 'not_analyzed'; + ->will( + $this->returnCallback( + function ($type) { + if ($type === 'no_index') { + return 'no'; + } elseif ($type === 'no_analyze') { + return 'not_analyzed'; + } } - } - )); + ) + ); $this->fieldNameResolver->expects($this->any()) ->method('getFieldName') ->with($this->anything()) - ->will($this->returnCallback( - function ($attributeMock, $context) use ($fieldName, $compositeFieldName, $sortFieldName) { - if (empty($context)) { - return $fieldName; - } elseif ($context['type'] === 'sort') { - return $sortFieldName; - } elseif ($context['type'] === 'text') { - return $compositeFieldName; + ->will( + $this->returnCallback( + function ($attributeMock, $context) use ($fieldName, $compositeFieldName, $sortFieldName) { + if (empty($context)) { + return $fieldName; + } elseif ($context['type'] === 'sort') { + return $sortFieldName; + } elseif ($context['type'] === 'text') { + return $compositeFieldName; + } } - } - )); + ) + ); $productAttributeMock = $this->getMockBuilder(AbstractAttribute::class) ->setMethods(['getAttributeCode']) @@ -189,7 +194,7 @@ function ($attributeMock, $context) use ($fieldName, $compositeFieldName, $sortF $attributeMock = $this->getMockBuilder(AttributeAdapter::class) ->disableOriginalConstructor() - ->setMethods(['isComplexType', 'getAttributeCode', 'isSortable']) + ->setMethods(['isComplexType', 'getAttributeCode', 'isSortable', 'isTextType']) ->getMock(); $attributeMock->expects($this->any()) ->method('isComplexType') @@ -197,6 +202,9 @@ function ($attributeMock, $context) use ($fieldName, $compositeFieldName, $sortF $attributeMock->expects($this->any()) ->method('isSortable') ->willReturn($isSortable); + $attributeMock->expects($this->any()) + ->method('isTextType') + ->willReturn($isTextType); $attributeMock->expects($this->any()) ->method('getAttributeCode') ->willReturn($attributeCode); @@ -207,22 +215,24 @@ function ($attributeMock, $context) use ($fieldName, $compositeFieldName, $sortF $this->fieldTypeConverter->expects($this->any()) ->method('convert') ->with($this->anything()) - ->will($this->returnCallback( - function ($type) use ($complexType) { - static $callCount = []; - $callCount[$type] = !isset($callCount[$type]) ? 1 : ++$callCount[$type]; + ->will( + $this->returnCallback( + function ($type) use ($complexType) { + static $callCount = []; + $callCount[$type] = !isset($callCount[$type]) ? 1 : ++$callCount[$type]; - if ($type === 'string') { - return 'string'; - } elseif ($type === 'float') { - return 'float'; - } elseif ($type === 'keyword') { - return 'string'; - } else { - return $complexType; + if ($type === 'string') { + return 'string'; + } elseif ($type === 'float') { + return 'float'; + } elseif ($type === 'keyword') { + return 'string'; + } else { + return $complexType; + } } - } - )); + ) + ); $this->assertEquals( $expected, @@ -243,13 +253,19 @@ public function attributeProvider() true, 'text', false, + true, 'category_ids', 'category_ids_value', '', [ 'category_ids' => [ 'type' => 'select', - 'index' => true + 'index' => true, + 'fields' => [ + 'keyword' => [ + 'type' => 'string', + ] + ] ], 'category_ids_value' => [ 'type' => 'string' @@ -267,13 +283,19 @@ public function attributeProvider() false, null, false, + true, 'attr_code', '', '', [ 'attr_code' => [ 'type' => 'text', - 'index' => 'no' + 'index' => 'no', + 'fields' => [ + 'keyword' => [ + 'type' => 'string', + ] + ] ], 'store_id' => [ 'type' => 'string', @@ -288,6 +310,7 @@ public function attributeProvider() false, null, false, + false, 'attr_code', '', '', @@ -308,6 +331,7 @@ public function attributeProvider() false, null, true, + false, 'attr_code', '', 'sort_attr_code', diff --git a/app/code/Magento/Elasticsearch/Test/Unit/Model/ResourceModel/Fulltext/Collection/SearchCriteriaResolverTest.php b/app/code/Magento/Elasticsearch/Test/Unit/Model/ResourceModel/Fulltext/Collection/SearchCriteriaResolverTest.php index 30a1642378b71..b2c0f5e341fc2 100644 --- a/app/code/Magento/Elasticsearch/Test/Unit/Model/ResourceModel/Fulltext/Collection/SearchCriteriaResolverTest.php +++ b/app/code/Magento/Elasticsearch/Test/Unit/Model/ResourceModel/Fulltext/Collection/SearchCriteriaResolverTest.php @@ -34,15 +34,18 @@ protected function setUp() } /** - * @param array|null $orders - * @param array|null $expected + * @param array $params + * @param array $expected * @dataProvider resolveSortOrderDataProvider */ - public function testResolve($orders, $expected) + public function testResolve($params, $expected) { $searchRequestName = 'test'; $currentPage = 1; - $size = 10; + $size = $params['size']; + $expectedSize = $expected['size']; + $orders = $params['orders']; + $expectedOrders = $expected['orders']; $searchCriteria = $this->getMockBuilder(SearchCriteria::class) ->disableOriginalConstructor() @@ -54,7 +57,7 @@ public function testResolve($orders, $expected) ->willReturn($searchCriteria); $searchCriteria->expects($this->once()) ->method('setSortOrders') - ->with($expected) + ->with($expectedOrders) ->willReturn($searchCriteria); $searchCriteria->expects($this->once()) ->method('setCurrentPage') @@ -64,10 +67,16 @@ public function testResolve($orders, $expected) $this->searchCriteriaBuilder->expects($this->once()) ->method('create') ->willReturn($searchCriteria); - $this->searchCriteriaBuilder->expects($this->once()) - ->method('setPageSize') - ->with($size) - ->willReturn($this->searchCriteriaBuilder); + + if ($expectedSize === null) { + $this->searchCriteriaBuilder->expects($this->never()) + ->method('setPageSize'); + } else { + $this->searchCriteriaBuilder->expects($this->once()) + ->method('setPageSize') + ->with($expectedSize) + ->willReturn($this->searchCriteriaBuilder); + } $objectManager = new ObjectManagerHelper($this); /** @var SearchCriteriaResolver $model */ @@ -92,12 +101,12 @@ public function resolveSortOrderDataProvider() { return [ [ - null, - null, + ['size' => 0, 'orders' => null], + ['size' => null, 'orders' => null], ], [ - ['test' => 'ASC'], - ['test' => 'ASC'], + ['size' => 10, 'orders' => ['test' => 'ASC']], + ['size' => null, 'orders' => ['test' => 'ASC']], ], ]; } diff --git a/app/code/Magento/Elasticsearch6/etc/di.xml b/app/code/Magento/Elasticsearch6/etc/di.xml index 011dfa1019738..580c61ffc8cdb 100644 --- a/app/code/Magento/Elasticsearch6/etc/di.xml +++ b/app/code/Magento/Elasticsearch6/etc/di.xml @@ -202,4 +202,23 @@ + + + + + 1 + 1 + 1 + + + 1 + 1 + 1 + 1 + 1 + 1 + 1 + + + diff --git a/app/code/Magento/Email/Model/Template/Filter.php b/app/code/Magento/Email/Model/Template/Filter.php index 0e27b2d4c418b..a29b1165d83c8 100644 --- a/app/code/Magento/Email/Model/Template/Filter.php +++ b/app/code/Magento/Email/Model/Template/Filter.php @@ -956,7 +956,6 @@ public function getCssFilesContent(array $files) } } catch (ContentProcessorException $exception) { $css = $exception->getMessage(); - // phpcs:disable Magento2.Exceptions.ThrowCatch } catch (\Magento\Framework\View\Asset\File\NotFoundException $exception) { $css = ''; } diff --git a/app/code/Magento/Email/Test/Mftf/ActionGroup/EmailTemplateActionGroup.xml b/app/code/Magento/Email/Test/Mftf/ActionGroup/EmailTemplateActionGroup.xml index 2a76b6d04f05e..1155930dd75ef 100644 --- a/app/code/Magento/Email/Test/Mftf/ActionGroup/EmailTemplateActionGroup.xml +++ b/app/code/Magento/Email/Test/Mftf/ActionGroup/EmailTemplateActionGroup.xml @@ -61,7 +61,8 @@ Clicks on Delete Template. Accepts the Popup. Validates that the Email Template is NOT present in the Email Templates Grid. - + + diff --git a/app/code/Magento/Email/Test/Mftf/Section/AdminEmailTemplateEditSection.xml b/app/code/Magento/Email/Test/Mftf/Section/AdminEmailTemplateEditSection.xml index 4dd4ac14cc040..4975074a9e63f 100644 --- a/app/code/Magento/Email/Test/Mftf/Section/AdminEmailTemplateEditSection.xml +++ b/app/code/Magento/Email/Test/Mftf/Section/AdminEmailTemplateEditSection.xml @@ -16,5 +16,6 @@ + diff --git a/app/code/Magento/Email/view/adminhtml/templates/template/edit.phtml b/app/code/Magento/Email/view/adminhtml/templates/template/edit.phtml index 9653156e85e80..1f236a21a7306 100644 --- a/app/code/Magento/Email/view/adminhtml/templates/template/edit.phtml +++ b/app/code/Magento/Email/view/adminhtml/templates/template/edit.phtml @@ -61,10 +61,11 @@ require([ "jquery", "wysiwygAdapter", "Magento_Ui/js/modal/alert", + 'Magento_Ui/js/modal/confirm', "mage/mage", "Magento_Email/js/variables", "prototype" -], function(jQuery, tinyMCE, alert){ +], function(jQuery, tinyMCE, alert, confirm){ //escapeJs($block->escapeHtml(__('Are you sure you want to strip tags?'))) ?>")) { - return false; - } - this.unconvertedText = $('template_text').value; - $('convert_button').hide(); - $('template_text').value = $('template_text').value.stripScripts().replace( - new RegExp('', 'img'), '' - ).stripTags().strip(); - $('convert_button_back').show(); - $('field_template_styles').hide(); - this.typeChange = true; - return false; + confirm({ + content: "= $block->escapeJs($block->escapeHtml(__('Are you sure you want to strip tags?'))) ?>", + actions: { + confirm: function () { + this.unconvertedText = $('template_text').value; + $('convert_button').hide(); + $('template_text').value = $('template_text').value.stripScripts().replace( + new RegExp('', 'img'), '' + ).stripTags().strip(); + $('convert_button_back').show(); + $('field_template_styles').hide(); + this.typeChange = true; + return false; + } + } + }); }, unStripTags: function () { $('convert_button').show(); @@ -164,9 +169,14 @@ require([ }, deleteTemplate: function() { - if(window.confirm("= $block->escapeJs($block->escapeHtml(__('Are you sure you want to delete this template?'))) ?>")) { - window.location.href = '= $block->escapeJs($block->escapeUrl($block->getDeleteUrl())) ?>'; - } + confirm({ + content: "= $block->escapeJs($block->escapeHtml(__('Are you sure you want to delete this template?'))) ?>", + actions: { + confirm: function () { + window.location.href = '= $block->escapeJs($block->escapeUrl($block->getDeleteUrl())) ?>'; + } + } + }); }, load: function() { diff --git a/app/code/Magento/EncryptionKey/Block/Adminhtml/Crypt/Key/Form.php b/app/code/Magento/EncryptionKey/Block/Adminhtml/Crypt/Key/Form.php index aae6fbfe1ce35..56c647057189f 100644 --- a/app/code/Magento/EncryptionKey/Block/Adminhtml/Crypt/Key/Form.php +++ b/app/code/Magento/EncryptionKey/Block/Adminhtml/Crypt/Key/Form.php @@ -45,7 +45,7 @@ protected function _prepareForm() 'name' => 'generate_random', 'label' => __('Auto-generate a Key'), 'options' => [0 => __('No'), 1 => __('Yes')], - 'onclick' => "var cryptKey = jQuery('#crypt_key'); var cryptKeyBlock = cryptKey.parent().parent(); ". + 'onchange' => "var cryptKey = jQuery('#crypt_key'); var cryptKeyBlock = cryptKey.parent().parent(); ". "cryptKey.prop('disabled', this.value === '1'); " . "if (cryptKey.prop('disabled')) { cryptKeyBlock.hide() } " . "else { cryptKeyBlock.show() }", diff --git a/app/code/Magento/Fedex/etc/adminhtml/system.xml b/app/code/Magento/Fedex/etc/adminhtml/system.xml index bbaea4023a794..fdbe10a1a352e 100644 --- a/app/code/Magento/Fedex/etc/adminhtml/system.xml +++ b/app/code/Magento/Fedex/etc/adminhtml/system.xml @@ -135,6 +135,7 @@ Sort Order + validate-number validate-zero-or-greater diff --git a/app/code/Magento/GiftMessage/Helper/Message.php b/app/code/Magento/GiftMessage/Helper/Message.php index 55cebe26c1fc2..f824c9454d2c1 100644 --- a/app/code/Magento/GiftMessage/Helper/Message.php +++ b/app/code/Magento/GiftMessage/Helper/Message.php @@ -120,6 +120,8 @@ public function getInline($type, \Magento\Framework\DataObject $entity, $dontDis } /** + * Skip page by page type + * * @param string $pageType * @return bool */ @@ -191,7 +193,7 @@ public function isMessagesAllowed($type, \Magento\Framework\DataObject $entity, } /** - * Check availablity of gift messages from store config if flag eq 2. + * Check availability of gift messages from store config if flag eq 2. * * @param bool $productConfig * @param \Magento\Store\Model\Store|int|null $store diff --git a/app/code/Magento/GraphQl/etc/schema.graphqls b/app/code/Magento/GraphQl/etc/schema.graphqls index eb6a88a4d487d..77bba5ea3a9d4 100644 --- a/app/code/Magento/GraphQl/etc/schema.graphqls +++ b/app/code/Magento/GraphQl/etc/schema.graphqls @@ -65,6 +65,20 @@ input FilterTypeInput @doc(description: "FilterTypeInput specifies which action nin: [String] @doc(description: "Not in. The value can contain a set of comma-separated values") } +input FilterEqualTypeInput @doc(description: "Defines a filter that matches the input exactly.") { + in: [String] @doc(description: "An array of values to filter on") + eq: String @doc(description: "A string to filter on") +} + +input FilterRangeTypeInput @doc(description: "Defines a filter that matches a range of values, such as prices or dates.") { + from: String @doc(description: "The beginning of the range") + to: String @doc(description: "The end of the range") +} + +input FilterMatchTypeInput @doc(description: "Defines a filter that performs a fuzzy search.") { + match: String @doc(description: "One or more words to filter on") +} + type SearchResultPageInfo @doc(description: "SearchResultPageInfo provides navigation for the query response") { page_size: Int @doc(description: "Specifies the maximum number of items to return") current_page: Int @doc(description: "Specifies which page of results to return") diff --git a/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdminAssociateGroupedProductToWebsitesTest.xml b/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdminAssociateGroupedProductToWebsitesTest.xml new file mode 100644 index 0000000000000..3827666252478 --- /dev/null +++ b/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdminAssociateGroupedProductToWebsitesTest.xml @@ -0,0 +1,116 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/code/Magento/ImportExport/Controller/Adminhtml/Export/File/Download.php b/app/code/Magento/ImportExport/Controller/Adminhtml/Export/File/Download.php index 0e6bca26d2062..8dbd9a0ae44ba 100644 --- a/app/code/Magento/ImportExport/Controller/Adminhtml/Export/File/Download.php +++ b/app/code/Magento/ImportExport/Controller/Adminhtml/Export/File/Download.php @@ -72,7 +72,6 @@ public function execute() DirectoryList::VAR_DIR ); } - // phpcs:ignore Magento2.Exceptions.ThrowCatch } catch (LocalizedException | \Exception $exception) { throw new LocalizedException(__('There are no export file with such name %1', $fileName)); } diff --git a/app/code/Magento/ImportExport/Test/Mftf/ActionGroup/AdminImportProductsActionGroup.xml b/app/code/Magento/ImportExport/Test/Mftf/ActionGroup/AdminImportProductsActionGroup.xml index 2ac790b953ec1..6bcca6c86c98c 100644 --- a/app/code/Magento/ImportExport/Test/Mftf/ActionGroup/AdminImportProductsActionGroup.xml +++ b/app/code/Magento/ImportExport/Test/Mftf/ActionGroup/AdminImportProductsActionGroup.xml @@ -14,21 +14,36 @@ + + - + + + - + - + + + - - - - + + + + + + + + + + + + + diff --git a/app/code/Magento/ImportExport/Test/Mftf/Page/AdminImportIndexPage.xml b/app/code/Magento/ImportExport/Test/Mftf/Page/AdminImportIndexPage.xml index 87807eb9b0e85..6262931179599 100644 --- a/app/code/Magento/ImportExport/Test/Mftf/Page/AdminImportIndexPage.xml +++ b/app/code/Magento/ImportExport/Test/Mftf/Page/AdminImportIndexPage.xml @@ -11,5 +11,6 @@ + diff --git a/app/code/Magento/ImportExport/Test/Mftf/Section/AdminImportMainSection.xml b/app/code/Magento/ImportExport/Test/Mftf/Section/AdminImportMainSection.xml index 2ce6b1e35777f..d44b93bf05c94 100644 --- a/app/code/Magento/ImportExport/Test/Mftf/Section/AdminImportMainSection.xml +++ b/app/code/Magento/ImportExport/Test/Mftf/Section/AdminImportMainSection.xml @@ -13,5 +13,7 @@ + + diff --git a/app/code/Magento/ImportExport/Test/Mftf/Section/AdminImportValidationMessagesSection.xml b/app/code/Magento/ImportExport/Test/Mftf/Section/AdminImportValidationMessagesSection.xml new file mode 100644 index 0000000000000..370d9546fa2f7 --- /dev/null +++ b/app/code/Magento/ImportExport/Test/Mftf/Section/AdminImportValidationMessagesSection.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + diff --git a/app/code/Magento/ImportExport/Test/Mftf/Test/AdminCheckDoubleImportOfProductsTest.xml b/app/code/Magento/ImportExport/Test/Mftf/Test/AdminCheckDoubleImportOfProductsTest.xml index 0f2dde99b9016..909c6101fe53e 100644 --- a/app/code/Magento/ImportExport/Test/Mftf/Test/AdminCheckDoubleImportOfProductsTest.xml +++ b/app/code/Magento/ImportExport/Test/Mftf/Test/AdminCheckDoubleImportOfProductsTest.xml @@ -60,14 +60,14 @@ - + - + diff --git a/app/code/Magento/ImportExport/Test/Mftf/Test/AdminImportProductsWithAddUpdateBehaviorTest.xml b/app/code/Magento/ImportExport/Test/Mftf/Test/AdminImportProductsWithAddUpdateBehaviorTest.xml index ceb4e93e4e9aa..796732d572290 100644 --- a/app/code/Magento/ImportExport/Test/Mftf/Test/AdminImportProductsWithAddUpdateBehaviorTest.xml +++ b/app/code/Magento/ImportExport/Test/Mftf/Test/AdminImportProductsWithAddUpdateBehaviorTest.xml @@ -72,7 +72,7 @@ - + @@ -109,7 +109,7 @@ - + diff --git a/app/code/Magento/ImportExport/Test/Mftf/Test/AdminImportProductsWithErrorEntriesTest.xml b/app/code/Magento/ImportExport/Test/Mftf/Test/AdminImportProductsWithErrorEntriesTest.xml new file mode 100644 index 0000000000000..94840a4ea6142 --- /dev/null +++ b/app/code/Magento/ImportExport/Test/Mftf/Test/AdminImportProductsWithErrorEntriesTest.xml @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/code/Magento/ImportExport/Test/Mftf/Test/AdminImportProductsWithReplaceBehaviorTest.xml b/app/code/Magento/ImportExport/Test/Mftf/Test/AdminImportProductsWithReplaceBehaviorTest.xml index d63a5546716b1..dc4ede1978de3 100644 --- a/app/code/Magento/ImportExport/Test/Mftf/Test/AdminImportProductsWithReplaceBehaviorTest.xml +++ b/app/code/Magento/ImportExport/Test/Mftf/Test/AdminImportProductsWithReplaceBehaviorTest.xml @@ -39,7 +39,7 @@ - + diff --git a/app/code/Magento/ImportExport/i18n/en_US.csv b/app/code/Magento/ImportExport/i18n/en_US.csv index d7680a71ac5f7..5787d6f7d02b6 100644 --- a/app/code/Magento/ImportExport/i18n/en_US.csv +++ b/app/code/Magento/ImportExport/i18n/en_US.csv @@ -123,3 +123,5 @@ Summary,Summary "New product data is added to existing product data entries in the database. All fields except SKU can be updated.","New product data is added to existing product data entries in the database. All fields except SKU can be updated." "All existing product data is replaced with the imported new data. Exercise caution when replacing data. All existing product data will be completely cleared and all references in the system will be lost.","All existing product data is replaced with the imported new data. Exercise caution when replacing data. All existing product data will be completely cleared and all references in the system will be lost." "Any entities in the import data that match existing entities in the database are deleted from the database.","Any entities in the import data that match existing entities in the database are deleted from the database." +"Invalid data","Invalid data" +"Invalid response","Invalid response" \ No newline at end of file diff --git a/app/code/Magento/ImportExport/view/adminhtml/templates/export/form/before.phtml b/app/code/Magento/ImportExport/view/adminhtml/templates/export/form/before.phtml index 26b241b999493..3e7a19a0c0d82 100644 --- a/app/code/Magento/ImportExport/view/adminhtml/templates/export/form/before.phtml +++ b/app/code/Magento/ImportExport/view/adminhtml/templates/export/form/before.phtml @@ -89,7 +89,7 @@ require([ form.action = oldAction; } else { alert({ - content: 'Invalid data' + content: '= $block->escapeHtml(__('Invalid data')); ?>' }); } }; diff --git a/app/code/Magento/ImportExport/view/adminhtml/templates/import/form/before.phtml b/app/code/Magento/ImportExport/view/adminhtml/templates/import/form/before.phtml index 628c1088a016c..bd88ec419d848 100644 --- a/app/code/Magento/ImportExport/view/adminhtml/templates/import/form/before.phtml +++ b/app/code/Magento/ImportExport/view/adminhtml/templates/import/form/before.phtml @@ -209,7 +209,7 @@ require([ postToFrameProcessResponse: function(response) { if ('object' != typeof(response)) { alert({ - content: 'Invalid response' + content: '= $block->escapeHtml(__('Invalid response')); ?>' }); return false; diff --git a/app/code/Magento/Indexer/Model/Indexer/CacheCleaner.php b/app/code/Magento/Indexer/Model/Indexer/CacheCleaner.php new file mode 100644 index 0000000000000..c75a3541ba9c3 --- /dev/null +++ b/app/code/Magento/Indexer/Model/Indexer/CacheCleaner.php @@ -0,0 +1,100 @@ +eventManager = $eventManager; + $this->cacheContext = $cacheContext; + $this->appCache = $appCache; + } + + /** + * Clean cache after full reindex. + * + * @param ActionInterface $subject + * @return void + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function afterExecuteFull(ActionInterface $subject) + { + $this->cleanCache(); + } + + /** + * Clean cache after reindexed list. + * + * @param ActionInterface $subject + * @return void + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function afterExecuteList(ActionInterface $subject) + { + $this->cleanCache(); + } + + /** + * Clean cache after reindexed row. + * + * @param ActionInterface $subject + * @return void + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function afterExecuteRow(ActionInterface $subject) + { + $this->cleanCache(); + } + + /** + * Clean cache. + * + * @return void + */ + private function cleanCache() + { + $this->eventManager->dispatch('clean_cache_by_tags', ['object' => $this->cacheContext]); + + $identities = $this->cacheContext->getIdentities(); + if (!empty($identities)) { + $this->appCache->clean($identities); + } + } +} diff --git a/app/code/Magento/Indexer/etc/di.xml b/app/code/Magento/Indexer/etc/di.xml index 76e7e7a46224b..9496f29cb1d87 100644 --- a/app/code/Magento/Indexer/etc/di.xml +++ b/app/code/Magento/Indexer/etc/di.xml @@ -67,4 +67,7 @@ Magento\Indexer\Model\Indexer + + + diff --git a/app/code/Magento/InstantPurchase/view/frontend/layout/catalog_product_view.xml b/app/code/Magento/InstantPurchase/view/frontend/layout/catalog_product_view.xml index 6f8682197f615..5699fe6e00c7f 100644 --- a/app/code/Magento/InstantPurchase/view/frontend/layout/catalog_product_view.xml +++ b/app/code/Magento/InstantPurchase/view/frontend/layout/catalog_product_view.xml @@ -5,7 +5,7 @@ * See COPYING.txt for license details. */ --> - + diff --git a/app/code/Magento/Integration/Block/Adminhtml/Integration/Edit/Tab/Info.php b/app/code/Magento/Integration/Block/Adminhtml/Integration/Edit/Tab/Info.php index d8684e635b943..4042c2ebde87d 100644 --- a/app/code/Magento/Integration/Block/Adminhtml/Integration/Edit/Tab/Info.php +++ b/app/code/Magento/Integration/Block/Adminhtml/Integration/Edit/Tab/Info.php @@ -146,6 +146,7 @@ protected function _addGeneralFieldset($form, $integrationData) 'label' => __('Callback URL'), 'name' => self::DATA_ENDPOINT, 'disabled' => $disabled, + 'class' => 'validate-url', // @codingStandardsIgnoreStart 'note' => __( 'Enter URL where Oauth credentials can be sent when using Oauth for token exchange. We strongly recommend using https://.' diff --git a/app/code/Magento/Integration/etc/adminhtml/system.xml b/app/code/Magento/Integration/etc/adminhtml/system.xml index fe80fe105493a..ddaae76700255 100644 --- a/app/code/Magento/Integration/etc/adminhtml/system.xml +++ b/app/code/Magento/Integration/etc/adminhtml/system.xml @@ -16,10 +16,12 @@ Customer Token Lifetime (hours) We will disable this feature if the value is empty. + required-entry validate-zero-or-greater validate-number Admin Token Lifetime (hours) We will disable this feature if the value is empty. + required-entry validate-zero-or-greater validate-number @@ -27,10 +29,12 @@ Cleanup Probability Integer. Launch cleanup in X OAuth requests. 0 (not recommended) - to disable cleanup + required-entry validate-zero-or-greater validate-digits Expiration Period Cleanup entries older than X minutes. + required-entry validate-zero-or-greater validate-number @@ -38,14 +42,17 @@ Expiration Period Consumer key/secret will expire if not used within X seconds after Oauth token exchange starts. + required-entry validate-zero-or-greater validate-number OAuth consumer credentials HTTP Post maxredirects Number of maximum redirects for OAuth consumer credentials Post request. + required-entry validate-zero-or-greater validate-digits OAuth consumer credentials HTTP Post timeout Timeout for OAuth consumer credentials Post request within X seconds. + required-entry validate-zero-or-greater validate-number @@ -53,13 +60,14 @@ Maximum Login Failures to Lock Out Account Maximum Number of authentication failures to lock out account. + required-entry validate-zero-or-greater validate-digits Lockout Time (seconds) Period of time in seconds after which account will be unlocked. + required-entry validate-zero-or-greater validate-number - diff --git a/app/code/Magento/LayeredNavigation/Block/Navigation.php b/app/code/Magento/LayeredNavigation/Block/Navigation.php index 4173469da8e42..e394fe7f6cf5b 100644 --- a/app/code/Magento/LayeredNavigation/Block/Navigation.php +++ b/app/code/Magento/LayeredNavigation/Block/Navigation.php @@ -3,22 +3,25 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - -/** - * Catalog layered navigation view block - * - * @author Magento Core Team - */ namespace Magento\LayeredNavigation\Block; use Magento\Framework\View\Element\Template; +use Magento\Catalog\Model\ResourceModel\Product\Collection; +use Magento\Catalog\Block\Product\ProductList\Toolbar; /** + * Catalog layered navigation view block + * * @api * @since 100.0.2 */ class Navigation extends \Magento\Framework\View\Element\Template { + /** + * Product listing toolbar block name + */ + private const PRODUCT_LISTING_TOOLBAR_BLOCK = 'product_list_toolbar'; + /** * Catalog layer * @@ -67,9 +70,20 @@ protected function _prepareLayout() $filter->apply($this->getRequest()); } $this->getLayer()->apply(); + return parent::_prepareLayout(); } + /** + * @inheritdoc + */ + protected function _beforeToHtml() + { + $this->configureToolbarBlock(); + + return parent::_beforeToHtml(); + } + /** * Get layer object * @@ -107,7 +121,8 @@ public function getFilters() */ public function canShowBlock() { - return $this->visibilityFlag->isEnabled($this->getLayer(), $this->getFilters()); + return $this->getLayer()->getCurrentCategory()->getDisplayMode() !== \Magento\Catalog\Model\Category::DM_PAGE + && $this->visibilityFlag->isEnabled($this->getLayer(), $this->getFilters()); } /** @@ -119,4 +134,20 @@ public function getClearUrl() { return $this->getChildBlock('state')->getClearUrl(); } + + /** + * Configures the Toolbar block + * + * @return void + */ + private function configureToolbarBlock(): void + { + /** @var Toolbar $toolbarBlock */ + $toolbarBlock = $this->getLayout()->getBlock(self::PRODUCT_LISTING_TOOLBAR_BLOCK); + if ($toolbarBlock) { + /** @var Collection $collection */ + $collection = $this->getLayer()->getProductCollection(); + $toolbarBlock->setCollection($collection); + } + } } diff --git a/app/code/Magento/LayeredNavigation/Test/Unit/Block/NavigationTest.php b/app/code/Magento/LayeredNavigation/Test/Unit/Block/NavigationTest.php index e37e58b14f027..f0243784dd618 100644 --- a/app/code/Magento/LayeredNavigation/Test/Unit/Block/NavigationTest.php +++ b/app/code/Magento/LayeredNavigation/Test/Unit/Block/NavigationTest.php @@ -6,6 +6,8 @@ namespace Magento\LayeredNavigation\Test\Unit\Block; +use Magento\Catalog\Model\Category; + class NavigationTest extends \PHPUnit\Framework\TestCase { /** @@ -98,9 +100,61 @@ public function testCanShowBlock() ->method('isEnabled') ->with($this->catalogLayerMock, $filters) ->will($this->returnValue($enabled)); + + $category = $this->createMock(Category::class); + $this->catalogLayerMock->expects($this->atLeastOnce())->method('getCurrentCategory')->willReturn($category); + $category->expects($this->once())->method('getDisplayMode')->willReturn(Category::DM_PRODUCT); + $this->assertEquals($enabled, $this->model->canShowBlock()); } + /** + * Test canShowBlock() with different category display types. + * + * @param string $mode + * @param bool $result + * + * @dataProvider canShowBlockDataProvider + */ + public function testCanShowBlockWithDifferentDisplayModes(string $mode, bool $result) + { + $filters = ['To' => 'be', 'or' => 'not', 'to' => 'be']; + + $this->filterListMock->expects($this->atLeastOnce())->method('getFilters') + ->with($this->catalogLayerMock) + ->will($this->returnValue($filters)); + $this->assertEquals($filters, $this->model->getFilters()); + + $this->visibilityFlagMock + ->expects($this->any()) + ->method('isEnabled') + ->with($this->catalogLayerMock, $filters) + ->will($this->returnValue(true)); + + $category = $this->createMock(Category::class); + $this->catalogLayerMock->expects($this->atLeastOnce())->method('getCurrentCategory')->willReturn($category); + $category->expects($this->once())->method('getDisplayMode')->willReturn($mode); + $this->assertEquals($result, $this->model->canShowBlock()); + } + + public function canShowBlockDataProvider() + { + return [ + [ + Category::DM_PRODUCT, + true, + ], + [ + Category::DM_PAGE, + false, + ], + [ + Category::DM_MIXED, + true, + ], + ]; + } + public function testGetClearUrl() { $this->filterListMock->expects($this->any())->method('getFilters')->will($this->returnValue([])); diff --git a/app/code/Magento/MediaStorage/Service/ImageResize.php b/app/code/Magento/MediaStorage/Service/ImageResize.php index 90cdcb7159b0c..63353b2536a5a 100644 --- a/app/code/Magento/MediaStorage/Service/ImageResize.php +++ b/app/code/Magento/MediaStorage/Service/ImageResize.php @@ -7,10 +7,12 @@ namespace Magento\MediaStorage\Service; +use Generator; use Magento\Catalog\Helper\Image as ImageHelper; use Magento\Catalog\Model\Product\Image\ParamsBuilder; use Magento\Catalog\Model\View\Asset\ImageFactory as AssertImageFactory; use Magento\Framework\App\Area; +use Magento\Framework\App\ObjectManager; use Magento\Framework\Exception\NotFoundException; use Magento\Framework\Filesystem; use Magento\Framework\Image; @@ -19,10 +21,12 @@ use Magento\Framework\App\State; use Magento\Framework\View\ConfigInterface as ViewConfig; use \Magento\Catalog\Model\ResourceModel\Product\Image as ProductImage; +use Magento\Store\Model\StoreManagerInterface; use Magento\Theme\Model\Config\Customization as ThemeCustomizationConfig; use Magento\Theme\Model\ResourceModel\Theme\Collection; use Magento\Framework\App\Filesystem\DirectoryList; use Magento\MediaStorage\Helper\File\Storage\Database; +use Magento\Theme\Model\Theme; /** * Image resize service. @@ -90,6 +94,10 @@ class ImageResize * @var Database */ private $fileStorageDatabase; + /** + * @var StoreManagerInterface + */ + private $storeManager; /** * @param State $appState @@ -103,6 +111,8 @@ class ImageResize * @param Collection $themeCollection * @param Filesystem $filesystem * @param Database $fileStorageDatabase + * @param StoreManagerInterface $storeManager + * @throws \Magento\Framework\Exception\FileSystemException * @internal param ProductImage $gallery * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ @@ -117,7 +127,8 @@ public function __construct( ThemeCustomizationConfig $themeCustomizationConfig, Collection $themeCollection, Filesystem $filesystem, - Database $fileStorageDatabase = null + Database $fileStorageDatabase = null, + StoreManagerInterface $storeManager = null ) { $this->appState = $appState; $this->imageConfig = $imageConfig; @@ -131,7 +142,8 @@ public function __construct( $this->mediaDirectory = $filesystem->getDirectoryWrite(DirectoryList::MEDIA); $this->filesystem = $filesystem; $this->fileStorageDatabase = $fileStorageDatabase ?: - \Magento\Framework\App\ObjectManager::getInstance()->get(Database::class); + ObjectManager::getInstance()->get(Database::class); + $this->storeManager = $storeManager ?? ObjectManager::getInstance()->get(StoreManagerInterface::class); } /** @@ -163,10 +175,10 @@ public function resizeFromImageName(string $originalImageName) * Create resized images of different sizes from themes. * * @param array|null $themes - * @return \Generator + * @return Generator * @throws NotFoundException */ - public function resizeFromThemes(array $themes = null): \Generator + public function resizeFromThemes(array $themes = null): Generator { $count = $this->productImage->getCountUsedProductImages(); if (!$count) { @@ -226,7 +238,8 @@ private function getThemesInUse(): array private function getViewImages(array $themes): array { $viewImages = []; - /** @var \Magento\Theme\Model\Theme $theme */ + $stores = $this->storeManager->getStores(true); + /** @var Theme $theme */ foreach ($themes as $theme) { $config = $this->viewConfig->getViewConfig( [ @@ -236,9 +249,12 @@ private function getViewImages(array $themes): array ); $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; + foreach ($stores as $store) { + $data = $this->paramsBuilder->build($imageData, (int) $store->getId()); + $uniqIndex = $this->getUniqueImageIndex($data); + $data['id'] = $imageId; + $viewImages[$uniqIndex] = $data; + } } } return $viewImages; @@ -280,13 +296,13 @@ private function makeImage(string $originalImagePath, array $imageParams): Image /** * Resize image. * - * @param array $viewImage + * @param array $imageParams * @param string $originalImagePath * @param string $originalImageName */ - private function resize(array $viewImage, string $originalImagePath, string $originalImageName) + private function resize(array $imageParams, string $originalImagePath, string $originalImageName) { - $imageParams = $this->paramsBuilder->build($viewImage); + unset($imageParams['id']); $image = $this->makeImage($originalImagePath, $imageParams); $imageAsset = $this->assertImageFactory->create( [ diff --git a/app/code/Magento/MediaStorage/Test/Unit/Service/ImageResizeTest.php b/app/code/Magento/MediaStorage/Test/Unit/Service/ImageResizeTest.php index d612fb9e3b67b..f0e1efa7806e4 100644 --- a/app/code/Magento/MediaStorage/Test/Unit/Service/ImageResizeTest.php +++ b/app/code/Magento/MediaStorage/Test/Unit/Service/ImageResizeTest.php @@ -17,6 +17,7 @@ use Magento\Framework\View\ConfigInterface as ViewConfig; use Magento\Framework\Config\View; use Magento\Catalog\Model\ResourceModel\Product\Image as ProductImage; +use Magento\Store\Model\StoreManagerInterface; use Magento\Theme\Model\Config\Customization as ThemeCustomizationConfig; use Magento\Theme\Model\ResourceModel\Theme\Collection; use Magento\MediaStorage\Helper\File\Storage\Database; @@ -119,7 +120,15 @@ class ImageResizeTest extends \PHPUnit\Framework\TestCase * @var string */ private $testfilepath; + /** + * @var \PHPUnit\Framework\MockObject\MockObject|StoreManagerInterface + */ + private $storeManager; + /** + * @inheritDoc + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ protected function setUp() { $this->testfilename = "image.jpg"; @@ -139,6 +148,7 @@ protected function setUp() $this->themeCollectionMock = $this->createMock(Collection::class); $this->filesystemMock = $this->createMock(Filesystem::class); $this->databaseMock = $this->createMock(Database::class); + $this->storeManager = $this->getMockForAbstractClass(StoreManagerInterface::class); $this->mediaDirectoryMock = $this->getMockBuilder(Filesystem::class) ->disableOriginalConstructor() @@ -203,6 +213,16 @@ protected function setUp() ->method('getViewConfig') ->willReturn($this->viewMock); + $store = $this->getMockForAbstractClass(\Magento\Store\Api\Data\StoreInterface::class); + $store + ->expects($this->any()) + ->method('getId') + ->willReturn(1); + $this->storeManager + ->expects($this->any()) + ->method('getStores') + ->willReturn([$store]); + $this->service = new \Magento\MediaStorage\Service\ImageResize( $this->appStateMock, $this->imageConfigMock, @@ -214,7 +234,8 @@ protected function setUp() $this->themeCustomizationConfigMock, $this->themeCollectionMock, $this->filesystemMock, - $this->databaseMock + $this->databaseMock, + $this->storeManager ); } diff --git a/app/code/Magento/MessageQueue/Setup/ConfigOptionsList.php b/app/code/Magento/MessageQueue/Setup/ConfigOptionsList.php new file mode 100644 index 0000000000000..b23dc7fbbf532 --- /dev/null +++ b/app/code/Magento/MessageQueue/Setup/ConfigOptionsList.php @@ -0,0 +1,108 @@ +selectOptions, + self::CONFIG_PATH_QUEUE_CONSUMERS_WAIT_FOR_MESSAGES, + 'Should consumers wait for a message from the queue? 1 - Yes, 0 - No', + self::DEFAULT_CONSUMERS_WAIT_FOR_MESSAGES + ), + ]; + } + + /** + * @inheritdoc + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function createConfig(array $data, DeploymentConfig $deploymentConfig) + { + $configData = new ConfigData(ConfigFilePool::APP_ENV); + + if (!$this->isDataEmpty($data, self::INPUT_KEY_QUEUE_CONSUMERS_WAIT_FOR_MESSAGES)) { + $configData->set( + self::CONFIG_PATH_QUEUE_CONSUMERS_WAIT_FOR_MESSAGES, + (int)$data[self::INPUT_KEY_QUEUE_CONSUMERS_WAIT_FOR_MESSAGES] + ); + } + + return [$configData]; + } + + /** + * @inheritdoc + */ + public function validate(array $options, DeploymentConfig $deploymentConfig) + { + $errors = []; + + if (!$this->isDataEmpty($options, self::INPUT_KEY_QUEUE_CONSUMERS_WAIT_FOR_MESSAGES) + && !in_array($options[self::INPUT_KEY_QUEUE_CONSUMERS_WAIT_FOR_MESSAGES], $this->selectOptions)) { + $errors[] = 'You can use only 1 or 0 for ' . self::INPUT_KEY_QUEUE_CONSUMERS_WAIT_FOR_MESSAGES . ' option'; + } + + return $errors; + } + + /** + * Check if data ($data) with key ($key) is empty + * + * @param array $data + * @param string $key + * @return bool + */ + private function isDataEmpty(array $data, $key) + { + if (isset($data[$key]) && $data[$key] !== '') { + return false; + } + + return true; + } +} diff --git a/app/code/Magento/Msrp/Setup/Patch/Data/ChangeMsrpAttributeLabel.php b/app/code/Magento/Msrp/Setup/Patch/Data/ChangeMsrpAttributeLabel.php new file mode 100644 index 0000000000000..547287066a30f --- /dev/null +++ b/app/code/Magento/Msrp/Setup/Patch/Data/ChangeMsrpAttributeLabel.php @@ -0,0 +1,66 @@ +categorySetupFactory = $categorySetupFactory; + } + + /** + * @inheritdoc + */ + public function apply() + { + /** @var \Magento\Catalog\Setup\CategorySetup $categorySetup */ + $categorySetup = $this->categorySetupFactory->create(); + $entityTypeId = $categorySetup->getEntityTypeId(Product::ENTITY); + $msrpAttribute = $categorySetup->getAttribute($entityTypeId, 'msrp'); + $categorySetup->updateAttribute( + $entityTypeId, + $msrpAttribute['attribute_id'], + 'frontend_label', + 'Minimum Advertised Price' + ); + } + + /** + * @inheritdoc + */ + public static function getDependencies() + { + return [ + InitializeMsrpAttributes::class, + ]; + } + + /** + * @inheritdoc + */ + public function getAliases() + { + return []; + } +} diff --git a/app/code/Magento/Msrp/view/base/templates/product/price/msrp.phtml b/app/code/Magento/Msrp/view/base/templates/product/price/msrp.phtml index 2ab40a7ec8299..b062e911876c3 100644 --- a/app/code/Magento/Msrp/view/base/templates/product/price/msrp.phtml +++ b/app/code/Magento/Msrp/view/base/templates/product/price/msrp.phtml @@ -59,7 +59,7 @@ $priceElementIdPrefix = $block->getPriceElementIdPrefix() ? $block->getPriceElem $priceElementId = $priceElementIdPrefix . $productId . $block->getIdSuffix(); $popupId = 'msrp-popup-' . $productId . $block->getRandomString(20); - $data = ['addToCart' => [ + $data = [ 'origin'=> 'msrp', 'popupId' => '#' . $popupId, 'productName' => $block->escapeJs($block->escapeHtml($product->getName())), @@ -72,11 +72,11 @@ $priceElementIdPrefix = $block->getPriceElementIdPrefix() ? $block->getPriceElem 'closeButtonId' => '#map-popup-close', 'addToCartUrl' => $addToCartUrl, 'paymentButtons' => '[data-label=or]' - ]]; + ]; if ($block->getRequest()->getFullActionName() === 'catalog_product_view') { - $data['addToCart']['addToCartButton'] = '#product_addtocart_form [type=submit]'; + $data['addToCartButton'] = '#product_addtocart_form [type=submit]'; } else { - $data['addToCart']['addToCartButton'] = sprintf( + $data['addToCartButton'] = sprintf( 'form:has(input[type="hidden"][name="product"][value="%s"]) button[type="submit"]', (int) $productId . ',' . sprintf( @@ -91,7 +91,7 @@ $priceElementIdPrefix = $block->getPriceElementIdPrefix() ? $block->getPriceElem id="= /* @noEscape */ ($popupId) ?>" class="action map-show-info" - data-mage-init='= /* @noEscape */ $this->helper(\Magento\Framework\Json\Helper\Data::class)->jsonEncode($data) ?>'> + data-mage-init='{"addToCart":= /* @noEscape */ $this->helper(\Magento\Framework\Json\Helper\Data::class)->jsonEncode($data) ?>}'> = $block->escapeHtml(__('Click for price')) ?> diff --git a/app/code/Magento/Msrp/view/frontend/layout/catalog_product_view.xml b/app/code/Magento/Msrp/view/frontend/layout/catalog_product_view.xml index 78ec742cc09c6..18a5cb3e09359 100644 --- a/app/code/Magento/Msrp/view/frontend/layout/catalog_product_view.xml +++ b/app/code/Magento/Msrp/view/frontend/layout/catalog_product_view.xml @@ -5,7 +5,7 @@ * See COPYING.txt for license details. */ --> - + diff --git a/app/code/Magento/Multishipping/Block/Checkout/Overview.php b/app/code/Magento/Multishipping/Block/Checkout/Overview.php index 5963e62e948f9..d17da90c58bef 100644 --- a/app/code/Magento/Multishipping/Block/Checkout/Overview.php +++ b/app/code/Magento/Multishipping/Block/Checkout/Overview.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Multishipping\Block\Checkout; use Magento\Framework\Pricing\PriceCurrencyInterface; @@ -12,8 +13,8 @@ * Multishipping checkout overview information * * @api - * @author Magento Core Team - * @since 100.0.2 + * @author Magento Core Team + * @since 100.0.2 */ class Overview extends \Magento\Sales\Block\Items\AbstractItems { @@ -48,13 +49,13 @@ class Overview extends \Magento\Sales\Block\Items\AbstractItems protected $totalsReader; /** - * @param \Magento\Framework\View\Element\Template\Context $context + * @param \Magento\Framework\View\Element\Template\Context $context * @param \Magento\Multishipping\Model\Checkout\Type\Multishipping $multishipping - * @param \Magento\Tax\Helper\Data $taxHelper - * @param PriceCurrencyInterface $priceCurrency - * @param \Magento\Quote\Model\Quote\TotalsCollector $totalsCollector - * @param \Magento\Quote\Model\Quote\TotalsReader $totalsReader - * @param array $data + * @param \Magento\Tax\Helper\Data $taxHelper + * @param PriceCurrencyInterface $priceCurrency + * @param \Magento\Quote\Model\Quote\TotalsCollector $totalsCollector + * @param \Magento\Quote\Model\Quote\TotalsReader $totalsReader + * @param array $data */ public function __construct( \Magento\Framework\View\Element\Template\Context $context, @@ -74,6 +75,37 @@ public function __construct( $this->totalsReader = $totalsReader; } + /** + * Overwrite the total value of shipping amount for viewing purpose + * + * @param array $totals + * @return mixed + * @throws \Exception + */ + private function getMultishippingTotals($totals) + { + if (isset($totals['shipping']) && !empty($totals['shipping'])) { + $total = $totals['shipping']; + $shippingMethod = $total->getAddress()->getShippingMethod(); + if (isset($shippingMethod) && !empty($shippingMethod)) { + $shippingRate = $total->getAddress()->getShippingRateByCode($shippingMethod); + $shippingPrice = $shippingRate->getPrice(); + } else { + $shippingPrice = $total->getAddress()->getShippingAmount(); + } + /** + * @var \Magento\Store\Api\Data\StoreInterface + */ + $store = $this->getQuote()->getStore(); + $amountPrice = $store->getBaseCurrency() + ->convert($shippingPrice, $store->getCurrentCurrencyCode()); + $total->setBaseShippingAmount($shippingPrice); + $total->setShippingAmount($amountPrice); + $total->setValue($amountPrice); + } + return $totals; + } + /** * Initialize default item renderer * @@ -98,6 +130,8 @@ public function getCheckout() } /** + * Get billing address + * * @return Address */ public function getBillingAddress() @@ -106,6 +140,8 @@ public function getBillingAddress() } /** + * Get payment info + * * @return string */ public function getPaymentHtml() @@ -124,6 +160,8 @@ public function getPayment() } /** + * Get shipping addresses + * * @return array */ public function getShippingAddresses() @@ -132,6 +170,8 @@ public function getShippingAddresses() } /** + * Get number of shipping addresses + * * @return int|mixed */ public function getShippingAddressCount() @@ -145,8 +185,10 @@ public function getShippingAddressCount() } /** - * @param Address $address - * @return bool + * Get shipping address rate + * + * @param Address $address + * @return bool * @SuppressWarnings(PHPMD.BooleanGetMethodName) */ public function getShippingAddressRate($address) @@ -159,27 +201,36 @@ public function getShippingAddressRate($address) } /** - * @param Address $address + * Get shipping price including tax + * + * @param Address $address * @return mixed */ public function getShippingPriceInclTax($address) { - $exclTax = $address->getShippingAmount(); + $rate = $address->getShippingRateByCode($address->getShippingMethod()); + $exclTax = $rate->getPrice(); $taxAmount = $address->getShippingTaxAmount(); return $this->formatPrice($exclTax + $taxAmount); } /** - * @param Address $address + * Get shipping price excluding tax + * + * @param Address $address * @return mixed */ public function getShippingPriceExclTax($address) { - return $this->formatPrice($address->getShippingAmount()); + $rate = $address->getShippingRateByCode($address->getShippingMethod()); + $shippingAmount = $rate->getPrice(); + return $this->formatPrice($shippingAmount); } /** - * @param float $price + * Format price + * + * @param float $price * @return mixed * * @codeCoverageIgnore @@ -195,7 +246,9 @@ public function formatPrice($price) } /** - * @param Address $address + * Get shipping address items + * + * @param Address $address * @return array */ public function getShippingAddressItems($address): array @@ -204,7 +257,9 @@ public function getShippingAddressItems($address): array } /** - * @param Address $address + * Get shipping address totals + * + * @param Address $address * @return mixed */ public function getShippingAddressTotals($address) @@ -223,6 +278,8 @@ public function getShippingAddressTotals($address) } /** + * Get total price + * * @return float */ public function getTotal() @@ -231,6 +288,8 @@ public function getTotal() } /** + * Get the Edit addresses URL + * * @return string */ public function getAddressesEditUrl() @@ -239,7 +298,9 @@ public function getAddressesEditUrl() } /** - * @param Address $address + * Get the Edit shipping address URL + * + * @param Address $address * @return string */ public function getEditShippingAddressUrl($address) @@ -248,7 +309,9 @@ public function getEditShippingAddressUrl($address) } /** - * @param Address $address + * Get the Edit billing address URL + * + * @param Address $address * @return string */ public function getEditBillingAddressUrl($address) @@ -257,6 +320,8 @@ public function getEditBillingAddressUrl($address) } /** + * Get the Edit shipping URL + * * @return string */ public function getEditShippingUrl() @@ -265,6 +330,8 @@ public function getEditShippingUrl() } /** + * Get Post ACtion URL + * * @return string */ public function getPostActionUrl() @@ -273,6 +340,8 @@ public function getPostActionUrl() } /** + * Get the Edit billing URL + * * @return string */ public function getEditBillingUrl() @@ -281,6 +350,8 @@ public function getEditBillingUrl() } /** + * Get back button URL + * * @return string */ public function getBackUrl() @@ -319,9 +390,11 @@ public function getQuote() } /** + * Get billin address totals + * + * @return mixed * @deprecated * typo in method name, see getBillingAddressTotals() - * @return mixed */ public function getBillinAddressTotals() { @@ -329,6 +402,8 @@ public function getBillinAddressTotals() } /** + * Get billing address totals + * * @return mixed */ public function getBillingAddressTotals() @@ -338,12 +413,17 @@ public function getBillingAddressTotals() } /** - * @param mixed $totals - * @param null $colspan + * Render total block + * + * @param mixed $totals + * @param null $colspan * @return string */ public function renderTotals($totals, $colspan = null) { + //check if the shipment is multi shipment + $totals = $this->getMultishippingTotals($totals); + if ($colspan === null) { $colspan = 3; } @@ -368,7 +448,7 @@ public function renderTotals($totals, $colspan = null) /** * Return row-level item html * - * @param \Magento\Framework\DataObject $item + * @param \Magento\Framework\DataObject $item * @return string */ public function getRowItemHtml(\Magento\Framework\DataObject $item) @@ -382,7 +462,7 @@ public function getRowItemHtml(\Magento\Framework\DataObject $item) /** * Retrieve renderer block for row-level item output * - * @param string $type + * @param string $type * @return \Magento\Framework\View\Element\AbstractBlock */ protected function _getRowItemRenderer($type) diff --git a/app/code/Magento/Multishipping/Controller/Checkout/Plugin.php b/app/code/Magento/Multishipping/Controller/Checkout/Plugin.php index edd313c106ed3..f88cdfc26fa9f 100644 --- a/app/code/Magento/Multishipping/Controller/Checkout/Plugin.php +++ b/app/code/Magento/Multishipping/Controller/Checkout/Plugin.php @@ -1,11 +1,15 @@ cart->getQuote()->setIsMultiShipping(0); + $quote = $this->cart->getQuote(); + if ($quote->getIsMultiShipping()) { + $quote->setIsMultiShipping(0); + $this->cart->saveQuote(); + } } } diff --git a/app/code/Magento/Multishipping/Model/Cart/CartTotalRepositoryPlugin.php b/app/code/Magento/Multishipping/Model/Cart/CartTotalRepositoryPlugin.php new file mode 100644 index 0000000000000..732bdee314f7c --- /dev/null +++ b/app/code/Magento/Multishipping/Model/Cart/CartTotalRepositoryPlugin.php @@ -0,0 +1,71 @@ +quoteRepository = $quoteRepository; + } + + /** + * Overwrite the CartTotalRepository quoteTotal and update the shipping price + * + * @param CartTotalRepository $subject + * @param Totals $quoteTotals + * @param String $cartId + * @return Totals + * @throws \Magento\Framework\Exception\NoSuchEntityException + * @SuppressWarnings(PHPMD.UnusedLocalVariable) + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function afterGet( + CartTotalRepository $subject, + Totals $quoteTotals, + String $cartId + ) { + $quote = $this->quoteRepository->getActive($cartId); + if ($quote->getIsMultiShipping()) { + $shippingMethod = $quote->getShippingAddress()->getShippingMethod(); + if (isset($shippingMethod) && !empty($shippingMethod)) { + $shippingRate = $quote->getShippingAddress()->getShippingRateByCode($shippingMethod); + $shippingPrice = $shippingRate->getPrice(); + } else { + $shippingPrice = $quote->getShippingAddress()->getShippingAmount(); + } + /** + * @var \Magento\Store\Api\Data\StoreInterface + */ + $store = $quote->getStore(); + $amountPrice = $store->getBaseCurrency() + ->convert($shippingPrice, $store->getCurrentCurrencyCode()); + $quoteTotals->setBaseShippingAmount($shippingPrice); + $quoteTotals->setShippingAmount($amountPrice); + } + return $quoteTotals; + } +} diff --git a/app/code/Magento/Multishipping/Model/Checkout/Type/Multishipping.php b/app/code/Magento/Multishipping/Model/Checkout/Type/Multishipping.php index 42f5289d2109a..7105fd4e9d26d 100644 --- a/app/code/Magento/Multishipping/Model/Checkout/Type/Multishipping.php +++ b/app/code/Magento/Multishipping/Model/Checkout/Type/Multishipping.php @@ -23,6 +23,7 @@ * @SuppressWarnings(PHPMD.TooManyFields) * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) * @since 100.0.2 */ class Multishipping extends \Magento\Framework\DataObject @@ -526,6 +527,7 @@ protected function _addShippingItem($quoteItemId, $data) $quoteItem->setQty($quoteItem->getMultishippingQty()); try { $address = $this->addressRepository->getById($addressId); + // phpcs:ignore Magento2.CodeAnalysis.EmptyBlock } catch (\Exception $e) { } if (isset($address)) { @@ -565,6 +567,7 @@ public function updateQuoteCustomerShippingAddress($addressId) } try { $address = $this->addressRepository->getById($addressId); + // phpcs:ignore Magento2.CodeAnalysis.EmptyBlock } catch (\Exception $e) { // } @@ -592,6 +595,7 @@ public function setQuoteCustomerBillingAddress($addressId) } try { $address = $this->addressRepository->getById($addressId); + // phpcs:ignore Magento2.CodeAnalysis.EmptyBlock } catch (\Exception $e) { // } @@ -716,7 +720,7 @@ protected function _prepareOrder(\Magento\Quote\Model\Quote\Address $address) ); $orderItem = $this->quoteItemToOrderItem->convert($item); if ($item->getParentItem()) { - $orderItem->setParentItem($order->getItemByQuoteItemId($item->getParentItem()->getId())); + $orderItem->setParentItem($order->getItemByQuoteItemId($_quoteItem->getParentItem()->getId())); } $order->addItem($orderItem); } @@ -825,7 +829,7 @@ public function createOrders() if ($order->getCanSendNewEmailFlag()) { $this->orderSender->send($order); } - $placedAddressItems = array_merge($placedAddressItems, $this->getQuoteAddressItems($order)); + $placedAddressItems = $this->getPlacedAddressItems($order); } $addressErrors = []; @@ -1090,10 +1094,14 @@ public function getCustomer() */ protected function isAddressIdApplicable($addressId) { - $applicableAddressIds = array_map(function ($address) { - /** @var \Magento\Customer\Api\Data\AddressInterface $address */ - return $address->getId(); - }, $this->getCustomer()->getAddresses()); + $applicableAddressIds = array_map( + function ($address) { + /** @var \Magento\Customer\Api\Data\AddressInterface $address */ + return $address->getId(); + }, + $this->getCustomer()->getAddresses() + ); + return !is_numeric($addressId) || in_array($addressId, $applicableAddressIds); } @@ -1279,4 +1287,20 @@ private function getQuoteAddressItems(OrderInterface $order): array return $placedAddressItems; } + + /** + * Returns placed address items + * + * @param OrderInterface $order + * @return array + */ + private function getPlacedAddressItems(OrderInterface $order): array + { + $placedAddressItems = []; + foreach ($this->getQuoteAddressItems($order) as $key => $quoteAddressItem) { + $placedAddressItems[$key] = $quoteAddressItem; + } + + return $placedAddressItems; + } } diff --git a/app/code/Magento/Multishipping/Test/Mftf/ActionGroup/CheckingWithMinicartActionGroup.xml b/app/code/Magento/Multishipping/Test/Mftf/ActionGroup/CheckingWithMinicartActionGroup.xml new file mode 100644 index 0000000000000..f648c1026b539 --- /dev/null +++ b/app/code/Magento/Multishipping/Test/Mftf/ActionGroup/CheckingWithMinicartActionGroup.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + $shippingMethodSubtotalPrice + $shippingMethodRadioText + + + + diff --git a/app/code/Magento/Multishipping/Test/Mftf/ActionGroup/CheckingWithMultipleAddressesActionGroup.xml b/app/code/Magento/Multishipping/Test/Mftf/ActionGroup/CheckingWithMultipleAddressesActionGroup.xml new file mode 100644 index 0000000000000..333c2aec6c28e --- /dev/null +++ b/app/code/Magento/Multishipping/Test/Mftf/ActionGroup/CheckingWithMultipleAddressesActionGroup.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/app/code/Magento/Multishipping/Test/Mftf/ActionGroup/PlaceOrderActionGroup.xml b/app/code/Magento/Multishipping/Test/Mftf/ActionGroup/PlaceOrderActionGroup.xml new file mode 100644 index 0000000000000..efb860e314780 --- /dev/null +++ b/app/code/Magento/Multishipping/Test/Mftf/ActionGroup/PlaceOrderActionGroup.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + diff --git a/app/code/Magento/Multishipping/Test/Mftf/ActionGroup/ReviewOrderActionGroup.xml b/app/code/Magento/Multishipping/Test/Mftf/ActionGroup/ReviewOrderActionGroup.xml new file mode 100644 index 0000000000000..af7d897910ca3 --- /dev/null +++ b/app/code/Magento/Multishipping/Test/Mftf/ActionGroup/ReviewOrderActionGroup.xml @@ -0,0 +1,39 @@ + + + + + + + + + + $shippingMethodSubtotalPrice + $shippingMethodBasePrice + + + + + + + + + $firstShippingMethodSubtotalPrice + $firstShippingMethodBasePrice + + + + + + $secondShippingMethodSubtotalPrice + $secondShippingMethodBasePrice + + + + + diff --git a/app/code/Magento/Multishipping/Test/Mftf/ActionGroup/SelectBillingInfoActionGroup.xml b/app/code/Magento/Multishipping/Test/Mftf/ActionGroup/SelectBillingInfoActionGroup.xml new file mode 100644 index 0000000000000..3f7578953df70 --- /dev/null +++ b/app/code/Magento/Multishipping/Test/Mftf/ActionGroup/SelectBillingInfoActionGroup.xml @@ -0,0 +1,16 @@ + + + + + + + + + + diff --git a/app/code/Magento/Multishipping/Test/Mftf/ActionGroup/SelectShippingInfoActionGroup.xml b/app/code/Magento/Multishipping/Test/Mftf/ActionGroup/SelectShippingInfoActionGroup.xml new file mode 100644 index 0000000000000..af0b2467862ba --- /dev/null +++ b/app/code/Magento/Multishipping/Test/Mftf/ActionGroup/SelectShippingInfoActionGroup.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/code/Magento/Multishipping/Test/Mftf/Page/MultishippingCheckoutPage.xml b/app/code/Magento/Multishipping/Test/Mftf/Page/MultishippingCheckoutPage.xml new file mode 100644 index 0000000000000..001002e98271c --- /dev/null +++ b/app/code/Magento/Multishipping/Test/Mftf/Page/MultishippingCheckoutPage.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + diff --git a/app/code/Magento/Multishipping/Test/Mftf/Section/MinicartSection.xml b/app/code/Magento/Multishipping/Test/Mftf/Section/MinicartSection.xml new file mode 100644 index 0000000000000..1a31911bd185e --- /dev/null +++ b/app/code/Magento/Multishipping/Test/Mftf/Section/MinicartSection.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + diff --git a/app/code/Magento/Multishipping/Test/Mftf/Section/MultishippingSection.xml b/app/code/Magento/Multishipping/Test/Mftf/Section/MultishippingSection.xml index e7d57af1172c6..45fafc3105c38 100644 --- a/app/code/Magento/Multishipping/Test/Mftf/Section/MultishippingSection.xml +++ b/app/code/Magento/Multishipping/Test/Mftf/Section/MultishippingSection.xml @@ -8,7 +8,17 @@ + + + + + + + + + + diff --git a/app/code/Magento/Multishipping/Test/Mftf/Section/PaymentMethodSection.xml b/app/code/Magento/Multishipping/Test/Mftf/Section/PaymentMethodSection.xml new file mode 100644 index 0000000000000..4e7f4a497ad4d --- /dev/null +++ b/app/code/Magento/Multishipping/Test/Mftf/Section/PaymentMethodSection.xml @@ -0,0 +1,14 @@ + + + + + + + + diff --git a/app/code/Magento/Multishipping/Test/Mftf/Section/ReviewOrderSection.xml b/app/code/Magento/Multishipping/Test/Mftf/Section/ReviewOrderSection.xml new file mode 100644 index 0000000000000..e13f28929dcc8 --- /dev/null +++ b/app/code/Magento/Multishipping/Test/Mftf/Section/ReviewOrderSection.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/app/code/Magento/Multishipping/Test/Mftf/Section/ShippingMethodSection.xml b/app/code/Magento/Multishipping/Test/Mftf/Section/ShippingMethodSection.xml new file mode 100644 index 0000000000000..6a2290bcf1a43 --- /dev/null +++ b/app/code/Magento/Multishipping/Test/Mftf/Section/ShippingMethodSection.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + diff --git a/app/code/Magento/Multishipping/Test/Mftf/Test/StoreFrontCheckingWithMultishipmentTest.xml b/app/code/Magento/Multishipping/Test/Mftf/Test/StoreFrontCheckingWithMultishipmentTest.xml new file mode 100644 index 0000000000000..271f6e707cd69 --- /dev/null +++ b/app/code/Magento/Multishipping/Test/Mftf/Test/StoreFrontCheckingWithMultishipmentTest.xml @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/code/Magento/Multishipping/Test/Mftf/Test/StoreFrontCheckingWithSingleShipmentTest.xml b/app/code/Magento/Multishipping/Test/Mftf/Test/StoreFrontCheckingWithSingleShipmentTest.xml new file mode 100644 index 0000000000000..f425a44130ecb --- /dev/null +++ b/app/code/Magento/Multishipping/Test/Mftf/Test/StoreFrontCheckingWithSingleShipmentTest.xml @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/code/Magento/Multishipping/Test/Mftf/Test/StoreFrontMinicartWithMultishipmentTest.xml b/app/code/Magento/Multishipping/Test/Mftf/Test/StoreFrontMinicartWithMultishipmentTest.xml new file mode 100644 index 0000000000000..8d5a58acc7e18 --- /dev/null +++ b/app/code/Magento/Multishipping/Test/Mftf/Test/StoreFrontMinicartWithMultishipmentTest.xml @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/code/Magento/Multishipping/Test/Unit/Controller/Checkout/PluginTest.php b/app/code/Magento/Multishipping/Test/Unit/Controller/Checkout/PluginTest.php index 1b1474dbed28a..a26f2661ebab1 100644 --- a/app/code/Magento/Multishipping/Test/Unit/Controller/Checkout/PluginTest.php +++ b/app/code/Magento/Multishipping/Test/Unit/Controller/Checkout/PluginTest.php @@ -4,6 +4,9 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +declare(strict_types=1); + namespace Magento\Multishipping\Test\Unit\Controller\Checkout; use Magento\Multishipping\Controller\Checkout\Plugin; @@ -30,16 +33,27 @@ protected function setUp() $this->cartMock = $this->createMock(\Magento\Checkout\Model\Cart::class); $this->quoteMock = $this->createPartialMock( \Magento\Quote\Model\Quote::class, - ['__wakeUp', 'setIsMultiShipping'] + ['__wakeUp', 'setIsMultiShipping', 'getIsMultiShipping'] ); $this->cartMock->expects($this->once())->method('getQuote')->will($this->returnValue($this->quoteMock)); $this->object = new \Magento\Multishipping\Controller\Checkout\Plugin($this->cartMock); } - public function testExecuteTurnsOffMultishippingModeOnQuote() + public function testExecuteTurnsOffMultishippingModeOnMultishippingQuote(): void { $subject = $this->createMock(\Magento\Checkout\Controller\Index\Index::class); + $this->quoteMock->expects($this->once())->method('getIsMultiShipping')->willReturn(1); $this->quoteMock->expects($this->once())->method('setIsMultiShipping')->with(0); + $this->cartMock->expects($this->once())->method('saveQuote'); + $this->object->beforeExecute($subject); + } + + public function testExecuteTurnsOffMultishippingModeOnNotMultishippingQuote(): void + { + $subject = $this->createMock(\Magento\Checkout\Controller\Index\Index::class); + $this->quoteMock->expects($this->once())->method('getIsMultiShipping')->willReturn(0); + $this->quoteMock->expects($this->never())->method('setIsMultiShipping'); + $this->cartMock->expects($this->never())->method('saveQuote'); $this->object->beforeExecute($subject); } } diff --git a/app/code/Magento/Multishipping/Test/Unit/Model/Cart/CartTotalRepositoryPluginTest.php b/app/code/Magento/Multishipping/Test/Unit/Model/Cart/CartTotalRepositoryPluginTest.php new file mode 100644 index 0000000000000..73b0b9ef3ca7a --- /dev/null +++ b/app/code/Magento/Multishipping/Test/Unit/Model/Cart/CartTotalRepositoryPluginTest.php @@ -0,0 +1,81 @@ +quoteRepositoryMock = $this->createMock(\Magento\Quote\Api\CartRepositoryInterface::class); + $this->modelRepository = new \Magento\Multishipping\Model\Cart\CartTotalRepositoryPlugin( + $this->quoteRepositoryMock + ); + } + + /** + * Test quotTotal from cartRepository after get($cartId) function is called + */ + public function testAfterGet() + { + $cartId = "10"; + $shippingMethod = 'flatrate_flatrate'; + $shippingPrice = '10.00'; + $quoteMock = $this->createPartialMock( + \Magento\Quote\Model\Cart\Totals::class, + [ + 'getStore', + 'getShippingAddress', + 'getIsMultiShipping' + ] + ); + $this->quoteRepositoryMock->expects($this->once())->method('getActive')->with($cartId)->willReturn($quoteMock); + $quoteMock->expects($this->once())->method('getIsMultiShipping')->willReturn(true); + $shippingAddressMock = $this->createPartialMock( + \Magento\Quote\Model\Quote\Address::class, + [ + 'getShippingMethod', + 'getShippingRateByCode', + 'getShippingAmount' + ] + ); + $quoteMock->expects($this->any())->method('getShippingAddress')->willReturn($shippingAddressMock); + + $shippingAddressMock->expects($this->once())->method('getShippingMethod')->willReturn($shippingMethod); + $shippingAddressMock->expects($this->any())->method('getShippingAmount')->willReturn($shippingPrice); + $shippingRateMock = $this->createPartialMock( + \Magento\Quote\Model\Quote\Address\Rate::class, + [ + 'getPrice' + ] + ); + $shippingAddressMock->expects($this->once())->method('getShippingRateByCode')->willReturn($shippingRateMock); + + $shippingRateMock->expects($this->once())->method('getPrice')->willReturn($shippingPrice); + + $storeMock = $this->getMockBuilder(\Magento\Store\Model\Store::class) + ->disableOriginalConstructor() + ->getMock(); + $quoteMock->expects($this->any())->method('getStore')->willReturn($storeMock); + $storeMock->expects($this->any())->method('getBaseCurrency')->willReturnSelf(); + + $this->modelRepository->afterGet( + $this->createMock(\Magento\Quote\Model\Cart\CartTotalRepository::class), + $quoteMock, + $cartId + ); + } +} diff --git a/app/code/Magento/Multishipping/etc/di.xml b/app/code/Magento/Multishipping/etc/di.xml new file mode 100644 index 0000000000000..3bccf0b74bcd8 --- /dev/null +++ b/app/code/Magento/Multishipping/etc/di.xml @@ -0,0 +1,12 @@ + + + + + + + diff --git a/app/code/Magento/MysqlMq/etc/adminhtml/system.xml b/app/code/Magento/MysqlMq/etc/adminhtml/system.xml index 2684f2e0c98bf..045a176a48e87 100644 --- a/app/code/Magento/MysqlMq/etc/adminhtml/system.xml +++ b/app/code/Magento/MysqlMq/etc/adminhtml/system.xml @@ -13,15 +13,19 @@ All the times are in minutes. Use "0" if you want to skip automatic clearance. Retry Messages In Progress After + validate-zero-or-greater validate-digits Successful Messages Lifetime + validate-zero-or-greater validate-digits Failed Messages Lifetime + validate-zero-or-greater validate-digits New Messages Lifetime + validate-zero-or-greater validate-digits diff --git a/app/code/Magento/Newsletter/Model/Config.php b/app/code/Magento/Newsletter/Model/Config.php new file mode 100644 index 0000000000000..c469d35e74f72 --- /dev/null +++ b/app/code/Magento/Newsletter/Model/Config.php @@ -0,0 +1,48 @@ +scopeConfig = $scopeConfig; + } + + /** + * Returns newsletter's enabled status + * + * @param string $scopeType + * @return bool + */ + public function isActive(string $scopeType = ScopeConfigInterface::SCOPE_TYPE_DEFAULT): bool + { + return $this->scopeConfig->isSetFlag(self::XML_PATH_NEWSLETTER_ACTIVE, $scopeType); + } +} diff --git a/app/code/Magento/Newsletter/Model/Subscriber.php b/app/code/Magento/Newsletter/Model/Subscriber.php index 117783495406a..c5eee5e3cf771 100644 --- a/app/code/Magento/Newsletter/Model/Subscriber.php +++ b/app/code/Magento/Newsletter/Model/Subscriber.php @@ -353,11 +353,7 @@ public function isStatusChanged() */ public function isSubscribed() { - if ($this->getId() && $this->getStatus() == self::STATUS_SUBSCRIBED) { - return true; - } - - return false; + return $this->getId() && (int)$this->getStatus() === self::STATUS_SUBSCRIBED; } /** @@ -495,7 +491,6 @@ public function subscribe($email) $this->sendConfirmationSuccessEmail(); } return $this->getStatus(); - // phpcs:ignore Magento2.Exceptions.ThrowCatch } catch (\Exception $e) { // phpcs:ignore Magento2.Exceptions.DirectThrow throw new \Exception($e->getMessage()); diff --git a/app/code/Magento/Newsletter/Observer/PredispatchNewsletterObserver.php b/app/code/Magento/Newsletter/Observer/PredispatchNewsletterObserver.php index 9860798b2b9f3..f63520e79496f 100644 --- a/app/code/Magento/Newsletter/Observer/PredispatchNewsletterObserver.php +++ b/app/code/Magento/Newsletter/Observer/PredispatchNewsletterObserver.php @@ -12,6 +12,8 @@ use Magento\Framework\Event\ObserverInterface; use Magento\Framework\UrlInterface; use Magento\Store\Model\ScopeInterface; +use Magento\Newsletter\Model\Config; +use Magento\Framework\App\ObjectManager; /** * Class PredispatchNewsletterObserver @@ -19,10 +21,16 @@ class PredispatchNewsletterObserver implements ObserverInterface { /** - * Configuration path to newsletter active setting + * @deprecated + * @see \Magento\Newsletter\Model\Config::isActive() */ const XML_PATH_NEWSLETTER_ACTIVE = 'newsletter/general/active'; + /** + * @var Config + */ + private $newsletterConfig; + /** * @var ScopeConfigInterface */ @@ -38,11 +46,16 @@ class PredispatchNewsletterObserver implements ObserverInterface * * @param ScopeConfigInterface $scopeConfig * @param UrlInterface $url + * @param Config|null $newsletterConfig */ - public function __construct(ScopeConfigInterface $scopeConfig, UrlInterface $url) - { + public function __construct( + ScopeConfigInterface $scopeConfig, + UrlInterface $url, + Config $newsletterConfig = null + ) { $this->scopeConfig = $scopeConfig; $this->url = $url; + $this->newsletterConfig = $newsletterConfig ?: ObjectManager::getInstance()->get(Config::class); } /** @@ -52,11 +65,7 @@ public function __construct(ScopeConfigInterface $scopeConfig, UrlInterface $url */ public function execute(Observer $observer) : void { - if (!$this->scopeConfig->getValue( - self::XML_PATH_NEWSLETTER_ACTIVE, - ScopeInterface::SCOPE_STORE - ) - ) { + if (!$this->newsletterConfig->isActive(ScopeInterface::SCOPE_STORE)) { $defaultNoRouteUrl = $this->scopeConfig->getValue( 'web/default/no_route', ScopeInterface::SCOPE_STORE diff --git a/app/code/Magento/Newsletter/Test/Mftf/Test/AdminAddImageToWYSIWYGNewsletterTest.xml b/app/code/Magento/Newsletter/Test/Mftf/Test/AdminAddImageToWYSIWYGNewsletterTest.xml index 510f3e16e8d8e..0371c0265d149 100644 --- a/app/code/Magento/Newsletter/Test/Mftf/Test/AdminAddImageToWYSIWYGNewsletterTest.xml +++ b/app/code/Magento/Newsletter/Test/Mftf/Test/AdminAddImageToWYSIWYGNewsletterTest.xml @@ -16,9 +16,6 @@ - - - diff --git a/app/code/Magento/Newsletter/Test/Unit/Observer/PredispatchNewsletterObserverTest.php b/app/code/Magento/Newsletter/Test/Unit/Observer/PredispatchNewsletterObserverTest.php index 38d69e5128af1..6846231319d69 100644 --- a/app/code/Magento/Newsletter/Test/Unit/Observer/PredispatchNewsletterObserverTest.php +++ b/app/code/Magento/Newsletter/Test/Unit/Observer/PredispatchNewsletterObserverTest.php @@ -13,6 +13,7 @@ use Magento\Framework\Event\Observer; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; use Magento\Framework\UrlInterface; +use Magento\Newsletter\Model\Config; use Magento\Newsletter\Observer\PredispatchNewsletterObserver; use Magento\Store\Model\ScopeInterface; use PHPUnit\Framework\TestCase; @@ -52,30 +53,29 @@ class PredispatchNewsletterObserverTest extends TestCase */ private $objectManager; + /** + * @var Config + */ + private $newsletterConfig; + /** * @inheritdoc */ protected function setUp() : void { - $this->configMock = $this->getMockBuilder(ScopeConfigInterface::class) - ->disableOriginalConstructor() - ->getMock(); - $this->urlMock = $this->getMockBuilder(UrlInterface::class) - ->disableOriginalConstructor() - ->getMock(); + $this->configMock = $this->createMock(ScopeConfigInterface::class); + $this->urlMock = $this->createMock(UrlInterface::class); $this->responseMock = $this->getMockBuilder(ResponseInterface::class) ->disableOriginalConstructor() ->setMethods(['setRedirect']) ->getMockForAbstractClass(); - $this->redirectMock = $this->getMockBuilder(RedirectInterface::class) - ->getMock(); + $this->redirectMock = $this->createMock(RedirectInterface::class); + $this->newsletterConfig = $this->createMock(Config::class); $this->objectManager = new ObjectManager($this); - $this->mockObject = $this->objectManager->getObject( - PredispatchNewsletterObserver::class, - [ - 'scopeConfig' => $this->configMock, - 'url' => $this->urlMock - ] + $this->mockObject = new PredispatchNewsletterObserver( + $this->configMock, + $this->urlMock, + $this->newsletterConfig ); } @@ -89,8 +89,9 @@ public function testNewsletterEnabled() : void ->setMethods(['getResponse', 'getData', 'setRedirect']) ->getMockForAbstractClass(); - $this->configMock->method('getValue') - ->with(PredispatchNewsletterObserver::XML_PATH_NEWSLETTER_ACTIVE, ScopeInterface::SCOPE_STORE) + $this->newsletterConfig->expects($this->once()) + ->method('isActive') + ->with(ScopeInterface::SCOPE_STORE) ->willReturn(true); $observerMock->expects($this->never()) ->method('getData') @@ -114,14 +115,13 @@ public function testNewsletterDisabled() : void ->setMethods(['getControllerAction', 'getResponse']) ->getMockForAbstractClass(); - $this->configMock->expects($this->at(0)) - ->method('getValue') - ->with(PredispatchNewsletterObserver::XML_PATH_NEWSLETTER_ACTIVE, ScopeInterface::SCOPE_STORE) + $this->newsletterConfig->expects($this->once()) + ->method('isActive') + ->with(ScopeInterface::SCOPE_STORE) ->willReturn(false); $expectedRedirectUrl = 'https://test.com/index'; - - $this->configMock->expects($this->at(1)) + $this->configMock->expects($this->once()) ->method('getValue') ->with('web/default/no_route', ScopeInterface::SCOPE_STORE) ->willReturn($expectedRedirectUrl); diff --git a/app/code/Magento/Newsletter/i18n/en_US.csv b/app/code/Magento/Newsletter/i18n/en_US.csv index 388b583f990b1..f390f6792635d 100644 --- a/app/code/Magento/Newsletter/i18n/en_US.csv +++ b/app/code/Magento/Newsletter/i18n/en_US.csv @@ -152,3 +152,4 @@ Store,Store "Store View","Store View" "Newsletter Subscriptions","Newsletter Subscriptions" "We have updated your subscription.","We have updated your subscription." +"Are you sure you want to delete the selected subscriber(s)?","Are you sure you want to delete the selected subscriber(s)?" diff --git a/app/code/Magento/Newsletter/view/adminhtml/layout/newsletter_subscriber_block.xml b/app/code/Magento/Newsletter/view/adminhtml/layout/newsletter_subscriber_block.xml index e8600c2d49d7b..aef84c068100a 100644 --- a/app/code/Magento/Newsletter/view/adminhtml/layout/newsletter_subscriber_block.xml +++ b/app/code/Magento/Newsletter/view/adminhtml/layout/newsletter_subscriber_block.xml @@ -29,6 +29,7 @@ Delete */*/massDelete + Are you sure you want to delete the selected subscriber(s)? diff --git a/app/code/Magento/Newsletter/view/adminhtml/templates/preview/iframeswitcher.phtml b/app/code/Magento/Newsletter/view/adminhtml/templates/preview/iframeswitcher.phtml index 5175080add914..20ff63a60a263 100644 --- a/app/code/Magento/Newsletter/view/adminhtml/templates/preview/iframeswitcher.phtml +++ b/app/code/Magento/Newsletter/view/adminhtml/templates/preview/iframeswitcher.phtml @@ -17,6 +17,7 @@ getEvent()->getPayment(); $instructionMethods = [ Banktransfer::PAYMENT_METHOD_BANKTRANSFER_CODE, Cashondelivery::PAYMENT_METHOD_CASHONDELIVERY_CODE ]; if (in_array($payment->getMethod(), $instructionMethods)) { - $payment->setAdditionalInformation( - 'instructions', - $payment->getMethodInstance()->getInstructions() - ); + $payment->setAdditionalInformation('instructions', $this->getInstructions($payment)); } elseif ($payment->getMethod() === Checkmo::PAYMENT_METHOD_CHECKMO_CODE) { $methodInstance = $payment->getMethodInstance(); if (!empty($methodInstance->getPayableTo())) { @@ -45,4 +45,17 @@ public function execute(\Magento\Framework\Event\Observer $observer) } } } + + /** + * Retrieve store-specific payment method instructions, or already saved if exists. + * + * @param Payment $payment + * @return string|null + * @throws \Magento\Framework\Exception\LocalizedException + */ + private function getInstructions(Payment $payment): ?string + { + return $payment->getAdditionalInformation('instructions') + ?: $payment->getMethodInstance()->getConfigData('instructions', $payment->getOrder()->getStoreId()); + } } diff --git a/app/code/Magento/OfflinePayments/Test/Unit/Observer/BeforeOrderPaymentSaveObserverTest.php b/app/code/Magento/OfflinePayments/Test/Unit/Observer/BeforeOrderPaymentSaveObserverTest.php index 51edaea0e659c..57c5ec533dc64 100644 --- a/app/code/Magento/OfflinePayments/Test/Unit/Observer/BeforeOrderPaymentSaveObserverTest.php +++ b/app/code/Magento/OfflinePayments/Test/Unit/Observer/BeforeOrderPaymentSaveObserverTest.php @@ -11,6 +11,7 @@ use Magento\OfflinePayments\Model\Banktransfer; use Magento\OfflinePayments\Model\Cashondelivery; use Magento\OfflinePayments\Observer\BeforeOrderPaymentSaveObserver; +use Magento\Sales\Model\Order; use Magento\Sales\Model\Order\Payment; use PHPUnit_Framework_MockObject_MockObject as MockObject; use Magento\OfflinePayments\Model\Checkmo; @@ -76,19 +77,12 @@ public function testBeforeOrderPaymentSaveWithInstructions($methodCode) $this->payment->expects(self::once()) ->method('getMethod') ->willReturn($methodCode); + $this->payment->method('getAdditionalInformation') + ->with('instructions') + ->willReturn('payment configuration'); $this->payment->expects(self::once()) ->method('setAdditionalInformation') ->with('instructions', 'payment configuration'); - $method = $this->getMockBuilder(Banktransfer::class) - ->disableOriginalConstructor() - ->getMock(); - - $method->expects(self::once()) - ->method('getInstructions') - ->willReturn('payment configuration'); - $this->payment->expects(self::once()) - ->method('getMethodInstance') - ->willReturn($method); $this->_model->execute($this->observer); } diff --git a/app/code/Magento/OfflinePayments/composer.json b/app/code/Magento/OfflinePayments/composer.json index 4de112ac72152..7bf4b78628a70 100644 --- a/app/code/Magento/OfflinePayments/composer.json +++ b/app/code/Magento/OfflinePayments/composer.json @@ -8,7 +8,8 @@ "php": "~7.1.3||~7.2.0||~7.3.0", "magento/framework": "*", "magento/module-checkout": "*", - "magento/module-payment": "*" + "magento/module-payment": "*", + "magento/module-sales": "*" }, "suggest": { "magento/module-config": "*" diff --git a/app/code/Magento/OfflineShipping/etc/adminhtml/system.xml b/app/code/Magento/OfflineShipping/etc/adminhtml/system.xml index 4db5f489aa4a2..2b29d2211b9d1 100644 --- a/app/code/Magento/OfflineShipping/etc/adminhtml/system.xml +++ b/app/code/Magento/OfflineShipping/etc/adminhtml/system.xml @@ -31,6 +31,7 @@ Sort Order + validate-number validate-zero-or-greater Title @@ -92,6 +93,7 @@ Sort Order + validate-number validate-zero-or-greater Title @@ -130,6 +132,7 @@ Sort Order + validate-number validate-zero-or-greater Title diff --git a/app/code/Magento/PageCache/Test/Mftf/Test/AdminFrontendAreaSessionMustNotAffectAdminAreaTest.xml b/app/code/Magento/PageCache/Test/Mftf/Test/AdminFrontendAreaSessionMustNotAffectAdminAreaTest.xml new file mode 100644 index 0000000000000..375211e5f2f51 --- /dev/null +++ b/app/code/Magento/PageCache/Test/Mftf/Test/AdminFrontendAreaSessionMustNotAffectAdminAreaTest.xml @@ -0,0 +1,89 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/code/Magento/Payment/Block/Info/Instructions.php b/app/code/Magento/Payment/Block/Info/Instructions.php index 687c6b54a2f4f..f670fa6925bfb 100644 --- a/app/code/Magento/Payment/Block/Info/Instructions.php +++ b/app/code/Magento/Payment/Block/Info/Instructions.php @@ -25,6 +25,18 @@ class Instructions extends \Magento\Payment\Block\Info */ protected $_template = 'Magento_Payment::info/instructions.phtml'; + /** + * Gets payment method title for appropriate store. + * + * @return string + * @throws \Magento\Framework\Exception\LocalizedException + */ + public function getTitle() + { + return $this->getInfo()->getAdditionalInformation('method_title') + ?: $this->getMethod()->getConfigData('title', $this->getInfo()->getOrder()->getStoreId()); + } + /** * Get instructions text from order payment * (or from config, if instructions are missed in payment) diff --git a/app/code/Magento/Payment/Test/Unit/Block/Info/InstructionsTest.php b/app/code/Magento/Payment/Test/Unit/Block/Info/InstructionsTest.php index 68c76d94e02ae..88144b6e05c62 100644 --- a/app/code/Magento/Payment/Test/Unit/Block/Info/InstructionsTest.php +++ b/app/code/Magento/Payment/Test/Unit/Block/Info/InstructionsTest.php @@ -4,11 +4,12 @@ * See COPYING.txt for license details. */ -/** - * Test class for \Magento\Payment\Block\Info\Instructions - */ namespace Magento\Payment\Test\Unit\Block\Info; +use Magento\Payment\Model\MethodInterface; +use Magento\Sales\Model\Order; +use PHPUnit\Framework\MockObject\MockObject; + class InstructionsTest extends \PHPUnit\Framework\TestCase { /** @@ -25,10 +26,59 @@ protected function setUp() { $context = $this->createMock(\Magento\Framework\View\Element\Template\Context::class); $this->_instructions = new \Magento\Payment\Block\Info\Instructions($context); - $this->_info = $this->createMock(\Magento\Payment\Model\Info::class); + $this->_info = $this->getMockBuilder(\Magento\Payment\Model\Info::class) + ->setMethods( + [ + 'getOrder', + 'getAdditionalInformation', + 'getMethodInstance' + ] + ) + ->disableOriginalConstructor() + ->getMock(); $this->_instructions->setData('info', $this->_info); } + /** + * @return void + * @throws \Magento\Framework\Exception\LocalizedException + */ + public function testGetTitleFromPaymentAdditionalData() + { + $this->_info->method('getAdditionalInformation') + ->with('method_title') + ->willReturn('payment_method_title'); + + $this->getMethod()->expects($this->never()) + ->method('getConfigData'); + + $this->assertEquals($this->_instructions->getTitle(), 'payment_method_title'); + } + + /** + * @return void + * @throws \Magento\Framework\Exception\LocalizedException + */ + public function testGetTitleFromPaymentMethodConfig() + { + $this->_info->method('getAdditionalInformation') + ->with('method_title') + ->willReturn(null); + + $this->getMethod()->expects($this->once()) + ->method('getConfigData') + ->with('title', null) + ->willReturn('payment_method_title'); + + $order = $this->getOrder(); + $this->_info->method('getOrder')->willReturn($order); + + $this->assertEquals($this->_instructions->getTitle(), 'payment_method_title'); + } + + /** + * @return void + */ public function testGetInstructionAdditionalInformation() { $this->_info->expects($this->once()) @@ -41,10 +91,13 @@ public function testGetInstructionAdditionalInformation() $this->assertEquals('get the instruction here', $this->_instructions->getInstructions()); } + /** + * @return void + */ public function testGetInstruction() { $methodInstance = $this->getMockBuilder( - \Magento\Payment\Model\MethodInterface::class + MethodInterface::class )->getMockForAbstractClass(); $methodInstance->expects($this->once()) ->method('getConfigData') @@ -59,4 +112,27 @@ public function testGetInstruction() ->willReturn($methodInstance); $this->assertEquals('get the instruction here', $this->_instructions->getInstructions()); } + + /** + * @return MethodInterface|MockObject + */ + private function getMethod() + { + $method = $this->getMockBuilder(MethodInterface::class) + ->getMockForAbstractClass(); + $this->_info->method('getMethodInstance') + ->willReturn($method); + + return $method; + } + + /** + * @return Order|MockObject + */ + private function getOrder() + { + return $this->getMockBuilder(Order::class) + ->disableOriginalConstructor() + ->getMock(); + } } diff --git a/app/code/Magento/Payment/view/adminhtml/templates/info/instructions.phtml b/app/code/Magento/Payment/view/adminhtml/templates/info/instructions.phtml index f60c1d063addf..904f0bd2e2cdc 100644 --- a/app/code/Magento/Payment/view/adminhtml/templates/info/instructions.phtml +++ b/app/code/Magento/Payment/view/adminhtml/templates/info/instructions.phtml @@ -9,7 +9,7 @@ * @see \Magento\Payment\Block\Info */ ?> -= $block->escapeHtml($block->getMethod()->getTitle()) ?> += $block->escapeHtml($block->getTitle()) ?> getInstructions()) : ?> diff --git a/app/code/Magento/Payment/view/frontend/templates/info/instructions.phtml b/app/code/Magento/Payment/view/frontend/templates/info/instructions.phtml index 60efae16b1711..a8d2b15c3ea31 100644 --- a/app/code/Magento/Payment/view/frontend/templates/info/instructions.phtml +++ b/app/code/Magento/Payment/view/frontend/templates/info/instructions.phtml @@ -10,7 +10,7 @@ */ ?> - = $block->escapeHtml($block->getMethod()->getTitle()) ?> + = $block->escapeHtml($block->getTitle()) ?> getInstructions()) : ?> = /* @noEscape */ nl2br($block->escapeHtml($block->getInstructions())) ?> diff --git a/app/code/Magento/Paypal/Block/Adminhtml/System/Config/MultiSelect/DisabledFundingOptions.php b/app/code/Magento/Paypal/Block/Adminhtml/System/Config/MultiSelect/DisabledFundingOptions.php index bad4dad4c0955..3888e32efed43 100644 --- a/app/code/Magento/Paypal/Block/Adminhtml/System/Config/MultiSelect/DisabledFundingOptions.php +++ b/app/code/Magento/Paypal/Block/Adminhtml/System/Config/MultiSelect/DisabledFundingOptions.php @@ -38,21 +38,6 @@ public function __construct( parent::__construct($context, $data); } - /** - * Render country field considering request parameter - * - * @param AbstractElement $element - * @return string - */ - public function render(AbstractElement $element) - { - if (!$this->isSelectedMerchantCountry('US')) { - $fundingOptions = $element->getValues(); - $element->setValues($this->filterValuesForPaypalCredit($fundingOptions)); - } - return parent::render($element); - } - /** * Getting the name of a UI attribute * @@ -62,30 +47,4 @@ protected function getDataAttributeName(): string { return 'disable-funding-options'; } - - /** - * Filters array for CREDIT - * - * @param array $options - * @return array - */ - private function filterValuesForPaypalCredit($options): array - { - return array_filter($options, function ($opt) { - return ($opt['value'] !== 'CREDIT'); - }); - } - - /** - * Checks for chosen Merchant country from the config/url - * - * @param string $country - * @return bool - */ - private function isSelectedMerchantCountry(string $country): bool - { - $merchantCountry = $this->getRequest()->getParam(StructurePlugin::REQUEST_PARAM_COUNTRY) - ?: $this->config->getMerchantCountry(); - return $merchantCountry === $country; - } } diff --git a/app/code/Magento/Paypal/Model/AbstractConfig.php b/app/code/Magento/Paypal/Model/AbstractConfig.php index 41f122ed9b3c9..cf7e8009dab65 100644 --- a/app/code/Magento/Paypal/Model/AbstractConfig.php +++ b/app/code/Magento/Paypal/Model/AbstractConfig.php @@ -293,7 +293,7 @@ public function isMethodActive($method) case Config::METHOD_WPS_BML: case Config::METHOD_WPP_BML: $disabledFunding = $this->_scopeConfig->getValue( - 'payment/paypal_express/disable_funding_options', + 'paypal/style/disable_funding_options', ScopeInterface::SCOPE_STORE, $this->_storeId ); diff --git a/app/code/Magento/Paypal/Model/Api/Nvp.php b/app/code/Magento/Paypal/Model/Api/Nvp.php index 624068395394d..2ec88df492fb9 100644 --- a/app/code/Magento/Paypal/Model/Api/Nvp.php +++ b/app/code/Magento/Paypal/Model/Api/Nvp.php @@ -6,11 +6,13 @@ namespace Magento\Paypal\Model\Api; +use Magento\Framework\DataObject; use Magento\Payment\Model\Cart; use Magento\Payment\Model\Method\Logger; /** * NVP API wrappers model + * * @TODO: move some parts to abstract, don't hesitate to throw exceptions on api calls * * @method string getToken() @@ -1085,11 +1087,11 @@ public function callUpdateBillingAgreement() * Import callback request array into $this public data * * @param array $request - * @return \Magento\Framework\DataObject + * @return DataObject */ public function prepareShippingOptionsCallbackAddress(array $request) { - $address = new \Magento\Framework\DataObject(); + $address = new DataObject(); \Magento\Framework\DataObject\Mapper::accumulateByMap($request, $address, $this->_callbackRequestMap); $address->setExportedKeys(array_values($this->_callbackRequestMap)); $this->_applyStreetAndRegionWorkarounds($address); @@ -1126,6 +1128,7 @@ protected function _addMethodToRequest($methodName, $request) /** * Additional response processing. + * * Hack to cut off length from API type response params. * * @param array $response @@ -1414,6 +1417,7 @@ protected function _validateResponse($method, $response) /** * Parse an NVP response string into an associative array + * * @param string $nvpstr * @return array */ @@ -1477,7 +1481,7 @@ protected function _exportAddressses($data) */ protected function _exportAddresses($data) { - $address = new \Magento\Framework\DataObject(); + $address = new DataObject(); \Magento\Framework\DataObject\Mapper::accumulateByMap($data, $address, $this->_billingAddressMap); $address->setExportedKeys(array_values($this->_billingAddressMap)); $this->_applyStreetAndRegionWorkarounds($address); @@ -1488,7 +1492,7 @@ protected function _exportAddresses($data) \Magento\Framework\DataObject\Mapper::accumulateByMap($data, $shippingAddress, $this->_shippingAddressMap); $this->_applyStreetAndRegionWorkarounds($shippingAddress); // PayPal doesn't provide detailed shipping name fields, so the name will be overwritten - $shippingAddress->addData(['firstname' => $data['SHIPTONAME']]); + $this->updateShippingAddressWithShipToName($shippingAddress, $data); $this->setExportedShippingAddress($shippingAddress); } } @@ -1496,10 +1500,10 @@ protected function _exportAddresses($data) /** * Adopt specified address object to be compatible with Magento * - * @param \Magento\Framework\DataObject $address + * @param DataObject $address * @return void */ - protected function _applyStreetAndRegionWorkarounds(\Magento\Framework\DataObject $address) + protected function _applyStreetAndRegionWorkarounds(DataObject $address) { // merge street addresses into 1 if ($address->getData('street2') !== null) { @@ -1515,11 +1519,10 @@ protected function _applyStreetAndRegionWorkarounds(\Magento\Framework\DataObjec )->setPageSize( 1 ); - foreach ($regions as $region) { - $address->setRegionId($region->getId()); - $address->setExportedKeys(array_merge($address->getExportedKeys(), ['region_id'])); - break; - } + $regionItems = $regions->getItems(); + $region = array_shift($regionItems); + $address->setRegionId($region->getId()); + $address->setExportedKeys(array_merge($address->getExportedKeys(), ['region_id'])); } } @@ -1757,4 +1760,23 @@ protected function _prepareExpressCheckoutCallRequest(&$requestFields) } } } + + /** + * Updates shipping address with 'ship to name' data + * + * @param DataObject $shippingAddress + * @param array $data + * @return void + */ + private function updateShippingAddressWithShipToName(DataObject $shippingAddress, array $data) + { + if (isset($data['SHIPTONAME'])) { + $nameParts = explode(' ', $data['SHIPTONAME'], 2); + $shippingAddress->addData(['firstname' => $nameParts[0]]); + + if (isset($nameParts[1])) { + $shippingAddress->addData(['lastname' => $nameParts[1]]); + } + } + } } diff --git a/app/code/Magento/Paypal/Model/Express/Checkout.php b/app/code/Magento/Paypal/Model/Express/Checkout.php index 72f166e8d07c1..389f20c757ae1 100644 --- a/app/code/Magento/Paypal/Model/Express/Checkout.php +++ b/app/code/Magento/Paypal/Model/Express/Checkout.php @@ -357,12 +357,14 @@ public function __construct( if (isset($params['config']) && $params['config'] instanceof PaypalConfig) { $this->_config = $params['config']; } else { + // phpcs:ignore Magento2.Exceptions.DirectThrow throw new \Exception('Config instance is required.'); } if (isset($params['quote']) && $params['quote'] instanceof \Magento\Quote\Model\Quote) { $this->_quote = $params['quote']; } else { + // phpcs:ignore Magento2.Exceptions.DirectThrow throw new \Exception('Quote instance is required.'); } } @@ -631,10 +633,9 @@ public function returnFromPaypal($token, string $payerIdentifier = null) if ($shippingAddress) { if ($exportedShippingAddress && $isButton) { $this->_setExportedAddressData($shippingAddress, $exportedShippingAddress); - // PayPal doesn't provide detailed shipping info: prefix, middlename, lastname, suffix + // PayPal doesn't provide detailed shipping info: prefix, middlename, suffix $shippingAddress->setPrefix(null); $shippingAddress->setMiddlename(null); - $shippingAddress->setLastname(null); $shippingAddress->setSuffix(null); $shippingAddress->setCollectShippingRates(true); $shippingAddress->setSameAsBilling(0); @@ -1037,7 +1038,7 @@ protected function _prepareShippingOptions(Address $address, $mayReturnEmpty = f // Magento will transfer only first 10 cheapest shipping options if there are more than 10 available. if (count($options) > 10) { - usort($options, [get_class($this), 'cmpShippingOptions']); + usort($options, [$this, 'cmpShippingOptions']); array_splice($options, 10); // User selected option will be always included in options list if ($userSelectedOption !== null && !in_array($userSelectedOption, $options)) { @@ -1058,7 +1059,7 @@ protected function _prepareShippingOptions(Address $address, $mayReturnEmpty = f * @param \Magento\Framework\DataObject $option2 * @return int */ - protected static function cmpShippingOptions(DataObject $option1, DataObject $option2) + protected function cmpShippingOptions(DataObject $option1, DataObject $option2) { return $option1->getAmount() <=> $option2->getAmount(); } diff --git a/app/code/Magento/Paypal/Model/Payflow/Transparent.php b/app/code/Magento/Paypal/Model/Payflow/Transparent.php index f90c8f3792428..6569bdb20edfe 100644 --- a/app/code/Magento/Paypal/Model/Payflow/Transparent.php +++ b/app/code/Magento/Paypal/Model/Payflow/Transparent.php @@ -195,7 +195,6 @@ public function authorize(InfoInterface $payment, $amount) } catch (LocalizedException $exception) { $payment->setParentTransactionId($response->getData(self::PNREF)); $this->void($payment); - // phpcs:ignore Magento2.Exceptions.ThrowCatch throw new LocalizedException(__("The payment couldn't be processed at this time. Please try again later.")); } diff --git a/app/code/Magento/Paypal/Model/SmartButtonConfig.php b/app/code/Magento/Paypal/Model/SmartButtonConfig.php index c491f2978e560..ede9cacf25d40 100644 --- a/app/code/Magento/Paypal/Model/SmartButtonConfig.php +++ b/app/code/Magento/Paypal/Model/SmartButtonConfig.php @@ -7,7 +7,10 @@ namespace Magento\Paypal\Model; +use Magento\Checkout\Helper\Data; +use Magento\Framework\App\Config\ScopeConfigInterface; use Magento\Framework\Locale\ResolverInterface; +use Magento\Store\Model\ScopeInterface; /** * Smart button config @@ -34,21 +37,29 @@ class SmartButtonConfig */ private $allowedFunding; + /** + * @var ScopeConfigInterface + */ + private $scopeConfig; + /** * @param ResolverInterface $localeResolver * @param ConfigFactory $configFactory + * @param ScopeConfigInterface $scopeConfig * @param array $defaultStyles * @param array $allowedFunding */ public function __construct( ResolverInterface $localeResolver, ConfigFactory $configFactory, + ScopeConfigInterface $scopeConfig, $defaultStyles = [], $allowedFunding = [] ) { $this->localeResolver = $localeResolver; $this->config = $configFactory->create(); $this->config->setMethod(Config::METHOD_EXPRESS); + $this->scopeConfig = $scopeConfig; $this->defaultStyles = $defaultStyles; $this->allowedFunding = $allowedFunding; } @@ -61,6 +72,10 @@ public function __construct( */ public function getConfig(string $page): array { + $isGuestCheckoutAllowed = $this->scopeConfig->isSetFlag( + Data::XML_PATH_GUEST_CHECKOUT, + ScopeInterface::SCOPE_STORE + ); return [ 'merchantId' => $this->config->getValue('merchant_id'), 'environment' => ((int)$this->config->getValue('sandbox_flag') ? 'sandbox' : 'production'), @@ -68,7 +83,8 @@ public function getConfig(string $page): array 'allowedFunding' => $this->getAllowedFunding($page), 'disallowedFunding' => $this->getDisallowedFunding(), 'styles' => $this->getButtonStyles($page), - 'isVisibleOnProductPage' => (int)$this->config->getValue('visible_on_product') + 'isVisibleOnProductPage' => $this->config->getValue('visible_on_product'), + 'isGuestCheckoutAllowed' => $isGuestCheckoutAllowed ]; } diff --git a/app/code/Magento/Paypal/Test/Mftf/ActionGroup/OtherPayPalConfigurationActionGroup.xml b/app/code/Magento/Paypal/Test/Mftf/ActionGroup/OtherPayPalConfigurationActionGroup.xml index 01f4c0e7bae64..c3e99854ce3ac 100644 --- a/app/code/Magento/Paypal/Test/Mftf/ActionGroup/OtherPayPalConfigurationActionGroup.xml +++ b/app/code/Magento/Paypal/Test/Mftf/ActionGroup/OtherPayPalConfigurationActionGroup.xml @@ -28,7 +28,21 @@ - + + + Expands the 'OTHER PAYPAL PAYMENT SOLUTIONS' tab on the Admin Configuration page. Enables the provided PayPal Config type for the provided Country Code without saving. + + + + + + + + + + + + Expands the 'OTHER PAYPAL PAYMENT SOLUTIONS' tab on the Admin Configuration page. Enables the provided PayPal Config type for the provided Country Code. diff --git a/app/code/Magento/Paypal/Test/Mftf/ActionGroup/StorefrontPayPalPaymentActionGroup.xml b/app/code/Magento/Paypal/Test/Mftf/ActionGroup/StorefrontPayPalPaymentActionGroup.xml new file mode 100644 index 0000000000000..331acc1de628a --- /dev/null +++ b/app/code/Magento/Paypal/Test/Mftf/ActionGroup/StorefrontPayPalPaymentActionGroup.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + diff --git a/app/code/Magento/Paypal/Test/Mftf/Section/PayPalExpressCheckoutConfigSection.xml b/app/code/Magento/Paypal/Test/Mftf/Section/PayPalExpressCheckoutConfigSection.xml index 85f94cd8691a5..8d1b594d44e61 100644 --- a/app/code/Magento/Paypal/Test/Mftf/Section/PayPalExpressCheckoutConfigSection.xml +++ b/app/code/Magento/Paypal/Test/Mftf/Section/PayPalExpressCheckoutConfigSection.xml @@ -45,6 +45,9 @@ + + + @@ -58,5 +61,7 @@ + + - \ No newline at end of file + diff --git a/app/code/Magento/Paypal/Test/Mftf/Test/AdminOnePayPalSolutionsEnabledAtTheSameTimeTest.xml b/app/code/Magento/Paypal/Test/Mftf/Test/AdminOnePayPalSolutionsEnabledAtTheSameTimeTest.xml new file mode 100644 index 0000000000000..c653c1f03fd74 --- /dev/null +++ b/app/code/Magento/Paypal/Test/Mftf/Test/AdminOnePayPalSolutionsEnabledAtTheSameTimeTest.xml @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/code/Magento/Paypal/Test/Unit/Block/Adminhtml/System/Config/Multiselect/DisabledFundingOptionsTest.php b/app/code/Magento/Paypal/Test/Unit/Block/Adminhtml/System/Config/Multiselect/DisabledFundingOptionsTest.php index 2c9a33ce43854..de39b5c233a8d 100644 --- a/app/code/Magento/Paypal/Test/Unit/Block/Adminhtml/System/Config/Multiselect/DisabledFundingOptionsTest.php +++ b/app/code/Magento/Paypal/Test/Unit/Block/Adminhtml/System/Config/Multiselect/DisabledFundingOptionsTest.php @@ -80,17 +80,25 @@ public function testIsPaypalCreditAvailable( ) { $this->request->expects($this->any()) ->method('getParam') - ->will($this->returnCallback(function ($param) use ($requestCountry) { - if ($param == StructurePlugin::REQUEST_PARAM_COUNTRY) { - return $requestCountry; - } - return $param; - })); + ->will( + $this->returnCallback( + function ($param) use ($requestCountry) { + if ($param == StructurePlugin::REQUEST_PARAM_COUNTRY) { + return $requestCountry; + } + return $param; + } + ) + ); $this->config->expects($this->any()) ->method('getMerchantCountry') - ->will($this->returnCallback(function () use ($merchantCountry) { - return $merchantCountry; - })); + ->will( + $this->returnCallback( + function () use ($merchantCountry) { + return $merchantCountry; + } + ) + ); $this->model->render($this->element); $payPalCreditOption = [ 'value' => 'CREDIT', @@ -113,9 +121,9 @@ public function isPaypalCreditAvailableDataProvider(): array [null, 'US', true], ['US', 'US', true], ['US', 'GB', true], - ['GB', 'GB', false], - ['GB', 'US', false], - ['GB', null, false], + ['GB', 'GB', true], + ['GB', 'US', true], + ['GB', null, true], ]; } diff --git a/app/code/Magento/Paypal/Test/Unit/Model/AbstractConfigTest.php b/app/code/Magento/Paypal/Test/Unit/Model/AbstractConfigTest.php index 7c528e5718c3b..20f9b09897c22 100644 --- a/app/code/Magento/Paypal/Test/Unit/Model/AbstractConfigTest.php +++ b/app/code/Magento/Paypal/Test/Unit/Model/AbstractConfigTest.php @@ -306,7 +306,7 @@ public function testIsMethodActiveBml($disableFundingOptions, $expectedFlag, $ex { $this->scopeConfigMock->method('getValue') ->with( - self::equalTo('payment/paypal_express/disable_funding_options'), + self::equalTo('paypal/style/disable_funding_options'), self::equalTo('store') ) ->willReturn($disableFundingOptions); diff --git a/app/code/Magento/Paypal/Test/Unit/Model/Api/NvpTest.php b/app/code/Magento/Paypal/Test/Unit/Model/Api/NvpTest.php index fcc7766ec298b..cf51798b90197 100644 --- a/app/code/Magento/Paypal/Test/Unit/Model/Api/NvpTest.php +++ b/app/code/Magento/Paypal/Test/Unit/Model/Api/NvpTest.php @@ -6,6 +6,7 @@ namespace Magento\Paypal\Test\Unit\Model\Api; +use Magento\Framework\Exception\LocalizedException; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; use Magento\Paypal\Model\Info; @@ -35,7 +36,7 @@ class NvpTest extends \PHPUnit\Framework\TestCase /** @var \Magento\Paypal\Model\Api\ProcessableException|\PHPUnit_Framework_MockObject_MockObject */ protected $processableException; - /** @var \Magento\Framework\Exception\LocalizedException|\PHPUnit_Framework_MockObject_MockObject */ + /** @var LocalizedException|\PHPUnit_Framework_MockObject_MockObject */ protected $exception; /** @var \Magento\Framework\HTTP\Adapter\Curl|\PHPUnit_Framework_MockObject_MockObject */ @@ -47,6 +48,9 @@ class NvpTest extends \PHPUnit\Framework\TestCase /** @var \Magento\Payment\Model\Method\Logger|\PHPUnit_Framework_MockObject_MockObject */ protected $customLoggerMock; + /** + * {@inheritDoc} + */ protected function setUp() { $this->customerAddressHelper = $this->createMock(\Magento\Customer\Helper\Address::class); @@ -64,26 +68,32 @@ protected function setUp() ); $processableExceptionFactory->expects($this->any()) ->method('create') - ->will($this->returnCallback(function ($arguments) { - $this->processableException = $this->getMockBuilder( - \Magento\Paypal\Model\Api\ProcessableException::class + ->will( + $this->returnCallback( + function ($arguments) { + $this->processableException = $this->getMockBuilder( + \Magento\Paypal\Model\Api\ProcessableException::class + )->setConstructorArgs([$arguments['phrase'], null, $arguments['code']])->getMock(); + return $this->processableException; + } ) - ->setConstructorArgs([$arguments['phrase'], null, $arguments['code']]) - ->getMock(); - return $this->processableException; - })); + ); $exceptionFactory = $this->createPartialMock( \Magento\Framework\Exception\LocalizedExceptionFactory::class, ['create'] ); $exceptionFactory->expects($this->any()) ->method('create') - ->will($this->returnCallback(function ($arguments) { - $this->exception = $this->getMockBuilder(\Magento\Framework\Exception\LocalizedException::class) - ->setConstructorArgs([$arguments['phrase']]) - ->getMock(); - return $this->exception; - })); + ->will( + $this->returnCallback( + function ($arguments) { + $this->exception = $this->getMockBuilder(LocalizedException::class) + ->setConstructorArgs([$arguments['phrase']]) + ->getMock(); + return $this->exception; + } + ) + ); $this->curl = $this->createMock(\Magento\Framework\HTTP\Adapter\Curl::class); $curlFactory = $this->createPartialMock(\Magento\Framework\HTTP\Adapter\CurlFactory::class, ['create']); $curlFactory->expects($this->any())->method('create')->will($this->returnValue($this->curl)); @@ -155,7 +165,7 @@ public function callDataProvider() [ "\r\n" . 'ACK=Failure&L_ERRORCODE0=10417&L_SHORTMESSAGE0=Message.&L_LONGMESSAGE0=Long%20Message.', [], - \Magento\Framework\Exception\LocalizedException::class, + LocalizedException::class, 'PayPal gateway has rejected request. Long Message (#10417: Message).', 0 ], @@ -184,27 +194,56 @@ public function callDataProvider() ]; } - public function testCallGetExpressCheckoutDetails() + /** + * Test getting of the ExpressCheckout details + * + * @param $input + * @param $expected + * @dataProvider callGetExpressCheckoutDetailsDataProvider + */ + public function testCallGetExpressCheckoutDetails($input, $expected) { $this->curl->expects($this->once()) ->method('read') - ->will($this->returnValue( - "\r\n" . 'ACK=Success&SHIPTONAME=Ship%20To%20Name' + ->will($this->returnValue($input)); + $this->model->callGetExpressCheckoutDetails(); + $address = $this->model->getExportedShippingAddress(); + $this->assertEquals($expected['firstName'], $address->getData('firstname')); + $this->assertEquals($expected['lastName'], $address->getData('lastname')); + $this->assertEquals($expected['street'], $address->getStreet()); + $this->assertEquals($expected['company'], $address->getCompany()); + $this->assertEquals($expected['city'], $address->getCity()); + $this->assertEquals($expected['telephone'], $address->getTelephone()); + $this->assertEquals($expected['region'], $address->getRegion()); + } + + /** + * Data Provider + * + * @return array + */ + public function callGetExpressCheckoutDetailsDataProvider() + { + return [ + [ + "\r\n" . 'ACK=Success&SHIPTONAME=Jane%20Doe' . '&SHIPTOSTREET=testStreet' . '&SHIPTOSTREET2=testApartment' . '&BUSINESS=testCompany' . '&SHIPTOCITY=testCity' . '&PHONENUM=223322' - . '&STATE=testSTATE' - )); - $this->model->callGetExpressCheckoutDetails(); - $address = $this->model->getExportedShippingAddress(); - $this->assertEquals('Ship To Name', $address->getData('firstname')); - $this->assertEquals(implode("\n", ['testStreet','testApartment']), $address->getStreet()); - $this->assertEquals('testCompany', $address->getCompany()); - $this->assertEquals('testCity', $address->getCity()); - $this->assertEquals('223322', $address->getTelephone()); - $this->assertEquals('testSTATE', $address->getRegion()); + . '&STATE=testSTATE', + [ + 'firstName' => 'Jane', + 'lastName' => 'Doe', + 'street' => 'testStreet' . "\n" . 'testApartment', + 'company' => 'testCompany', + 'city' => 'testCity', + 'telephone' => '223322', + 'region' => 'testSTATE', + ] + ] + ]; } /** @@ -243,6 +282,9 @@ public function testCallDoReauthorization() $this->assertEquals($expectedImportedData, $this->model->getData()); } + /** + * Test replace keys for debug data + */ public function testGetDebugReplacePrivateDataKeys() { $debugReplacePrivateDataKeys = $this->_invokeNvpProperty($this->model, '_debugReplacePrivateDataKeys'); diff --git a/app/code/Magento/Paypal/Test/Unit/Model/ConfigTest.php b/app/code/Magento/Paypal/Test/Unit/Model/ConfigTest.php index dd3cf11b87ebe..ea14cffddc02a 100644 --- a/app/code/Magento/Paypal/Test/Unit/Model/ConfigTest.php +++ b/app/code/Magento/Paypal/Test/Unit/Model/ConfigTest.php @@ -9,6 +9,9 @@ use Magento\Store\Model\ScopeInterface; use Magento\Framework\App\Config\ScopeConfigInterface; +/** + * Class ConfigTest + */ class ConfigTest extends \PHPUnit\Framework\TestCase { /** @@ -122,7 +125,7 @@ public function testIsMethodAvailableForIsMethodActive($methodName, $expected) $valueMap = [ ['paypal/general/merchant_country', ScopeConfigInterface::SCOPE_TYPE_DEFAULT, null, 'US'], ['paypal/general/merchant_country', ScopeInterface::SCOPE_STORE, null, 'US'], - ['payment/paypal_express/disable_funding_options', ScopeConfigInterface::SCOPE_TYPE_DEFAULT, null, []], + ['paypal/style/disable_funding_options', ScopeConfigInterface::SCOPE_TYPE_DEFAULT, null, []], ]; $this->scopeConfig ->method('getValue') @@ -362,11 +365,15 @@ public function testGetBmlDisplay($section, $expectedValue, $expectedFlag, $expe ->will($this->returnValue($expectedFlag)); $this->scopeConfig->expects($this->any()) ->method('getValue') - ->will($this->returnValueMap([ - ['payment/' . Config::METHOD_WPP_BML . '/' . $section . '_display', 'store', 1, $expectedValue], - ['payment/' . Config::METHOD_WPP_BML . '/active', 'store', 1, $expectedValue], - ['payment/' . Config::METHOD_WPP_PE_BML . '/active', 'store', 1, $expectedValue], - ])); + ->will( + $this->returnValueMap( + [ + ['payment/' . Config::METHOD_WPP_BML . '/' . $section . '_display', 'store', 1, $expectedValue], + ['payment/' . Config::METHOD_WPP_BML . '/active', 'store', 1, $expectedValue], + ['payment/' . Config::METHOD_WPP_PE_BML . '/active', 'store', 1, $expectedValue], + ] + ) + ); $this->assertEquals($expected, $this->model->getBmlDisplay($section)); } @@ -407,11 +414,13 @@ public function testGetExpressCheckoutShortcutImageUrl( $this->scopeConfig->expects($this->any()) ->method('getValue') - ->willReturnMap([ - ['paypal/wpp/button_flavor', ScopeInterface::SCOPE_STORE, 123, $areButtonDynamic], - ['paypal/wpp/sandbox_flag', ScopeInterface::SCOPE_STORE, 123, $sandboxFlag], - ['paypal/wpp/button_type', ScopeInterface::SCOPE_STORE, 123, $buttonType], - ]); + ->willReturnMap( + [ + ['paypal/wpp/button_flavor', ScopeInterface::SCOPE_STORE, 123, $areButtonDynamic], + ['paypal/wpp/sandbox_flag', ScopeInterface::SCOPE_STORE, 123, $sandboxFlag], + ['paypal/wpp/button_type', ScopeInterface::SCOPE_STORE, 123, $buttonType], + ] + ); $this->assertEquals( $result, @@ -475,10 +484,12 @@ public function testGetPaymentMarkImageUrl( $this->scopeConfig->expects($this->any()) ->method('getValue') - ->willReturnMap([ - ['paypal/wpp/button_flavor', ScopeInterface::SCOPE_STORE, 123, $areButtonDynamic], - ['paypal/wpp/sandbox_flag', ScopeInterface::SCOPE_STORE, 123, $sandboxFlag], - ]); + ->willReturnMap( + [ + ['paypal/wpp/button_flavor', ScopeInterface::SCOPE_STORE, 123, $areButtonDynamic], + ['paypal/wpp/sandbox_flag', ScopeInterface::SCOPE_STORE, 123, $sandboxFlag], + ] + ); $this->assertEquals( $result, diff --git a/app/code/Magento/Paypal/Test/Unit/Model/SmartButtonConfigTest.php b/app/code/Magento/Paypal/Test/Unit/Model/SmartButtonConfigTest.php index ed62efe36c472..5aa3dee0874b2 100644 --- a/app/code/Magento/Paypal/Test/Unit/Model/SmartButtonConfigTest.php +++ b/app/code/Magento/Paypal/Test/Unit/Model/SmartButtonConfigTest.php @@ -7,10 +7,15 @@ namespace Magento\Paypal\Test\Unit\Model; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Paypal\Model\Config; use Magento\Paypal\Model\SmartButtonConfig; use Magento\Framework\Locale\ResolverInterface; use Magento\Paypal\Model\ConfigFactory; +/** + * Class SmartButtonConfigTest + */ class SmartButtonConfigTest extends \PHPUnit\Framework\TestCase { /** @@ -31,9 +36,15 @@ class SmartButtonConfigTest extends \PHPUnit\Framework\TestCase protected function setUp() { $this->localeResolverMock = $this->getMockForAbstractClass(ResolverInterface::class); - $this->configMock = $this->getMockBuilder(\Magento\Paypal\Model\Config::class) + $this->configMock = $this->getMockBuilder(Config::class) ->disableOriginalConstructor() ->getMock(); + + /** @var ScopeConfigInterface|\PHPUnit_Framework_MockObject_MockObject $scopeConfigMock */ + $scopeConfigMock = $this->getMockForAbstractClass(ScopeConfigInterface::class); + $scopeConfigMock->method('isSetFlag') + ->willReturn(true); + /** @var \PHPUnit_Framework_MockObject_MockObject $configFactoryMock */ $configFactoryMock = $this->getMockBuilder(ConfigFactory::class) ->disableOriginalConstructor() @@ -43,12 +54,15 @@ protected function setUp() $this->model = new SmartButtonConfig( $this->localeResolverMock, $configFactoryMock, + $scopeConfigMock, $this->getDefaultStyles(), $this->getAllowedFundings() ); } /** + * Tests config. + * * @param string $page * @param string $locale * @param string $disallowedFundings @@ -78,22 +92,33 @@ public function testGetConfig( array $expected = [] ) { $this->localeResolverMock->expects($this->any())->method('getLocale')->willReturn($locale); - $this->configMock->expects($this->any())->method('getValue')->will($this->returnValueMap([ - ['merchant_id', null, 'merchant'], - ['sandbox_flag', null, true], - ['disable_funding_options', null, $disallowedFundings], - ["{$page}_page_button_customize", null, $isCustomize], - ["{$page}_page_button_layout", null, $layout], - ["{$page}_page_button_size", null, $size], - ["{$page}_page_button_color", null, $color], - ["{$page}_page_button_shape", null, $shape], - ["{$page}_page_button_label", null, $label], - [$page . '_page_button_' . $installmentPeriodLocale . '_installment_period', null, $installmentPeriodLabel] - ])); + $this->configMock->method('getValue')->will( + $this->returnValueMap( + [ + ['merchant_id', null, 'merchant'], + ['sandbox_flag', null, true], + ['disable_funding_options', null, $disallowedFundings], + ["{$page}_page_button_customize", null, $isCustomize], + ["{$page}_page_button_layout", null, $layout], + ["{$page}_page_button_size", null, $size], + ["{$page}_page_button_color", null, $color], + ["{$page}_page_button_shape", null, $shape], + ["{$page}_page_button_label", null, $label], + [ + $page . '_page_button_' . $installmentPeriodLocale . '_installment_period', + null, + $installmentPeriodLabel + ] + ] + ) + ); self::assertEquals($expected, $this->model->getConfig($page)); } + /** + * @return array + */ public function getConfigDataProvider() { return include __DIR__ . '/_files/expected_config.php'; diff --git a/app/code/Magento/Paypal/Test/Unit/Model/_files/expected_config.php b/app/code/Magento/Paypal/Test/Unit/Model/_files/expected_config.php index 1442642a324b9..478607f9956e6 100644 --- a/app/code/Magento/Paypal/Test/Unit/Model/_files/expected_config.php +++ b/app/code/Magento/Paypal/Test/Unit/Model/_files/expected_config.php @@ -32,7 +32,8 @@ 'label' => 'installment', 'installmentperiod' => 0 ], - 'isVisibleOnProductPage' => 0 + 'isVisibleOnProductPage' => 0, + 'isGuestCheckoutAllowed' => true ] ], 'checkout' => [ @@ -61,7 +62,8 @@ 'label' => 'installment', 'installmentperiod' => 0 ], - 'isVisibleOnProductPage' => 0 + 'isVisibleOnProductPage' => 0, + 'isGuestCheckoutAllowed' => true ] ], 'mini_cart' => [ @@ -89,7 +91,8 @@ 'shape' => 'rect', 'label' => 'paypal' ], - 'isVisibleOnProductPage' => 0 + 'isVisibleOnProductPage' => 0, + 'isGuestCheckoutAllowed' => true ] ], 'mini_cart' => [ @@ -117,7 +120,8 @@ 'shape' => 'rect', 'label' => 'paypal' ], - 'isVisibleOnProductPage' => 0 + 'isVisibleOnProductPage' => 0, + 'isGuestCheckoutAllowed' => true ] ], 'product' => [ @@ -145,7 +149,8 @@ 'shape' => 'rect', 'label' => 'paypal', ], - 'isVisibleOnProductPage' => 0 + 'isVisibleOnProductPage' => 0, + 'isGuestCheckoutAllowed' => true ] ] ]; diff --git a/app/code/Magento/Paypal/etc/adminhtml/rules/payment_ca.xml b/app/code/Magento/Paypal/etc/adminhtml/rules/payment_ca.xml index 51297a96438d2..36da32c9c14b1 100644 --- a/app/code/Magento/Paypal/etc/adminhtml/rules/payment_ca.xml +++ b/app/code/Magento/Paypal/etc/adminhtml/rules/payment_ca.xml @@ -193,6 +193,7 @@ wps_other payflow_link_ca + diff --git a/app/code/Magento/Paypal/etc/adminhtml/rules/payment_es.xml b/app/code/Magento/Paypal/etc/adminhtml/rules/payment_es.xml index 28cc075e0c619..28555d3ee5599 100644 --- a/app/code/Magento/Paypal/etc/adminhtml/rules/payment_es.xml +++ b/app/code/Magento/Paypal/etc/adminhtml/rules/payment_es.xml @@ -85,6 +85,7 @@ wps_other + diff --git a/app/code/Magento/Paypal/etc/adminhtml/rules/payment_fr.xml b/app/code/Magento/Paypal/etc/adminhtml/rules/payment_fr.xml index 7f1fcc08334fe..fea756ba66f3a 100644 --- a/app/code/Magento/Paypal/etc/adminhtml/rules/payment_fr.xml +++ b/app/code/Magento/Paypal/etc/adminhtml/rules/payment_fr.xml @@ -85,6 +85,7 @@ wps_other + diff --git a/app/code/Magento/Paypal/etc/adminhtml/rules/payment_gb.xml b/app/code/Magento/Paypal/etc/adminhtml/rules/payment_gb.xml index d8b765b9b4d22..0ce5cba8fe1dd 100644 --- a/app/code/Magento/Paypal/etc/adminhtml/rules/payment_gb.xml +++ b/app/code/Magento/Paypal/etc/adminhtml/rules/payment_gb.xml @@ -85,6 +85,7 @@ wps_express + diff --git a/app/code/Magento/Paypal/etc/adminhtml/rules/payment_hk.xml b/app/code/Magento/Paypal/etc/adminhtml/rules/payment_hk.xml index 50ce14e66ee0c..8fdc15c15b233 100644 --- a/app/code/Magento/Paypal/etc/adminhtml/rules/payment_hk.xml +++ b/app/code/Magento/Paypal/etc/adminhtml/rules/payment_hk.xml @@ -85,6 +85,7 @@ wps_other + diff --git a/app/code/Magento/Paypal/etc/adminhtml/rules/payment_it.xml b/app/code/Magento/Paypal/etc/adminhtml/rules/payment_it.xml index de059dcc59c39..75615209ec986 100644 --- a/app/code/Magento/Paypal/etc/adminhtml/rules/payment_it.xml +++ b/app/code/Magento/Paypal/etc/adminhtml/rules/payment_it.xml @@ -85,6 +85,7 @@ wps_other + diff --git a/app/code/Magento/Paypal/etc/adminhtml/rules/payment_jp.xml b/app/code/Magento/Paypal/etc/adminhtml/rules/payment_jp.xml index d9fc7ef3f201c..af0dab810e128 100644 --- a/app/code/Magento/Paypal/etc/adminhtml/rules/payment_jp.xml +++ b/app/code/Magento/Paypal/etc/adminhtml/rules/payment_jp.xml @@ -85,6 +85,7 @@ wps_other + diff --git a/app/code/Magento/Paypal/etc/adminhtml/rules/payment_nz.xml b/app/code/Magento/Paypal/etc/adminhtml/rules/payment_nz.xml index c5b8b09c3a2cf..320a66b1a9b36 100644 --- a/app/code/Magento/Paypal/etc/adminhtml/rules/payment_nz.xml +++ b/app/code/Magento/Paypal/etc/adminhtml/rules/payment_nz.xml @@ -85,6 +85,7 @@ wps_other + diff --git a/app/code/Magento/Paypal/etc/adminhtml/rules/payment_us.xml b/app/code/Magento/Paypal/etc/adminhtml/rules/payment_us.xml index b7924e770aa22..6a51330e060f7 100644 --- a/app/code/Magento/Paypal/etc/adminhtml/rules/payment_us.xml +++ b/app/code/Magento/Paypal/etc/adminhtml/rules/payment_us.xml @@ -414,6 +414,7 @@ paypal_payflowpro_with_express_checkout payflow_link_us + diff --git a/app/code/Magento/Paypal/etc/adminhtml/system/express_checkout.xml b/app/code/Magento/Paypal/etc/adminhtml/system/express_checkout.xml index d5287659ee7d2..04d5fae435816 100644 --- a/app/code/Magento/Paypal/etc/adminhtml/system/express_checkout.xml +++ b/app/code/Magento/Paypal/etc/adminhtml/system/express_checkout.xml @@ -117,13 +117,15 @@ 1 - + Proxy Port + Please enter at least 0 and at most 65535 paypal/wpp/proxy_port 1 1 + validate-digits validate-digits-range digits-range-0-65535 @@ -348,6 +350,7 @@ Order + validate-zero-or-greater validate-digits Order Valid Period (days) @@ -357,6 +360,7 @@ Order + validate-zero-or-greater validate-digits Number of Child Authorizations diff --git a/app/code/Magento/Paypal/etc/adminhtml/system/payflow_advanced.xml b/app/code/Magento/Paypal/etc/adminhtml/system/payflow_advanced.xml index cba36916c3305..1ae0b52146c38 100644 --- a/app/code/Magento/Paypal/etc/adminhtml/system/payflow_advanced.xml +++ b/app/code/Magento/Paypal/etc/adminhtml/system/payflow_advanced.xml @@ -69,13 +69,15 @@ 1 - + Proxy Port + Please enter at least 0 and at most 65535 payment/payflow_advanced/proxy_port 1 1 + validate-digits validate-digits-range digits-range-0-65535 Magento\Paypal\Block\Adminhtml\System\Config\Payflowlink\Advanced diff --git a/app/code/Magento/Paypal/etc/adminhtml/system/payflow_link.xml b/app/code/Magento/Paypal/etc/adminhtml/system/payflow_link.xml index ed11e8ba18d07..ead70eca3fadd 100644 --- a/app/code/Magento/Paypal/etc/adminhtml/system/payflow_link.xml +++ b/app/code/Magento/Paypal/etc/adminhtml/system/payflow_link.xml @@ -70,13 +70,15 @@ 1 - + Proxy Port + Please enter at least 0 and at most 65535 payment/payflow_link/proxy_port 1 1 + validate-digits validate-digits-range digits-range-0-65535 Magento\Paypal\Block\Adminhtml\System\Config\Payflowlink\Info diff --git a/app/code/Magento/Paypal/etc/adminhtml/system/paypal_payflowpro.xml b/app/code/Magento/Paypal/etc/adminhtml/system/paypal_payflowpro.xml index e5a0319bdc1bf..c87a781f36c00 100644 --- a/app/code/Magento/Paypal/etc/adminhtml/system/paypal_payflowpro.xml +++ b/app/code/Magento/Paypal/etc/adminhtml/system/paypal_payflowpro.xml @@ -65,13 +65,15 @@ 1 - + Proxy Port + Please enter at least 0 and at most 65535 payment/payflowpro/proxy_port 1 1 + validate-digits validate-digits-range digits-range-0-65535 diff --git a/app/code/Magento/Paypal/i18n/en_US.csv b/app/code/Magento/Paypal/i18n/en_US.csv index e7264a6de807f..4e47c4c1f9e9f 100644 --- a/app/code/Magento/Paypal/i18n/en_US.csv +++ b/app/code/Magento/Paypal/i18n/en_US.csv @@ -737,3 +737,4 @@ User,User "PayPal Credit","PayPal Credit" "PayPal Guest Checkout Credit Card Icons","PayPal Guest Checkout Credit Card Icons" "Elektronisches Lastschriftverfahren - German ELV","Elektronisches Lastschriftverfahren - German ELV" +"Please enter at least 0 and at most 65535","Please enter at least 0 and at most 65535" diff --git a/app/code/Magento/Paypal/view/frontend/templates/express/in-context/component.phtml b/app/code/Magento/Paypal/view/frontend/templates/express/in-context/component.phtml index 6efb678ffdae6..ba98f5caeb15a 100644 --- a/app/code/Magento/Paypal/view/frontend/templates/express/in-context/component.phtml +++ b/app/code/Magento/Paypal/view/frontend/templates/express/in-context/component.phtml @@ -8,24 +8,20 @@ use Magento\Paypal\Block\Express\InContext\Minicart\Button; /** @var \Magento\Paypal\Block\Express\InContext\Component $block */ $configuration = [ - '*' => [ - 'Magento_Paypal/js/in-context/express-checkout' => [ - 'id' => Button::PAYPAL_BUTTON_ID, - 'path' => $block->getUrl( - 'paypal/express/gettoken', - [ - '_secure' => $block->getRequest()->isSecure() - ] - ), - 'merchantId' => $block->getMerchantId(), - 'button' => $block->isButtonContext(), - 'clientConfig' => [ - 'locale' => $block->getLocale(), - 'environment' => $block->getEnvironment(), - 'button' => [ - Button::PAYPAL_BUTTON_ID, - ], - ] + 'id' => Button::PAYPAL_BUTTON_ID, + 'path' => $block->getUrl( + 'paypal/express/gettoken', + [ + '_secure' => $block->getRequest()->isSecure() + ] + ), + 'merchantId' => $block->getMerchantId(), + 'button' => $block->isButtonContext(), + 'clientConfig' => [ + 'locale' => $block->getLocale(), + 'environment' => $block->getEnvironment(), + 'button' => [ + Button::PAYPAL_BUTTON_ID, ] ] ]; @@ -33,5 +29,9 @@ $configuration = [ ?> diff --git a/app/code/Magento/Paypal/view/frontend/templates/express/in-context/shortcut/button.phtml b/app/code/Magento/Paypal/view/frontend/templates/express/in-context/shortcut/button.phtml index 66dddfb0bda95..86c3c5c25f0bc 100644 --- a/app/code/Magento/Paypal/view/frontend/templates/express/in-context/shortcut/button.phtml +++ b/app/code/Magento/Paypal/view/frontend/templates/express/in-context/shortcut/button.phtml @@ -7,7 +7,9 @@ /** * @var \Magento\Paypal\Block\Express\InContext\Minicart\SmartButton $block */ +$widget = $this->helper(\Magento\Framework\Json\Helper\Data::class)->jsonDecode($block->getJsInitParams()); +$widgetConfig = $this->helper(\Magento\Framework\Json\Helper\Data::class)->jsonEncode($widget['Magento_Paypal/js/in-context/button']); ?> - - \ No newline at end of file + diff --git a/app/code/Magento/Paypal/view/frontend/templates/express/shortcut_button.phtml b/app/code/Magento/Paypal/view/frontend/templates/express/shortcut_button.phtml index a30bc2cce6d4f..76d034f462a7a 100644 --- a/app/code/Magento/Paypal/view/frontend/templates/express/shortcut_button.phtml +++ b/app/code/Magento/Paypal/view/frontend/templates/express/shortcut_button.phtml @@ -5,7 +5,11 @@ */ /** - * @var \Magento\Paypal\Block\Express\Shortcut $block + * @var \Magento\Paypal\Block\Express\InContext\SmartButton $block */ +$widget = $this->helper(\Magento\Framework\Json\Helper\Data::class)->jsonDecode($block->getJsInitParams()); +$widgetConfig = $this->helper(\Magento\Framework\Json\Helper\Data::class)->jsonEncode( + $widget['Magento_Paypal/js/in-context/product-express-checkout'] +); ?> - + diff --git a/app/code/Magento/Paypal/view/frontend/web/js/in-context/product-express-checkout.js b/app/code/Magento/Paypal/view/frontend/web/js/in-context/product-express-checkout.js index f4adaae06a112..b2be5fe2b3d2b 100644 --- a/app/code/Magento/Paypal/view/frontend/web/js/in-context/product-express-checkout.js +++ b/app/code/Magento/Paypal/view/frontend/web/js/in-context/product-express-checkout.js @@ -21,15 +21,22 @@ define([ /** @inheritdoc */ initialize: function (config, element) { var cart = customerData.get('cart'), - customer = customerData.get('customer'); + customer = customerData.get('customer'), + isGuestCheckoutAllowed; this._super(); + isGuestCheckoutAllowed = cart().isGuestCheckoutAllowed; + + if (typeof isGuestCheckoutAllowed === 'undefined') { + isGuestCheckoutAllowed = config.clientConfig.isGuestCheckoutAllowed; + } + if (config.clientConfig.isVisibleOnProductPage) { this.renderPayPalButtons(element); } - this.declinePayment = !customer().firstname && !cart().isGuestCheckoutAllowed; + this.declinePayment = !customer().firstname && !isGuestCheckoutAllowed; return this; }, diff --git a/app/code/Magento/Persistent/Observer/CheckExpirePersistentQuoteObserver.php b/app/code/Magento/Persistent/Observer/CheckExpirePersistentQuoteObserver.php index 1261a90b5843b..6a8772358f010 100644 --- a/app/code/Magento/Persistent/Observer/CheckExpirePersistentQuoteObserver.php +++ b/app/code/Magento/Persistent/Observer/CheckExpirePersistentQuoteObserver.php @@ -120,6 +120,7 @@ public function execute(\Magento\Framework\Event\Observer $observer) $this->_eventManager->dispatch('persistent_session_expired'); $this->quoteManager->expire(); $this->_checkoutSession->clearQuote(); + $this->_customerSession->setCustomerId(null)->setCustomerGroupId(null); return; } diff --git a/app/code/Magento/ProductVideo/view/frontend/layout/catalog_product_view.xml b/app/code/Magento/ProductVideo/view/frontend/layout/catalog_product_view.xml index 0b783e29ca0d3..38f7bf619f8fc 100644 --- a/app/code/Magento/ProductVideo/view/frontend/layout/catalog_product_view.xml +++ b/app/code/Magento/ProductVideo/view/frontend/layout/catalog_product_view.xml @@ -5,7 +5,7 @@ * See COPYING.txt for license details. */ --> - + diff --git a/app/code/Magento/Quote/Model/Quote/Address/Total/Shipping.php b/app/code/Magento/Quote/Model/Quote/Address/Total/Shipping.php index e9a63dad6e169..3ce148ee80b8c 100644 --- a/app/code/Magento/Quote/Model/Quote/Address/Total/Shipping.php +++ b/app/code/Magento/Quote/Model/Quote/Address/Total/Shipping.php @@ -3,6 +3,9 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +declare(strict_types=1); + namespace Magento\Quote\Model\Quote\Address\Total; use Magento\Framework\Pricing\PriceCurrencyInterface; @@ -112,17 +115,21 @@ public function collect( */ public function fetch(\Magento\Quote\Model\Quote $quote, \Magento\Quote\Model\Quote\Address\Total $total) { - $amount = $total->getShippingAmount(); - $shippingDescription = $total->getShippingDescription(); - $title = ($shippingDescription) - ? __('Shipping & Handling (%1)', $shippingDescription) - : __('Shipping & Handling'); + if (!$quote->getIsVirtual()) { + $amount = $total->getShippingAmount(); + $shippingDescription = $total->getShippingDescription(); + $title = ($shippingDescription) + ? __('Shipping & Handling (%1)', $shippingDescription) + : __('Shipping & Handling'); - return [ - 'code' => $this->getCode(), - 'title' => $title, - 'value' => $amount - ]; + return [ + 'code' => $this->getCode(), + 'title' => $title, + 'value' => $amount + ]; + } else { + return []; + } } /** diff --git a/app/code/Magento/Quote/Model/Quote/Item/Repository.php b/app/code/Magento/Quote/Model/Quote/Item/Repository.php index 1fb0a2d7107f1..6fb512a619de4 100644 --- a/app/code/Magento/Quote/Model/Quote/Item/Repository.php +++ b/app/code/Magento/Quote/Model/Quote/Item/Repository.php @@ -1,34 +1,40 @@ quoteRepository = $quoteRepository; $this->productRepository = $productRepository; $this->itemDataFactory = $itemDataFactory; + $this->cartItemOptionsProcessor = $cartItemOptionsProcessor; $this->cartItemProcessors = $cartItemProcessors; } /** - * {@inheritdoc} + * @inheritdoc */ public function getList($cartId) { @@ -71,21 +80,26 @@ public function getList($cartId) /** @var \Magento\Quote\Model\Quote\Item $item */ foreach ($quote->getAllVisibleItems() as $item) { - $item = $this->getCartItemOptionsProcessor()->addProductOptions($item->getProductType(), $item); - $output[] = $this->getCartItemOptionsProcessor()->applyCustomOptions($item); + $item = $this->cartItemOptionsProcessor->addProductOptions($item->getProductType(), $item); + $output[] = $this->cartItemOptionsProcessor->applyCustomOptions($item); } return $output; } /** - * {@inheritdoc} + * @inheritdoc */ public function save(\Magento\Quote\Api\Data\CartItemInterface $cartItem) { /** @var \Magento\Quote\Model\Quote $quote */ $cartId = $cartItem->getQuoteId(); - $quote = $this->quoteRepository->getActive($cartId); + if (!$cartId) { + throw new InputException( + __('"%fieldName" is required. Enter and try again.', ['fieldName' => 'quoteId']) + ); + } + $quote = $this->quoteRepository->getActive($cartId); $quoteItems = $quote->getItems(); $quoteItems[] = $cartItem; $quote->setItems($quoteItems); @@ -95,7 +109,7 @@ public function save(\Magento\Quote\Api\Data\CartItemInterface $cartItem) } /** - * {@inheritdoc} + * @inheritdoc */ public function deleteById($cartId, $itemId) { @@ -116,17 +130,4 @@ public function deleteById($cartId, $itemId) return true; } - - /** - * @return CartItemOptionsProcessor - * @deprecated 100.1.0 - */ - private function getCartItemOptionsProcessor() - { - if (!$this->cartItemOptionsProcessor instanceof CartItemOptionsProcessor) { - $this->cartItemOptionsProcessor = ObjectManager::getInstance()->get(CartItemOptionsProcessor::class); - } - - return $this->cartItemOptionsProcessor; - } } diff --git a/app/code/Magento/Quote/Model/QuoteManagement.php b/app/code/Magento/Quote/Model/QuoteManagement.php index 5bfbc80452bf9..84ef699b6209e 100644 --- a/app/code/Magento/Quote/Model/QuoteManagement.php +++ b/app/code/Magento/Quote/Model/QuoteManagement.php @@ -688,7 +688,6 @@ private function rollbackAddresses( 'exception' => $e, ] ); - // phpcs:ignore Magento2.Exceptions.ThrowCatch } catch (\Exception $consecutiveException) { $message = sprintf( "An exception occurred on 'sales_model_service_quote_submit_failure' event: %s", diff --git a/app/code/Magento/Quote/Test/Unit/Model/Quote/Item/RepositoryTest.php b/app/code/Magento/Quote/Test/Unit/Model/Quote/Item/RepositoryTest.php index 4ecd8b021d7f0..dd4f2f6e7470c 100644 --- a/app/code/Magento/Quote/Test/Unit/Model/Quote/Item/RepositoryTest.php +++ b/app/code/Magento/Quote/Test/Unit/Model/Quote/Item/RepositoryTest.php @@ -7,64 +7,75 @@ namespace Magento\Quote\Test\Unit\Model\Quote\Item; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Model\CustomOptions\CustomOptionProcessor; +use Magento\Catalog\Model\Product; +use Magento\Quote\Api\CartRepositoryInterface; +use Magento\Quote\Api\Data\CartItemInterfaceFactory; +use Magento\Quote\Model\Quote; +use Magento\Quote\Model\Quote\Address; +use Magento\Quote\Model\Quote\Item; +use Magento\Quote\Model\Quote\Item\CartItemOptionsProcessor; +use Magento\Quote\Model\Quote\Item\Repository; +use PHPUnit\Framework\MockObject\MockObject; + /** * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class RepositoryTest extends \PHPUnit\Framework\TestCase { /** - * @var \Magento\Framework\TestFramework\Unit\Helper\ObjectManager + * @var Repository */ - private $objectManager; + private $repository; /** - * @var \Magento\Quote\Api\CartItemRepositoryInterface + * @var CartRepositoryInterface|MockObject */ - protected $repository; + private $quoteRepositoryMock; /** - * @var \PHPUnit_Framework_MockObject_MockObject + * @var ProductRepositoryInterface|MockObject */ - protected $quoteRepositoryMock; + private $productRepositoryMock; /** - * @var \PHPUnit_Framework_MockObject_MockObject + * @var MockObject */ - protected $productRepositoryMock; + private $itemMock; /** - * @var \PHPUnit_Framework_MockObject_MockObject + * @var MockObject */ - protected $itemMock; + private $quoteMock; /** - * @var \PHPUnit_Framework_MockObject_MockObject + * @var MockObject */ - protected $quoteMock; + private $productMock; /** - * @var \PHPUnit_Framework_MockObject_MockObject + * @var MockObject */ - protected $productMock; + private $quoteItemMock; /** - * @var \PHPUnit_Framework_MockObject_MockObject + * @var CartItemInterfaceFactory|MockObject */ - protected $quoteItemMock; + private $itemDataFactoryMock; /** - * @var \PHPUnit_Framework_MockObject_MockObject + * @var CustomOptionProcessor|MockObject */ - protected $itemDataFactoryMock; - - /** @var \Magento\Catalog\Model\CustomOptions\CustomOptionProcessor|\PHPUnit_Framework_MockObject_MockObject */ - protected $customOptionProcessor; + private $customOptionProcessor; - /** @var \PHPUnit_Framework_MockObject_MockObject */ - protected $shippingAddressMock; + /** + * @var Address|MockObject + */ + private $shippingAddressMock; /** - * @var \PHPUnit_Framework_MockObject_MockObject + * @var CartItemOptionsProcessor|MockObject */ private $optionsProcessorMock; @@ -73,40 +84,29 @@ class RepositoryTest extends \PHPUnit\Framework\TestCase */ protected function setUp() { - $this->objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); - $this->quoteRepositoryMock = $this->createMock(\Magento\Quote\Api\CartRepositoryInterface::class); - $this->productRepositoryMock = $this->createMock(\Magento\Catalog\Api\ProductRepositoryInterface::class); - $this->itemDataFactoryMock = - $this->createPartialMock(\Magento\Quote\Api\Data\CartItemInterfaceFactory::class, ['create']); - $this->itemMock = $this->createMock(\Magento\Quote\Model\Quote\Item::class); - $this->quoteMock = $this->createMock(\Magento\Quote\Model\Quote::class); - $this->productMock = $this->createMock(\Magento\Catalog\Model\Product::class); + $this->quoteRepositoryMock = $this->createMock(CartRepositoryInterface::class); + $this->productRepositoryMock = $this->createMock(ProductRepositoryInterface::class); + $this->itemDataFactoryMock = $this->createPartialMock(CartItemInterfaceFactory::class, ['create']); + $this->itemMock = $this->createMock(Item::class); + $this->quoteMock = $this->createMock(Quote::class); + $this->productMock = $this->createMock(Product::class); $methods = ['getId', 'getSku', 'getQty', 'setData', '__wakeUp', 'getProduct', 'addProduct']; $this->quoteItemMock = - $this->createPartialMock(\Magento\Quote\Model\Quote\Item::class, $methods); - $this->customOptionProcessor = $this->createMock( - \Magento\Catalog\Model\CustomOptions\CustomOptionProcessor::class - ); + $this->createPartialMock(Item::class, $methods); + $this->customOptionProcessor = $this->createMock(CustomOptionProcessor::class); $this->shippingAddressMock = $this->createPartialMock( - \Magento\Quote\Model\Quote\Address::class, + Address::class, ['setCollectShippingRates'] ); + $this->optionsProcessorMock = $this->createMock(CartItemOptionsProcessor::class); - $this->optionsProcessorMock = $this->createMock( - \Magento\Quote\Model\Quote\Item\CartItemOptionsProcessor::class - ); - - $this->repository = new \Magento\Quote\Model\Quote\Item\Repository( + $this->repository = new Repository( $this->quoteRepositoryMock, $this->productRepositoryMock, $this->itemDataFactoryMock, + $this->optionsProcessorMock, ['custom_options' => $this->customOptionProcessor] ); - $this->objectManager->setBackwardCompatibleProperty( - $this->repository, - 'cartItemOptionsProcessor', - $this->optionsProcessorMock - ); } /** @@ -118,7 +118,7 @@ public function testSave() $itemId = 20; $quoteMock = $this->createPartialMock( - \Magento\Quote\Model\Quote::class, + Quote::class, ['getItems', 'setItems', 'collectTotals', 'getLastAddedItem'] ); @@ -197,11 +197,11 @@ public function testDeleteWithCouldNotSaveException() public function testGetList() { $productType = 'type'; - $quoteMock = $this->createMock(\Magento\Quote\Model\Quote::class); + $quoteMock = $this->createMock(Quote::class); $this->quoteRepositoryMock->expects($this->once())->method('getActive') ->with(33) ->will($this->returnValue($quoteMock)); - $itemMock = $this->createMock(\Magento\Quote\Model\Quote\Item::class); + $itemMock = $this->createMock(Item::class); $quoteMock->expects($this->once())->method('getAllVisibleItems')->will($this->returnValue([$itemMock])); $itemMock->expects($this->once())->method('getProductType')->willReturn($productType); diff --git a/app/code/Magento/QuoteGraphQl/Model/Cart/AddSimpleProductToCart.php b/app/code/Magento/QuoteGraphQl/Model/Cart/AddSimpleProductToCart.php index fb2a9706792cc..11719db2d1b8f 100644 --- a/app/code/Magento/QuoteGraphQl/Model/Cart/AddSimpleProductToCart.php +++ b/app/code/Magento/QuoteGraphQl/Model/Cart/AddSimpleProductToCart.php @@ -55,7 +55,7 @@ public function execute(Quote $cart, array $cartItemData): void $sku = $this->extractSku($cartItemData); try { - $product = $this->productRepository->get($sku); + $product = $this->productRepository->get($sku, false, null, true); } catch (NoSuchEntityException $e) { throw new GraphQlNoSuchEntityException(__('Could not find a product with SKU "%sku"', ['sku' => $sku])); } diff --git a/app/code/Magento/QuoteGraphQl/Model/Cart/SetShippingAddressesOnCart.php b/app/code/Magento/QuoteGraphQl/Model/Cart/SetShippingAddressesOnCart.php index 77719bed5b16f..260f1343556f0 100644 --- a/app/code/Magento/QuoteGraphQl/Model/Cart/SetShippingAddressesOnCart.php +++ b/app/code/Magento/QuoteGraphQl/Model/Cart/SetShippingAddressesOnCart.php @@ -53,6 +53,10 @@ public function execute(ContextInterface $context, CartInterface $cart, array $s $customerAddressId = $shippingAddressInput['customer_address_id'] ?? null; $addressInput = $shippingAddressInput['address'] ?? null; + if ($addressInput) { + $addressInput['customer_notes'] = $shippingAddressInput['customer_notes'] ?? ''; + } + if (null === $customerAddressId && null === $addressInput) { throw new GraphQlInputException( __('The shipping address must contain either "customer_address_id" or "address".') diff --git a/app/code/Magento/QuoteGraphQl/Model/Resolver/CartIsVirtual.php b/app/code/Magento/QuoteGraphQl/Model/Resolver/CartIsVirtual.php new file mode 100644 index 0000000000000..3aec0a8365da8 --- /dev/null +++ b/app/code/Magento/QuoteGraphQl/Model/Resolver/CartIsVirtual.php @@ -0,0 +1,34 @@ +getIsVirtual(); + } +} diff --git a/app/code/Magento/QuoteGraphQl/Model/Resolver/CartTotalQuantity.php b/app/code/Magento/QuoteGraphQl/Model/Resolver/CartTotalQuantity.php new file mode 100644 index 0000000000000..014b7cf90f3eb --- /dev/null +++ b/app/code/Magento/QuoteGraphQl/Model/Resolver/CartTotalQuantity.php @@ -0,0 +1,34 @@ +getItemsSummaryQty(); + } +} diff --git a/app/code/Magento/QuoteGraphQl/Model/Resolver/SetPaymentAndPlaceOrder.php b/app/code/Magento/QuoteGraphQl/Model/Resolver/SetPaymentAndPlaceOrder.php index 0efbde5d6b218..03e1e6ffe822d 100644 --- a/app/code/Magento/QuoteGraphQl/Model/Resolver/SetPaymentAndPlaceOrder.php +++ b/app/code/Magento/QuoteGraphQl/Model/Resolver/SetPaymentAndPlaceOrder.php @@ -20,7 +20,11 @@ use Magento\Sales\Api\OrderRepositoryInterface; /** - * @inheritdoc + * Resolver for setting payment method and placing order + * + * @deprecated Should use setPaymentMethodOnCart and placeOrder mutations in single request. + * @see \Magento\QuoteGraphQl\Model\Resolver\SetPaymentMethodOnCart + * @see \Magento\QuoteGraphQl\Model\Resolver\PlaceOrder */ class SetPaymentAndPlaceOrder implements ResolverInterface { diff --git a/app/code/Magento/QuoteGraphQl/Model/Resolver/ShippingAddress/AvailableShippingMethods.php b/app/code/Magento/QuoteGraphQl/Model/Resolver/ShippingAddress/AvailableShippingMethods.php index e5dd1d73b80b5..eebed5aab6cc9 100644 --- a/app/code/Magento/QuoteGraphQl/Model/Resolver/ShippingAddress/AvailableShippingMethods.php +++ b/app/code/Magento/QuoteGraphQl/Model/Resolver/ShippingAddress/AvailableShippingMethods.php @@ -75,8 +75,7 @@ public function resolve(Field $field, $context, ResolveInfo $info, array $value ); $methods[] = $this->processMoneyTypeData( $methodData, - $cart->getQuoteCurrencyCode(), - $context->getExtensionAttributes()->getStore() + $cart->getQuoteCurrencyCode() ); } } @@ -88,21 +87,17 @@ public function resolve(Field $field, $context, ResolveInfo $info, array $value * * @param array $data * @param string $quoteCurrencyCode - * @param StoreInterface $store * @return array * @throws NoSuchEntityException */ - private function processMoneyTypeData(array $data, string $quoteCurrencyCode, StoreInterface $store): array + private function processMoneyTypeData(array $data, string $quoteCurrencyCode): array { if (isset($data['amount'])) { $data['amount'] = ['value' => $data['amount'], 'currency' => $quoteCurrencyCode]; } - if (isset($data['base_amount'])) { - /** @var Currency $currency */ - $currency = $store->getBaseCurrency(); - $data['base_amount'] = ['value' => $data['base_amount'], 'currency' => $currency->getCode()]; - } + /** @deprecated The field should not be used on the storefront */ + $data['base_amount'] = null; if (isset($data['price_excl_tax'])) { $data['price_excl_tax'] = ['value' => $data['price_excl_tax'], 'currency' => $quoteCurrencyCode]; diff --git a/app/code/Magento/QuoteGraphQl/Model/Resolver/ShippingAddress/SelectedShippingMethod.php b/app/code/Magento/QuoteGraphQl/Model/Resolver/ShippingAddress/SelectedShippingMethod.php index f2dacf6d007f3..c6f25dd78823b 100644 --- a/app/code/Magento/QuoteGraphQl/Model/Resolver/ShippingAddress/SelectedShippingMethod.php +++ b/app/code/Magento/QuoteGraphQl/Model/Resolver/ShippingAddress/SelectedShippingMethod.php @@ -46,9 +46,6 @@ public function resolve(Field $field, $context, ResolveInfo $info, array $value } } - /** @var Currency $currency */ - $currency = $context->getExtensionAttributes()->getStore()->getBaseCurrency(); - $data = [ 'carrier_code' => $carrierCode, 'method_code' => $methodCode, @@ -58,20 +55,11 @@ public function resolve(Field $field, $context, ResolveInfo $info, array $value 'value' => $address->getShippingAmount(), 'currency' => $address->getQuote()->getQuoteCurrencyCode(), ], - 'base_amount' => [ - 'value' => $address->getBaseShippingAmount(), - 'currency' => $currency->getCode(), - ], - ]; - } else { - $data = [ - 'carrier_code' => null, - 'method_code' => null, - 'carrier_title' => $carrierTitle, - 'method_title' => $methodTitle, - 'amount' => null, + /** @deprecated The field should not be used on the storefront */ 'base_amount' => null, ]; + } else { + $data = null; } return $data; } diff --git a/app/code/Magento/QuoteGraphQl/Model/Resolver/ShippingAddresses.php b/app/code/Magento/QuoteGraphQl/Model/Resolver/ShippingAddresses.php index eb3b0966740eb..ae97f7efda45f 100644 --- a/app/code/Magento/QuoteGraphQl/Model/Resolver/ShippingAddresses.php +++ b/app/code/Magento/QuoteGraphQl/Model/Resolver/ShippingAddresses.php @@ -13,6 +13,8 @@ use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; use Magento\Quote\Model\Quote; use Magento\QuoteGraphQl\Model\Cart\ExtractQuoteAddressData; +use Magento\Framework\GraphQl\Schema\Type\TypeRegistry; +use Magento\Framework\App\ObjectManager; /** * @inheritdoc @@ -24,12 +26,21 @@ class ShippingAddresses implements ResolverInterface */ private $extractQuoteAddressData; + /** + * @var TypeRegistry + */ + private $typeRegistry; + /** * @param ExtractQuoteAddressData $extractQuoteAddressData + * @param TypeRegistry|null $typeRegistry */ - public function __construct(ExtractQuoteAddressData $extractQuoteAddressData) - { + public function __construct( + ExtractQuoteAddressData $extractQuoteAddressData, + TypeRegistry $typeRegistry = null + ) { $this->extractQuoteAddressData = $extractQuoteAddressData; + $this->typeRegistry = $typeRegistry ?: ObjectManager::getInstance()->get(TypeRegistry::class); } /** @@ -48,9 +59,38 @@ public function resolve(Field $field, $context, ResolveInfo $info, array $value if (count($shippingAddresses)) { foreach ($shippingAddresses as $shippingAddress) { - $addressesData[] = $this->extractQuoteAddressData->execute($shippingAddress); + $address = $this->extractQuoteAddressData->execute($shippingAddress); + + if ($this->validateAddressFromSchema($address)) { + $addressesData[] = $address; + } } } return $addressesData; } + + /** + * Validate data from address against mandatory fields from graphql schema for address + * + * @param array $address + * @return bool + */ + private function validateAddressFromSchema(array $address) : bool + { + /** @var \Magento\Framework\GraphQL\Schema\Type\Input\InputObjectType $cartAddressInput */ + $cartAddressInput = $this->typeRegistry->get('CartAddressInput'); + $fields = $cartAddressInput->getFields(); + + foreach ($fields as $field) { + if ($field->getType() instanceof \Magento\Framework\GraphQL\Schema\Type\NonNull) { + // an array key has to exist but it's value should not be null + if (array_key_exists($field->name, $address) + && !is_array($address[$field->name]) + && !isset($address[$field->name])) { + return false; + } + } + } + return true; + } } diff --git a/app/code/Magento/QuoteGraphQl/etc/schema.graphqls b/app/code/Magento/QuoteGraphQl/etc/schema.graphqls index 6d7f4daf40ded..a86eea46aa864 100644 --- a/app/code/Magento/QuoteGraphQl/etc/schema.graphqls +++ b/app/code/Magento/QuoteGraphQl/etc/schema.graphqls @@ -18,7 +18,7 @@ type Mutation { setShippingMethodsOnCart(input: SetShippingMethodsOnCartInput): SetShippingMethodsOnCartOutput @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\SetShippingMethodsOnCart") setPaymentMethodOnCart(input: SetPaymentMethodOnCartInput): SetPaymentMethodOnCartOutput @resolver(class: "Magento\\QuoteGraphQl\\Model\\Resolver\\SetPaymentMethodOnCart") setGuestEmailOnCart(input: SetGuestEmailOnCartInput): SetGuestEmailOnCartOutput @resolver(class: "Magento\\QuoteGraphQl\\Model\\Resolver\\SetGuestEmailOnCart") - setPaymentMethodAndPlaceOrder(input: SetPaymentMethodAndPlaceOrderInput): PlaceOrderOutput @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\SetPaymentAndPlaceOrder") + setPaymentMethodAndPlaceOrder(input: SetPaymentMethodAndPlaceOrderInput): PlaceOrderOutput @deprecated(reason: "Should use setPaymentMethodOnCart and placeOrder mutations in single request.") @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\SetPaymentAndPlaceOrder") placeOrder(input: PlaceOrderInput): PlaceOrderOutput @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\PlaceOrder") } @@ -85,6 +85,7 @@ input SetShippingAddressesOnCartInput { input ShippingAddressInput { customer_address_id: Int # If provided then will be used address from address book address: CartAddressInput + customer_notes: String } input SetBillingAddressOnCartInput { @@ -197,6 +198,8 @@ type Cart { available_payment_methods: [AvailablePaymentMethod] @resolver(class: "Magento\\QuoteGraphQl\\Model\\Resolver\\AvailablePaymentMethods") @doc(description: "Available payment methods") selected_payment_method: SelectedPaymentMethod @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\SelectedPaymentMethod") prices: CartPrices @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\CartPrices") + total_quantity: Float! @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\CartTotalQuantity") + is_virtual: Boolean! @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\CartIsVirtual") } interface CartAddressInterface @typeResolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\CartAddressTypeResolver") { @@ -209,7 +212,6 @@ interface CartAddressInterface @typeResolver(class: "\\Magento\\QuoteGraphQl\\Mo postcode: String country: CartAddressCountry telephone: String - customer_notes: String } type ShippingCartAddress implements CartAddressInterface { @@ -217,9 +219,11 @@ type ShippingCartAddress implements CartAddressInterface { selected_shipping_method: SelectedShippingMethod @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\ShippingAddress\\SelectedShippingMethod") items_weight: Float cart_items: [CartItemQuantity] + customer_notes: String } type BillingCartAddress implements CartAddressInterface { + customer_notes: String @deprecated (reason: "The field is used only in shipping address") } type CartItemQuantity { @@ -243,7 +247,7 @@ type SelectedShippingMethod { carrier_title: String method_title: String amount: Money - base_amount: Money + base_amount: Money @deprecated(reason: "The field should not be used on the storefront") } type AvailableShippingMethod { @@ -253,7 +257,7 @@ type AvailableShippingMethod { method_title: String @doc(description: "Could be null if method is not available") error_message: String amount: Money! - base_amount: Money @doc(description: "Could be null if method is not available") + base_amount: Money @deprecated(reason: "The field should not be used on the storefront") price_excl_tax: Money! price_incl_tax: Money! available: Boolean! diff --git a/app/code/Magento/ReleaseNotification/README.md b/app/code/Magento/ReleaseNotification/README.md index 1f6cac764b565..c53e3cbde1d0f 100644 --- a/app/code/Magento/ReleaseNotification/README.md +++ b/app/code/Magento/ReleaseNotification/README.md @@ -1,4 +1,4 @@ - # Magento_ReleaseNotification Module +# Magento_ReleaseNotification module The **Release Notification Module** serves to provide a notification delivery platform for displaying new features of a Magento installation or upgrade as well as any other required release notifications. diff --git a/app/code/Magento/Reports/Test/Mftf/Page/LowStockReportPage.xml b/app/code/Magento/Reports/Test/Mftf/Page/LowStockReportPage.xml new file mode 100644 index 0000000000000..5cfedd876cd43 --- /dev/null +++ b/app/code/Magento/Reports/Test/Mftf/Page/LowStockReportPage.xml @@ -0,0 +1,13 @@ + + + + + + + + diff --git a/app/code/Magento/Reports/Test/Mftf/Section/LowStockProductGridSection.xml b/app/code/Magento/Reports/Test/Mftf/Section/LowStockProductGridSection.xml new file mode 100644 index 0000000000000..efc3ee98d2e68 --- /dev/null +++ b/app/code/Magento/Reports/Test/Mftf/Section/LowStockProductGridSection.xml @@ -0,0 +1,16 @@ + + + + + + + + + + diff --git a/app/code/Magento/Reports/Test/Mftf/Section/LowStockReportMainSection.xml b/app/code/Magento/Reports/Test/Mftf/Section/LowStockReportMainSection.xml new file mode 100644 index 0000000000000..022641328d354 --- /dev/null +++ b/app/code/Magento/Reports/Test/Mftf/Section/LowStockReportMainSection.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + diff --git a/app/code/Magento/Review/Block/Adminhtml/Add.php b/app/code/Magento/Review/Block/Adminhtml/Add.php index 260685395e106..2edd76879d8dc 100644 --- a/app/code/Magento/Review/Block/Adminhtml/Add.php +++ b/app/code/Magento/Review/Block/Adminhtml/Add.php @@ -17,20 +17,18 @@ class Add extends \Magento\Backend\Block\Widget\Form\Container * Initialize add review * * @return void + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ protected function _construct() { parent::_construct(); - $this->_blockGroup = 'Magento_Review'; $this->_controller = 'adminhtml'; $this->_mode = 'add'; - $this->buttonList->update('save', 'label', __('Save Review')); $this->buttonList->update('save', 'id', 'save_button'); - $this->buttonList->update('reset', 'id', 'reset_button'); - + $this->buttonList->update('reset', 'onclick', 'window.review.formReset()'); $this->_formScripts[] = ' require(["prototype"], function(){ toggleParentVis("add_review_form"); @@ -38,12 +36,13 @@ protected function _construct() toggleVis("reset_button"); }); '; - // @codingStandardsIgnoreStart $this->_formInitScripts[] = ' - require(["jquery","prototype"], function(jQuery){ + require(["jquery","Magento_Review/js/rating","prototype"], function(jQuery, rating){ window.review = function() { return { + reviewFormEditSelector: "#edit_form", + ratingSelector: "[data-widget=ratingControl]", productInfoUrl : null, formHidden : true, gridRowClick : function(data, click) { @@ -72,6 +71,10 @@ protected function _construct() toggleVis("save_button"); toggleVis("reset_button"); }, + formReset: function() { + jQuery(review.reviewFormEditSelector).trigger(\'reset\'); + jQuery(review.ratingSelector).ratingControl(\'removeRating\'); + }, updateRating: function() { elements = [$("select_stores"), $("rating_detail").getElementsBySelector("input[type=\'radio\']")].flatten(); $(\'save_button\').disabled = true; diff --git a/app/code/Magento/Review/Block/Adminhtml/Grid.php b/app/code/Magento/Review/Block/Adminhtml/Grid.php index 92fc03aa1e981..02477bb89610f 100644 --- a/app/code/Magento/Review/Block/Adminhtml/Grid.php +++ b/app/code/Magento/Review/Block/Adminhtml/Grid.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); namespace Magento\Review\Block\Adminhtml; @@ -15,8 +16,6 @@ * @method \Magento\Review\Block\Adminhtml\Grid setCustomerId() setCustomerId(int $customerId) * @method \Magento\Review\Block\Adminhtml\Grid setMassactionIdFieldOnlyIndexValue() * setMassactionIdFieldOnlyIndexValue(bool $onlyIndex) - * - * @author Magento Core Team */ class Grid extends \Magento\Backend\Block\Widget\Grid\Extended { @@ -239,7 +238,13 @@ protected function _prepareColumns() if (!$this->_storeManager->isSingleStoreMode()) { $this->addColumn( 'visible_in', - ['header' => __('Visibility'), 'index' => 'stores', 'type' => 'store', 'store_view' => true] + [ + 'header' => __('Visibility'), + 'index' => 'stores', + 'type' => 'store', + 'store_view' => true, + 'sortable' => false + ] ); } @@ -349,6 +354,18 @@ protected function _prepareMassaction() ); } + /** + * @inheritdoc + */ + protected function _prepareMassactionColumn() + { + parent::_prepareMassactionColumn(); + /** needs for correct work of mass action select functionality */ + $this->setMassactionIdField('rt.review_id'); + + return $this; + } + /** * Get row url * diff --git a/app/code/Magento/Review/Block/Adminhtml/Main.php b/app/code/Magento/Review/Block/Adminhtml/Main.php index 45d14a1c60e69..5fae6acbce48a 100644 --- a/app/code/Magento/Review/Block/Adminhtml/Main.php +++ b/app/code/Magento/Review/Block/Adminhtml/Main.php @@ -3,12 +3,19 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); /** * Adminhtml review main block */ namespace Magento\Review\Block\Adminhtml; +use Magento\Framework\App\ObjectManager; +use Magento\Catalog\Model\ResourceModel\Product\CollectionFactory as ProductCollectionFactory; + +/** + * Class \Magento\Review\Block\Adminhtml\Main + */ class Main extends \Magento\Backend\Block\Widget\Grid\Container { /** @@ -37,6 +44,13 @@ class Main extends \Magento\Backend\Block\Widget\Grid\Container */ protected $_customerViewHelper; + /** + * Product Collection + * + * @var ProductCollectionFactory + */ + private $productCollectionFactory; + /** * @param \Magento\Backend\Block\Widget\Context $context * @param \Magento\Customer\Api\CustomerRepositoryInterface $customerRepository @@ -44,6 +58,7 @@ class Main extends \Magento\Backend\Block\Widget\Grid\Container * @param \Magento\Framework\Registry $registry * @param \Magento\Customer\Helper\View $customerViewHelper * @param array $data + * @param ProductCollectionFactory $productCollectionFactory */ public function __construct( \Magento\Backend\Block\Widget\Context $context, @@ -51,12 +66,15 @@ public function __construct( \Magento\Catalog\Model\ProductFactory $productFactory, \Magento\Framework\Registry $registry, \Magento\Customer\Helper\View $customerViewHelper, - array $data = [] + array $data = [], + ProductCollectionFactory $productCollectionFactory = null ) { $this->_coreRegistry = $registry; $this->customerRepository = $customerRepository; $this->_productFactory = $productFactory; $this->_customerViewHelper = $customerViewHelper; + $this->productCollectionFactory = $productCollectionFactory ?: ObjectManager::getInstance() + ->get(ProductCollectionFactory::class); parent::__construct($context, $data); } @@ -73,6 +91,10 @@ protected function _construct() $this->_blockGroup = 'Magento_Review'; $this->_controller = 'adminhtml'; + if (!$this->productCollectionFactory->create()->getSize()) { + $this->removeButton('add'); + } + // lookup customer, if id is specified $customerId = $this->getRequest()->getParam('customerId', false); $customerName = ''; diff --git a/app/code/Magento/Review/Model/Rss.php b/app/code/Magento/Review/Model/Rss.php index df8a5dbb96841..f5abdbb4d3c9e 100644 --- a/app/code/Magento/Review/Model/Rss.php +++ b/app/code/Magento/Review/Model/Rss.php @@ -3,11 +3,17 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +declare(strict_types=1); + namespace Magento\Review\Model; +use Magento\Framework\App\ObjectManager; + /** - * Class Rss - * @package Magento\Catalog\Model\Rss\Product + * Model Rss + * + * Class \Magento\Catalog\Model\Rss\Product\Rss */ class Rss extends \Magento\Framework\Model\AbstractModel { @@ -24,18 +30,35 @@ class Rss extends \Magento\Framework\Model\AbstractModel protected $eventManager; /** + * Rss constructor. + * * @param \Magento\Framework\Event\ManagerInterface $eventManager * @param ReviewFactory $reviewFactory + * @param \Magento\Framework\Model\Context|null $context + * @param \Magento\Framework\Registry|null $registry + * @param \Magento\Framework\Model\ResourceModel\AbstractResource|null $resource + * @param \Magento\Framework\Data\Collection\AbstractDb|null $resourceCollection + * @param array $data */ public function __construct( \Magento\Framework\Event\ManagerInterface $eventManager, - \Magento\Review\Model\ReviewFactory $reviewFactory + \Magento\Review\Model\ReviewFactory $reviewFactory, + \Magento\Framework\Model\Context $context = null, + \Magento\Framework\Registry $registry = null, + \Magento\Framework\Model\ResourceModel\AbstractResource $resource = null, + \Magento\Framework\Data\Collection\AbstractDb $resourceCollection = null, + array $data = [] ) { $this->reviewFactory = $reviewFactory; $this->eventManager = $eventManager; + $context = $context ?? ObjectManager::getInstance()->get(\Magento\Framework\Model\Context::class); + $registry = $registry ?? ObjectManager::getInstance()->get(\Magento\Framework\Registry::class); + parent::__construct($context, $registry, $resource, $resourceCollection, $data); } /** + * Get Product Collection + * * @return $this|\Magento\Framework\Model\ResourceModel\Db\Collection\AbstractCollection */ public function getProductCollection() diff --git a/app/code/Magento/Review/Test/Unit/Block/Adminhtml/MainTest.php b/app/code/Magento/Review/Test/Unit/Block/Adminhtml/MainTest.php index c7ff721d4ba22..41cabf68262d1 100644 --- a/app/code/Magento/Review/Test/Unit/Block/Adminhtml/MainTest.php +++ b/app/code/Magento/Review/Test/Unit/Block/Adminhtml/MainTest.php @@ -3,39 +3,59 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); namespace Magento\Review\Test\Unit\Block\Adminhtml; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; +use Magento\Customer\Api\CustomerRepositoryInterface; +use Magento\Customer\Helper\View as ViewHelper; +use Magento\Customer\Api\Data\CustomerInterface; +use Magento\Framework\App\RequestInterface; +use Magento\Review\Block\Adminhtml\Main as MainBlock; +use Magento\Framework\DataObject; +use Magento\Catalog\Model\ResourceModel\Product\Collection; +use Magento\Catalog\Model\ResourceModel\Product\CollectionFactory; +/** + * Unit Test For Main Block + * + * Class \Magento\Review\Test\Unit\Block\Adminhtml\MainTest + */ class MainTest extends \PHPUnit\Framework\TestCase { /** - * @var \Magento\Review\Block\Adminhtml\Main + * @var MainBlock */ protected $model; /** - * @var \Magento\Framework\App\RequestInterface|\PHPUnit_Framework_MockObject_MockObject + * @var RequestInterface|\PHPUnit_Framework_MockObject_MockObject */ protected $request; /** - * @var \Magento\Customer\Api\CustomerRepositoryInterface|\PHPUnit_Framework_MockObject_MockObject + * @var CustomerRepositoryInterface|\PHPUnit_Framework_MockObject_MockObject */ protected $customerRepository; /** - * @var \Magento\Customer\Helper\View|\PHPUnit_Framework_MockObject_MockObject + * @var ViewHelper|\PHPUnit_Framework_MockObject_MockObject */ protected $customerViewHelper; + /** + * @var CollectionFactory|\PHPUnit_Framework_MockObject_MockObject + */ + protected $collectionFactory; + public function testConstruct() { $this->customerRepository = $this - ->getMockForAbstractClass(\Magento\Customer\Api\CustomerRepositoryInterface::class); - $this->customerViewHelper = $this->createMock(\Magento\Customer\Helper\View::class); - $dummyCustomer = $this->getMockForAbstractClass(\Magento\Customer\Api\Data\CustomerInterface::class); + ->getMockForAbstractClass(CustomerRepositoryInterface::class); + $this->customerViewHelper = $this->createMock(ViewHelper::class); + $this->collectionFactory = $this->createMock(CollectionFactory::class); + $dummyCustomer = $this->getMockForAbstractClass(CustomerInterface::class); $this->customerRepository->expects($this->once()) ->method('getById') @@ -44,8 +64,8 @@ public function testConstruct() $this->customerViewHelper->expects($this->once()) ->method('getCustomerName') ->with($dummyCustomer) - ->will($this->returnValue(new \Magento\Framework\DataObject())); - $this->request = $this->getMockForAbstractClass(\Magento\Framework\App\RequestInterface::class); + ->will($this->returnValue(new DataObject())); + $this->request = $this->getMockForAbstractClass(RequestInterface::class); $this->request->expects($this->at(0)) ->method('getParam') ->with('customerId', false) @@ -54,14 +74,21 @@ public function testConstruct() ->method('getParam') ->with('productId', false) ->will($this->returnValue(false)); + $productCollection = $this->getMockBuilder(Collection::class) + ->disableOriginalConstructor() + ->getMock(); + $this->collectionFactory->expects($this->once()) + ->method('create') + ->will($this->returnValue($productCollection)); $objectManagerHelper = new ObjectManagerHelper($this); $this->model = $objectManagerHelper->getObject( - \Magento\Review\Block\Adminhtml\Main::class, + MainBlock::class, [ 'request' => $this->request, 'customerRepository' => $this->customerRepository, - 'customerViewHelper' => $this->customerViewHelper + 'customerViewHelper' => $this->customerViewHelper, + 'productCollectionFactory' => $this->collectionFactory ] ); } diff --git a/app/code/Magento/Review/view/adminhtml/web/js/rating.js b/app/code/Magento/Review/view/adminhtml/web/js/rating.js index b8d1b1b241b8f..63e6eaa0a2c50 100644 --- a/app/code/Magento/Review/view/adminhtml/web/js/rating.js +++ b/app/code/Magento/Review/view/adminhtml/web/js/rating.js @@ -62,6 +62,15 @@ define([ checkedInputs.nextAll('label').addBack().css('color', this.options.colorFilled).data('checked', true); checkedInputs.prevAll('label').css('color', this.options.colorUnfilled).data('checked', false); + }, + + /** + * Remove rating when form reset + */ + removeRating: function () { + var checkedInputs = this.element.find('input[type="radio"]'); + + checkedInputs.nextAll('label').css('color', this.options.colorUnfilled).data('checked', false); } }); diff --git a/app/code/Magento/Review/view/frontend/templates/product/view/list.phtml b/app/code/Magento/Review/view/frontend/templates/product/view/list.phtml index 347686d5c2ba4..d00c310069573 100644 --- a/app/code/Magento/Review/view/frontend/templates/product/view/list.phtml +++ b/app/code/Magento/Review/view/frontend/templates/product/view/list.phtml @@ -11,9 +11,11 @@ $format = $block->getDateFormat() ?: \IntlDateFormatter::SHORT; ?> - - = $block->escapeHtml(__('Customer Reviews')) ?> - + getHideTitle()) : ?> + + = $block->escapeHtml(__('Customer Reviews')) ?> + + = $block->getChildHtml('toolbar') ?> diff --git a/app/code/Magento/Sales/Block/Adminhtml/Order/Status/NewStatus/Form.php b/app/code/Magento/Sales/Block/Adminhtml/Order/Status/NewStatus/Form.php index c4d8907c9762b..1b275c4d809cb 100644 --- a/app/code/Magento/Sales/Block/Adminhtml/Order/Status/NewStatus/Form.php +++ b/app/code/Magento/Sales/Block/Adminhtml/Order/Status/NewStatus/Form.php @@ -81,7 +81,7 @@ protected function _addStoresFieldset($model, $form) if (!$this->_storeManager->isSingleStoreMode()) { $fieldset = $form->addFieldset( 'store_labels_fieldset', - ['legend' => __('Store View Specific Labels'), 'class' => 'store-scope'] + ['legend' => __('Store View Specific Labels'), 'class' => 'store-scope no-margin-top-tooltip'] ); } else { $fieldset = $form->addFieldset( diff --git a/app/code/Magento/Sales/Controller/Adminhtml/Order/ReviewPayment.php b/app/code/Magento/Sales/Controller/Adminhtml/Order/ReviewPayment.php index 09a52a113617a..93c8305ec2396 100644 --- a/app/code/Magento/Sales/Controller/Adminhtml/Order/ReviewPayment.php +++ b/app/code/Magento/Sales/Controller/Adminhtml/Order/ReviewPayment.php @@ -3,11 +3,18 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +declare(strict_types=1); + namespace Magento\Sales\Controller\Adminhtml\Order; use Magento\Backend\App\Action; +use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; -class ReviewPayment extends \Magento\Sales\Controller\Adminhtml\Order +/** + * Class \Magento\Sales\Controller\Adminhtml\Order\ReviewPayment + */ +class ReviewPayment extends \Magento\Sales\Controller\Adminhtml\Order implements HttpGetActionInterface { /** * Authorization level of a basic admin session @@ -21,7 +28,7 @@ class ReviewPayment extends \Magento\Sales\Controller\Adminhtml\Order * * Either denies or approves a payment that is in "review" state * - * @return \Magento\Backend\Model\View\Result\Redirect + * @return \Magento\Framework\Controller\Result\Redirect */ public function execute() { @@ -50,21 +57,23 @@ public function execute() } break; default: - throw new \Exception(sprintf('Action "%s" is not supported.', $action)); + throw new \Magento\Framework\Exception\NotFoundException( + __('Action "%1" is not supported.', $action) + ); } $this->orderRepository->save($order); $this->messageManager->addSuccessMessage($message); + $resultRedirect->setPath('sales/order/view', ['order_id' => $order->getEntityId()]); } else { $resultRedirect->setPath('sales/*/'); return $resultRedirect; } + // phpcs:ignore Magento2.Exceptions.ThrowCatch } catch (\Magento\Framework\Exception\LocalizedException $e) { - $this->messageManager->addErrorMessage($e->getMessage()); - } catch (\Exception $e) { $this->messageManager->addErrorMessage(__('We can\'t update the payment right now.')); $this->logger->critical($e); + $resultRedirect->setPath('sales/*/'); } - $resultRedirect->setPath('sales/order/view', ['order_id' => $order->getEntityId()]); return $resultRedirect; } } diff --git a/app/code/Magento/Sales/Cron/CleanExpiredQuotes.php b/app/code/Magento/Sales/Cron/CleanExpiredQuotes.php index 021e7b66cd13f..a5c7f71df66c5 100644 --- a/app/code/Magento/Sales/Cron/CleanExpiredQuotes.php +++ b/app/code/Magento/Sales/Cron/CleanExpiredQuotes.php @@ -57,7 +57,6 @@ public function execute() $quotes->addFieldToFilter('store_id', $storeId); $quotes->addFieldToFilter('updated_at', ['to' => date("Y-m-d", time() - $lifetime)]); - $quotes->addFieldToFilter('is_active', 0); foreach ($this->getExpireQuotesAdditionalFilterFields() as $field => $condition) { $quotes->addFieldToFilter($field, $condition); diff --git a/app/code/Magento/Sales/Model/AdminOrder/Create.php b/app/code/Magento/Sales/Model/AdminOrder/Create.php index d51fa0778b192..fd1fb472719d4 100644 --- a/app/code/Magento/Sales/Model/AdminOrder/Create.php +++ b/app/code/Magento/Sales/Model/AdminOrder/Create.php @@ -1135,7 +1135,6 @@ public function updateQuoteItems($items) } catch (\Magento\Framework\Exception\LocalizedException $e) { $this->recollectCart(); throw $e; - // phpcs:ignore Magento2.Exceptions.ThrowCatch } catch (\Exception $e) { $this->_logger->critical($e); } @@ -1992,6 +1991,7 @@ protected function _validate() /** @var \Magento\Quote\Model\Quote\Item $item */ $messages = $item->getMessage(false); if ($item->getHasError() && is_array($messages) && !empty($messages)) { + // phpcs:ignore Magento2.Performance.ForeachArrayMerge $this->_errors = array_merge($this->_errors, $messages); } } @@ -2011,7 +2011,6 @@ protected function _validate() } else { try { $method->validate(); - // phpcs:ignore Magento2.Exceptions.ThrowCatch } catch (\Magento\Framework\Exception\LocalizedException $e) { $this->_errors[] = $e->getMessage(); } diff --git a/app/code/Magento/Sales/Model/Order/CustomerAssignment.php b/app/code/Magento/Sales/Model/Order/CustomerAssignment.php new file mode 100644 index 0000000000000..194b03c2bc9bd --- /dev/null +++ b/app/code/Magento/Sales/Model/Order/CustomerAssignment.php @@ -0,0 +1,72 @@ +eventManager = $eventManager; + $this->orderRepository = $orderRepository; + } + + /** + * Assign customer to order. + * + * @param OrderInterface $order + * @param CustomerInterface $customer + */ + public function execute(OrderInterface $order, CustomerInterface $customer): void + { + $order->setCustomerId($customer->getId()) + ->setCustomerIsGuest(false) + ->setCustomerEmail($customer->getEmail()) + ->setCustomerFirstname($customer->getFirstname()) + ->setCustomerLastname($customer->getLastname()) + ->setCustomerMiddlename($customer->getMiddlename()) + ->setCustomerPrefix($customer->getPrefix()) + ->setCustomerSuffix($customer->getSuffix()) + ->setCustomerGroupId($customer->getGroupId()); + + $this->orderRepository->save($order); + + $this->eventManager->dispatch( + 'sales_order_customer_assign_after', + [ + 'order' => $order, + 'customer' => $customer + ] + ); + } +} diff --git a/app/code/Magento/Sales/Model/Order/Email/Sender.php b/app/code/Magento/Sales/Model/Order/Email/Sender.php index 564fd1e2a4b98..ab1a158336d30 100644 --- a/app/code/Magento/Sales/Model/Order/Email/Sender.php +++ b/app/code/Magento/Sales/Model/Order/Email/Sender.php @@ -12,8 +12,9 @@ /** * Class Sender - * @api * + * phpcs:disable Magento2.Classes.AbstractApi + * @api * @since 100.0.2 */ abstract class Sender @@ -87,10 +88,12 @@ protected function checkAndSend(Order $order) $this->logger->error($e->getMessage()); return false; } - try { - $sender->sendCopyTo(); - } catch (\Exception $e) { - $this->logger->error($e->getMessage()); + if ($this->identityContainer->getCopyMethod() == 'copy') { + try { + $sender->sendCopyTo(); + } catch (\Exception $e) { + $this->logger->error($e->getMessage()); + } } return true; } diff --git a/app/code/Magento/Sales/Model/Order/Email/SenderBuilder.php b/app/code/Magento/Sales/Model/Order/Email/SenderBuilder.php index 1ae5d7479952b..c4523981ac729 100644 --- a/app/code/Magento/Sales/Model/Order/Email/SenderBuilder.php +++ b/app/code/Magento/Sales/Model/Order/Email/SenderBuilder.php @@ -84,7 +84,7 @@ public function sendCopyTo() { $copyTo = $this->identityContainer->getEmailCopyTo(); - if (!empty($copyTo) && $this->identityContainer->getCopyMethod() == 'copy') { + if (!empty($copyTo)) { $this->configureEmailTemplate(); foreach ($copyTo as $email) { $this->transportBuilder->addTo($email); diff --git a/app/code/Magento/Sales/Model/Order/Payment.php b/app/code/Magento/Sales/Model/Order/Payment.php index 5d1d3f0d040a7..dcf6d86b44cae 100644 --- a/app/code/Magento/Sales/Model/Order/Payment.php +++ b/app/code/Magento/Sales/Model/Order/Payment.php @@ -684,7 +684,6 @@ public function refund($creditmemo) $gateway->refund($this, $baseAmountToRefund); $creditmemo->setTransactionId($this->getLastTransId()); - // phpcs:ignore Magento2.Exceptions.ThrowCatch } catch (\Magento\Framework\Exception\LocalizedException $e) { if (!$captureTxn) { throw new \Magento\Framework\Exception\LocalizedException( diff --git a/app/code/Magento/Sales/Model/Order/Pdf/Creditmemo.php b/app/code/Magento/Sales/Model/Order/Pdf/Creditmemo.php index 74f3eebf8fcb9..f1430757939e7 100644 --- a/app/code/Magento/Sales/Model/Order/Pdf/Creditmemo.php +++ b/app/code/Magento/Sales/Model/Order/Pdf/Creditmemo.php @@ -3,10 +3,14 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +declare(strict_types=1); + namespace Magento\Sales\Model\Order\Pdf; /** * Sales Order Creditmemo PDF model + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class Creditmemo extends AbstractPdf @@ -180,11 +184,11 @@ public function getPdf($creditmemos = []) } /* Add totals */ $this->insertTotals($page, $creditmemo); + if ($creditmemo->getStoreId()) { + $this->_localeResolver->revert(); + } } $this->_afterGetPdf(); - if ($creditmemo->getStoreId()) { - $this->_localeResolver->revert(); - } return $pdf; } diff --git a/app/code/Magento/Sales/Model/OrderRepository.php b/app/code/Magento/Sales/Model/OrderRepository.php index 79548cb190754..f93de4c32d888 100644 --- a/app/code/Magento/Sales/Model/OrderRepository.php +++ b/app/code/Magento/Sales/Model/OrderRepository.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); namespace Magento\Sales\Model; @@ -247,8 +248,11 @@ public function deleteById($id) /** * Perform persist operations for one entity * - * @param \Magento\Sales\Api\Data\OrderInterface $entity - * @return \Magento\Sales\Api\Data\OrderInterface + * @param OrderInterface $entity + * @return OrderInterface + * @throws InputException + * @throws NoSuchEntityException + * @throws \Magento\Framework\Exception\AlreadyExistsException */ public function save(\Magento\Sales\Api\Data\OrderInterface $entity) { @@ -262,6 +266,7 @@ public function save(\Magento\Sales\Api\Data\OrderInterface $entity) $entity->setShippingMethod($shipping->getMethod()); } } + $this->metadata->getMapper()->save($entity); $this->registry[$entity->getEntityId()] = $entity; return $this->registry[$entity->getEntityId()]; diff --git a/app/code/Magento/Sales/Model/ResourceModel/EntityAbstract.php b/app/code/Magento/Sales/Model/ResourceModel/EntityAbstract.php index 80612277e68d5..72ce60d32877c 100644 --- a/app/code/Magento/Sales/Model/ResourceModel/EntityAbstract.php +++ b/app/code/Magento/Sales/Model/ResourceModel/EntityAbstract.php @@ -3,6 +3,8 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Sales\Model\ResourceModel; use Magento\Framework\Model\ResourceModel\Db\VersionControl\AbstractDb; @@ -14,6 +16,7 @@ /** * Abstract sales entity provides to its children knowledge about eventPrefix and eventObject * + * phpcs:disable Magento2.Classes.AbstractApi * @api * @SuppressWarnings(PHPMD.NumberOfChildren) * @since 100.0.2 @@ -96,6 +99,7 @@ public function saveAttribute(\Magento\Framework\Model\AbstractModel $object, $a /** * Prepares data for saving and removes update time (if exists). + * * This prevents saving same update time on each entity update. * * @param \Magento\Framework\Model\AbstractModel $object @@ -114,6 +118,7 @@ protected function _prepareDataForSave(\Magento\Framework\Model\AbstractModel $o /** * Perform actions before object save + * * Perform actions before object save, calculate next sequence value for increment Id * * @param \Magento\Framework\Model\AbstractModel|\Magento\Framework\DataObject $object @@ -122,7 +127,7 @@ protected function _prepareDataForSave(\Magento\Framework\Model\AbstractModel $o protected function _beforeSave(\Magento\Framework\Model\AbstractModel $object) { /** @var \Magento\Sales\Model\AbstractModel $object */ - if ($object instanceof EntityInterface && $object->getIncrementId() == null) { + if ($object instanceof EntityInterface && $object->getEntityId() == null && $object->getIncrementId() == null) { $store = $object->getStore(); $storeId = $store->getId(); if ($storeId === null) { diff --git a/app/code/Magento/Sales/Model/Rss/OrderStatus.php b/app/code/Magento/Sales/Model/Rss/OrderStatus.php index c3c4456c6b7ca..e03cbbee1b50c 100644 --- a/app/code/Magento/Sales/Model/Rss/OrderStatus.php +++ b/app/code/Magento/Sales/Model/Rss/OrderStatus.php @@ -217,11 +217,12 @@ protected function getEntries() if ($type && $type != 'order') { $urlAppend = $type; } - $type = __(ucwords($type)); - $title = __('Details for %1 #%2', $type, $result['increment_id']); - $description = '' . __('Notified Date: %1', $this->localeDate->formatDate($result['created_at'])) + $type = __(ucwords($type))->render(); + $title = __('Details for %1 #%2', $type, $result['increment_id'])->render(); + $description = '' + . __('Notified Date: %1', $this->localeDate->formatDate($result['created_at']))->render() . '' - . __('Comment: %1', $result['comment']) . ''; + . __('Comment: %1', $result['comment'])->render() . ''; $url = $this->urlBuilder->getUrl( 'sales/order/' . $urlAppend, ['order_id' => $this->order->getId()] @@ -233,10 +234,10 @@ protected function getEntries() 'Order #%1 created at %2', $this->order->getIncrementId(), $this->localeDate->formatDate($this->order->getCreatedAt()) - ); + )->render(); $url = $this->urlBuilder->getUrl('sales/order/view', ['order_id' => $this->order->getId()]); - $description = '' . __('Current Status: %1', $this->order->getStatusLabel()) . - __('Total: %1', $this->order->formatPrice($this->order->getGrandTotal())) . ''; + $description = '' . __('Current Status: %1', $this->order->getStatusLabel())->render() . + __('Total: %1', $this->order->formatPrice($this->order->getGrandTotal()))->render() . ''; $entries[] = ['title' => $title, 'link' => $url, 'description' => $description]; @@ -250,7 +251,7 @@ protected function getEntries() */ protected function getHeader() { - $title = __('Order # %1 Notification(s)', $this->order->getIncrementId()); + $title = __('Order # %1 Notification(s)', $this->order->getIncrementId())->render(); $newUrl = $this->urlBuilder->getUrl('sales/order/view', ['order_id' => $this->order->getId()]); return ['title' => $title, 'description' => $title, 'link' => $newUrl, 'charset' => 'UTF-8']; diff --git a/app/code/Magento/Sales/Observer/AssignOrderToCustomerObserver.php b/app/code/Magento/Sales/Observer/AssignOrderToCustomerObserver.php index 5883bde175101..546e2f5e510e2 100644 --- a/app/code/Magento/Sales/Observer/AssignOrderToCustomerObserver.php +++ b/app/code/Magento/Sales/Observer/AssignOrderToCustomerObserver.php @@ -11,6 +11,7 @@ use Magento\Framework\Event\Observer; use Magento\Framework\Event\ObserverInterface; use Magento\Sales\Api\OrderRepositoryInterface; +use Magento\Sales\Model\Order\CustomerAssignment; /** * Assign order to customer created after issuing guest order. @@ -23,11 +24,22 @@ class AssignOrderToCustomerObserver implements ObserverInterface private $orderRepository; /** + * @var CustomerAssignment + */ + private $assignmentService; + + /** + * AssignOrderToCustomerObserver constructor. + * * @param OrderRepositoryInterface $orderRepository + * @param CustomerAssignment $assignmentService */ - public function __construct(OrderRepositoryInterface $orderRepository) - { + public function __construct( + OrderRepositoryInterface $orderRepository, + CustomerAssignment $assignmentService + ) { $this->orderRepository = $orderRepository; + $this->assignmentService = $assignmentService; } /** @@ -43,18 +55,8 @@ public function execute(Observer $observer) if (array_key_exists('__sales_assign_order_id', $delegateData)) { $orderId = $delegateData['__sales_assign_order_id']; $order = $this->orderRepository->get($orderId); - if (!$order->getCustomerId()) { - //assign customer info to order after customer creation. - $order->setCustomerId($customer->getId()) - ->setCustomerIsGuest(0) - ->setCustomerEmail($customer->getEmail()) - ->setCustomerFirstname($customer->getFirstname()) - ->setCustomerLastname($customer->getLastname()) - ->setCustomerMiddlename($customer->getMiddlename()) - ->setCustomerPrefix($customer->getPrefix()) - ->setCustomerSuffix($customer->getSuffix()) - ->setCustomerGroupId($customer->getGroupId()); - $this->orderRepository->save($order); + if (!$order->getCustomerId() && $customer->getId()) { + $this->assignmentService->execute($order, $customer); } } } diff --git a/app/code/Magento/Sales/Setup/Patch/Data/UpdateCreditmemoGridCurrencyCode.php b/app/code/Magento/Sales/Setup/Patch/Data/UpdateCreditmemoGridCurrencyCode.php new file mode 100644 index 0000000000000..b143ae7c3ba7f --- /dev/null +++ b/app/code/Magento/Sales/Setup/Patch/Data/UpdateCreditmemoGridCurrencyCode.php @@ -0,0 +1,85 @@ +moduleDataSetup = $moduleDataSetup; + $this->salesSetupFactory = $salesSetupFactory; + } + + /** + * @inheritdoc + */ + public function apply() + { + /** @var SalesSetup $salesSetup */ + $salesSetup = $this->salesSetupFactory->create(['setup' => $this->moduleDataSetup]); + /** @var Mysql $connection */ + $connection = $salesSetup->getConnection(); + $creditMemoGridTable = $salesSetup->getTable('sales_creditmemo_grid'); + $orderTable = $salesSetup->getTable('sales_order'); + $select = $connection->select(); + $condition = 'so.entity_id = scg.order_id'; + $select->join(['so' => $orderTable], $condition, ['order_currency_code', 'base_currency_code']); + $sql = $connection->updateFromSelect($select, ['scg' => $creditMemoGridTable]); + $connection->query($sql); + } + + /** + * @inheritdoc + */ + public static function getDependencies() + { + return []; + } + + /** + * @inheritdoc + */ + public static function getVersion() + { + return '2.0.13'; + } + + /** + * @inheritdoc + */ + public function getAliases() + { + return []; + } +} diff --git a/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminOrderActionGroup.xml b/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminOrderActionGroup.xml index 2ad40de685088..3f178ae02102a 100644 --- a/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminOrderActionGroup.xml +++ b/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminOrderActionGroup.xml @@ -535,4 +535,7 @@ + + + diff --git a/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderInvoicesTabSection.xml b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderInvoicesTabSection.xml index f63979c4ac54b..88d90bc716576 100644 --- a/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderInvoicesTabSection.xml +++ b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderInvoicesTabSection.xml @@ -17,7 +17,7 @@ - - + + \ No newline at end of file diff --git a/app/code/Magento/Sales/Test/Mftf/Test/StorefrontOrderPagerDisplayedTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/StorefrontOrderPagerDisplayedTest.xml index b8772f24a2a42..0e58bb84988a2 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/StorefrontOrderPagerDisplayedTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/StorefrontOrderPagerDisplayedTest.xml @@ -129,7 +129,7 @@ - + diff --git a/app/code/Magento/Sales/Test/Mftf/Test/StorefrontOrderPagerIsAbsentTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/StorefrontOrderPagerIsAbsentTest.xml index 9909fca44fe2c..3ff8a7791d88b 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/StorefrontOrderPagerIsAbsentTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/StorefrontOrderPagerIsAbsentTest.xml @@ -125,7 +125,7 @@ - + diff --git a/app/code/Magento/Sales/Test/Unit/Cron/CleanExpiredQuotesTest.php b/app/code/Magento/Sales/Test/Unit/Cron/CleanExpiredQuotesTest.php index e424cae85f223..ad6a3e03ba679 100644 --- a/app/code/Magento/Sales/Test/Unit/Cron/CleanExpiredQuotesTest.php +++ b/app/code/Magento/Sales/Test/Unit/Cron/CleanExpiredQuotesTest.php @@ -59,7 +59,7 @@ public function testExecute($lifetimes, $additionalFilterFields) $this->quoteFactoryMock->expects($this->exactly(count($lifetimes))) ->method('create') ->will($this->returnValue($quotesMock)); - $quotesMock->expects($this->exactly((3 + count($additionalFilterFields)) * count($lifetimes))) + $quotesMock->expects($this->exactly((2 + count($additionalFilterFields)) * count($lifetimes))) ->method('addFieldToFilter'); if (!empty($lifetimes)) { $quotesMock->expects($this->exactly(count($lifetimes))) diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/Creditmemo/Sender/EmailSenderTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/Creditmemo/Sender/EmailSenderTest.php index 467476c9bb406..13ed0739348b2 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/Order/Creditmemo/Sender/EmailSenderTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/Order/Creditmemo/Sender/EmailSenderTest.php @@ -283,6 +283,10 @@ public function testSend($configValue, $forceSyncMode, $isComment, $emailSending ->willReturn($emailSendingResult); if ($emailSendingResult) { + $this->identityContainerMock->expects($this->once()) + ->method('getCopyMethod') + ->willReturn('copy'); + $this->senderBuilderFactoryMock->expects($this->once()) ->method('create') ->willReturn($this->senderMock); diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/CustomerAssigmentTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/CustomerAssigmentTest.php new file mode 100644 index 0000000000000..2ceda3018befd --- /dev/null +++ b/app/code/Magento/Sales/Test/Unit/Model/Order/CustomerAssigmentTest.php @@ -0,0 +1,155 @@ +configureOrderMock($data); + $this->configureCustomerMock($data); + $this->orderRepositoryMock->expects($this->once())->method('save')->with($this->orderMock); + $this->eventManagerMock->expects($this->once())->method('dispatch')->with( + 'sales_order_customer_assign_after', + [ + 'order' => $this->orderMock, + 'customer' => $this->customerMock + ] + ); + + $this->customerAssignment->execute($this->orderMock, $this->customerMock); + } + + /** + * + * Data provider for testExecute. + * @return array + */ + public function executeDataProvider(): array + { + return [ + [ + [ + 'customerId' => 1, + 'customerIsGuest' => false, + 'customerEmail' => 'customerEmail', + 'customerFirstname' => 'customerFirstname', + 'customerLastname' => 'customerLastname', + 'customerMiddlename' => 'customerMiddlename', + 'customerPrefix' => 'customerPrefix', + 'customerSuffix' => 'customerSuffix', + 'customerGroupId' => 'customerGroupId', + ], + ], + ]; + } + + /** + * @return void + */ + protected function setUp() + { + $objectManager = new ObjectManager($this); + $this->orderMock = $this->createMock(OrderInterface::class); + $this->customerMock = $this->createMock(CustomerInterface::class); + $this->orderRepositoryMock = $this->createMock(OrderRepositoryInterface::class); + $this->eventManagerMock = $this->createMock(ManagerInterface::class); + $this->customerAssignment = $objectManager->getObject( + CustomerAssignment::class, + [ + 'eventManager' => $this->eventManagerMock, + 'orderRepository' => $this->orderRepositoryMock + ] + ); + } + + /** + * Set up order mock. + * + * @param array $data + */ + private function configureOrderMock(array $data): void + { + $this->orderMock->expects($this->once())->method('setCustomerId')->with($data['customerId']) + ->willReturn($this->orderMock); + $this->orderMock->expects($this->once())->method('setCustomerIsGuest')->with($data['customerIsGuest']) + ->willReturn($this->orderMock); + $this->orderMock->expects($this->once())->method('setCustomerEmail')->with($data['customerEmail']) + ->willReturn($this->orderMock); + $this->orderMock->expects($this->once())->method('setCustomerFirstname')->with($data['customerFirstname']) + ->willReturn($this->orderMock); + $this->orderMock->expects($this->once())->method('setCustomerLastname')->with($data['customerLastname']) + ->willReturn($this->orderMock); + $this->orderMock->expects($this->once())->method('setCustomerMiddlename')->with($data['customerMiddlename']) + ->willReturn($this->orderMock); + $this->orderMock->expects($this->once())->method('setCustomerPrefix')->with($data['customerPrefix']) + ->willReturn($this->orderMock); + $this->orderMock->expects($this->once())->method('setCustomerSuffix')->with($data['customerSuffix']) + ->willReturn($this->orderMock); + $this->orderMock->expects($this->once())->method('setCustomerGroupId')->with($data['customerGroupId']) + ->willReturn($this->orderMock); + } + + /** + * Set up customer mock. + * + * @param array $data + */ + private function configureCustomerMock(array $data): void + { + $this->customerMock->expects($this->once())->method('getId')->willReturn($data['customerId']); + $this->customerMock->expects($this->once())->method('getEmail')->willReturn($data['customerEmail']); + $this->customerMock->expects($this->once())->method('getFirstname')->willReturn($data['customerFirstname']); + $this->customerMock->expects($this->once())->method('getLastname')->willReturn($data['customerLastname']); + $this->customerMock->expects($this->once())->method('getMiddlename')->willReturn($data['customerMiddlename']); + $this->customerMock->expects($this->once())->method('getPrefix')->willReturn($data['customerPrefix']); + $this->customerMock->expects($this->once())->method('getSuffix')->willReturn($data['customerSuffix']); + $this->customerMock->expects($this->once())->method('getGroupId')->willReturn($data['customerGroupId']); + } +} diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Sender/CreditmemoSenderTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Sender/CreditmemoSenderTest.php index 1f074d7262f4d..287daa2fba4b9 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Sender/CreditmemoSenderTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Sender/CreditmemoSenderTest.php @@ -57,7 +57,7 @@ protected function setUp() $this->identityContainerMock = $this->createPartialMock( \Magento\Sales\Model\Order\Email\Container\CreditmemoIdentity::class, - ['getStore', 'isEnabled', 'getConfigValue', 'getTemplateId', 'getGuestTemplateId'] + ['getStore', 'isEnabled', 'getConfigValue', 'getTemplateId', 'getGuestTemplateId', 'getCopyMethod'] ); $this->identityContainerMock->expects($this->any()) ->method('getStore') @@ -138,6 +138,10 @@ public function testSend($configValue, $forceSyncMode, $customerNoteNotify, $ema ->willReturn($emailSendingResult); if ($emailSendingResult) { + $this->identityContainerMock->expects($this->once()) + ->method('getCopyMethod') + ->willReturn('copy'); + $this->senderBuilderFactoryMock->expects($this->once()) ->method('create') ->willReturn($this->senderMock); diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Sender/InvoiceSenderTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Sender/InvoiceSenderTest.php index d1aa5af53da4d..3315ec8eb4196 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Sender/InvoiceSenderTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Sender/InvoiceSenderTest.php @@ -57,7 +57,7 @@ protected function setUp() $this->identityContainerMock = $this->createPartialMock( \Magento\Sales\Model\Order\Email\Container\InvoiceIdentity::class, - ['getStore', 'isEnabled', 'getConfigValue', 'getTemplateId', 'getGuestTemplateId'] + ['getStore', 'isEnabled', 'getConfigValue', 'getTemplateId', 'getGuestTemplateId', 'getCopyMethod'] ); $this->identityContainerMock->expects($this->any()) ->method('getStore') @@ -144,6 +144,10 @@ public function testSend($configValue, $forceSyncMode, $customerNoteNotify, $ema ->willReturn($emailSendingResult); if ($emailSendingResult) { + $this->identityContainerMock->expects($this->once()) + ->method('getCopyMethod') + ->willReturn('copy'); + $this->senderBuilderFactoryMock->expects($this->once()) ->method('create') ->willReturn($this->senderMock); diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Sender/OrderSenderTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Sender/OrderSenderTest.php index 88053ea684ce8..bfea2d63ef1bb 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Sender/OrderSenderTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Sender/OrderSenderTest.php @@ -30,7 +30,7 @@ protected function setUp() $this->identityContainerMock = $this->createPartialMock( \Magento\Sales\Model\Order\Email\Container\OrderIdentity::class, - ['getStore', 'isEnabled', 'getConfigValue', 'getTemplateId', 'getGuestTemplateId'] + ['getStore', 'isEnabled', 'getConfigValue', 'getTemplateId', 'getGuestTemplateId', 'getCopyMethod'] ); $this->identityContainerMock->expects($this->any()) ->method('getStore') @@ -77,6 +77,10 @@ public function testSend($configValue, $forceSyncMode, $emailSendingResult, $sen ->willReturn($emailSendingResult); if ($emailSendingResult) { + $this->identityContainerMock->expects($senderSendException ? $this->never() : $this->once()) + ->method('getCopyMethod') + ->willReturn('copy'); + $addressMock = $this->createMock(\Magento\Sales\Model\Order\Address::class); $this->addressRenderer->expects($this->any()) @@ -214,6 +218,10 @@ public function testSendVirtualOrder($isVirtualOrder, $formatCallCount, $expecte ->method('isEnabled') ->willReturn(true); + $this->identityContainerMock->expects($this->once()) + ->method('getCopyMethod') + ->willReturn('copy'); + $addressMock = $this->createMock(\Magento\Sales\Model\Order\Address::class); $this->addressRenderer->expects($this->exactly($formatCallCount)) diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Sender/ShipmentSenderTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Sender/ShipmentSenderTest.php index 2d7b42bccae5a..96bbb1aea7abd 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Sender/ShipmentSenderTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Sender/ShipmentSenderTest.php @@ -57,7 +57,7 @@ protected function setUp() $this->identityContainerMock = $this->createPartialMock( \Magento\Sales\Model\Order\Email\Container\ShipmentIdentity::class, - ['getStore', 'isEnabled', 'getConfigValue', 'getTemplateId', 'getGuestTemplateId'] + ['getStore', 'isEnabled', 'getConfigValue', 'getTemplateId', 'getGuestTemplateId', 'getCopyMethod'] ); $this->identityContainerMock->expects($this->any()) ->method('getStore') @@ -144,6 +144,10 @@ public function testSend($configValue, $forceSyncMode, $customerNoteNotify, $ema ->willReturn($emailSendingResult); if ($emailSendingResult) { + $this->identityContainerMock->expects($this->once()) + ->method('getCopyMethod') + ->willReturn('copy'); + $this->senderBuilderFactoryMock->expects($this->once()) ->method('create') ->willReturn($this->senderMock); diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/Email/SenderBuilderTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/Email/SenderBuilderTest.php index 24cd54e3a46b3..adfb697e70331 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/Order/Email/SenderBuilderTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/Order/Email/SenderBuilderTest.php @@ -48,11 +48,14 @@ protected function setUp() ['getTemplateVars', 'getTemplateOptions', 'getTemplateId'] ); - $this->storeMock = $this->createPartialMock(\Magento\Store\Model\Store::class, [ - 'getStoreId', - '__wakeup', - 'getId', - ]); + $this->storeMock = $this->createPartialMock( + \Magento\Store\Model\Store::class, + [ + 'getStoreId', + '__wakeup', + 'getId', + ] + ); $this->identityContainerMock = $this->createPartialMock( \Magento\Sales\Model\Order\Email\Container\ShipmentIdentity::class, @@ -165,9 +168,6 @@ public function testSendCopyTo() $transportMock = $this->createMock( \Magento\Sales\Test\Unit\Model\Order\Email\Stub\TransportInterfaceMock::class ); - $this->identityContainerMock->expects($this->once()) - ->method('getCopyMethod') - ->will($this->returnValue('copy')); $this->identityContainerMock->expects($this->never()) ->method('getCustomerEmail'); $this->identityContainerMock->expects($this->never()) diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/Invoice/Sender/EmailSenderTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/Invoice/Sender/EmailSenderTest.php index dcf689cf7d53b..6db1ec0392e0e 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/Order/Invoice/Sender/EmailSenderTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/Order/Invoice/Sender/EmailSenderTest.php @@ -282,6 +282,10 @@ public function testSend($configValue, $forceSyncMode, $isComment, $emailSending ->willReturn($emailSendingResult); if ($emailSendingResult) { + $this->identityContainerMock->expects($this->once()) + ->method('getCopyMethod') + ->willReturn('copy'); + $this->senderBuilderFactoryMock->expects($this->once()) ->method('create') ->willReturn($this->senderMock); diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/Shipment/Sender/EmailSenderTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/Shipment/Sender/EmailSenderTest.php index 391e99ba6f835..2262fbf03c1a1 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/Order/Shipment/Sender/EmailSenderTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/Order/Shipment/Sender/EmailSenderTest.php @@ -284,6 +284,10 @@ public function testSend($configValue, $forceSyncMode, $isComment, $emailSending ->willReturn($emailSendingResult); if ($emailSendingResult) { + $this->identityContainerMock->expects($this->once()) + ->method('getCopyMethod') + ->willReturn('copy'); + $this->senderBuilderFactoryMock->expects($this->once()) ->method('create') ->willReturn($this->senderMock); diff --git a/app/code/Magento/Sales/Test/Unit/Model/ResourceModel/OrderTest.php b/app/code/Magento/Sales/Test/Unit/Model/ResourceModel/OrderTest.php index 00cd7ebc7b070..067e44757a67b 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/ResourceModel/OrderTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/ResourceModel/OrderTest.php @@ -3,6 +3,8 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Sales\Test\Unit\Model\ResourceModel; use Magento\Framework\Model\ResourceModel\Db\VersionControl\RelationComposite; @@ -198,7 +200,7 @@ public function testSave() ->with('10000001') ->willReturnSelf(); $this->orderMock->expects($this->once()) - ->method('getIncrementId') + ->method('getEntityId') ->willReturn(null); $this->orderMock->expects($this->once()) ->method('getData') diff --git a/app/code/Magento/Sales/Test/Unit/Observer/AssignOrderToCustomerObserverTest.php b/app/code/Magento/Sales/Test/Unit/Observer/AssignOrderToCustomerObserverTest.php index e919b45667f24..80ab948a9ad71 100644 --- a/app/code/Magento/Sales/Test/Unit/Observer/AssignOrderToCustomerObserverTest.php +++ b/app/code/Magento/Sales/Test/Unit/Observer/AssignOrderToCustomerObserverTest.php @@ -12,6 +12,7 @@ use Magento\Framework\Event\Observer; use Magento\Sales\Api\Data\OrderInterface; use Magento\Sales\Api\OrderRepositoryInterface; +use Magento\Sales\Model\Order\CustomerAssignment; use Magento\Sales\Observer\AssignOrderToCustomerObserver; use PHPUnit\Framework\TestCase; use PHPUnit_Framework_MockObject_MockObject; @@ -27,6 +28,9 @@ class AssignOrderToCustomerObserverTest extends TestCase /** @var OrderRepositoryInterface|PHPUnit_Framework_MockObject_MockObject */ protected $orderRepositoryMock; + /** @var CustomerAssignment | PHPUnit_Framework_MockObject_MockObject */ + protected $assignmentMock; + /** * Set Up */ @@ -35,17 +39,23 @@ protected function setUp() $this->orderRepositoryMock = $this->getMockBuilder(OrderRepositoryInterface::class) ->disableOriginalConstructor() ->getMock(); - $this->sut = new AssignOrderToCustomerObserver($this->orderRepositoryMock); + + $this->assignmentMock = $this->getMockBuilder(CustomerAssignment::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->sut = new AssignOrderToCustomerObserver($this->orderRepositoryMock, $this->assignmentMock); } /** * Test assigning order to customer after issuing guest order * * @dataProvider getCustomerIds + * @param null|int $orderCustomerId * @param null|int $customerId * @return void */ - public function testAssignOrderToCustomerAfterGuestOrder($customerId) + public function testAssignOrderToCustomerAfterGuestOrder($orderCustomerId, $customerId) { $orderId = 1; /** @var Observer|PHPUnit_Framework_MockObject_MockObject $observerMock */ @@ -62,31 +72,24 @@ public function testAssignOrderToCustomerAfterGuestOrder($customerId) ->getMockForAbstractClass(); $observerMock->expects($this->once())->method('getEvent')->willReturn($eventMock); $eventMock->expects($this->any())->method('getData') - ->willReturnMap([ - ['delegate_data', null, ['__sales_assign_order_id' => $orderId]], - ['customer_data_object', null, $customerMock] - ]); - $orderMock->expects($this->once())->method('getCustomerId')->willReturn($customerId); + ->willReturnMap( + [ + ['delegate_data', null, ['__sales_assign_order_id' => $orderId]], + ['customer_data_object', null, $customerMock] + ] + ); + $orderMock->expects($this->once())->method('getCustomerId')->willReturn($orderCustomerId); $this->orderRepositoryMock->expects($this->once())->method('get')->with($orderId) ->willReturn($orderMock); - $orderMock->expects($this->once())->method('setCustomerId')->willReturn($orderMock); - $orderMock->expects($this->once())->method('setCustomerIsGuest')->willReturn($orderMock); - $orderMock->expects($this->once())->method('setCustomerEmail')->willReturn($orderMock); - $orderMock->expects($this->once())->method('setCustomerFirstname')->willReturn($orderMock); - $orderMock->expects($this->once())->method('setCustomerLastname')->willReturn($orderMock); - $orderMock->expects($this->once())->method('setCustomerMiddlename')->willReturn($orderMock); - $orderMock->expects($this->once())->method('setCustomerPrefix')->willReturn($orderMock); - $orderMock->expects($this->once())->method('setCustomerSuffix')->willReturn($orderMock); - $orderMock->expects($this->once())->method('setCustomerGroupId')->willReturn($orderMock); - - if (!$customerId) { - $this->orderRepositoryMock->expects($this->once())->method('save')->with($orderMock); + if (!$orderCustomerId) { + $customerMock->expects($this->once())->method('getId')->willReturn($customerId); + $this->assignmentMock->expects($this->once())->method('execute')->with($orderMock, $customerMock); $this->sut->execute($observerMock); - return ; + return; } - $this->orderRepositoryMock->expects($this->never())->method('save')->with($orderMock); + $this->assignmentMock->expects($this->never())->method('execute'); $this->sut->execute($observerMock); } @@ -97,6 +100,9 @@ public function testAssignOrderToCustomerAfterGuestOrder($customerId) */ public function getCustomerIds() { - return [[null, 1]]; + return [ + [null, 1], + [1, 1], + ]; } } diff --git a/app/code/Magento/Sales/etc/db_schema.xml b/app/code/Magento/Sales/etc/db_schema.xml index 82e6d5d10b53a..1f781604491bf 100644 --- a/app/code/Magento/Sales/etc/db_schema.xml +++ b/app/code/Magento/Sales/etc/db_schema.xml @@ -1386,6 +1386,8 @@ comment="Adjustment Negative"/> + + diff --git a/app/code/Magento/Sales/etc/db_schema_whitelist.json b/app/code/Magento/Sales/etc/db_schema_whitelist.json index 8790523c97c21..a7215d08c1f10 100644 --- a/app/code/Magento/Sales/etc/db_schema_whitelist.json +++ b/app/code/Magento/Sales/etc/db_schema_whitelist.json @@ -815,7 +815,9 @@ "shipping_and_handling": true, "adjustment_positive": true, "adjustment_negative": true, - "order_base_grand_total": true + "order_base_grand_total": true, + "order_currency_code": true, + "base_currency_code": true }, "index": { "SALES_CREDITMEMO_GRID_ORDER_INCREMENT_ID": true, diff --git a/app/code/Magento/Sales/etc/di.xml b/app/code/Magento/Sales/etc/di.xml index f6618c9884d60..a0dbb0488acb3 100644 --- a/app/code/Magento/Sales/etc/di.xml +++ b/app/code/Magento/Sales/etc/di.xml @@ -641,6 +641,8 @@ sales_creditmemo.adjustment_positive sales_creditmemo.adjustment_negative sales_order.base_grand_total + sales_order.order_currency_code + sales_order.base_currency_code diff --git a/app/code/Magento/Sales/etc/module.xml b/app/code/Magento/Sales/etc/module.xml index 11eebaa3d5a3d..e8af6b761a8a4 100644 --- a/app/code/Magento/Sales/etc/module.xml +++ b/app/code/Magento/Sales/etc/module.xml @@ -6,7 +6,7 @@ */ --> - + diff --git a/app/code/Magento/Sales/i18n/en_US.csv b/app/code/Magento/Sales/i18n/en_US.csv index 84cdfb2824334..0c8631acfd562 100644 --- a/app/code/Magento/Sales/i18n/en_US.csv +++ b/app/code/Magento/Sales/i18n/en_US.csv @@ -799,3 +799,4 @@ Refunds,Refunds "Allow Zero GrandTotal","Allow Zero GrandTotal" Email is required field for Admin order creation,Email is required field for Admin order creation If set YES Email field will be required during Admin order creation for new Customer.,If set YES Email field will be required during Admin order creation for new Customer. +"Please enter a coupon code!","Please enter a coupon code!" diff --git a/app/code/Magento/Sales/view/adminhtml/templates/items/column/name.phtml b/app/code/Magento/Sales/view/adminhtml/templates/items/column/name.phtml index 6e4d67ef51f22..c9b2f7c8de254 100644 --- a/app/code/Magento/Sales/view/adminhtml/templates/items/column/name.phtml +++ b/app/code/Magento/Sales/view/adminhtml/templates/items/column/name.phtml @@ -28,9 +28,8 @@ getFormattedOption($_option['value']); ?> - = $block->escapeHtml($_option['value'], ['a']) ?> ... - - = $block->escapeHtml($_option['remainder'], ['a']) ?> + + = $block->escapeHtml($_option['value'], ['a', 'br']) ?> ...= $block->escapeHtml($_option['remainder'], ['a']) ?> diff --git a/app/code/Magento/Sales/view/adminhtml/ui_component/sales_order_creditmemo_grid.xml b/app/code/Magento/Sales/view/adminhtml/ui_component/sales_order_creditmemo_grid.xml index e0b7dae8fdb1a..98f8b1edecf34 100644 --- a/app/code/Magento/Sales/view/adminhtml/ui_component/sales_order_creditmemo_grid.xml +++ b/app/code/Magento/Sales/view/adminhtml/ui_component/sales_order_creditmemo_grid.xml @@ -119,7 +119,7 @@ textRange - Refunded + Refunded (Base) @@ -194,7 +194,7 @@ false - + textRange Subtotal diff --git a/app/code/Magento/Sales/view/adminhtml/ui_component/sales_order_view_invoice_grid.xml b/app/code/Magento/Sales/view/adminhtml/ui_component/sales_order_view_invoice_grid.xml index ac1233c5e4961..c0ed1e01460bc 100644 --- a/app/code/Magento/Sales/view/adminhtml/ui_component/sales_order_view_invoice_grid.xml +++ b/app/code/Magento/Sales/view/adminhtml/ui_component/sales_order_view_invoice_grid.xml @@ -130,7 +130,7 @@ Status - + textRange Amount diff --git a/app/code/Magento/Sales/view/adminhtml/web/order/create/scripts.js b/app/code/Magento/Sales/view/adminhtml/web/order/create/scripts.js index 5f20325eb686e..3fe9d08782880 100644 --- a/app/code/Magento/Sales/view/adminhtml/web/order/create/scripts.js +++ b/app/code/Magento/Sales/view/adminhtml/web/order/create/scripts.js @@ -445,8 +445,8 @@ define([ */ loadShippingRates: function () { var addressContainer = this.shippingAsBilling ? - 'billingAddressContainer' : - 'shippingAddressContainer', + 'billingAddressContainer' : + 'shippingAddressContainer', data = this.serializeData(this[addressContainer]).toObject(); data['collect_shipping_rates'] = 1; diff --git a/app/code/Magento/Sales/view/frontend/templates/order/items.phtml b/app/code/Magento/Sales/view/frontend/templates/order/items.phtml index 98a1f1ebb7545..8762718c6d849 100644 --- a/app/code/Magento/Sales/view/frontend/templates/order/items.phtml +++ b/app/code/Magento/Sales/view/frontend/templates/order/items.phtml @@ -29,12 +29,12 @@ getItems(); ?> - getParentItem()) : continue; endif; ?> + = $block->getItemHtml($item) ?> helper(\Magento\GiftMessage\Helper\Message::class)->isMessagesAllowed('order_item', $item) && $item->getGiftMessageId()) : ?> helper(\Magento\GiftMessage\Helper\Message::class)->getGiftMessageForEntity($item); ?> @@ -65,8 +65,8 @@ + - isPagerDisplayed()) : ?> diff --git a/app/code/Magento/SalesRule/Block/Adminhtml/Promo/Quote/Edit/Tab/Coupons/Grid.php b/app/code/Magento/SalesRule/Block/Adminhtml/Promo/Quote/Edit/Tab/Coupons/Grid.php index 20014c3c02705..bf92e21827a01 100644 --- a/app/code/Magento/SalesRule/Block/Adminhtml/Promo/Quote/Edit/Tab/Coupons/Grid.php +++ b/app/code/Magento/SalesRule/Block/Adminhtml/Promo/Quote/Edit/Tab/Coupons/Grid.php @@ -46,9 +46,7 @@ public function __construct( } /** - * Constructor - * - * @return void + * @inheritdoc */ protected function _construct() { @@ -58,9 +56,7 @@ protected function _construct() } /** - * Prepare collection for grid - * - * @return $this + * @inheritdoc */ protected function _prepareCollection() { @@ -71,15 +67,20 @@ protected function _prepareCollection() */ $collection = $this->_salesRuleCoupon->create()->addRuleToFilter($priceRule)->addGeneratedCouponsFilter(); + if ($this->_isExport && $this->getMassactionBlock()->isAvailable()) { + $itemIds = $this->getMassactionBlock()->getSelected(); + if (!empty($itemIds)) { + $collection->addFieldToFilter('coupon_id', ['in' => $itemIds]); + } + } + $this->setCollection($collection); return parent::_prepareCollection(); } /** - * Define grid columns - * - * @return $this + * @inheritdoc */ protected function _prepareColumns() { @@ -121,9 +122,7 @@ protected function _prepareColumns() } /** - * Configure grid mass actions - * - * @return $this + * @inheritdoc */ protected function _prepareMassaction() { @@ -146,9 +145,7 @@ protected function _prepareMassaction() } /** - * Get grid url - * - * @return string + * @inheritdoc */ public function getGridUrl() { diff --git a/app/code/Magento/SalesRule/Controller/Adminhtml/Promo/Quote/NewActionHtml.php b/app/code/Magento/SalesRule/Controller/Adminhtml/Promo/Quote/NewActionHtml.php index 89a0d6e579727..56c08864c90c4 100644 --- a/app/code/Magento/SalesRule/Controller/Adminhtml/Promo/Quote/NewActionHtml.php +++ b/app/code/Magento/SalesRule/Controller/Adminhtml/Promo/Quote/NewActionHtml.php @@ -1,12 +1,19 @@ getRequest()->getParam('id'); - $formName = $this->getRequest()->getParam('form'); + $id = $this->getRequest() + ->getParam('id'); + $formName = $this->getRequest() + ->getParam('form_namespace'); $typeArr = explode('|', str_replace('-', '/', $this->getRequest()->getParam('type'))); $type = $typeArr[0]; @@ -27,7 +36,7 @@ public function execute() )->setType( $type )->setRule( - $this->_objectManager->create(\Magento\SalesRule\Model\Rule::class) + $this->_objectManager->create(Rule::class) )->setPrefix( 'actions' ); @@ -35,12 +44,14 @@ public function execute() $model->setAttribute($typeArr[1]); } - if ($model instanceof \Magento\Rule\Model\Condition\AbstractCondition) { + if ($model instanceof AbstractCondition) { $model->setJsFormObject($formName); + $model->setFormName($formName); $html = $model->asHtmlRecursive(); } else { $html = ''; } - $this->getResponse()->setBody($html); + $this->getResponse() + ->setBody($html); } } diff --git a/app/code/Magento/SalesRule/Model/Validator.php b/app/code/Magento/SalesRule/Model/Validator.php index ea0221d8f072d..dad35165051ce 100644 --- a/app/code/Magento/SalesRule/Model/Validator.php +++ b/app/code/Magento/SalesRule/Model/Validator.php @@ -243,6 +243,8 @@ public function canApplyRules(AbstractItem $item) public function reset(Address $address) { $this->validatorUtility->resetRoundingDeltas(); + $address->setBaseSubtotalWithDiscount($address->getBaseSubtotal()); + $address->setSubtotalWithDiscount($address->getSubtotal()); if ($this->_isFirstTimeResetRun) { $address->setAppliedRuleIds(''); $address->getQuote()->setAppliedRuleIds(''); diff --git a/app/code/Magento/SalesRule/Observer/AssignCouponDataAfterOrderCustomerAssignObserver.php b/app/code/Magento/SalesRule/Observer/AssignCouponDataAfterOrderCustomerAssignObserver.php new file mode 100644 index 0000000000000..2d771e4560fcf --- /dev/null +++ b/app/code/Magento/SalesRule/Observer/AssignCouponDataAfterOrderCustomerAssignObserver.php @@ -0,0 +1,53 @@ +updateCouponUsages = $updateCouponUsages; + } + + /** + * @inheritDoc + */ + public function execute(Observer $observer) + { + $event = $observer->getEvent(); + /** @var OrderInterface $order */ + $order = $event->getData(self::EVENT_KEY_ORDER); + + if ($order->getCustomerId()) { + $this->updateCouponUsages->execute($order, true); + } + } +} diff --git a/app/code/Magento/SalesRule/Test/Unit/Observer/CouponCodeValidationTest.php b/app/code/Magento/SalesRule/Test/Unit/Observer/CouponCodeValidationTest.php new file mode 100644 index 0000000000000..2fe2069b0c8b1 --- /dev/null +++ b/app/code/Magento/SalesRule/Test/Unit/Observer/CouponCodeValidationTest.php @@ -0,0 +1,175 @@ +codeLimitManagerMock = $this->createMock(CodeLimitManagerInterface::class); + $this->observerMock = $this->createMock(Observer::class); + $this->searchCriteriaMock = $this->getMockBuilder(SearchCriteria::class) + ->disableOriginalConstructor()->getMockForAbstractClass(); + $this->cartRepositoryMock = $this->getMockBuilder(CartRepositoryInterface::class) + ->setMethods(['getItems']) + ->disableOriginalConstructor()->getMockForAbstractClass(); + $this->searchCriteriaBuilderMock = $this->getMockBuilder(SearchCriteriaBuilder::class) + ->setMethods(['addFilter', 'create']) + ->disableOriginalConstructor()->getMockForAbstractClass(); + $this->quoteMock = $this->createPartialMock( + Quote::class, + ['getCouponCode', 'setCouponCode', 'getId'] + ); + + $this->couponCodeValidation = new CouponCodeValidation( + $this->codeLimitManagerMock, + $this->cartRepositoryMock, + $this->searchCriteriaBuilderMock + ); + } + + /** + * Testing the coupon code that haven't reached the request limit + */ + public function testCouponCodeNotReachedTheLimit() + { + $couponCode = 'AB123'; + $this->observerMock->expects($this->once())->method('getData')->with('quote') + ->willReturn($this->quoteMock); + $this->quoteMock->expects($this->once())->method('getCouponCode')->willReturn($couponCode); + $this->searchCriteriaBuilderMock->expects($this->once())->method('addFilter')->willReturnSelf(); + $this->searchCriteriaBuilderMock->expects($this->once())->method('create') + ->willReturn($this->searchCriteriaMock); + $this->quoteMock->expects($this->once())->method('getId')->willReturn(123); + $this->cartRepositoryMock->expects($this->any())->method('getList')->willReturnSelf(); + $this->cartRepositoryMock->expects($this->any())->method('getItems')->willReturn([]); + $this->codeLimitManagerMock->expects($this->once())->method('checkRequest')->with($couponCode); + $this->quoteMock->expects($this->never())->method('setCouponCode')->with(''); + + $this->couponCodeValidation->execute($this->observerMock); + } + + /** + * Testing with the changed coupon code + */ + public function testCouponCodeNotReachedTheLimitWithNewCouponCode() + { + $couponCode = 'AB123'; + $newCouponCode = 'AB234'; + + $this->observerMock->expects($this->once())->method('getData')->with('quote') + ->willReturn($this->quoteMock); + $this->quoteMock->expects($this->once())->method('getCouponCode')->willReturn($couponCode); + $this->searchCriteriaBuilderMock->expects($this->once())->method('addFilter')->willReturnSelf(); + $this->searchCriteriaBuilderMock->expects($this->once())->method('create') + ->willReturn($this->searchCriteriaMock); + $this->quoteMock->expects($this->once())->method('getId')->willReturn(123); + $this->cartRepositoryMock->expects($this->any())->method('getList')->willReturnSelf(); + $this->cartRepositoryMock->expects($this->any())->method('getItems') + ->willReturn([new DataObject(['coupon_code' => $newCouponCode])]); + $this->codeLimitManagerMock->expects($this->once())->method('checkRequest')->with($couponCode); + $this->quoteMock->expects($this->never())->method('setCouponCode')->with(''); + + $this->couponCodeValidation->execute($this->observerMock); + } + + /** + * Testing the coupon code that reached the request limit + * + * @expectedException \Magento\SalesRule\Api\Exception\CodeRequestLimitException + * @expectedExceptionMessage Too many coupon code requests, please try again later. + */ + public function testReachingLimitForCouponCode() + { + $couponCode = 'AB123'; + $this->observerMock->expects($this->once())->method('getData')->with('quote') + ->willReturn($this->quoteMock); + $this->quoteMock->expects($this->once())->method('getCouponCode')->willReturn($couponCode); + $this->searchCriteriaBuilderMock->expects($this->once())->method('addFilter')->willReturnSelf(); + $this->searchCriteriaBuilderMock->expects($this->once())->method('create') + ->willReturn($this->searchCriteriaMock); + $this->quoteMock->expects($this->once())->method('getId')->willReturn(123); + $this->cartRepositoryMock->expects($this->any())->method('getList')->willReturnSelf(); + $this->cartRepositoryMock->expects($this->any())->method('getItems')->willReturn([]); + $this->codeLimitManagerMock->expects($this->once())->method('checkRequest')->with($couponCode) + ->willThrowException( + new CodeRequestLimitException(__('Too many coupon code requests, please try again later.')) + ); + $this->quoteMock->expects($this->once())->method('setCouponCode')->with(''); + + $this->couponCodeValidation->execute($this->observerMock); + } + + /** + * Testing the quote that doesn't have a coupon code set + */ + public function testQuoteWithNoCouponCode() + { + $couponCode = null; + $this->observerMock->expects($this->once())->method('getData')->with('quote') + ->willReturn($this->quoteMock); + $this->quoteMock->expects($this->once())->method('getCouponCode')->willReturn($couponCode); + $this->quoteMock->expects($this->never())->method('getId')->willReturn(123); + $this->quoteMock->expects($this->never())->method('setCouponCode')->with(''); + + $this->couponCodeValidation->execute($this->observerMock); + } +} diff --git a/app/code/Magento/SalesRule/etc/events.xml b/app/code/Magento/SalesRule/etc/events.xml index fb0f711144e27..e421aafa96b55 100644 --- a/app/code/Magento/SalesRule/etc/events.xml +++ b/app/code/Magento/SalesRule/etc/events.xml @@ -24,6 +24,9 @@ + + + diff --git a/app/code/Magento/SalesRule/view/adminhtml/templates/promo/salesrulejs.phtml b/app/code/Magento/SalesRule/view/adminhtml/templates/promo/salesrulejs.phtml index 2eed5ba6c548b..64918c24cdc61 100644 --- a/app/code/Magento/SalesRule/view/adminhtml/templates/promo/salesrulejs.phtml +++ b/app/code/Magento/SalesRule/view/adminhtml/templates/promo/salesrulejs.phtml @@ -64,7 +64,7 @@ function generateCouponCodes(idPrefix, generateUrl, grid) { try { response = JSON.parse(transport.responseText); } catch (e) { - console.warn('An error occured while parsing response'); + console.warn('An error occurred while parsing response'); } } if (couponCodesGrid) { diff --git a/app/code/Magento/Search/Model/QueryFactory.php b/app/code/Magento/Search/Model/QueryFactory.php index 2122451742402..4186b5c3055f4 100644 --- a/app/code/Magento/Search/Model/QueryFactory.php +++ b/app/code/Magento/Search/Model/QueryFactory.php @@ -5,13 +5,15 @@ */ namespace Magento\Search\Model; -use Magento\Search\Helper\Data; use Magento\Framework\App\Config\ScopeConfigInterface; use Magento\Framework\App\Helper\Context; use Magento\Framework\ObjectManagerInterface; use Magento\Framework\Stdlib\StringUtils as StdlibString; +use Magento\Search\Helper\Data; /** + * Search Query Factory + * * @api * @since 100.0.2 */ @@ -72,7 +74,7 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc */ public function get() { @@ -82,9 +84,7 @@ public function get() $rawQueryText = $this->getRawQueryText(); $preparedQueryText = $this->getPreparedQueryText($rawQueryText, $maxQueryLength); $query = $this->create()->loadByQueryText($preparedQueryText); - if (!$query->getId()) { - $query->setQueryText($preparedQueryText); - } + $query->setQueryText($preparedQueryText); $query->setIsQueryTextExceeded($this->isQueryTooLong($rawQueryText, $maxQueryLength)); $query->setIsQueryTextShort($this->isQueryTooShort($rawQueryText, $minQueryLength)); $this->query = $query; @@ -117,6 +117,8 @@ private function getRawQueryText() } /** + * Prepare query text + * * @param string $queryText * @param int|string $maxQueryLength * @return string @@ -130,6 +132,8 @@ private function getPreparedQueryText($queryText, $maxQueryLength) } /** + * Check if the provided text exceeds the provided length + * * @param string $queryText * @param int|string $maxQueryLength * @return bool @@ -140,6 +144,8 @@ private function isQueryTooLong($queryText, $maxQueryLength) } /** + * Check if the provided text is shorter than the provided length + * * @param string $queryText * @param int|string $minQueryLength * @return bool diff --git a/app/code/Magento/Search/Test/Mftf/ActionGroup/AdminSearchTermActionGroup.xml b/app/code/Magento/Search/Test/Mftf/ActionGroup/AdminSearchTermActionGroup.xml index 85e34d7c0d294..aeadf69050912 100644 --- a/app/code/Magento/Search/Test/Mftf/ActionGroup/AdminSearchTermActionGroup.xml +++ b/app/code/Magento/Search/Test/Mftf/ActionGroup/AdminSearchTermActionGroup.xml @@ -36,4 +36,12 @@ + + + + + + + + diff --git a/app/code/Magento/Search/Test/Mftf/Test/StorefrontVerifySearchSuggestionByProductDescriptionTest.xml b/app/code/Magento/Search/Test/Mftf/Test/StorefrontVerifySearchSuggestionByProductDescriptionTest.xml index 63a861b697a86..0ec33c48f259e 100644 --- a/app/code/Magento/Search/Test/Mftf/Test/StorefrontVerifySearchSuggestionByProductDescriptionTest.xml +++ b/app/code/Magento/Search/Test/Mftf/Test/StorefrontVerifySearchSuggestionByProductDescriptionTest.xml @@ -16,64 +16,69 @@ - - - - + - + + + + + + + + - - - - + + + + + - - + + - + - + - - + + - - + + - + - - + + - - + + - - + + diff --git a/app/code/Magento/Search/Test/Unit/Model/QueryFactoryTest.php b/app/code/Magento/Search/Test/Unit/Model/QueryFactoryTest.php index 3df457b0d4497..f66c1c7dd9e3f 100644 --- a/app/code/Magento/Search/Test/Unit/Model/QueryFactoryTest.php +++ b/app/code/Magento/Search/Test/Unit/Model/QueryFactoryTest.php @@ -5,14 +5,14 @@ */ namespace Magento\Search\Test\Unit\Model; -use Magento\Search\Helper\Data; use Magento\Framework\App\Helper\Context; use Magento\Framework\App\RequestInterface; use Magento\Framework\ObjectManagerInterface; -use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; -use Magento\Search\Model\QueryFactory; use Magento\Framework\Stdlib\StringUtils; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\Search\Helper\Data; use Magento\Search\Model\Query; +use Magento\Search\Model\QueryFactory; /** * Class QueryFactoryTest tests Magento\Search\Model\QueryFactory @@ -67,7 +67,7 @@ protected function setUp() ->disableOriginalConstructor() ->getMock(); $this->query = $this->getMockBuilder(Query::class) - ->setMethods(['setIsQueryTextExceeded', 'setIsQueryTextShort', 'loadByQueryText', 'getId', 'setQueryText']) + ->setMethods(['setIsQueryTextExceeded', 'setIsQueryTextShort', 'loadByQueryText', 'getId']) ->disableOriginalConstructor() ->getMock(); @@ -124,7 +124,6 @@ public function testGetNewQuery() $isQueryTextExceeded = false; $isQueryTextShort = false; - $this->mockSetQueryTextNeverExecute($cleanedRawText); $this->mockString($cleanedRawText); $this->mockQueryLengths($maxQueryLength, $minQueryLength); $this->mockGetRawQueryText($rawQueryText); @@ -135,6 +134,7 @@ public function testGetNewQuery() $result = $this->model->get(); $this->assertSame($this->query, $result); + $this->assertSearchQuery($cleanedRawText); } /** @@ -150,7 +150,6 @@ public function testGetQueryTwice() $isQueryTextExceeded = false; $isQueryTextShort = false; - $this->mockSetQueryTextNeverExecute($cleanedRawText); $this->mockString($cleanedRawText); $this->mockQueryLengths($maxQueryLength, $minQueryLength); $this->mockGetRawQueryText($rawQueryText); @@ -163,6 +162,7 @@ public function testGetQueryTwice() $result = $this->model->get(); $this->assertSame($this->query, $result, 'After second execution queries are not same'); + $this->assertSearchQuery($cleanedRawText); } /** @@ -184,7 +184,6 @@ public function testGetTooLongQuery() ->withConsecutive([$cleanedRawText, 0, $maxQueryLength]) ->willReturn($subRawText); - $this->mockSetQueryTextNeverExecute($cleanedRawText); $this->mockString($cleanedRawText); $this->mockQueryLengths($maxQueryLength, $minQueryLength); $this->mockGetRawQueryText($rawQueryText); @@ -194,6 +193,7 @@ public function testGetTooLongQuery() $result = $this->model->get(); $this->assertSame($this->query, $result); + $this->assertSearchQuery($subRawText); } /** @@ -209,7 +209,6 @@ public function testGetTooShortQuery() $isQueryTextExceeded = false; $isQueryTextShort = true; - $this->mockSetQueryTextNeverExecute($cleanedRawText); $this->mockString($cleanedRawText); $this->mockQueryLengths($maxQueryLength, $minQueryLength); $this->mockGetRawQueryText($rawQueryText); @@ -219,6 +218,7 @@ public function testGetTooShortQuery() $result = $this->model->get(); $this->assertSame($this->query, $result); + $this->assertSearchQuery($cleanedRawText); } /** @@ -234,7 +234,6 @@ public function testGetQueryWithoutId() $isQueryTextExceeded = false; $isQueryTextShort = false; - $this->mockSetQueryTextOnceExecute($cleanedRawText); $this->mockString($cleanedRawText); $this->mockQueryLengths($maxQueryLength, $minQueryLength); $this->mockGetRawQueryText($rawQueryText); @@ -244,6 +243,35 @@ public function testGetQueryWithoutId() $result = $this->model->get(); $this->assertSame($this->query, $result); + $this->assertSearchQuery($cleanedRawText); + } + + /** + * Test for inaccurate match of search query in query_text table + * + * Because of inaccurate string comparison of utf8_general_ci, + * the search_query result text may be different from the original text (e.g organos, Organos, Órganos) + */ + public function testInaccurateQueryTextMatch() + { + $queryId = 1; + $maxQueryLength = 100; + $minQueryLength = 3; + $rawQueryText = 'Órganos'; + $cleanedRawText = 'Órganos'; + $isQueryTextExceeded = false; + $isQueryTextShort = false; + + $this->mockString($cleanedRawText); + $this->mockQueryLengths($maxQueryLength, $minQueryLength); + $this->mockGetRawQueryText($rawQueryText); + $this->mockSimpleQuery($cleanedRawText, $queryId, $isQueryTextExceeded, $isQueryTextShort, 'Organos'); + + $this->mockCreateQuery(); + + $result = $this->model->get(); + $this->assertSame($this->query, $result); + $this->assertSearchQuery($cleanedRawText); } /** @@ -305,15 +333,25 @@ private function mockCreateQuery() * @param int $queryId * @param bool $isQueryTextExceeded * @param bool $isQueryTextShort + * @param string $matchedQueryText * @return void */ - private function mockSimpleQuery($cleanedRawText, $queryId, $isQueryTextExceeded, $isQueryTextShort) - { + private function mockSimpleQuery( + string $cleanedRawText, + ?int $queryId, + bool $isQueryTextExceeded, + bool $isQueryTextShort, + string $matchedQueryText = null + ) { + if (null === $matchedQueryText) { + $matchedQueryText = $cleanedRawText; + } $this->query->expects($this->once()) ->method('loadByQueryText') ->withConsecutive([$cleanedRawText]) ->willReturnSelf(); - $this->query->expects($this->once()) + $this->query->setData(['query_text' => $matchedQueryText]); + $this->query->expects($this->any()) ->method('getId') ->willReturn($queryId); $this->query->expects($this->once()) @@ -328,23 +366,8 @@ private function mockSimpleQuery($cleanedRawText, $queryId, $isQueryTextExceeded * @param string $cleanedRawText * @return void */ - private function mockSetQueryTextNeverExecute($cleanedRawText) + private function assertSearchQuery($cleanedRawText) { - $this->query->expects($this->never()) - ->method('setQueryText') - ->withConsecutive([$cleanedRawText]) - ->willReturnSelf(); - } - - /** - * @param string $cleanedRawText - * @return void - */ - private function mockSetQueryTextOnceExecute($cleanedRawText) - { - $this->query->expects($this->once()) - ->method('setQueryText') - ->withConsecutive([$cleanedRawText]) - ->willReturnSelf(); + $this->assertEquals($cleanedRawText, $this->query->getQueryText()); } } diff --git a/app/code/Magento/SendFriend/composer.json b/app/code/Magento/SendFriend/composer.json index f06f1b4a9e3e3..064b45e97d6c5 100644 --- a/app/code/Magento/SendFriend/composer.json +++ b/app/code/Magento/SendFriend/composer.json @@ -11,7 +11,8 @@ "magento/module-customer": "*", "magento/module-store": "*", "magento/module-captcha": "*", - "magento/module-authorization": "*" + "magento/module-authorization": "*", + "magento/module-theme": "*" }, "type": "magento2-module", "license": [ diff --git a/app/code/Magento/Shipping/Test/Mftf/Data/FlatRateShippingMethodData.xml b/app/code/Magento/Shipping/Test/Mftf/Data/FlatRateShippingMethodData.xml index 6d877dac5cbf4..6ab1d71933826 100644 --- a/app/code/Magento/Shipping/Test/Mftf/Data/FlatRateShippingMethodData.xml +++ b/app/code/Magento/Shipping/Test/Mftf/Data/FlatRateShippingMethodData.xml @@ -51,7 +51,7 @@ 5.00 - F + 0 This shipping method is not available. To use this shipping method, please contact us. diff --git a/app/code/Magento/Sitemap/Controller/Adminhtml/Sitemap/Edit.php b/app/code/Magento/Sitemap/Controller/Adminhtml/Sitemap/Edit.php index 14771e7f03a3b..117f73311b644 100644 --- a/app/code/Magento/Sitemap/Controller/Adminhtml/Sitemap/Edit.php +++ b/app/code/Magento/Sitemap/Controller/Adminhtml/Sitemap/Edit.php @@ -6,25 +6,30 @@ namespace Magento\Sitemap\Controller\Adminhtml\Sitemap; +use Magento\Backend\App\Action\Context; +use Magento\Backend\Block\Template; +use Magento\Backend\Model\Session; use Magento\Framework\App\Action\HttpGetActionInterface; +use Magento\Framework\Registry; +use Magento\Sitemap\Controller\Adminhtml\Sitemap; /** * Controller class Edit. Responsible for rendering of a sitemap edit page */ -class Edit extends \Magento\Sitemap\Controller\Adminhtml\Sitemap implements HttpGetActionInterface +class Edit extends Sitemap implements HttpGetActionInterface { /** * Core registry * - * @var \Magento\Framework\Registry + * @var Registry */ - protected $_coreRegistry = null; + protected $_coreRegistry; /** - * @param \Magento\Backend\App\Action\Context $context - * @param \Magento\Framework\Registry $coreRegistry + * @param Context $context + * @param Registry $coreRegistry */ - public function __construct(\Magento\Backend\App\Action\Context $context, \Magento\Framework\Registry $coreRegistry) + public function __construct(Context $context, Registry $coreRegistry) { $this->_coreRegistry = $coreRegistry; parent::__construct($context); @@ -53,7 +58,7 @@ public function execute() } // 3. Set entered data if was error when we do save - $data = $this->_objectManager->get(\Magento\Backend\Model\Session::class)->getFormData(true); + $data = $this->_objectManager->get(Session::class)->getFormData(true); if (!empty($data)) { $model->setData($data); } @@ -67,6 +72,8 @@ public function execute() $id ? __('Edit Sitemap') : __('New Sitemap') )->_addContent( $this->_view->getLayout()->createBlock(\Magento\Sitemap\Block\Adminhtml\Edit::class) + )->_addJs( + $this->_view->getLayout()->createBlock(Template::class)->setTemplate('Magento_Sitemap::js.phtml') ); $this->_view->getPage()->getConfig()->getTitle()->prepend(__('Site Map')); $this->_view->getPage()->getConfig()->getTitle()->prepend( diff --git a/app/code/Magento/Sitemap/Model/Sitemap.php b/app/code/Magento/Sitemap/Model/Sitemap.php index d50e857c51513..0d69634ccfa5e 100644 --- a/app/code/Magento/Sitemap/Model/Sitemap.php +++ b/app/code/Magento/Sitemap/Model/Sitemap.php @@ -362,7 +362,6 @@ protected function _initSitemapItems() self::OPEN_TAG_KEY => '' . PHP_EOL . '' . PHP_EOL, self::CLOSE_TAG_KEY => '', diff --git a/app/code/Magento/Sitemap/Test/Unit/Model/_files/sitemap-1-1.xml b/app/code/Magento/Sitemap/Test/Unit/Model/_files/sitemap-1-1.xml index 68b27ca4f6560..e14224125fd37 100644 --- a/app/code/Magento/Sitemap/Test/Unit/Model/_files/sitemap-1-1.xml +++ b/app/code/Magento/Sitemap/Test/Unit/Model/_files/sitemap-1-1.xml @@ -6,7 +6,6 @@ */ --> http://store.com/category.html diff --git a/app/code/Magento/Sitemap/Test/Unit/Model/_files/sitemap-1-2.xml b/app/code/Magento/Sitemap/Test/Unit/Model/_files/sitemap-1-2.xml index 188eab6e5e045..4dfd32bfad70f 100644 --- a/app/code/Magento/Sitemap/Test/Unit/Model/_files/sitemap-1-2.xml +++ b/app/code/Magento/Sitemap/Test/Unit/Model/_files/sitemap-1-2.xml @@ -6,7 +6,6 @@ */ --> http://store.com/category/sub-category.html diff --git a/app/code/Magento/Sitemap/Test/Unit/Model/_files/sitemap-1-3.xml b/app/code/Magento/Sitemap/Test/Unit/Model/_files/sitemap-1-3.xml index 3f2263e69b851..ae9106a51f0d6 100644 --- a/app/code/Magento/Sitemap/Test/Unit/Model/_files/sitemap-1-3.xml +++ b/app/code/Magento/Sitemap/Test/Unit/Model/_files/sitemap-1-3.xml @@ -6,7 +6,6 @@ */ --> http://store.com/product.html diff --git a/app/code/Magento/Sitemap/Test/Unit/Model/_files/sitemap-1-4.xml b/app/code/Magento/Sitemap/Test/Unit/Model/_files/sitemap-1-4.xml index 7111154efbf85..f31c390c04ec6 100644 --- a/app/code/Magento/Sitemap/Test/Unit/Model/_files/sitemap-1-4.xml +++ b/app/code/Magento/Sitemap/Test/Unit/Model/_files/sitemap-1-4.xml @@ -6,7 +6,6 @@ */ --> http://store.com/product2.html diff --git a/app/code/Magento/Sitemap/Test/Unit/Model/_files/sitemap-single.xml b/app/code/Magento/Sitemap/Test/Unit/Model/_files/sitemap-single.xml index 7e3616cd3fc1c..8f8db58959c8d 100644 --- a/app/code/Magento/Sitemap/Test/Unit/Model/_files/sitemap-single.xml +++ b/app/code/Magento/Sitemap/Test/Unit/Model/_files/sitemap-single.xml @@ -6,7 +6,6 @@ */ --> http://store.com/category.html diff --git a/app/code/Magento/Sitemap/etc/adminhtml/system.xml b/app/code/Magento/Sitemap/etc/adminhtml/system.xml index c65311fc5e0d0..c3c9c85027354 100644 --- a/app/code/Magento/Sitemap/etc/adminhtml/system.xml +++ b/app/code/Magento/Sitemap/etc/adminhtml/system.xml @@ -83,10 +83,12 @@ Sitemap File Limits Maximum No of URLs Per File + validate-number validate-greater-than-zero Maximum File Size File size in bytes. + validate-number validate-greater-than-zero diff --git a/app/code/Magento/Sitemap/view/adminhtml/templates/js.phtml b/app/code/Magento/Sitemap/view/adminhtml/templates/js.phtml new file mode 100644 index 0000000000000..4e7ed34ed4a7e --- /dev/null +++ b/app/code/Magento/Sitemap/view/adminhtml/templates/js.phtml @@ -0,0 +1,14 @@ + + + diff --git a/app/code/Magento/Sitemap/view/adminhtml/web/js/form-submit-loader.js b/app/code/Magento/Sitemap/view/adminhtml/web/js/form-submit-loader.js new file mode 100644 index 0000000000000..6b7ba6be65b22 --- /dev/null +++ b/app/code/Magento/Sitemap/view/adminhtml/web/js/form-submit-loader.js @@ -0,0 +1,19 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +define([ + 'jquery' +], function ($) { + 'use strict'; + + return function (data, element) { + + $(element).on('save', function () { + if ($(this).valid()) { + $('body').trigger('processStart'); + } + }); + }; +}); diff --git a/app/code/Magento/Store/App/Action/Plugin/Context.php b/app/code/Magento/Store/App/Action/Plugin/Context.php index b5d2e3d913f4c..1e03cc92a5898 100644 --- a/app/code/Magento/Store/App/Action/Plugin/Context.php +++ b/app/code/Magento/Store/App/Action/Plugin/Context.php @@ -11,8 +11,11 @@ use Magento\Framework\App\RequestInterface; use Magento\Framework\Exception\NoSuchEntityException; use Magento\Framework\Exception\NotFoundException; +use Magento\Framework\Session\SessionManagerInterface; use Magento\Store\Api\Data\StoreInterface; use Magento\Store\Api\StoreCookieManagerInterface; +use Magento\Store\Model\ScopeInterface; +use Magento\Store\Model\StoreManager; use Magento\Store\Model\StoreManagerInterface; /** @@ -21,17 +24,17 @@ class Context { /** - * @var \Magento\Framework\Session\SessionManagerInterface + * @var SessionManagerInterface */ protected $session; /** - * @var \Magento\Framework\App\Http\Context + * @var HttpContext */ protected $httpContext; /** - * @var \Magento\Store\Model\StoreManagerInterface + * @var StoreManagerInterface */ protected $storeManager; @@ -41,15 +44,15 @@ class Context protected $storeCookieManager; /** - * @param \Magento\Framework\Session\SessionManagerInterface $session - * @param \Magento\Framework\App\Http\Context $httpContext - * @param \Magento\Store\Model\StoreManagerInterface $storeManager + * @param SessionManagerInterface $session + * @param HttpContext $httpContext + * @param StoreManagerInterface $storeManager * @param StoreCookieManagerInterface $storeCookieManager */ public function __construct( - \Magento\Framework\Session\SessionManagerInterface $session, - \Magento\Framework\App\Http\Context $httpContext, - \Magento\Store\Model\StoreManagerInterface $storeManager, + SessionManagerInterface $session, + HttpContext $httpContext, + StoreManagerInterface $storeManager, StoreCookieManagerInterface $storeCookieManager ) { $this->session = $session; @@ -78,41 +81,42 @@ public function beforeDispatch( /** @var string|array|null $storeCode */ $storeCode = $request->getParam( - \Magento\Store\Model\StoreManagerInterface::PARAM_NAME, + StoreManagerInterface::PARAM_NAME, $this->storeCookieManager->getStoreCodeFromCookie() ); if (is_array($storeCode)) { if (!isset($storeCode['_data']['code'])) { - $this->processInvalidStoreRequested(); + $this->processInvalidStoreRequested($request); } $storeCode = $storeCode['_data']['code']; } if ($storeCode === '') { //Empty code - is an invalid code and it was given explicitly //(the value would be null if the code wasn't found). - $this->processInvalidStoreRequested(); + $this->processInvalidStoreRequested($request); } try { $currentStore = $this->storeManager->getStore($storeCode); + $this->updateContext($request, $currentStore); } catch (NoSuchEntityException $exception) { - $this->processInvalidStoreRequested($exception); + $this->processInvalidStoreRequested($request, $exception); } - - $this->updateContext($currentStore); } /** * Take action in case of invalid store requested. * - * @param \Throwable|null $previousException + * @param RequestInterface $request + * @param NoSuchEntityException|null $previousException * @return void * @throws NotFoundException */ private function processInvalidStoreRequested( - \Throwable $previousException = null + RequestInterface $request, + NoSuchEntityException $previousException = null ) { $store = $this->storeManager->getStore(); - $this->updateContext($store); + $this->updateContext($request, $store); throw new NotFoundException( $previousException @@ -125,24 +129,34 @@ private function processInvalidStoreRequested( /** * Update context accordingly to the store found. * + * @param RequestInterface $request * @param StoreInterface $store * @return void * @throws \Magento\Framework\Exception\LocalizedException */ - private function updateContext(StoreInterface $store) + private function updateContext(RequestInterface $request, StoreInterface $store) { + switch (true) { + case $store->isUseStoreInUrl(): + $defaultStore = $store; + break; + case ScopeInterface::SCOPE_STORE == $request->getServerValue(StoreManager::PARAM_RUN_TYPE): + $defaultStoreCode = $request->getServerValue(StoreManager::PARAM_RUN_CODE); + $defaultStore = $this->storeManager->getStore($defaultStoreCode); + break; + default: + $defaultStoreCode = $this->storeManager->getDefaultStoreView()->getCode(); + $defaultStore = $this->storeManager->getStore($defaultStoreCode); + break; + } $this->httpContext->setValue( StoreManagerInterface::CONTEXT_STORE, $store->getCode(), - $store->isUseStoreInUrl() ? $store->getCode() : $this->storeManager->getDefaultStoreView()->getCode() + $defaultStore->getCode() ); - - /** @var StoreInterface $defaultStore */ - $defaultStore = $this->storeManager->getWebsite()->getDefaultStore(); $this->httpContext->setValue( HttpContext::CONTEXT_CURRENCY, - $this->session->getCurrencyCode() - ?: $store->getDefaultCurrencyCode(), + $this->session->getCurrencyCode() ?: $store->getDefaultCurrencyCode(), $defaultStore->getDefaultCurrencyCode() ); } diff --git a/app/code/Magento/Store/Model/Store.php b/app/code/Magento/Store/Model/Store.php index dab9c55c216d9..0bc371da0aab9 100644 --- a/app/code/Magento/Store/Model/Store.php +++ b/app/code/Magento/Store/Model/Store.php @@ -912,7 +912,7 @@ public function setCurrentCurrencyCode($code) $defaultCode = ($this->_storeManager->getStore() !== null) ? $this->_storeManager->getStore()->getDefaultCurrency()->getCode() : $this->_storeManager->getWebsite()->getDefaultStore()->getDefaultCurrency()->getCode(); - + $this->_httpContext->setValue(Context::CONTEXT_CURRENCY, $code, $defaultCode); } return $this; @@ -1279,7 +1279,20 @@ public function isActive() public function beforeDelete() { $this->_configDataResource->clearScopeData(ScopeInterface::SCOPE_STORES, $this->getId()); - return parent::beforeDelete(); + parent::beforeDelete(); + if ($this->getId() === $this->getGroup()->getDefaultStoreId()) { + $ids = $this->getGroup()->getStoreIds(); + if (!empty($ids) && count($ids) > 1) { + unset($ids[$this->getId()]); + $defaultId = current($ids); + } else { + $defaultId = null; + } + $this->getGroup()->setDefaultStoreId($defaultId); + $this->getGroup()->save(); + } + + return $this; } /** @@ -1300,18 +1313,6 @@ function () use ($store) { parent::afterDelete(); $this->_configCacheType->clean(); - if ($this->getId() === $this->getGroup()->getDefaultStoreId()) { - $ids = $this->getGroup()->getStoreIds(); - if (!empty($ids) && count($ids) > 1) { - unset($ids[$this->getId()]); - $defaultId = current($ids); - } else { - $defaultId = null; - } - $this->getGroup()->setDefaultStoreId($defaultId); - $this->getGroup()->save(); - } - return $this; } diff --git a/app/code/Magento/Store/Model/System/Store.php b/app/code/Magento/Store/Model/System/Store.php index 86eec555f1d59..cf75f26aed9b7 100644 --- a/app/code/Magento/Store/Model/System/Store.php +++ b/app/code/Magento/Store/Model/System/Store.php @@ -3,6 +3,9 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +declare(strict_types=1); + namespace Magento\Store\Model\System; use Magento\Framework\Data\OptionSourceInterface; @@ -118,6 +121,7 @@ public function getStoreValuesForForm($empty = false, $all = false) $options[] = ['label' => __('All Store Views'), 'value' => 0]; } + // phpcs:ignore Magento2.Functions.DiscouragedFunction $nonEscapableNbspChar = html_entity_decode(' ', ENT_NOQUOTES, 'UTF-8'); foreach ($this->_websiteCollection as $website) { @@ -126,7 +130,7 @@ public function getStoreValuesForForm($empty = false, $all = false) if ($website->getId() != $group->getWebsiteId()) { continue; } - $groupShow = false; + $values = []; foreach ($this->_storeCollection as $store) { if ($group->getId() != $store->getGroupId()) { continue; @@ -135,16 +139,12 @@ public function getStoreValuesForForm($empty = false, $all = false) $options[] = ['label' => $website->getName(), 'value' => []]; $websiteShow = true; } - if (!$groupShow) { - $groupShow = true; - $values = []; - } $values[] = [ 'label' => str_repeat($nonEscapableNbspChar, 4) . $store->getName(), 'value' => $store->getId(), ]; } - if ($groupShow) { + if (!empty($values)) { $options[] = [ 'label' => str_repeat($nonEscapableNbspChar, 4) . $group->getName(), 'value' => $values, @@ -398,6 +398,7 @@ public function getStoreCollection() /** * Load/Reload collection(s) by type + * * Allowed types: website, group, store or null for all * * @param string $type diff --git a/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminCreateNewStoreGroupActionGroup.xml b/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminCreateNewStoreGroupActionGroup.xml index cd9016ebf6d7f..4c00071da6b61 100644 --- a/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminCreateNewStoreGroupActionGroup.xml +++ b/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminCreateNewStoreGroupActionGroup.xml @@ -71,6 +71,50 @@ + + + Edit store group. + + + + + + + + + + + + + + + + + + Change the default store view for provided store group. + + + + + + + + + + + + + + + + Asserts that the provided store view is default in provided store group. + + + + + + + Clicks on the 1st Store in the 'Stores' grid. Validates that the provided Details (Website, Store Group Name, Store Group Code and Root Category) are present and correct. diff --git a/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminDeleteStoreViewActionGroup.xml b/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminDeleteStoreViewActionGroup.xml index 94856bb083da8..63dc4b0ded4f9 100644 --- a/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminDeleteStoreViewActionGroup.xml +++ b/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminDeleteStoreViewActionGroup.xml @@ -18,6 +18,7 @@ + diff --git a/app/code/Magento/Store/Test/Mftf/Section/AdminEditStoreGroupSection.xml b/app/code/Magento/Store/Test/Mftf/Section/AdminEditStoreGroupSection.xml new file mode 100644 index 0000000000000..343373c61da9b --- /dev/null +++ b/app/code/Magento/Store/Test/Mftf/Section/AdminEditStoreGroupSection.xml @@ -0,0 +1,12 @@ + + + + + + + diff --git a/app/code/Magento/Store/Test/Mftf/Test/AdminCreateStoreGroupTest.xml b/app/code/Magento/Store/Test/Mftf/Test/AdminCreateStoreGroupTest.xml index 25e93f8f6ff4c..ed879a82d3f59 100644 --- a/app/code/Magento/Store/Test/Mftf/Test/AdminCreateStoreGroupTest.xml +++ b/app/code/Magento/Store/Test/Mftf/Test/AdminCreateStoreGroupTest.xml @@ -21,6 +21,14 @@ + + + + + + + + diff --git a/app/code/Magento/Store/Test/Mftf/Test/AdminDeleteDefaultStoreViewTest.xml b/app/code/Magento/Store/Test/Mftf/Test/AdminDeleteDefaultStoreViewTest.xml new file mode 100644 index 0000000000000..85fd8561f90b5 --- /dev/null +++ b/app/code/Magento/Store/Test/Mftf/Test/AdminDeleteDefaultStoreViewTest.xml @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/code/Magento/Store/Test/Unit/App/Action/Plugin/ContextNonDefaultStoreDirectLinkTest.php b/app/code/Magento/Store/Test/Unit/App/Action/Plugin/ContextNonDefaultStoreDirectLinkTest.php deleted file mode 100644 index bb2705bff9aab..0000000000000 --- a/app/code/Magento/Store/Test/Unit/App/Action/Plugin/ContextNonDefaultStoreDirectLinkTest.php +++ /dev/null @@ -1,162 +0,0 @@ -createPartialMock(Generic::class, ['getCurrencyCode']); - $httpContextMock = $this->createMock(HttpContext::class); - $storeManager = $this->createMock(StoreManagerInterface::class); - $storeCookieManager = $this->createMock(StoreCookieManagerInterface::class); - $storeMock = $this->createMock(Store::class); - $currentStoreMock = $this->createMock(Store::class); - $requestMock = $this->getMockBuilder(RequestInterface::class)->getMockForAbstractClass(); - $subjectMock = $this->getMockBuilder(AbstractAction::class) - ->disableOriginalConstructor() - ->getMockForAbstractClass(); - - $httpContextMock->expects($this->once()) - ->method('getValue') - ->with(StoreManagerInterface::CONTEXT_STORE) - ->willReturn(null); - - $websiteMock = $this->createPartialMock( - Website::class, - ['getDefaultStore', '__wakeup'] - ); - - $plugin = (new ObjectManager($this))->getObject( - Context::class, - [ - 'session' => $sessionMock, - 'httpContext' => $httpContextMock, - 'storeManager' => $storeManager, - 'storeCookieManager' => $storeCookieManager, - ] - ); - - $storeManager->method('getDefaultStoreView') - ->willReturn($storeMock); - - $storeCookieManager->expects($this->once()) - ->method('getStoreCodeFromCookie') - ->willReturn('storeCookie'); - - $currentStoreMock->expects($this->any()) - ->method('getDefaultCurrencyCode') - ->willReturn(self::CURRENCY_CURRENT_STORE); - - $currentStoreMock->expects($this->any()) - ->method('getCode') - ->willReturn($customStore); - - $currentStoreMock->method('isUseStoreInUrl')->willReturn($useStoreInUrl); - - $storeManager->expects($this->any()) - ->method('getWebsite') - ->willReturn($websiteMock); - - $websiteMock->expects($this->any()) - ->method('getDefaultStore') - ->willReturn($storeMock); - - $storeMock->expects($this->any()) - ->method('getDefaultCurrencyCode') - ->willReturn(self::CURRENCY_DEFAULT); - - $storeMock->expects($this->any()) - ->method('getCode') - ->willReturn($defaultStore); - - $requestMock->expects($this->any()) - ->method('getParam') - ->with($this->equalTo('___store')) - ->willReturn($defaultStore); - - $storeManager->method('getStore') - ->with($defaultStore) - ->willReturn($currentStoreMock); - - $sessionMock->expects($this->any()) - ->method('getCurrencyCode') - ->willReturn(self::CURRENCY_SESSION); - - $httpContextMock->expects($this->at(1))->method( - 'setValue' - )->with(StoreManagerInterface::CONTEXT_STORE, $customStore, $expectedDefaultStore); - - $httpContextMock->expects($this->at(2))->method('setValue'); - - $plugin->beforeDispatch( - $subjectMock, - $requestMock - ); - } - - public function cacheHitOnDirectLinkToNonDefaultStoreView() - { - return [ - [ - 'custom_store', - 'default', - 'custom_store', - true, - ], - [ - 'custom_store', - 'default', - 'default', - false, - ], - [ - 'default', - 'default', - 'default', - true, - ], - ]; - } -} diff --git a/app/code/Magento/Store/Test/Unit/App/Action/Plugin/ContextTest.php b/app/code/Magento/Store/Test/Unit/App/Action/Plugin/ContextTest.php deleted file mode 100644 index 616851465b49c..0000000000000 --- a/app/code/Magento/Store/Test/Unit/App/Action/Plugin/ContextTest.php +++ /dev/null @@ -1,367 +0,0 @@ -sessionMock = $this->createPartialMock(\Magento\Framework\Session\Generic::class, ['getCurrencyCode']); - $this->httpContextMock = $this->createMock(HttpContext::class); - $this->httpContextMock->expects($this->once()) - ->method('getValue') - ->with(StoreManagerInterface::CONTEXT_STORE) - ->willReturn(null); - $this->storeManager = $this->createMock(\Magento\Store\Model\StoreManagerInterface::class); - $this->storeCookieManager = $this->createMock(\Magento\Store\Api\StoreCookieManagerInterface::class); - $this->storeMock = $this->createMock(\Magento\Store\Model\Store::class); - $this->currentStoreMock = $this->createMock(\Magento\Store\Model\Store::class); - $this->websiteMock = $this->createPartialMock( - \Magento\Store\Model\Website::class, - ['getDefaultStore', '__wakeup'] - ); - $this->requestMock = $this->getMockBuilder(RequestInterface::class)->getMockForAbstractClass(); - $this->subjectMock = $this->getMockBuilder(AbstractAction::class) - ->disableOriginalConstructor() - ->getMockForAbstractClass(); - - $this->plugin = (new ObjectManager($this))->getObject( - \Magento\Store\App\Action\Plugin\Context::class, - [ - 'session' => $this->sessionMock, - 'httpContext' => $this->httpContextMock, - 'storeManager' => $this->storeManager, - 'storeCookieManager' => $this->storeCookieManager, - ] - ); - - $this->storeManager->method('getDefaultStoreView') - ->willReturn($this->storeMock); - $this->storeCookieManager->expects($this->once()) - ->method('getStoreCodeFromCookie') - ->willReturn('storeCookie'); - $this->currentStoreMock->expects($this->any()) - ->method('getDefaultCurrencyCode') - ->willReturn(self::CURRENCY_CURRENT_STORE); - } - - public function testBeforeDispatchCurrencyFromSession() - { - $this->storeManager->expects($this->once()) - ->method('getWebsite') - ->willReturn($this->websiteMock); - $this->websiteMock->expects($this->once()) - ->method('getDefaultStore') - ->willReturn($this->storeMock); - - $this->storeMock->expects($this->once()) - ->method('getDefaultCurrencyCode') - ->willReturn(self::CURRENCY_DEFAULT); - - $this->storeMock->expects($this->once()) - ->method('getCode') - ->willReturn('default'); - $this->currentStoreMock->expects($this->once()) - ->method('getCode') - ->willReturn('custom_store'); - - $this->requestMock->expects($this->once()) - ->method('getParam') - ->with($this->equalTo('___store')) - ->willReturn('default'); - - $this->storeManager->method('getStore') - ->with('default') - ->willReturn($this->currentStoreMock); - - $this->sessionMock->expects($this->any()) - ->method('getCurrencyCode') - ->willReturn(self::CURRENCY_SESSION); - - $this->httpContextMock->expects($this->at(1)) - ->method('setValue') - ->with( - StoreManagerInterface::CONTEXT_STORE, - 'custom_store', - 'default' - ); - // Make sure that current currency is taken from session if available. - $this->httpContextMock->expects($this->at(2)) - ->method('setValue') - ->with( - Context::CONTEXT_CURRENCY, - self::CURRENCY_SESSION, - self::CURRENCY_DEFAULT - ); - - $this->plugin->beforeDispatch( - $this->subjectMock, - $this->requestMock - ); - } - - public function testDispatchCurrentStoreCurrency() - { - $this->storeManager->expects($this->once()) - ->method('getWebsite') - ->willReturn($this->websiteMock); - $this->websiteMock->expects($this->once()) - ->method('getDefaultStore') - ->willReturn($this->storeMock); - - $this->storeMock->expects($this->once()) - ->method('getDefaultCurrencyCode') - ->willReturn(self::CURRENCY_DEFAULT); - - $this->storeMock->expects($this->once()) - ->method('getCode') - ->willReturn('default'); - $this->currentStoreMock->expects($this->once()) - ->method('getCode') - ->willReturn('custom_store'); - - $this->requestMock->expects($this->once()) - ->method('getParam') - ->with($this->equalTo('___store')) - ->willReturn('default'); - - $this->storeManager->method('getStore') - ->with('default') - ->willReturn($this->currentStoreMock); - - $this->httpContextMock->expects($this->at(1)) - ->method('setValue') - ->with( - StoreManagerInterface::CONTEXT_STORE, - 'custom_store', - 'default' - ); - // Make sure that current currency is taken from current store - //if no value is provided in session. - $this->httpContextMock->expects($this->at(2)) - ->method('setValue') - ->with( - Context::CONTEXT_CURRENCY, - self::CURRENCY_CURRENT_STORE, - self::CURRENCY_DEFAULT - ); - - $this->plugin->beforeDispatch( - $this->subjectMock, - $this->requestMock - ); - } - - public function testDispatchStoreParameterIsArray() - { - $this->storeManager->expects($this->once()) - ->method('getWebsite') - ->willReturn($this->websiteMock); - $this->websiteMock->expects($this->once()) - ->method('getDefaultStore') - ->willReturn($this->storeMock); - - $this->storeMock->expects($this->once()) - ->method('getDefaultCurrencyCode') - ->willReturn(self::CURRENCY_DEFAULT); - - $this->storeMock->expects($this->once()) - ->method('getCode') - ->willReturn('default'); - $this->currentStoreMock->expects($this->once()) - ->method('getCode') - ->willReturn('custom_store'); - - $store = [ - '_data' => [ - 'code' => 500, - ] - ]; - - $this->requestMock->expects($this->once()) - ->method('getParam') - ->with($this->equalTo('___store')) - ->willReturn($store); - - $this->storeManager->expects($this->once()) - ->method('getStore') - ->with('500') - ->willReturn($this->currentStoreMock); - - $this->httpContextMock->expects($this->at(1)) - ->method('setValue') - ->with( - StoreManagerInterface::CONTEXT_STORE, - 'custom_store', - 'default' - ); - //Make sure that current currency is taken from current store - //if no value is provided in session. - $this->httpContextMock->expects($this->at(2)) - ->method('setValue') - ->with( - Context::CONTEXT_CURRENCY, - self::CURRENCY_CURRENT_STORE, - self::CURRENCY_DEFAULT - ); - - $this->plugin->beforeDispatch( - $this->subjectMock, - $this->requestMock - ); - } - - /** - * @expectedException \Magento\Framework\Exception\NotFoundException - */ - public function testDispatchStoreParameterIsInvalidArray() - { - $this->storeManager->expects($this->once()) - ->method('getWebsite') - ->willReturn($this->websiteMock); - $this->websiteMock->expects($this->once()) - ->method('getDefaultStore') - ->willReturn($this->storeMock); - $this->storeMock->expects($this->exactly(2)) - ->method('getDefaultCurrencyCode') - ->willReturn(self::CURRENCY_DEFAULT); - - $this->storeMock->expects($this->exactly(2)) - ->method('getCode') - ->willReturn('default'); - $this->currentStoreMock->expects($this->never()) - ->method('getCode') - ->willReturn('custom_store'); - - $store = [ - 'some' => [ - 'code' => 500, - ] - ]; - - $this->requestMock->expects($this->once()) - ->method('getParam') - ->with($this->equalTo('___store')) - ->willReturn($store); - $this->storeManager->expects($this->once()) - ->method('getStore') - ->with() - ->willReturn($this->storeMock); - $this->plugin->beforeDispatch( - $this->subjectMock, - $this->requestMock - ); - } - - /** - * @return void - * @expectedException \Magento\Framework\Exception\NotFoundException - */ - public function testDispatchNonExistingStore() - { - $storeId = 'NonExisting'; - $this->requestMock->expects($this->once()) - ->method('getParam') - ->with('___store') - ->willReturn($storeId); - $this->storeManager->expects($this->at(0)) - ->method('getStore') - ->with($storeId) - ->willThrowException(new NoSuchEntityException()); - $this->storeManager->expects($this->at(1)) - ->method('getStore') - ->with() - ->willReturn($this->storeMock); - $this->storeManager->expects($this->once()) - ->method('getWebsite') - ->willReturn($this->websiteMock); - $this->websiteMock->expects($this->once()) - ->method('getDefaultStore') - ->willReturn($this->storeMock); - $this->storeMock->expects($this->exactly(2)) - ->method('getDefaultCurrencyCode') - ->willReturn(self::CURRENCY_DEFAULT); - - $this->storeMock->expects($this->exactly(2)) - ->method('getCode') - ->willReturn('default'); - $this->currentStoreMock->expects($this->never()) - ->method('getCode') - ->willReturn('custom_store'); - - $this->plugin->beforeDispatch($this->subjectMock, $this->requestMock); - } -} diff --git a/app/code/Magento/Store/Ui/Component/Form/Field/StoreView.php b/app/code/Magento/Store/Ui/Component/Form/Field/StoreView.php new file mode 100644 index 0000000000000..3113cee49abe2 --- /dev/null +++ b/app/code/Magento/Store/Ui/Component/Form/Field/StoreView.php @@ -0,0 +1,61 @@ +storeManager = $storeManager; + } + + /** + * Prepare component configuration + * + * @return void + */ + public function prepare() + { + parent::prepare(); + if ($this->storeManager->isSingleStoreMode()) { + $this->_data['config']['componentDisabled'] = true; + } + } +} diff --git a/app/code/Magento/Swatches/Test/Mftf/ActionGroup/AddSwatchToProductActionGroup.xml b/app/code/Magento/Swatches/Test/Mftf/ActionGroup/AddSwatchToProductActionGroup.xml index cd8470fe47fde..8b95b86065b7d 100644 --- a/app/code/Magento/Swatches/Test/Mftf/ActionGroup/AddSwatchToProductActionGroup.xml +++ b/app/code/Magento/Swatches/Test/Mftf/ActionGroup/AddSwatchToProductActionGroup.xml @@ -84,4 +84,40 @@ + + + + Add text swatch property attribute. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/code/Magento/Swatches/Test/Mftf/ActionGroup/StorefrontProductActionGroup.xml b/app/code/Magento/Swatches/Test/Mftf/ActionGroup/StorefrontProductActionGroup.xml index a67f9d8999b59..6ca0c220778d6 100644 --- a/app/code/Magento/Swatches/Test/Mftf/ActionGroup/StorefrontProductActionGroup.xml +++ b/app/code/Magento/Swatches/Test/Mftf/ActionGroup/StorefrontProductActionGroup.xml @@ -15,6 +15,14 @@ + + + + + + + + diff --git a/app/code/Magento/Swatches/Test/Mftf/Data/SwatchAttributeData.xml b/app/code/Magento/Swatches/Test/Mftf/Data/SwatchAttributeData.xml index d7ee6dabce7ed..b05c9cc9e7a9a 100644 --- a/app/code/Magento/Swatches/Test/Mftf/Data/SwatchAttributeData.xml +++ b/app/code/Magento/Swatches/Test/Mftf/Data/SwatchAttributeData.xml @@ -13,4 +13,9 @@ Visual Swatch visual_swatch_attr + + Text Swatch + TextSwatchAttr + text_swatch_attr + diff --git a/app/code/Magento/Swatches/Test/Mftf/Test/AdminSaveConfigurableProductWithAttributesImagesAndSwatchesTest.xml b/app/code/Magento/Swatches/Test/Mftf/Test/AdminSaveConfigurableProductWithAttributesImagesAndSwatchesTest.xml new file mode 100644 index 0000000000000..f94314fe94806 --- /dev/null +++ b/app/code/Magento/Swatches/Test/Mftf/Test/AdminSaveConfigurableProductWithAttributesImagesAndSwatchesTest.xml @@ -0,0 +1,110 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontConfigurableProductSwatchMinimumPriceTest.xml b/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontConfigurableProductSwatchMinimumPriceTest.xml new file mode 100644 index 0000000000000..0a98e7a721c17 --- /dev/null +++ b/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontConfigurableProductSwatchMinimumPriceTest.xml @@ -0,0 +1,167 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/code/Magento/Swatches/Test/Unit/Block/Product/Renderer/Listing/ConfigurableTest.php b/app/code/Magento/Swatches/Test/Unit/Block/Product/Renderer/Listing/ConfigurableTest.php index 49dbb5cdc962d..a2c5128046828 100644 --- a/app/code/Magento/Swatches/Test/Unit/Block/Product/Renderer/Listing/ConfigurableTest.php +++ b/app/code/Magento/Swatches/Test/Unit/Block/Product/Renderer/Listing/ConfigurableTest.php @@ -5,6 +5,7 @@ */ namespace Magento\Swatches\Test\Unit\Block\Product\Renderer\Listing; +use Magento\Catalog\Model\Product\Attribute\Source\Status; use Magento\Swatches\Block\Product\Renderer\Configurable; /** @@ -130,14 +131,16 @@ public function testGetJsonSwatchNotUsedInProductListing() $this->configurable->setProduct($this->product); $this->swatchHelper->expects($this->once())->method('getSwatchAttributesAsArray') ->with($this->product) - ->willReturn([ - 1 => [ - 'options' => [1 => 'testA', 3 => 'testB'], - 'use_product_image_for_swatch' => true, - 'used_in_product_listing' => false, - 'attribute_code' => 'code', - ], - ]); + ->willReturn( + [ + 1 => [ + 'options' => [1 => 'testA', 3 => 'testB'], + 'use_product_image_for_swatch' => true, + 'used_in_product_listing' => false, + 'attribute_code' => 'code', + ], + ] + ); $this->swatchHelper->expects($this->once())->method('getSwatchesByOptionsId') ->willReturn([]); $this->jsonEncoder->expects($this->once())->method('encode')->with([]); @@ -163,19 +166,19 @@ public function testGetJsonSwatchUsedInProductListing() $this->configurable->setProduct($this->product); $this->swatchHelper->expects($this->once())->method('getSwatchAttributesAsArray') ->with($this->product) - ->willReturn([ - 1 => [ - 'options' => $products, - 'use_product_image_for_swatch' => true, - 'used_in_product_listing' => true, - 'attribute_code' => 'code', - ], - ]); + ->willReturn( + [ + 1 => [ + 'options' => $products, + 'use_product_image_for_swatch' => true, + 'used_in_product_listing' => true, + 'attribute_code' => 'code', + ], + ] + ); $this->swatchHelper->expects($this->once())->method('getSwatchesByOptionsId') ->with([1, 3]) - ->willReturn([ - 3 => ['type' => $expected['type'], 'value' => $expected['value']] - ]); + ->willReturn([3 => ['type' => $expected['type'], 'value' => $expected['value']]]); $this->jsonEncoder->expects($this->once())->method('encode'); $this->configurable->getJsonSwatchConfig(); } @@ -183,11 +186,13 @@ public function testGetJsonSwatchUsedInProductListing() private function prepareGetJsonSwatchConfig() { $product1 = $this->createMock(\Magento\Catalog\Model\Product::class); - $product1->expects($this->atLeastOnce())->method('isSaleable')->willReturn(true); + $product1->expects($this->any())->method('isSaleable')->willReturn(true); + $product1->expects($this->atLeastOnce())->method('getStatus')->willReturn(Status::STATUS_ENABLED); $product1->expects($this->any())->method('getData')->with('code')->willReturn(1); $product2 = $this->createMock(\Magento\Catalog\Model\Product::class); - $product2->expects($this->atLeastOnce())->method('isSaleable')->willReturn(true); + $product2->expects($this->any())->method('isSaleable')->willReturn(true); + $product2->expects($this->atLeastOnce())->method('getStatus')->willReturn(Status::STATUS_ENABLED); $product2->expects($this->any())->method('getData')->with('code')->willReturn(3); $simpleProducts = [$product1, $product2]; diff --git a/app/code/Magento/Swatches/view/frontend/web/js/swatch-renderer.js b/app/code/Magento/Swatches/view/frontend/web/js/swatch-renderer.js index d6302cff83bff..f78dcc7a915ce 100644 --- a/app/code/Magento/Swatches/view/frontend/web/js/swatch-renderer.js +++ b/app/code/Magento/Swatches/view/frontend/web/js/swatch-renderer.js @@ -750,11 +750,11 @@ define([ $(document).trigger('updateMsrpPriceBlock', [ - parseInt($this.attr('index'), 10) + 1, + _.findKey($widget.options.jsonConfig.index, $widget.options.jsonConfig.defaultValues), $widget.options.jsonConfig.optionPrices ]); - if (checkAdditionalData['update_product_preview_image'] === '1') { + if (parseInt(checkAdditionalData['update_product_preview_image'], 10) === 1) { $widget._loadMedia(); } @@ -927,19 +927,10 @@ define([ var $widget = this, $product = $widget.element.parents($widget.options.selectorProduct), $productPrice = $product.find(this.options.selectorProductPrice), - options = _.object(_.keys($widget.optionsMap), {}), - result, + result = $widget._getNewPrices(), tierPriceHtml, isShow; - $widget.element.find('.' + $widget.options.classes.attributeClass + '[option-selected]').each(function () { - var attributeId = $(this).attr('attribute-id'); - - options[attributeId] = $(this).attr('option-selected'); - }); - - result = $widget.options.jsonConfig.optionPrices[_.findKey($widget.options.jsonConfig.index, options)]; - $productPrice.trigger( 'updatePrice', { @@ -951,7 +942,7 @@ define([ $product.find(this.options.slyOldPriceSelector)[isShow ? 'show' : 'hide'](); - if (typeof result != 'undefined' && result.tierPrices.length) { + if (typeof result != 'undefined' && result.tierPrices && result.tierPrices.length) { if (this.options.tierPriceTemplate) { tierPriceHtml = mageTemplate( this.options.tierPriceTemplate, @@ -985,6 +976,35 @@ define([ }.bind(this)); }, + /** + * Get new prices for selected options + * + * @returns {*} + * @private + */ + _getNewPrices: function () { + var $widget = this, + optionPriceDiff = 0, + allowedProduct = this._getAllowedProductWithMinPrice(this._CalcProducts()), + optionPrices = this.options.jsonConfig.optionPrices, + basePrice = parseFloat(this.options.jsonConfig.prices.basePrice.amount), + optionFinalPrice, + newPrices; + + if (!_.isEmpty(allowedProduct)) { + optionFinalPrice = parseFloat(optionPrices[allowedProduct].finalPrice.amount); + optionPriceDiff = optionFinalPrice - basePrice; + } + + if (optionPriceDiff !== 0) { + newPrices = this.options.jsonConfig.optionPrices[allowedProduct]; + } else { + newPrices = $widget.options.jsonConfig.prices; + } + + return newPrices; + }, + /** * Get prices * @@ -994,27 +1014,11 @@ define([ * @private */ _getPrices: function (newPrices, displayPrices) { - var $widget = this, - optionPriceDiff = 0, - allowedProduct, optionPrices, basePrice, optionFinalPrice; + var $widget = this; if (_.isEmpty(newPrices)) { - allowedProduct = this._getAllowedProductWithMinPrice(this._CalcProducts()); - optionPrices = this.options.jsonConfig.optionPrices; - basePrice = parseFloat(this.options.jsonConfig.prices.basePrice.amount); - - if (!_.isEmpty(allowedProduct)) { - optionFinalPrice = parseFloat(optionPrices[allowedProduct].finalPrice.amount); - optionPriceDiff = optionFinalPrice - basePrice; - } - - if (optionPriceDiff !== 0) { - newPrices = this.options.jsonConfig.optionPrices[allowedProduct]; - } else { - newPrices = $widget.options.jsonConfig.prices; - } + newPrices = $widget._getNewPrices(); } - _.each(displayPrices, function (price, code) { if (newPrices[code]) { diff --git a/app/code/Magento/Tax/Observer/GetPriceConfigurationObserver.php b/app/code/Magento/Tax/Observer/GetPriceConfigurationObserver.php index 21828c35a81dc..bad9757dafd89 100644 --- a/app/code/Magento/Tax/Observer/GetPriceConfigurationObserver.php +++ b/app/code/Magento/Tax/Observer/GetPriceConfigurationObserver.php @@ -5,10 +5,13 @@ */ namespace Magento\Tax\Observer; -use Magento\Framework\Event\ObserverInterface; use Magento\Catalog\Pricing\Price\BasePrice; use Magento\Catalog\Pricing\Price\RegularPrice; +use Magento\Framework\Event\ObserverInterface; +/** + * Modifies the bundle config for the front end to resemble the tax included price when tax included prices. + */ class GetPriceConfigurationObserver implements ObserverInterface { /** @@ -23,6 +26,11 @@ class GetPriceConfigurationObserver implements ObserverInterface */ protected $registry; + /** + * @var array Cache of the current bundle selection items + */ + private $selectionCache = []; + /** * @param \Magento\Framework\Registry $registry * @param \Magento\Tax\Helper\Data $taxData @@ -44,6 +52,7 @@ public function __construct( */ public function execute(\Magento\Framework\Event\Observer $observer) { + $this->selectionCache = []; if ($this->taxData->displayPriceIncludingTax()) { /** @var \Magento\Catalog\Model\Product $product */ $product = $this->registry->registry('current_product'); @@ -78,12 +87,11 @@ private function recurConfigAndUpdatePrice($input, $searchKey) if (is_array($el)) { $holder[$key] = $this->recurConfigAndUpdatePrice($el, $searchKey); - if ($key === $searchKey) { - if ((array_key_exists('basePrice', $holder[$key]))) { - if (array_key_exists('optionId', $input)) { - $holder = $this->updatePriceForBundle($holder, $key); - } - } + if ($key === $searchKey + && array_key_exists('optionId', $input) + && array_key_exists('basePrice', $holder[$key]) + ) { + $holder = $this->updatePriceForBundle($holder, $key); } } else { $holder[$key] = $el; @@ -102,11 +110,12 @@ private function recurConfigAndUpdatePrice($input, $searchKey) */ private function updatePriceForBundle($holder, $key) { - if (array_key_exists($key, $holder)) { - if (array_key_exists('basePrice', $holder[$key])) { - /** @var \Magento\Catalog\Model\Product $product */ - $product = $this->registry->registry('current_product'); - if ($product->getTypeId() == \Magento\Catalog\Model\Product\Type::TYPE_BUNDLE) { + if (array_key_exists($key, $holder) + && array_key_exists('basePrice', $holder[$key])) { + /** @var \Magento\Catalog\Model\Product $product */ + $product = $this->registry->registry('current_product'); + if ($product->getTypeId() == \Magento\Catalog\Model\Product\Type::TYPE_BUNDLE) { + if (!isset($this->selectionCache[$product->getId()])) { $typeInstance = $product->getTypeInstance(); $typeInstance->setStoreFilter($product->getStoreId(), $product); @@ -114,20 +123,22 @@ private function updatePriceForBundle($holder, $key) $typeInstance->getOptionsIds($product), $product ); + $this->selectionCache[$product->getId()] = $selectionCollection->getItems(); + } + $arrSelections = $this->selectionCache[$product->getId()]; - foreach ($selectionCollection->getItems() as $selectionItem) { - if ($holder['optionId'] == $selectionItem->getId()) { - /** @var \Magento\Framework\Pricing\Amount\Base $baseAmount */ - $baseAmount = $selectionItem->getPriceInfo()->getPrice(BasePrice::PRICE_CODE)->getAmount(); - /** @var \Magento\Framework\Pricing\Amount\Base $oldAmount */ - $oldAmount = + foreach ($arrSelections as $selectionItem) { + if ($holder['optionId'] == $selectionItem->getId()) { + /** @var \Magento\Framework\Pricing\Amount\Base $baseAmount */ + $baseAmount = $selectionItem->getPriceInfo()->getPrice(BasePrice::PRICE_CODE)->getAmount(); + /** @var \Magento\Framework\Pricing\Amount\Base $oldAmount */ + $oldAmount = $selectionItem->getPriceInfo()->getPrice(RegularPrice::PRICE_CODE)->getAmount(); - if ($baseAmount->hasAdjustment('tax')) { - $holder[$key]['basePrice']['amount'] = + if ($baseAmount->hasAdjustment('tax')) { + $holder[$key]['basePrice']['amount'] = $baseAmount->getBaseAmount() + $baseAmount->getAdjustmentAmount('tax'); - $holder[$key]['oldPrice']['amount'] = + $holder[$key]['oldPrice']['amount'] = $oldAmount->getBaseAmount() + $oldAmount->getAdjustmentAmount('tax'); - } } } } diff --git a/app/code/Magento/Tax/Test/Unit/Observer/GetPriceConfigurationObserverTest.php b/app/code/Magento/Tax/Test/Unit/Observer/GetPriceConfigurationObserverTest.php index 1dd1efceb9dbd..e8fcf03807e6e 100644 --- a/app/code/Magento/Tax/Test/Unit/Observer/GetPriceConfigurationObserverTest.php +++ b/app/code/Magento/Tax/Test/Unit/Observer/GetPriceConfigurationObserverTest.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Tax\Test\Unit\Observer; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; @@ -118,7 +119,7 @@ public function testExecute($testArray, $expectedArray) $product = $this->createPartialMock( \Magento\Bundle\Model\Product\Type::class, - ['getTypeInstance', 'getTypeId', 'getStoreId', 'getSelectionsCollection'] + ['getTypeInstance', 'getTypeId', 'getStoreId', 'getSelectionsCollection', 'getId'] ); $product->expects($this->any()) ->method('getTypeInstance') diff --git a/app/code/Magento/Tax/view/adminhtml/templates/rule/edit.phtml b/app/code/Magento/Tax/view/adminhtml/templates/rule/edit.phtml index 81bdd874ead6c..3558d359aa4d6 100644 --- a/app/code/Magento/Tax/view/adminhtml/templates/rule/edit.phtml +++ b/app/code/Magento/Tax/view/adminhtml/templates/rule/edit.phtml @@ -10,11 +10,12 @@ require([ 'jquery', 'Magento_Ui/js/modal/alert', + 'Magento_Ui/js/modal/confirm', "jquery/ui", 'mage/multiselect', "mage/mage", 'Magento_Ui/js/modal/modal' -], function($, alert){ +], function($, alert, confirm) { $.widget("adminhtml.dialogRates", $.mage.modal, { options: { @@ -160,53 +161,58 @@ require([ taxRateField.find('.mselect-list') .on('click.mselect-edit', '.mselect-edit', this.edit) .on("click.mselect-delete", ".mselect-delete", function () { - if (!confirm('= $block->escapeJs(__('Do you really want to delete this tax rate?')) ?>')) { - return; - } - var that = $(this), select = that.closest('.mselect-list').prev(), rateValue = that.parent().find('input[type="checkbox"]').val(); - $('body').trigger('processStart'); - var ajaxOptions = { - type: 'POST', - data: { - tax_calculation_rate_id: rateValue, - form_key: $('input[name="form_key"]').val() - }, - dataType: 'json', - url: '= $block->escapeJs($block->escapeUrl($block->getTaxRateDeleteUrl())) ?>', - success: function(result, status) { - $('body').trigger('processStop'); - if (result.success) { - that.parent().remove(); - select.find('option').each(function() { - if (this.value === rateValue) { - $(this).remove(); + confirm({ + content: '= $block->escapeJs(__('Do you really want to delete this tax rate?')) ?>', + actions: { + /** + * Confirm action. + */ + confirm: function () { + $('body').trigger('processStart'); + var ajaxOptions = { + type: 'POST', + data: { + tax_calculation_rate_id: rateValue, + form_key: $('input[name="form_key"]').val() + }, + dataType: 'json', + url: '= $block->escapeJs($block->escapeUrl($block->getTaxRateDeleteUrl())) ?>', + success: function(result, status) { + $('body').trigger('processStop'); + if (result.success) { + that.parent().remove(); + select.find('option').each(function() { + if (this.value === rateValue) { + $(this).remove(); + } + }); + select.trigger('change.hiddenSelect'); + } else { + if (result.error_message) + alert({ + content: result.error_message + }); + else + alert({ + content: '= $block->escapeJs($block->escapeHtml(__('An error occurred'))) ?>' + }); + } + }, + error: function () { + $('body').trigger('processStop'); + alert({ + content: '= $block->escapeJs($block->escapeHtml(__('An error occurred'))) ?>' + }); } - }); - select.trigger('change.hiddenSelect'); - } else { - if (result.error_message) - alert({ - content: result.error_message - }); - else - alert({ - content: '= $block->escapeJs($block->escapeHtml(__('An error occurred'))) ?>' - }); + }; + $.ajax(ajaxOptions); } - }, - error: function () { - $('body').trigger('processStop'); - alert({ - content: '= $block->escapeJs($block->escapeHtml(__('An error occurred'))) ?>' - }); } - }; - $.ajax(ajaxOptions); - + }); }) .on('click.mselectAdd', '.mselect-button-add', function () { taxRateForm diff --git a/app/code/Magento/Theme/Block/Html/Topmenu.php b/app/code/Magento/Theme/Block/Html/Topmenu.php index fd8aaa7708cf3..77b9144069502 100644 --- a/app/code/Magento/Theme/Block/Html/Topmenu.php +++ b/app/code/Magento/Theme/Block/Html/Topmenu.php @@ -133,7 +133,7 @@ protected function _countItems($items) * * @param Menu $items * @param int $limit - * @return array|void + * @return array * * @todo: Add Depth Level limit, and better logic for columns */ @@ -141,7 +141,7 @@ protected function _columnBrake($items, $limit) { $total = $this->_countItems($items); if ($total <= $limit) { - return; + return []; } $result[] = ['total' => $total, 'max' => (int)ceil($total / ceil($total / $limit))]; diff --git a/app/code/Magento/Theme/Controller/Adminhtml/System/Design/Theme/UploadJs.php b/app/code/Magento/Theme/Controller/Adminhtml/System/Design/Theme/UploadJs.php index 89636ad3de50e..fc396615e71e7 100644 --- a/app/code/Magento/Theme/Controller/Adminhtml/System/Design/Theme/UploadJs.php +++ b/app/code/Magento/Theme/Controller/Adminhtml/System/Design/Theme/UploadJs.php @@ -52,7 +52,6 @@ public function execute() \Magento\Framework\View\Design\Theme\Customization\File\Js::TYPE ); $result = ['error' => false, 'files' => $customization->generateFileInfo($customJsFiles)]; - // phpcs:disable Magento2.Exceptions.ThrowCatch } catch (\Magento\Framework\Exception\LocalizedException $e) { $result = ['error' => true, 'message' => $e->getMessage()]; } catch (\Exception $e) { diff --git a/app/code/Magento/Theme/Test/Mftf/ActionGroup/AdminChangeStorefrontThemeActionGroup.xml b/app/code/Magento/Theme/Test/Mftf/ActionGroup/AdminChangeStorefrontThemeActionGroup.xml new file mode 100644 index 0000000000000..0620b9b73ba96 --- /dev/null +++ b/app/code/Magento/Theme/Test/Mftf/ActionGroup/AdminChangeStorefrontThemeActionGroup.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + diff --git a/app/code/Magento/Theme/Test/Mftf/ActionGroup/StorefrontCheckElementColorActionGroup.xml b/app/code/Magento/Theme/Test/Mftf/ActionGroup/StorefrontCheckElementColorActionGroup.xml new file mode 100644 index 0000000000000..66e98d5e41527 --- /dev/null +++ b/app/code/Magento/Theme/Test/Mftf/ActionGroup/StorefrontCheckElementColorActionGroup.xml @@ -0,0 +1,24 @@ + + + + + + + Checks element color on storefront. + + + + + + + + + + + diff --git a/app/code/Magento/Theme/Test/Mftf/Data/DesignData.xml b/app/code/Magento/Theme/Test/Mftf/Data/DesignData.xml index ec28e8ed7a999..1a3c10745f5a7 100644 --- a/app/code/Magento/Theme/Test/Mftf/Data/DesignData.xml +++ b/app/code/Magento/Theme/Test/Mftf/Data/DesignData.xml @@ -11,4 +11,10 @@ 1 column + + Magento Blank + + + Magento Luma + diff --git a/app/code/Magento/Theme/Test/Mftf/Data/NavigationMenuColorData.xml b/app/code/Magento/Theme/Test/Mftf/Data/NavigationMenuColorData.xml new file mode 100644 index 0000000000000..7af07753d7c9c --- /dev/null +++ b/app/code/Magento/Theme/Test/Mftf/Data/NavigationMenuColorData.xml @@ -0,0 +1,16 @@ + + + + + + rgb(232, 232, 232) + rgb(255, 255, 255) + rgb(255, 85, 1) + + diff --git a/app/code/Magento/Theme/Test/Mftf/Section/AdminDesignConfigSection.xml b/app/code/Magento/Theme/Test/Mftf/Section/AdminDesignConfigSection.xml index c2652f33f7606..069068163ccaf 100644 --- a/app/code/Magento/Theme/Test/Mftf/Section/AdminDesignConfigSection.xml +++ b/app/code/Magento/Theme/Test/Mftf/Section/AdminDesignConfigSection.xml @@ -31,5 +31,7 @@ + + diff --git a/app/code/Magento/Theme/Test/Mftf/Section/StorefrontNavigationMenuSection.xml b/app/code/Magento/Theme/Test/Mftf/Section/StorefrontNavigationMenuSection.xml new file mode 100644 index 0000000000000..5741b50f877f6 --- /dev/null +++ b/app/code/Magento/Theme/Test/Mftf/Section/StorefrontNavigationMenuSection.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + diff --git a/app/code/Magento/Theme/view/adminhtml/ui_component/design_config_listing.xml b/app/code/Magento/Theme/view/adminhtml/ui_component/design_config_listing.xml index 83b3576a6de13..57a7b0aee7a20 100644 --- a/app/code/Magento/Theme/view/adminhtml/ui_component/design_config_listing.xml +++ b/app/code/Magento/Theme/view/adminhtml/ui_component/design_config_listing.xml @@ -78,7 +78,7 @@ Store View - + theme/design_config/edit diff --git a/app/code/Magento/Theme/view/base/layout/default.xml b/app/code/Magento/Theme/view/base/layout/default.xml deleted file mode 100644 index b950b8efa963e..0000000000000 --- a/app/code/Magento/Theme/view/base/layout/default.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - - diff --git a/app/code/Magento/Theme/view/base/requirejs-config.js b/app/code/Magento/Theme/view/base/requirejs-config.js index 2822d6db008a1..f5580461f7d9e 100644 --- a/app/code/Magento/Theme/view/base/requirejs-config.js +++ b/app/code/Magento/Theme/view/base/requirejs-config.js @@ -19,20 +19,14 @@ var config = { 'jquery/hover-intent': ['jquery'], 'mage/adminhtml/backup': ['prototype'], 'mage/captcha': ['prototype'], - 'mage/common': ['jquery'], 'mage/new-gallery': ['jquery'], 'mage/webapi': ['jquery'], 'jquery/ui': ['jquery'], 'MutationObserver': ['es6-collections'], - 'moment': { - 'exports': 'moment' - }, 'matchMedia': { 'exports': 'mediaCheck' }, - 'jquery/jquery-storageapi': { - 'deps': ['jquery/jquery.cookie'] - } + 'magnifier/magnifier': ['jquery'] }, 'paths': { 'jquery/validate': 'jquery/jquery.validate', diff --git a/app/code/Magento/Theme/view/frontend/requirejs-config.js b/app/code/Magento/Theme/view/frontend/requirejs-config.js index d1cf76b83ebb4..c41a0602ef3e8 100644 --- a/app/code/Magento/Theme/view/frontend/requirejs-config.js +++ b/app/code/Magento/Theme/view/frontend/requirejs-config.js @@ -44,7 +44,7 @@ var config = { 'Magento_Theme/js/view/breadcrumbs': { 'Magento_Theme/js/view/add-home-breadcrumb': true }, - 'jquery/jquery-ui': { + 'jquery/ui-modules/dialog': { 'jquery/patches/jquery-ui': true } } diff --git a/app/code/Magento/Theme/view/frontend/templates/html/sections.phtml b/app/code/Magento/Theme/view/frontend/templates/html/sections.phtml index 602749ba04deb..7f1128b3b07c7 100644 --- a/app/code/Magento/Theme/view/frontend/templates/html/sections.phtml +++ b/app/code/Magento/Theme/view/frontend/templates/html/sections.phtml @@ -10,13 +10,12 @@ $group = $block->getGroupName(); $groupCss = $block->getGroupCss(); -$groupBehavior = $block->getGroupBehaviour() ? $block->getGroupBehaviour() : '{"tabs":{"openedState":"active"}}'; ?> getGroupChildNames($group, 'getChildHtml')) :?> getLayout(); ?> + data-mage-init='{"tabs":{"openedState":"active"}}'> renderElement($name); diff --git a/app/code/Magento/Tinymce3/view/base/web/tiny_mce/plugins/media/editor_plugin_src.js b/app/code/Magento/Tinymce3/view/base/web/tiny_mce/plugins/media/editor_plugin_src.js index a0d4ef2ae38b8..0da100efcbf50 100644 --- a/app/code/Magento/Tinymce3/view/base/web/tiny_mce/plugins/media/editor_plugin_src.js +++ b/app/code/Magento/Tinymce3/view/base/web/tiny_mce/plugins/media/editor_plugin_src.js @@ -255,7 +255,7 @@ vspace : data.vspace, src : self.editor.theme.url + '/img/trans.gif', 'class' : 'mceItemMedia mceItem' + self.getType(data.type).name, - 'data-mce-json' : JSON.serialize(data, "'") + 'data-mce-json' : JSON.serialize(data) }); img.width = data.width || (data.type == 'audio' ? "300" : "320"); @@ -880,7 +880,7 @@ vspace : vspace, align : align, bgcolor : bgcolor, - "data-mce-json" : JSON.serialize(data, "'") + "data-mce-json" : JSON.serialize(data) }); } }); diff --git a/app/code/Magento/Translation/composer.json b/app/code/Magento/Translation/composer.json index c01791c88f99f..e88f44e7cd039 100644 --- a/app/code/Magento/Translation/composer.json +++ b/app/code/Magento/Translation/composer.json @@ -9,7 +9,8 @@ "magento/framework": "*", "magento/module-backend": "*", "magento/module-developer": "*", - "magento/module-store": "*" + "magento/module-store": "*", + "magento/module-theme": "*" }, "suggest": { "magento/module-deploy": "*" diff --git a/app/code/Magento/Translation/etc/di.xml b/app/code/Magento/Translation/etc/di.xml index 93ca7c7b6b736..d17dac23933ee 100644 --- a/app/code/Magento/Translation/etc/di.xml +++ b/app/code/Magento/Translation/etc/di.xml @@ -43,6 +43,7 @@ Magento\Framework\Phrase\Renderer\Translate + Magento\Framework\Phrase\Renderer\MessageFormatter Magento\Framework\Phrase\Renderer\Placeholder Magento\Framework\Phrase\Renderer\Inline diff --git a/app/code/Magento/Ui/Component/Form/Element/DataType/Date.php b/app/code/Magento/Ui/Component/Form/Element/DataType/Date.php index 470767af6d319..31d2fe786cfd8 100644 --- a/app/code/Magento/Ui/Component/Form/Element/DataType/Date.php +++ b/app/code/Magento/Ui/Component/Form/Element/DataType/Date.php @@ -111,14 +111,7 @@ public function getComponentName() public function convertDate($date, $hour = 0, $minute = 0, $second = 0, $setUtcTimeZone = true) { try { - $dateObj = $this->localeDate->date( - new \DateTime( - $date, - new \DateTimeZone($this->localeDate->getConfigTimezone()) - ), - $this->getLocale(), - true - ); + $dateObj = $this->localeDate->date($date, $this->getLocale(), true); $dateObj->setTime($hour, $minute, $second); //convert store date to default date in UTC timezone without DST if ($setUtcTimeZone) { diff --git a/app/code/Magento/Ui/Component/Form/Field/DefaultValue.php b/app/code/Magento/Ui/Component/Form/Field/DefaultValue.php index 99c124e88787f..aaf836bf2dc94 100644 --- a/app/code/Magento/Ui/Component/Form/Field/DefaultValue.php +++ b/app/code/Magento/Ui/Component/Form/Field/DefaultValue.php @@ -7,11 +7,13 @@ namespace Magento\Ui\Component\Form\Field; -use Magento\Framework\View\Element\UiComponentFactory; -use Magento\Framework\View\Element\UiComponent\ContextInterface; use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\View\Element\UiComponent\ContextInterface; +use Magento\Framework\View\Element\UiComponentFactory; -/** Field class has dynamic default value based on System Configuration path */ +/** + * Field class has dynamic default value based on System Configuration path + */ class DefaultValue extends \Magento\Ui\Component\Form\Field { /** @@ -55,16 +57,18 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc */ public function prepare() { parent::prepare(); $store = $this->storeManager->getStore(); - $this->_data['config']['default'] = $this->scopeConfig->getValue( - $this->path, - \Magento\Store\Model\ScopeInterface::SCOPE_STORE, - $store - ); + if (empty($this->_data['config']['default'])) { + $this->_data['config']['default'] = $this->scopeConfig->getValue( + $this->path, + \Magento\Store\Model\ScopeInterface::SCOPE_STORE, + $store + ); + } } } diff --git a/app/code/Magento/Ui/Test/Mftf/ActionGroup/AdminDataGridPaginationActionGroup.xml b/app/code/Magento/Ui/Test/Mftf/ActionGroup/AdminDataGridPaginationActionGroup.xml index a9bf80aa58196..11c5ef039f3e4 100644 --- a/app/code/Magento/Ui/Test/Mftf/ActionGroup/AdminDataGridPaginationActionGroup.xml +++ b/app/code/Magento/Ui/Test/Mftf/ActionGroup/AdminDataGridPaginationActionGroup.xml @@ -18,7 +18,7 @@ - + diff --git a/app/code/Magento/Ui/view/base/layout/default.xml b/app/code/Magento/Ui/view/base/layout/default.xml index 979e6f74880a7..6c8f240660424 100644 --- a/app/code/Magento/Ui/view/base/layout/default.xml +++ b/app/code/Magento/Ui/view/base/layout/default.xml @@ -9,6 +9,7 @@ + diff --git a/app/code/Magento/Ui/view/base/web/js/form/client.js b/app/code/Magento/Ui/view/base/web/js/form/client.js index 1c1274512c979..a16c211607e8a 100644 --- a/app/code/Magento/Ui/view/base/web/js/form/client.js +++ b/app/code/Magento/Ui/view/base/web/js/form/client.js @@ -63,6 +63,9 @@ define([ var $wrapper = $('').addClass(messagesClass).html(msg); $('.page-main-actions', selectorPrefix).after($wrapper); + $('html, body').animate({ + scrollTop: $('.page-main-actions', selectorPrefix).offset().top + }); } }); }); diff --git a/app/code/Magento/Ui/view/base/web/js/lib/validation/rules.js b/app/code/Magento/Ui/view/base/web/js/lib/validation/rules.js index d2523ab436123..3402d1d1df03b 100644 --- a/app/code/Magento/Ui/view/base/web/js/lib/validation/rules.js +++ b/app/code/Magento/Ui/view/base/web/js/lib/validation/rules.js @@ -651,7 +651,8 @@ define([ 'validate-number': [ function (value) { return utils.isEmptyNoTrim(value) || - !isNaN(utils.parseNumber(value)) && /^\s*-?\d*(,\d*)*(\.\d*)?\s*$/.test(value); + !isNaN(utils.parseNumber(value)) && + /^\s*-?\d*(?:[.,|'|\s]\d+)*(?:[.,|'|\s]\d{2})?-?\s*$/.test(value); }, $.mage.__('Please enter a valid number in this field.') ], diff --git a/app/code/Magento/Ups/etc/adminhtml/system.xml b/app/code/Magento/Ups/etc/adminhtml/system.xml index 8b9dc30a0188b..f1b8b22820cba 100644 --- a/app/code/Magento/Ups/etc/adminhtml/system.xml +++ b/app/code/Magento/Ups/etc/adminhtml/system.xml @@ -97,6 +97,7 @@ Sort Order + validate-number validate-zero-or-greater Title diff --git a/app/code/Magento/UrlRewrite/Model/Storage/DbStorage.php b/app/code/Magento/UrlRewrite/Model/Storage/DbStorage.php index 2ac1bdd712114..f0e94e8379ad2 100644 --- a/app/code/Magento/UrlRewrite/Model/Storage/DbStorage.php +++ b/app/code/Magento/UrlRewrite/Model/Storage/DbStorage.php @@ -49,9 +49,9 @@ class DbStorage extends AbstractStorage private $logger; /** - * @param UrlRewriteFactory $urlRewriteFactory - * @param DataObjectHelper $dataObjectHelper - * @param ResourceConnection $resource + * @param UrlRewriteFactory $urlRewriteFactory + * @param DataObjectHelper $dataObjectHelper + * @param ResourceConnection $resource * @param LoggerInterface|null $logger */ public function __construct( @@ -71,7 +71,7 @@ public function __construct( /** * Prepare select statement for specific filter * - * @param array $data + * @param array $data * @return Select */ protected function prepareSelect(array $data) @@ -106,12 +106,14 @@ protected function doFindOneByData(array $data) $requestPath = $data[UrlRewrite::REQUEST_PATH]; $decodedRequestPath = urldecode($requestPath); - $data[UrlRewrite::REQUEST_PATH] = array_unique([ + $data[UrlRewrite::REQUEST_PATH] = array_unique( + [ rtrim($requestPath, '/'), rtrim($requestPath, '/') . '/', rtrim($decodedRequestPath, '/'), rtrim($decodedRequestPath, '/') . '/', - ]); + ] + ); $resultsFromDb = $this->connection->fetchAll($this->prepareSelect($data)); if ($resultsFromDb) { @@ -128,8 +130,8 @@ protected function doFindOneByData(array $data) /** * Extract most relevant url rewrite from url rewrites list * - * @param string $requestPath - * @param array $urlRewrites + * @param string $requestPath + * @param array $urlRewrites * @return array|null */ private function extractMostRelevantUrlRewrite(string $requestPath, array $urlRewrites): ?array @@ -166,8 +168,8 @@ private function extractMostRelevantUrlRewrite(string $requestPath, array $urlRe * If request path matches the DB value or it's redirect - we can return result from DB * Otherwise return 301 redirect to request path from DB results * - * @param string $requestPath - * @param array $urlRewrite + * @param string $requestPath + * @param array $urlRewrite * @return array */ private function prepareUrlRewrite(string $requestPath, array $urlRewrite): array @@ -197,7 +199,7 @@ private function prepareUrlRewrite(string $requestPath, array $urlRewrite): arra /** * Delete old URLs from DB. * - * @param UrlRewrite[] $urls + * @param UrlRewrite[] $urls * @return void */ private function deleteOldUrls(array $urls): void @@ -242,7 +244,7 @@ private function deleteOldUrls(array $urls): void /** * Prepare array with unique entities * - * @param UrlRewrite[] $urls + * @param UrlRewrite[] $urls * @return array */ private function prepareUniqueEntities(array $urls): array @@ -258,23 +260,33 @@ private function prepareUniqueEntities(array $urls): array } $uniqueEntities[$url->getStoreId()][$url->getEntityType()] = $entityIds; } + return $uniqueEntities; } /** * @inheritDoc */ - protected function doReplace(array $urls) + protected function doReplace(array $urls): array { - $this->deleteOldUrls($urls); + $this->connection->beginTransaction(); - $data = []; - foreach ($urls as $url) { - $data[] = $url->toArray(); - } try { + $this->deleteOldUrls($urls); + + $data = []; + foreach ($urls as $url) { + $data[] = $url->toArray(); + } + $this->insertMultiple($data); + + $this->connection->commit(); + // @codingStandardsIgnoreStart } catch (\Magento\Framework\Exception\AlreadyExistsException $e) { + // @codingStandardsIgnoreEnd + $this->connection->rollBack(); + /** @var \Magento\UrlRewrite\Service\V1\Data\UrlRewrite[] $urlConflicted */ $urlConflicted = []; foreach ($urls as $url) { @@ -298,6 +310,9 @@ protected function doReplace(array $urls) } else { throw $e->getPrevious() ?: $e; } + } catch (\Exception $e) { + $this->connection->rollBack(); + throw $e; } return $urls; @@ -306,12 +321,12 @@ protected function doReplace(array $urls) /** * Insert multiple * - * @param array $data + * @param array $data * @return void * @throws \Magento\Framework\Exception\AlreadyExistsException|\Exception * @throws \Exception */ - protected function insertMultiple($data) + protected function insertMultiple($data): void { try { $this->connection->insertMultiple($this->resource->getTableName(self::TABLE_NAME), $data); @@ -331,11 +346,11 @@ protected function insertMultiple($data) /** * Get filter for url rows deletion due to provided urls * - * @param UrlRewrite[] $urls - * @return array + * @param UrlRewrite[] $urls + * @return array * @deprecated Not used anymore. */ - protected function createFilterDataBasedOnUrls($urls) + protected function createFilterDataBasedOnUrls($urls): array { $data = []; foreach ($urls as $url) { diff --git a/app/code/Magento/UrlRewrite/Model/StoreSwitcher/RewriteUrl.php b/app/code/Magento/UrlRewrite/Model/StoreSwitcher/RewriteUrl.php index 16e9c37ee4e52..873eed4466715 100644 --- a/app/code/Magento/UrlRewrite/Model/StoreSwitcher/RewriteUrl.php +++ b/app/code/Magento/UrlRewrite/Model/StoreSwitcher/RewriteUrl.php @@ -111,7 +111,7 @@ private function findCurrentRewrite(UrlRewrite $oldRewrite, StoreInterface $targ if (!$currentRewrite) { $currentRewrite = $this->urlFinder->findOneByData( [ - UrlRewrite::REQUEST_PATH => $oldRewrite->getTargetPath(), + UrlRewrite::REQUEST_PATH => $oldRewrite->getRequestPath(), UrlRewrite::STORE_ID => $targetStore->getId(), ] ); diff --git a/app/code/Magento/User/Model/ResourceModel/User.php b/app/code/Magento/User/Model/ResourceModel/User.php index b3e4936266a3f..4eaf6116056dd 100644 --- a/app/code/Magento/User/Model/ResourceModel/User.php +++ b/app/code/Magento/User/Model/ResourceModel/User.php @@ -4,6 +4,8 @@ * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\User\Model\ResourceModel; use Magento\Authorization\Model\Acl\Role\Group as RoleGroup; @@ -208,27 +210,23 @@ protected function _createUserRole($parentId, ModelUser $user) if ($parentId > 0) { /** @var \Magento\Authorization\Model\Role $parentRole */ $parentRole = $this->_roleFactory->create()->load($parentId); - } else { - $role = new \Magento\Framework\DataObject(); - $role->setTreeLevel(0); - } - - if ($parentRole->getId()) { - $data = new \Magento\Framework\DataObject( - [ - 'parent_id' => $parentRole->getId(), - 'tree_level' => $parentRole->getTreeLevel() + 1, - 'sort_order' => 0, - 'role_type' => RoleUser::ROLE_TYPE, - 'user_id' => $user->getId(), - 'user_type' => UserContextInterface::USER_TYPE_ADMIN, - 'role_name' => $user->getFirstName(), - ] - ); - - $insertData = $this->_prepareDataForTable($data, $this->getTable('authorization_role')); - $this->getConnection()->insert($this->getTable('authorization_role'), $insertData); - $this->aclDataCache->clean(); + if ($parentRole->getId()) { + $data = new \Magento\Framework\DataObject( + [ + 'parent_id' => $parentRole->getId(), + 'tree_level' => $parentRole->getTreeLevel() + 1, + 'sort_order' => 0, + 'role_type' => RoleUser::ROLE_TYPE, + 'user_id' => $user->getId(), + 'user_type' => UserContextInterface::USER_TYPE_ADMIN, + 'role_name' => $user->getFirstName(), + ] + ); + + $insertData = $this->_prepareDataForTable($data, $this->getTable('authorization_role')); + $this->getConnection()->insert($this->getTable('authorization_role'), $insertData); + $this->aclDataCache->clean(); + } } } @@ -267,8 +265,6 @@ public function delete(\Magento\Framework\Model\AbstractModel $user) ['user_id = ?' => $uid, 'user_type = ?' => UserContextInterface::USER_TYPE_ADMIN] ); } catch (\Magento\Framework\Exception\LocalizedException $e) { - throw $e; - } catch (\Exception $e) { $connection->rollBack(); return false; } @@ -476,7 +472,7 @@ public function updateRoleUsersAcl(\Magento\Authorization\Model\Role $role) $users = $role->getRoleUsers(); $rowsCount = 0; - if (sizeof($users) > 0) { + if (count($users) > 0) { $bind = ['reload_acl_flag' => 1]; $where = ['user_id IN(?)' => $users]; $rowsCount = $connection->update($this->getTable('admin_user'), $bind, $where); @@ -618,6 +614,7 @@ public function trackPassword($user, $passwordHash, $lifetime = 0) /** * Get latest password for specified user id + * * Possible false positive when password was changed several times with different lifetime configuration * * @param int $userId diff --git a/app/code/Magento/User/Model/User.php b/app/code/Magento/User/Model/User.php index dc0aa0cd38343..d79f2013241e6 100644 --- a/app/code/Magento/User/Model/User.php +++ b/app/code/Magento/User/Model/User.php @@ -55,7 +55,11 @@ class User extends AbstractModel implements StorageInterface, UserInterface */ const XML_PATH_USER_NOTIFICATION_TEMPLATE = 'admin/emails/user_notification_template'; - /** @deprecated */ + /** + * Configuration paths for admin user reset password email template + * + * @deprecated + */ const XML_PATH_RESET_PASSWORD_TEMPLATE = 'admin/emails/reset_password_template'; const MESSAGE_ID_PASSWORD_EXPIRED = 'magento_user_password_expired'; @@ -921,7 +925,6 @@ public function performIdentityCheck($passwordString) { try { $isCheckSuccessful = $this->verifyIdentity($passwordString); - // phpcs:ignore Magento2.Exceptions.ThrowCatch } catch (\Magento\Framework\Exception\AuthenticationException $e) { $isCheckSuccessful = false; } diff --git a/app/code/Magento/Usps/etc/adminhtml/system.xml b/app/code/Magento/Usps/etc/adminhtml/system.xml index 0bdaf49297f05..0849572e7eb1c 100644 --- a/app/code/Magento/Usps/etc/adminhtml/system.xml +++ b/app/code/Magento/Usps/etc/adminhtml/system.xml @@ -136,6 +136,7 @@ Sort Order + validate-number validate-zero-or-greater diff --git a/app/code/Magento/Variable/etc/di.xml b/app/code/Magento/Variable/etc/di.xml index 41759e1f1582b..99f299e9197f1 100644 --- a/app/code/Magento/Variable/etc/di.xml +++ b/app/code/Magento/Variable/etc/di.xml @@ -21,6 +21,10 @@ 1 1 + + 1 + 1 + 1 1 diff --git a/app/code/Magento/Vault/Model/Method/Vault.php b/app/code/Magento/Vault/Model/Method/Vault.php index 2de934072daa3..da1cf5bd3c67f 100644 --- a/app/code/Magento/Vault/Model/Method/Vault.php +++ b/app/code/Magento/Vault/Model/Method/Vault.php @@ -5,6 +5,7 @@ */ namespace Magento\Vault\Model\Method; +use Exception; use Magento\Framework\Event\ManagerInterface; use Magento\Framework\ObjectManagerInterface; use Magento\Payment\Gateway\Command; @@ -21,6 +22,7 @@ use Magento\Vault\Api\PaymentTokenManagementInterface; use Magento\Vault\Block\Form; use Magento\Vault\Model\VaultPaymentInterface; +use Magento\Framework\Serialize\Serializer\Json; /** * Class Vault @@ -103,6 +105,11 @@ class Vault implements VaultPaymentInterface */ private $code; + /** + * @var Json + */ + private $jsonSerializer; + /** * Constructor * @@ -116,6 +123,7 @@ class Vault implements VaultPaymentInterface * @param PaymentTokenManagementInterface $tokenManagement * @param OrderPaymentExtensionInterfaceFactory $paymentExtensionFactory * @param string $code + * @param Json|null $jsonSerializer * * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ @@ -129,7 +137,8 @@ public function __construct( Command\CommandManagerPoolInterface $commandManagerPool, PaymentTokenManagementInterface $tokenManagement, OrderPaymentExtensionInterfaceFactory $paymentExtensionFactory, - $code + $code, + Json $jsonSerializer = null ) { $this->config = $config; $this->configFactory = $configFactory; @@ -141,21 +150,14 @@ public function __construct( $this->tokenManagement = $tokenManagement; $this->paymentExtensionFactory = $paymentExtensionFactory; $this->code = $code; - } - - /** - * @return MethodInterface - */ - private function getVaultProvider() - { - return $this->vaultProvider; + $this->jsonSerializer = $jsonSerializer ?: $this->objectManager->get(Json::class); } /** * Unifies configured value handling logic * * @param string $field - * @param null $storeId + * @param int|null $storeId * @return mixed */ private function getConfiguredValue($field, $storeId = null) @@ -226,8 +228,8 @@ public function canOrder() */ public function canAuthorize() { - return $this->getVaultProvider()->canAuthorize() - && $this->getVaultProvider()->getConfigData(static::CAN_AUTHORIZE); + return $this->vaultProvider->canAuthorize() + && $this->vaultProvider->getConfigData(static::CAN_AUTHORIZE); } /** @@ -236,8 +238,8 @@ public function canAuthorize() */ public function canCapture() { - return $this->getVaultProvider()->canCapture() - && $this->getVaultProvider()->getConfigData(static::CAN_CAPTURE); + return $this->vaultProvider->canCapture() + && $this->vaultProvider->getConfigData(static::CAN_CAPTURE); } /** @@ -255,7 +257,7 @@ public function canCapturePartial() */ public function canCaptureOnce() { - return $this->getVaultProvider()->canCaptureOnce(); + return $this->vaultProvider->canCaptureOnce(); } /** @@ -294,7 +296,7 @@ public function canUseInternal() $isInternalAllowed = $this->getConfiguredValue('can_use_internal'); // if config has't been specified for Vault, need to check payment provider option if ($isInternalAllowed === null) { - return $this->getVaultProvider()->canUseInternal(); + return $this->vaultProvider->canUseInternal(); } return (bool) $isInternalAllowed; } @@ -305,7 +307,7 @@ public function canUseInternal() */ public function canUseCheckout() { - return $this->getVaultProvider()->canUseCheckout(); + return $this->vaultProvider->canUseCheckout(); } /** @@ -314,7 +316,7 @@ public function canUseCheckout() */ public function canEdit() { - return $this->getVaultProvider()->canEdit(); + return $this->vaultProvider->canEdit(); } /** @@ -341,7 +343,7 @@ public function fetchTransactionInfo(InfoInterface $payment, $transactionId) */ public function isGateway() { - return $this->getVaultProvider()->isGateway(); + return $this->vaultProvider->isGateway(); } /** @@ -350,7 +352,7 @@ public function isGateway() */ public function isOffline() { - return $this->getVaultProvider()->isOffline(); + return $this->vaultProvider->isOffline(); } /** @@ -359,7 +361,7 @@ public function isOffline() */ public function isInitializeNeeded() { - return $this->getVaultProvider()->isInitializeNeeded(); + return $this->vaultProvider->isInitializeNeeded(); } /** @@ -368,7 +370,7 @@ public function isInitializeNeeded() */ public function canUseForCountry($country) { - return $this->getVaultProvider()->canUseForCountry($country); + return $this->vaultProvider->canUseForCountry($country); } /** @@ -377,7 +379,7 @@ public function canUseForCountry($country) */ public function canUseForCurrency($currencyCode) { - return $this->getVaultProvider()->canUseForCurrency($currencyCode); + return $this->vaultProvider->canUseForCurrency($currencyCode); } /** @@ -386,7 +388,7 @@ public function canUseForCurrency($currencyCode) */ public function getInfoBlockType() { - return $this->getVaultProvider()->getInfoBlockType(); + return $this->vaultProvider->getInfoBlockType(); } /** @@ -395,7 +397,7 @@ public function getInfoBlockType() */ public function getInfoInstance() { - return $this->getVaultProvider()->getInfoInstance(); + return $this->vaultProvider->getInfoInstance(); } /** @@ -404,7 +406,7 @@ public function getInfoInstance() */ public function setInfoInstance(InfoInterface $info) { - $this->getVaultProvider()->setInfoInstance($info); + $this->vaultProvider->setInfoInstance($info); } /** @@ -413,7 +415,7 @@ public function setInfoInstance(InfoInterface $info) */ public function validate() { - return $this->getVaultProvider()->validate(); + return $this->vaultProvider->validate(); } /** @@ -437,9 +439,10 @@ public function authorize(\Magento\Payment\Model\InfoInterface $payment, $amount /** @var $payment OrderPaymentInterface */ $this->attachTokenExtensionAttribute($payment); + $this->attachCreditCardInfo($payment); $commandExecutor = $this->commandManagerPool->get( - $this->getVaultProvider()->getCode() + $this->vaultProvider->getCode() ); $commandExecutor->executeByCode( @@ -450,7 +453,7 @@ public function authorize(\Magento\Payment\Model\InfoInterface $payment, $amount ] ); - $payment->setMethod($this->getVaultProvider()->getCode()); + $payment->setMethod($this->vaultProvider->getCode()); return $this; } @@ -473,7 +476,7 @@ public function capture(\Magento\Payment\Model\InfoInterface $payment, $amount) $this->attachTokenExtensionAttribute($payment); $commandExecutor = $this->commandManagerPool->get( - $this->getVaultProvider()->getCode() + $this->vaultProvider->getCode() ); $commandExecutor->executeByCode( @@ -484,10 +487,12 @@ public function capture(\Magento\Payment\Model\InfoInterface $payment, $amount) ] ); - $payment->setMethod($this->getVaultProvider()->getCode()); + $payment->setMethod($this->vaultProvider->getCode()); } /** + * Attaches token extension attribute. + * * @param OrderPaymentInterface $orderPayment * @return void */ @@ -514,6 +519,8 @@ private function attachTokenExtensionAttribute(OrderPaymentInterface $orderPayme } /** + * Returns Payment's extension attributes. + * * @param OrderPaymentInterface $payment * @return \Magento\Sales\Api\Data\OrderPaymentExtensionInterface */ @@ -528,6 +535,33 @@ private function getPaymentExtensionAttributes(OrderPaymentInterface $payment) return $extensionAttributes; } + /** + * Attaches credit card info. + * + * @param OrderPaymentInterface $payment + * @return void + */ + private function attachCreditCardInfo(OrderPaymentInterface $payment): void + { + $paymentToken = $payment->getExtensionAttributes() + ->getVaultPaymentToken(); + if ($paymentToken === null) { + return; + } + + $tokenDetails = $paymentToken->getTokenDetails(); + if ($tokenDetails === null) { + return; + } + + if (is_string($tokenDetails)) { + $tokenDetails = $this->jsonSerializer->unserialize($paymentToken->getTokenDetails()); + } + if (is_array($tokenDetails)) { + $payment->addData($tokenDetails); + } + } + /** * @inheritdoc * @since 100.1.0 @@ -615,7 +649,7 @@ public function assignData(\Magento\Framework\DataObject $data) ] ); - return $this->getVaultProvider()->assignData($data); + return $this->vaultProvider->assignData($data); } /** @@ -624,7 +658,7 @@ public function assignData(\Magento\Framework\DataObject $data) */ public function isAvailable(\Magento\Quote\Api\Data\CartInterface $quote = null) { - return $this->getVaultProvider()->isAvailable($quote) + return $this->vaultProvider->isAvailable($quote) && $this->config->getValue(self::$activeKey, $this->getStore() ?: ($quote ? $quote->getStoreId() : null)); } @@ -634,7 +668,7 @@ public function isAvailable(\Magento\Quote\Api\Data\CartInterface $quote = null) */ public function isActive($storeId = null) { - return $this->getVaultProvider()->isActive($storeId) + return $this->vaultProvider->isActive($storeId) && $this->config->getValue(self::$activeKey, $this->getStore() ?: $storeId); } @@ -653,7 +687,7 @@ public function initialize($paymentAction, $stateObject) */ public function getConfigPaymentAction() { - return $this->getVaultProvider()->getConfigPaymentAction(); + return $this->vaultProvider->getConfigPaymentAction(); } /** @@ -662,6 +696,6 @@ public function getConfigPaymentAction() */ public function getProviderCode() { - return $this->getVaultProvider()->getCode(); + return $this->vaultProvider->getCode(); } } diff --git a/app/code/Magento/Vault/Test/Unit/Model/Method/VaultTest.php b/app/code/Magento/Vault/Test/Unit/Model/Method/VaultTest.php index 7e3f9b67a58ec..ae6f35822276f 100644 --- a/app/code/Magento/Vault/Test/Unit/Model/Method/VaultTest.php +++ b/app/code/Magento/Vault/Test/Unit/Model/Method/VaultTest.php @@ -23,6 +23,7 @@ use Magento\Vault\Api\PaymentTokenManagementInterface; use Magento\Vault\Model\Method\Vault; use Magento\Vault\Model\VaultPaymentInterface; +use Magento\Framework\Serialize\Serializer\Json; use PHPUnit_Framework_MockObject_MockObject as MockObject; /** @@ -42,10 +43,19 @@ class VaultTest extends \PHPUnit\Framework\TestCase */ private $vaultProvider; + /** + * @var Json|MockObject + */ + private $jsonSerializer; + + /** + * @inheritdoc + */ public function setUp() { $this->objectManager = new ObjectManager($this); $this->vaultProvider = $this->createMock(MethodInterface::class); + $this->jsonSerializer = $this->createMock(Json::class); } /** @@ -152,6 +162,19 @@ public function testAuthorize() $tokenManagement = $this->createMock(PaymentTokenManagementInterface::class); $token = $this->createMock(PaymentTokenInterface::class); + $tokenDetails = [ + 'cc_last4' => '1111', + 'cc_type' => 'VI', + 'cc_exp_year' => '2020', + 'cc_exp_month' => '01', + ]; + + $extensionAttributes->method('getVaultPaymentToken') + ->willReturn($token); + + $this->jsonSerializer->method('unserialize') + ->willReturn($tokenDetails); + $paymentModel->expects(static::once()) ->method('getAdditionalInformation') ->willReturn( @@ -164,8 +187,7 @@ public function testAuthorize() ->method('getByPublicHash') ->with($publicHash, $customerId) ->willReturn($token); - $paymentModel->expects(static::once()) - ->method('getExtensionAttributes') + $paymentModel->method('getExtensionAttributes') ->willReturn($extensionAttributes); $extensionAttributes->expects(static::once()) ->method('setVaultPaymentToken') @@ -198,7 +220,8 @@ public function testAuthorize() [ 'tokenManagement' => $tokenManagement, 'commandManagerPool' => $commandManagerPool, - 'vaultProvider' => $this->vaultProvider + 'vaultProvider' => $this->vaultProvider, + 'jsonSerializer' => $this->jsonSerializer, ] ); $model->authorize($paymentModel, $amount); diff --git a/app/code/Magento/Vault/composer.json b/app/code/Magento/Vault/composer.json index 7dc2e0be78640..c37bc51f9d432 100644 --- a/app/code/Magento/Vault/composer.json +++ b/app/code/Magento/Vault/composer.json @@ -12,7 +12,8 @@ "magento/module-payment": "*", "magento/module-quote": "*", "magento/module-sales": "*", - "magento/module-store": "*" + "magento/module-store": "*", + "magento/module-theme": "*" }, "type": "magento2-module", "license": [ diff --git a/app/code/Magento/Vault/view/frontend/layout/customer_account.xml b/app/code/Magento/Vault/view/frontend/layout/customer_account.xml index 73ce9247fef0a..05044da272e6d 100644 --- a/app/code/Magento/Vault/view/frontend/layout/customer_account.xml +++ b/app/code/Magento/Vault/view/frontend/layout/customer_account.xml @@ -11,7 +11,6 @@ vault/cards/listaction diff --git a/app/code/Magento/Version/Test/Unit/Controller/Index/IndexTest.php b/app/code/Magento/Version/Test/Unit/Controller/Index/IndexTest.php new file mode 100644 index 0000000000000..6f8daa9d8008d --- /dev/null +++ b/app/code/Magento/Version/Test/Unit/Controller/Index/IndexTest.php @@ -0,0 +1,97 @@ +context = $this->getMockBuilder(Context::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->productMetadata = $this->getMockBuilder(ProductMetadataInterface::class) + ->disableOriginalConstructor() + ->setMethods(['getName', 'getEdition', 'getVersion']) + ->getMock(); + + $this->response = $this->getMockBuilder(ResponseInterface::class) + ->disableOriginalConstructor() + ->setMethods(['setBody', 'sendResponse']) + ->getMock(); + + $this->context->expects($this->any()) + ->method('getResponse') + ->willReturn($this->response); + + $helper = new ObjectManager($this); + + $this->model = $helper->getObject( + 'Magento\Version\Controller\Index\Index', + [ + 'context' => $this->context, + 'productMetadata' => $this->productMetadata + ] + ); + } + + /** + * Test with Git Base version + */ + public function testExecuteWithGitBase() + { + $this->productMetadata->expects($this->any())->method('getVersion')->willReturn('dev-2.3'); + $this->assertNull($this->model->execute()); + } + + /** + * Test with Community Version + */ + public function testExecuteWithCommunityVersion() + { + $this->productMetadata->expects($this->any())->method('getVersion')->willReturn('2.3.3'); + $this->productMetadata->expects($this->any())->method('getEdition')->willReturn('Community'); + $this->productMetadata->expects($this->any())->method('getName')->willReturn('Magento'); + $this->response->expects($this->once())->method('setBody') + ->with('Magento/2.3 (Community)') + ->will($this->returnSelf()); + $this->model->execute(); + } +} diff --git a/app/code/Magento/Webapi/Model/Rest/Swagger/Generator.php b/app/code/Magento/Webapi/Model/Rest/Swagger/Generator.php index 51b474cc215bf..3ddb2e441ef91 100644 --- a/app/code/Magento/Webapi/Model/Rest/Swagger/Generator.php +++ b/app/code/Magento/Webapi/Model/Rest/Swagger/Generator.php @@ -738,7 +738,8 @@ protected function getQueryParamNames($name, $type, $description, $prefix = '') */ private function handleComplex($name, $type, $prefix, $isArray) { - $parameters = $this->typeProcessor->getTypeData($type)['parameters']; + $typeData = $this->typeProcessor->getTypeData($type); + $parameters = $typeData['parameters'] ?? []; $queryNames = []; foreach ($parameters as $subParameterName => $subParameterInfo) { $subParameterType = $subParameterInfo['type']; diff --git a/app/code/Magento/Weee/Model/Attribute/Backend/Weee/Tax.php b/app/code/Magento/Weee/Model/Attribute/Backend/Weee/Tax.php index 35835e6c5f63f..472db552ecb5f 100644 --- a/app/code/Magento/Weee/Model/Attribute/Backend/Weee/Tax.php +++ b/app/code/Magento/Weee/Model/Attribute/Backend/Weee/Tax.php @@ -3,12 +3,16 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); namespace Magento\Weee\Model\Attribute\Backend\Weee; use Magento\Framework\Exception\LocalizedException; use Magento\Catalog\Model\Attribute\ScopeOverriddenValue; +/** + * Class with fixed product taxes. + */ class Tax extends \Magento\Catalog\Model\Product\Attribute\Backend\Price { /** @@ -62,10 +66,14 @@ public function __construct( } /** + * Get backend model name. + * * @return string + * phpcs:disable Magento2.Functions.StaticFunction */ public static function getBackendModelName() { + // phpcs:enable Magento2.Functions.StaticFunction return \Magento\Weee\Model\Attribute\Backend\Weee\Tax::class; } @@ -91,8 +99,10 @@ public function validate($object) $key1 = implode('-', [$tax['website_id'], $tax['country'], $state]); if (!empty($dup[$key1])) { throw new LocalizedException( - __('Set unique country-state combinations within the same fixed product tax. ' - . 'Verify the combinations and try again.') + __( + 'Set unique country-state combinations within the same fixed product tax. ' + . 'Verify the combinations and try again.' + ) ); } $dup[$key1] = 1; @@ -130,7 +140,7 @@ public function afterLoad($object) } /** - * {@inheritdoc} + * @inheritdoc * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) */ @@ -170,7 +180,7 @@ public function afterSave($object) } /** - * {@inheritdoc} + * @inheritdoc */ public function afterDelete($object) { @@ -179,7 +189,7 @@ public function afterDelete($object) } /** - * {@inheritdoc} + * @inheritdoc */ public function getTable() { diff --git a/app/code/Magento/Weee/Test/Mftf/Section/AdminProductAddFPTValueSection.xml b/app/code/Magento/Weee/Test/Mftf/Section/AdminProductAddFPTValueSection.xml index ebf9f41047124..8a9e9ff6a7169 100644 --- a/app/code/Magento/Weee/Test/Mftf/Section/AdminProductAddFPTValueSection.xml +++ b/app/code/Magento/Weee/Test/Mftf/Section/AdminProductAddFPTValueSection.xml @@ -14,5 +14,7 @@ + + diff --git a/app/code/Magento/Weee/Test/Mftf/Test/AdminFixedTaxValSavedForSpecificWebsiteTest.xml b/app/code/Magento/Weee/Test/Mftf/Test/AdminFixedTaxValSavedForSpecificWebsiteTest.xml new file mode 100644 index 0000000000000..85ed044644d5c --- /dev/null +++ b/app/code/Magento/Weee/Test/Mftf/Test/AdminFixedTaxValSavedForSpecificWebsiteTest.xml @@ -0,0 +1,118 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/code/Magento/Weee/Ui/DataProvider/Product/Form/Modifier/Manager/Website.php b/app/code/Magento/Weee/Ui/DataProvider/Product/Form/Modifier/Manager/Website.php index 2374e2e57a543..476e20bd9861d 100644 --- a/app/code/Magento/Weee/Ui/DataProvider/Product/Form/Modifier/Manager/Website.php +++ b/app/code/Magento/Weee/Ui/DataProvider/Product/Form/Modifier/Manager/Website.php @@ -3,6 +3,8 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Weee\Ui\DataProvider\Product\Form\Modifier\Manager; use Magento\Catalog\Api\Data\ProductInterface; @@ -12,6 +14,8 @@ use Magento\Directory\Model\Currency; use Magento\Store\Api\Data\WebsiteInterface; use Magento\Store\Model\StoreManagerInterface; +use Magento\Catalog\Helper\Data; +use Magento\Framework\App\ObjectManager; /** * Class Website @@ -23,19 +27,27 @@ class Website */ private $websites; + /** + * @var Data + */ + private $catalogHelper; + /** * @param LocatorInterface $locator * @param StoreManagerInterface $storeManager * @param DirectoryHelper $directoryHelper + * @param Data|null $catalogHelper */ public function __construct( LocatorInterface $locator, StoreManagerInterface $storeManager, - DirectoryHelper $directoryHelper + DirectoryHelper $directoryHelper, + Data $catalogHelper = null ) { $this->locator = $locator; $this->storeManager = $storeManager; $this->directoryHelper = $directoryHelper; + $this->catalogHelper = $catalogHelper ?: ObjectManager::getInstance()->get(Data::class); } /** @@ -60,6 +72,7 @@ public function getWebsites(ProductInterface $product, EavAttribute $eavAttribut if ($this->storeManager->hasSingleStore() || ($eavAttribute->getEntityAttribute() && $eavAttribute->getEntityAttribute()->isScopeGlobal() + || $this->catalogHelper->isPriceGlobal() ) ) { return $this->websites = $websites; diff --git a/app/code/Magento/Widget/Block/Adminhtml/Widget/Instance/Edit/Tab/Main.php b/app/code/Magento/Widget/Block/Adminhtml/Widget/Instance/Edit/Tab/Main.php index 73a6faec9b675..55b6874116289 100644 --- a/app/code/Magento/Widget/Block/Adminhtml/Widget/Instance/Edit/Tab/Main.php +++ b/app/code/Magento/Widget/Block/Adminhtml/Widget/Instance/Edit/Tab/Main.php @@ -3,7 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - +declare(strict_types=1); /** * Widget Instance Main tab block * @@ -12,6 +12,8 @@ namespace Magento\Widget\Block\Adminhtml\Widget\Instance\Edit\Tab; /** + * Edit Main Tab + * * @api * @since 100.0.2 */ @@ -198,7 +200,7 @@ protected function _prepareForm() 'name' => 'sort_order', 'label' => __('Sort Order'), 'title' => __('Sort Order'), - 'class' => '', + 'class' => 'validate-number', 'required' => false, 'note' => __('Sort Order of widget instances in the same container') ] diff --git a/app/code/Magento/Widget/Controller/Adminhtml/Widget/Instance/MassDelete.php b/app/code/Magento/Widget/Controller/Adminhtml/Widget/Instance/MassDelete.php new file mode 100644 index 0000000000000..1134dd219059b --- /dev/null +++ b/app/code/Magento/Widget/Controller/Adminhtml/Widget/Instance/MassDelete.php @@ -0,0 +1,125 @@ +deleteWidgetById = $deleteWidgetById; + } + + /** + * Execute action + * + * @return Redirect + * @throws \Exception + */ + public function execute(): Redirect + { + $deletedInstances = 0; + $notDeletedInstances = []; + /** @var array $instanceIds */ + $instanceIds = $this->getInstanceIds(); + + if (!count($instanceIds)) { + $this->messageManager->addErrorMessage(__('No widget instance IDs were provided to be deleted.')); + + /** @var Redirect $resultRedirect */ + $resultRedirect = $this->getResultPage(); + + return $resultRedirect->setPath('*/*/'); + } + + foreach ($instanceIds as $instanceId) { + try { + $this->deleteWidgetById->execute((int)$instanceId); + $deletedInstances++; + } catch (NoSuchEntityException $e) { + $notDeletedInstances[] = $instanceId; + } + } + + if ($deletedInstances) { + $this->messageManager->addSuccessMessage(__('A total of %1 record(s) have been deleted.', $deletedInstances)); + } + + if (count($notDeletedInstances)) { + $this->messageManager->addErrorMessage( + __( + 'Widget(s) with ID(s) %1 were not found', + trim(implode(', ', $notDeletedInstances)) + ) + ); + } + + /** @var Redirect $resultRedirect */ + $resultRedirect = $this->getResultPage(); + + return $resultRedirect->setPath('*/*/'); + } + + /** + * Get instance IDs. + * + * @return array + */ + private function getInstanceIds(): array + { + $instanceIds = $this->getRequest()->getParam('delete'); + + if (!is_array($instanceIds)) { + return []; + } + + return $instanceIds; + } + + /** + * Get result page. + * + * @return ResultInterface|null + */ + private function getResultPage(): ?ResultInterface + { + return $this->resultFactory->create(ResultFactory::TYPE_REDIRECT); + } +} diff --git a/app/code/Magento/Widget/Model/DeleteWidgetById.php b/app/code/Magento/Widget/Model/DeleteWidgetById.php new file mode 100644 index 0000000000000..4c5d23d5a056b --- /dev/null +++ b/app/code/Magento/Widget/Model/DeleteWidgetById.php @@ -0,0 +1,81 @@ +resourceModel = $resourceModel; + $this->instanceFactory = $instanceFactory; + } + + /** + * Delete widget instance by given instance ID + * + * @param int $instanceId + * @return void + * @throws \Exception + */ + public function execute(int $instanceId) : void + { + $model = $this->getWidgetById($instanceId); + + $this->resourceModel->delete($model); + } + + /** + * Get widget instance by given instance ID + * + * @param int $instanceId + * @return WidgetInstance + * @throws NoSuchEntityException + */ + private function getWidgetById(int $instanceId): WidgetInstance + { + /** @var WidgetInstance $widgetInstance */ + $widgetInstance = $this->instanceFactory->create(); + + $this->resourceModel->load($widgetInstance, $instanceId); + + if (!$widgetInstance->getId()) { + throw new NoSuchEntityException( + __( + 'No such entity with instance_id = %instance_id', + ['instance_id' => $instanceId] + ) + ); + } + + return $widgetInstance; + } +} diff --git a/app/code/Magento/Widget/Model/Widget/Config.php b/app/code/Magento/Widget/Model/Widget/Config.php index 00b055b35a69d..68761b68b9f06 100644 --- a/app/code/Magento/Widget/Model/Widget/Config.php +++ b/app/code/Magento/Widget/Model/Widget/Config.php @@ -3,6 +3,9 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +declare(strict_types=1); + namespace Magento\Widget\Model\Widget; /** @@ -198,8 +201,9 @@ public function decodeWidgetsFromQuery($queryParam) */ public function getAvailableWidgets($config) { + $result = []; + if (!$config->hasData('widget_types')) { - $result = []; $allWidgets = $this->_widgetFactory->create()->getWidgetsArray(); $skipped = $this->_getSkippedWidgets(); foreach ($allWidgets as $widget) { diff --git a/app/code/Magento/Widget/view/adminhtml/layout/adminhtml_widget_instance_block.xml b/app/code/Magento/Widget/view/adminhtml/layout/adminhtml_widget_instance_block.xml index 8ca3fab413b25..c78f9ec225be4 100644 --- a/app/code/Magento/Widget/view/adminhtml/layout/adminhtml_widget_instance_block.xml +++ b/app/code/Magento/Widget/view/adminhtml/layout/adminhtml_widget_instance_block.xml @@ -15,6 +15,20 @@ ASC Magento\Widget\Model\ResourceModel\Widget\Instance\Collection + + + instance_id + delete + 1 + + + Delete + */*/massDelete + 0 + + + + diff --git a/app/code/Magento/Wishlist/Block/Customer/Wishlist/Item/Options.php b/app/code/Magento/Wishlist/Block/Customer/Wishlist/Item/Options.php index 63a480801d5d4..82133927e1201 100644 --- a/app/code/Magento/Wishlist/Block/Customer/Wishlist/Item/Options.php +++ b/app/code/Magento/Wishlist/Block/Customer/Wishlist/Item/Options.php @@ -114,7 +114,7 @@ public function getConfiguredOptions() $option['value'][$key] = $this->escapeHtml($value); } } else { - $option['value'] = $this->escapeHtml($option['value']); + $option['value'] = $this->escapeHtml($option['value'], ["a"]); } } $options[$index]['value'] = $option['value']; diff --git a/app/code/Magento/Wishlist/Controller/Index/DownloadCustomOption.php b/app/code/Magento/Wishlist/Controller/Index/DownloadCustomOption.php index 742b2a91e9317..dc0ea8d5a093a 100644 --- a/app/code/Magento/Wishlist/Controller/Index/DownloadCustomOption.php +++ b/app/code/Magento/Wishlist/Controller/Index/DownloadCustomOption.php @@ -99,7 +99,7 @@ public function execute() $this->_fileResponseFactory->create( $info['title'], ['value' => $info['quote_path'], 'type' => 'filename'], - DirectoryList::ROOT, + DirectoryList::MEDIA, $info['type'] ); } diff --git a/app/code/Magento/Wishlist/Helper/Data.php b/app/code/Magento/Wishlist/Helper/Data.php index 6c1ebd87b4e8d..e8280db3e1f21 100644 --- a/app/code/Magento/Wishlist/Helper/Data.php +++ b/app/code/Magento/Wishlist/Helper/Data.php @@ -3,6 +3,9 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +declare(strict_types=1); + namespace Magento\Wishlist\Helper; use Magento\Framework\App\ActionInterface; @@ -117,6 +120,9 @@ class Data extends \Magento\Framework\App\Helper\AbstractHelper * @param \Magento\Customer\Helper\View $customerViewHelper * @param WishlistProviderInterface $wishlistProvider * @param \Magento\Catalog\Api\ProductRepositoryInterface $productRepository + * @param Escaper $escaper + * + * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( \Magento\Framework\App\Helper\Context $context, @@ -127,7 +133,8 @@ public function __construct( \Magento\Framework\Data\Helper\PostHelper $postDataHelper, \Magento\Customer\Helper\View $customerViewHelper, WishlistProviderInterface $wishlistProvider, - \Magento\Catalog\Api\ProductRepositoryInterface $productRepository + \Magento\Catalog\Api\ProductRepositoryInterface $productRepository, + Escaper $escaper = null ) { $this->_coreRegistry = $coreRegistry; $this->_customerSession = $customerSession; @@ -137,7 +144,7 @@ public function __construct( $this->_customerViewHelper = $customerViewHelper; $this->wishlistProvider = $wishlistProvider; $this->productRepository = $productRepository; - $this->escaper = ObjectManager::getInstance()->get(Escaper::class); + $this->escaper = $escaper ?? ObjectManager::getInstance()->get(Escaper::class); parent::__construct($context); } @@ -352,7 +359,6 @@ public function getAddParams($item, array $params = []) * Retrieve params for adding product to wishlist * * @param int $itemId - * * @return string */ public function getMoveFromCartParams($itemId) @@ -366,7 +372,6 @@ public function getMoveFromCartParams($itemId) * Retrieve params for updating product in wishlist * * @param \Magento\Catalog\Model\Product|\Magento\Wishlist\Model\Item $item - * * @return string|false */ public function getUpdateParams($item) @@ -541,6 +546,7 @@ public function getCustomerName() */ public function getRssUrl($wishlistId = null) { + $params = []; $customer = $this->_getCurrentCustomer(); if ($customer) { $key = $customer->getId() . ',' . $customer->getEmail(); @@ -574,6 +580,7 @@ public function getDefaultWishlistName() /** * Calculate count of wishlist items and put value to customer session. + * * Method called after wishlist modifications and trigger 'wishlist_items_renewed' event. * Depends from configuration. * @@ -639,6 +646,7 @@ public function getProductUrl($item, $additional = []) $product = $item->getProduct(); } $buyRequest = $item->getBuyRequest(); + $fragment = []; if (is_object($buyRequest)) { $config = $buyRequest->getSuperProductConfig(); if ($config && !empty($config['product_id'])) { @@ -648,7 +656,16 @@ public function getProductUrl($item, $additional = []) $this->_storeManager->getStore()->getStoreId() ); } + $fragment = $buyRequest->getSuperAttribute() ?? []; + if ($buyRequest->getQty()) { + $additional['_query']['qty'] = $buyRequest->getQty(); + } } - return $product->getUrlModel()->getUrl($product, $additional); + $url = $product->getUrlModel()->getUrl($product, $additional); + if ($fragment) { + $url .= '#' . http_build_query($fragment); + } + + return $url; } } diff --git a/app/code/Magento/Wishlist/Model/Item/Option.php b/app/code/Magento/Wishlist/Model/Item/Option.php index 61acfcb666531..d3205b676b08c 100644 --- a/app/code/Magento/Wishlist/Model/Item/Option.php +++ b/app/code/Magento/Wishlist/Model/Item/Option.php @@ -6,13 +6,15 @@ namespace Magento\Wishlist\Model\Item; use Magento\Catalog\Model\Product; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\Exception\NoSuchEntityException; use Magento\Wishlist\Model\Item; use Magento\Catalog\Api\ProductRepositoryInterface; /** * Item option model - * @method int getProductId() * + * @method int getProductId() * @api * @since 100.0.2 */ @@ -34,6 +36,11 @@ class Option extends \Magento\Framework\Model\AbstractModel implements */ protected $productRepository; + /** + * @var \Psr\Log\LoggerInterface + */ + private $logger; + /** * @param \Magento\Framework\Model\Context $context * @param \Magento\Framework\Registry $registry @@ -41,6 +48,7 @@ class Option extends \Magento\Framework\Model\AbstractModel implements * @param \Magento\Framework\Model\ResourceModel\AbstractResource|null $resource * @param \Magento\Framework\Data\Collection\AbstractDb|null $resourceCollection * @param array $data + * @param \Psr\Log\LoggerInterface|null $logger */ public function __construct( \Magento\Framework\Model\Context $context, @@ -48,10 +56,12 @@ public function __construct( ProductRepositoryInterface $productRepository, \Magento\Framework\Model\ResourceModel\AbstractResource $resource = null, \Magento\Framework\Data\Collection\AbstractDb $resourceCollection = null, - array $data = [] + array $data = [], + \Psr\Log\LoggerInterface $logger = null ) { parent::__construct($context, $registry, $resource, $resourceCollection, $data); $this->productRepository = $productRepository; + $this->logger = $logger ?? ObjectManager::getInstance()->get(\Psr\Log\LoggerInterface::class); } /** @@ -123,7 +133,11 @@ public function getProduct() { //In some cases product_id is present instead product instance if (null === $this->_product && $this->getProductId()) { - $this->_product = $this->productRepository->getById($this->getProductId()); + try { + $this->_product = $this->productRepository->getById($this->getProductId()); + } catch (NoSuchEntityException $exception) { + $this->logger->error($exception); + } } return $this->_product; } diff --git a/app/code/Magento/Wishlist/Model/ResourceModel/Item/Collection/Grid.php b/app/code/Magento/Wishlist/Model/ResourceModel/Item/Collection/Grid.php index 36e70e9bdb1af..fb6b647811abb 100644 --- a/app/code/Magento/Wishlist/Model/ResourceModel/Item/Collection/Grid.php +++ b/app/code/Magento/Wishlist/Model/ResourceModel/Item/Collection/Grid.php @@ -4,14 +4,13 @@ * See COPYING.txt for license details. */ -/** - * Wishlist item collection grouped by customer id - */ namespace Magento\Wishlist\Model\ResourceModel\Item\Collection; use Magento\Customer\Controller\RegistryConstants as RegistryConstants; /** + * Wishlist item collection grouped by customer id + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class Grid extends \Magento\Wishlist\Model\ResourceModel\Item\Collection @@ -98,7 +97,12 @@ protected function _initSelect() parent::_initSelect(); $this->addCustomerIdFilter( $this->_registryManager->registry(RegistryConstants::CURRENT_CUSTOMER_ID) - )->resetSortOrder()->addDaysInWishlist()->addStoreData(); + ) + ->resetSortOrder() + ->addDaysInWishlist() + ->addStoreData() + ->setVisibilityFilter() + ->setInStockFilter(); return $this; } @@ -122,6 +126,18 @@ public function setOrder($field, $direction = self::SORT_ORDER_DESC) } } + /** + * Add quantity to filter + * + * @param string $field + * @param array $condition + * @return \Magento\Wishlist\Model\ResourceModel\Item\Collection + */ + private function addQtyFilter(string $field, array $condition) + { + return parent::addFieldToFilter('main_table.' . $field, $condition); + } + /** * Add field filter to collection * @@ -146,6 +162,11 @@ public function addFieldToFilter($field, $condition = null) if (!isset($condition['datetime'])) { return $this->addDaysFilter($condition); } + break; + case 'qty': + if (isset($condition['from']) || isset($condition['to'])) { + return $this->addQtyFilter($field, $condition); + } } return parent::addFieldToFilter($field, $condition); } diff --git a/app/code/Magento/Wishlist/Model/WishlistCleaner.php b/app/code/Magento/Wishlist/Model/WishlistCleaner.php new file mode 100644 index 0000000000000..0189ad32d7655 --- /dev/null +++ b/app/code/Magento/Wishlist/Model/WishlistCleaner.php @@ -0,0 +1,67 @@ +itemOptionResourceModel = $itemOptionResourceModel; + $this->itemResourceModel = $itemResourceModel; + } + + /** + * Deletes all wishlist items related the specified product + * + * @param ProductInterface $product + * @throws LocalizedException + */ + public function execute(ProductInterface $product) + { + $connection = $this->itemResourceModel->getConnection(); + + $selectQuery = $connection + ->select() + ->from(['w_item' => $this->itemResourceModel->getMainTable()]) + ->join( + ['w_item_option' => $this->itemOptionResourceModel->getMainTable()], + 'w_item.wishlist_item_id = w_item_option.wishlist_item_id' + ) + ->where('w_item_option.product_id = ?', $product->getId()); + + $connection->query($selectQuery->deleteFromSelect('w_item')); + } +} diff --git a/app/code/Magento/Wishlist/Plugin/Helper/Product/View.php b/app/code/Magento/Wishlist/Plugin/Helper/Product/View.php new file mode 100644 index 0000000000000..58deb33a788ff --- /dev/null +++ b/app/code/Magento/Wishlist/Plugin/Helper/Product/View.php @@ -0,0 +1,51 @@ +getRequest()->getParam('qty'); + if ($qty) { + if (null === $params || !$params instanceof DataObject) { + $params = new DataObject((array) $params); + } + if (!$params->getBuyRequest()) { + $params->setBuyRequest(new DataObject([])); + } + $params->getBuyRequest()->setQty($qty); + } + + return [$resultPage, $productId, $controller, $params]; + } +} diff --git a/app/code/Magento/Wishlist/Plugin/Model/ResourceModel/Product.php b/app/code/Magento/Wishlist/Plugin/Model/ResourceModel/Product.php new file mode 100644 index 0000000000000..e5492cd686356 --- /dev/null +++ b/app/code/Magento/Wishlist/Plugin/Model/ResourceModel/Product.php @@ -0,0 +1,52 @@ +wishlistCleaner = $wishlistCleaner; + } + + /** + * Cleans up wishlist items referencing the product being deleted + * + * @param ProductResourceModel $productResourceModel + * @param mixed $product + * @return void + * @throws LocalizedException + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function beforeDelete( + ProductResourceModel $productResourceModel, + $product + ) { + if ($product instanceof ProductInterface) { + $this->wishlistCleaner->execute($product); + } + } +} diff --git a/app/code/Magento/Wishlist/Pricing/ConfiguredPrice/ConfigurableProduct.php b/app/code/Magento/Wishlist/Pricing/ConfiguredPrice/ConfigurableProduct.php index e1fafe35e43ff..7a16b83eaf51d 100644 --- a/app/code/Magento/Wishlist/Pricing/ConfiguredPrice/ConfigurableProduct.php +++ b/app/code/Magento/Wishlist/Pricing/ConfiguredPrice/ConfigurableProduct.php @@ -31,11 +31,11 @@ class ConfigurableProduct extends AbstractPrice */ public function getConfiguredAmount(): \Magento\Framework\Pricing\Amount\AmountInterface { - /** @var \Magento\Wishlist\Model\Item\Option $customOption */ - $customOption = $this->getProduct()->getCustomOption('simple_product'); - $product = $customOption ? $customOption->getProduct() : $this->getProduct(); - - return $product->getPriceInfo()->getPrice(ConfiguredPriceInterface::CONFIGURED_PRICE_CODE)->getAmount(); + return $this + ->getProduct() + ->getPriceInfo() + ->getPrice(ConfiguredPriceInterface::CONFIGURED_PRICE_CODE) + ->getAmount(); } /** @@ -45,11 +45,11 @@ public function getConfiguredAmount(): \Magento\Framework\Pricing\Amount\AmountI */ public function getConfiguredRegularAmount(): \Magento\Framework\Pricing\Amount\AmountInterface { - /** @var \Magento\Wishlist\Model\Item\Option $customOption */ - $customOption = $this->getProduct()->getCustomOption('simple_product'); - $product = $customOption ? $customOption->getProduct() : $this->getProduct(); - - return $product->getPriceInfo()->getPrice(ConfiguredPriceInterface::CONFIGURED_REGULAR_PRICE_CODE)->getAmount(); + return $this + ->getProduct() + ->getPriceInfo() + ->getPrice(ConfiguredPriceInterface::CONFIGURED_REGULAR_PRICE_CODE) + ->getAmount(); } /** @@ -57,10 +57,7 @@ public function getConfiguredRegularAmount(): \Magento\Framework\Pricing\Amount\ */ public function getValue() { - /** @var \Magento\Wishlist\Model\Item\Option $customOption */ - $customOption = $this->getProduct()->getCustomOption('simple_product'); - $product = $customOption ? $customOption->getProduct() : $this->getProduct(); - $price = $product->getPriceInfo()->getPrice(self::PRICE_CODE)->getValue(); + $price = $this->getProduct()->getPriceInfo()->getPrice(self::PRICE_CODE)->getValue(); return max(0, $price); } @@ -73,4 +70,17 @@ public function setItem(ItemInterface $item) $this->item = $item; return $this; } + + /** + * @inheritDoc + */ + public function getProduct() + { + /** @var \Magento\Catalog\Model\Product $product */ + $product = parent::getProduct(); + /** @var \Magento\Wishlist\Model\Item\Option $customOption */ + $customOption = $product->getCustomOption('simple_product'); + + return $customOption ? ($customOption->getProduct() ?? $product) : $product; + } } diff --git a/app/code/Magento/Wishlist/Test/Mftf/ActionGroup/setEmailTextLengthLimitActionGroup.xml b/app/code/Magento/Wishlist/Test/Mftf/ActionGroup/setEmailTextLengthLimitActionGroup.xml new file mode 100755 index 0000000000000..685e1ab9f8714 --- /dev/null +++ b/app/code/Magento/Wishlist/Test/Mftf/ActionGroup/setEmailTextLengthLimitActionGroup.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/code/Magento/Wishlist/Test/Mftf/Data/WishlistData.xml b/app/code/Magento/Wishlist/Test/Mftf/Data/WishlistData.xml old mode 100644 new mode 100755 index a8220ad0cfca3..4a25a8d449dd3 --- a/app/code/Magento/Wishlist/Test/Mftf/Data/WishlistData.xml +++ b/app/code/Magento/Wishlist/Test/Mftf/Data/WishlistData.xml @@ -14,5 +14,8 @@ JohnDoe123456789@example.com,JohnDoe987654321@example.com,JohnDoe123456abc@example.com Sharing message. + 255 + 1 + 10000 diff --git a/app/code/Magento/Wishlist/Test/Mftf/Section/WishListShareOptionsSection.xml b/app/code/Magento/Wishlist/Test/Mftf/Section/WishListShareOptionsSection.xml new file mode 100755 index 0000000000000..b69740572cea6 --- /dev/null +++ b/app/code/Magento/Wishlist/Test/Mftf/Section/WishListShareOptionsSection.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + diff --git a/app/code/Magento/Wishlist/Test/Mftf/Test/AdminCustomerWishListShareOptionsInputValidationTest.xml b/app/code/Magento/Wishlist/Test/Mftf/Test/AdminCustomerWishListShareOptionsInputValidationTest.xml new file mode 100755 index 0000000000000..da51bdf917e37 --- /dev/null +++ b/app/code/Magento/Wishlist/Test/Mftf/Test/AdminCustomerWishListShareOptionsInputValidationTest.xml @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {{Wishlist.min_email_text_length_limit}} + minimumWishListTextLengthLimit + + + + + + + + + {{Wishlist.max_email_text_length_limit}} + maximumWishListTextLengthLimit + + + + + + + + + The value is not within the specified range. + enterWishListTextLengthLimitLowerThanMinimum + + + + + + + + + The value is not within the specified range. + enterWishListTextLengthLimitHigherThanMaximum + + + diff --git a/app/code/Magento/Wishlist/Test/Unit/Helper/DataTest.php b/app/code/Magento/Wishlist/Test/Unit/Helper/DataTest.php index 1769306172aab..263f3c5d1688e 100644 --- a/app/code/Magento/Wishlist/Test/Unit/Helper/DataTest.php +++ b/app/code/Magento/Wishlist/Test/Unit/Helper/DataTest.php @@ -18,6 +18,7 @@ use Magento\Wishlist\Controller\WishlistProviderInterface; use Magento\Wishlist\Model\Item as WishlistItem; use Magento\Wishlist\Model\Wishlist; +use Magento\Customer\Model\Session; /** * @SuppressWarnings(PHPMD.CouplingBetweenObjects) @@ -63,6 +64,9 @@ class DataTest extends \PHPUnit\Framework\TestCase /** @var Context |\PHPUnit_Framework_MockObject_MockObject */ protected $context; + /** @var Session |\PHPUnit_Framework_MockObject_MockObject */ + protected $customerSession; + /** * Set up mock objects for tested class * @@ -121,12 +125,13 @@ protected function setUp() $this->wishlistItem = $this->getMockBuilder(\Magento\Wishlist\Model\Item::class) ->disableOriginalConstructor() - ->setMethods([ - 'getProduct', - 'getWishlistItemId', - 'getQty', - ]) - ->getMock(); + ->setMethods( + [ + 'getProduct', + 'getWishlistItemId', + 'getQty', + ] + )->getMock(); $this->wishlist = $this->getMockBuilder(\Magento\Wishlist\Model\Wishlist::class) ->disableOriginalConstructor() @@ -136,11 +141,16 @@ protected function setUp() ->disableOriginalConstructor() ->getMock(); + $this->customerSession = $this->getMockBuilder(\Magento\Customer\Model\Session::class) + ->disableOriginalConstructor() + ->getMock(); + $objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); $this->model = $objectManager->getObject( \Magento\Wishlist\Helper\Data::class, [ 'context' => $this->context, + 'customerSession' => $this->customerSession, 'storeManager' => $this->storeManager, 'wishlistProvider' => $this->wishlistProvider, 'coreRegistry' => $this->coreRegistry, @@ -431,4 +441,20 @@ public function testGetSharedAddAllToCartUrl() $this->assertEquals($url, $this->model->getSharedAddAllToCartUrl()); } + + public function testGetRssUrlWithCustomerNotLogin() + { + $url = 'result url'; + + $this->customerSession->expects($this->once()) + ->method('isLoggedIn') + ->willReturn(false); + + $this->urlBuilder->expects($this->once()) + ->method('getUrl') + ->with('wishlist/index/rss', []) + ->willReturn($url); + + $this->assertEquals($url, $this->model->getRssUrl()); + } } diff --git a/app/code/Magento/Wishlist/Test/Unit/Model/WishlistCleanerTest.php b/app/code/Magento/Wishlist/Test/Unit/Model/WishlistCleanerTest.php new file mode 100644 index 0000000000000..7eca21f9cee08 --- /dev/null +++ b/app/code/Magento/Wishlist/Test/Unit/Model/WishlistCleanerTest.php @@ -0,0 +1,87 @@ +itemOptionResourceModel = $this->createMock(ItemOptionResourceModel::class); + $this->itemResourceModel = $this->createMock(ItemResourceModel::class); + $this->model = new WishlistCleaner($this->itemOptionResourceModel, $this->itemResourceModel); + } + + /** + * Asserts that wishlist items related to a specific product are deleted + */ + public function testExecute() + { + $productId = 1; + $itemTable = 'table_item'; + $itemOptionTable = 'table_item_option'; + $product = $this->createMock(ProductInterface::class); + $product->expects($this->once())->method('getId')->willReturn($productId); + $connection = $this->createMock(AdapterInterface::class); + $this->itemResourceModel->expects($this->once())->method('getConnection')->willReturn($connection); + $this->itemResourceModel->expects($this->once())->method('getMainTable')->willReturn($itemTable); + $this->itemOptionResourceModel->expects($this->once())->method('getMainTable')->willReturn($itemOptionTable); + $select = $this->createMock(Select::class); + $connection->expects($this->once())->method('query')->with($select); + $connection->expects($this->once()) + ->method('select') + ->willReturn($select); + $select->expects($this->once()) + ->method('from') + ->with(['w_item' => $itemTable]) + ->willReturnSelf(); + $select->expects($this->once()) + ->method('join') + ->with(['w_item_option' => $itemOptionTable], 'w_item.wishlist_item_id = w_item_option.wishlist_item_id') + ->willReturnSelf(); + $select->expects($this->once()) + ->method('where') + ->with('w_item_option.product_id = ?', $productId) + ->willReturnSelf(); + $select->expects($this->once()) + ->method('deleteFromSelect') + ->with('w_item') + ->willReturnSelf(); + + $this->model->execute($product); + } +} diff --git a/app/code/Magento/Wishlist/Test/Unit/Plugin/Model/ResourceModel/ProductTest.php b/app/code/Magento/Wishlist/Test/Unit/Plugin/Model/ResourceModel/ProductTest.php new file mode 100644 index 0000000000000..ab999902f61bd --- /dev/null +++ b/app/code/Magento/Wishlist/Test/Unit/Plugin/Model/ResourceModel/ProductTest.php @@ -0,0 +1,53 @@ +wishlistCleaner = $this->createMock(WishlistCleaner::class); + $this->model = new Plugin($this->wishlistCleaner); + } + + /** + * Asserts that item option cleaner is executed when product is deleted + * + * @return void + */ + public function testExecute() + { + $product = $this->createMock(ProductInterface::class); + $productResourceModel = $this->createMock(ProductResourceModel::class); + $this->wishlistCleaner->expects($this->once())->method('execute')->with($product); + $this->model->beforeDelete($productResourceModel, $product); + } +} diff --git a/app/code/Magento/Wishlist/etc/adminhtml/di.xml b/app/code/Magento/Wishlist/etc/adminhtml/di.xml index 2e222f8193840..124b8c17c3f36 100644 --- a/app/code/Magento/Wishlist/etc/adminhtml/di.xml +++ b/app/code/Magento/Wishlist/etc/adminhtml/di.xml @@ -21,4 +21,7 @@ Magento\Wishlist\Model\Session\Storage + + + diff --git a/app/code/Magento/Wishlist/etc/adminhtml/system.xml b/app/code/Magento/Wishlist/etc/adminhtml/system.xml index 1e26a1195a7fe..e61c07abca993 100644 --- a/app/code/Magento/Wishlist/etc/adminhtml/system.xml +++ b/app/code/Magento/Wishlist/etc/adminhtml/system.xml @@ -29,7 +29,7 @@ Email Text Length Limit - 255 by default + 255 by default. Max - 10000 validate-digits validate-digits-range digits-range-1-10000 diff --git a/app/code/Magento/Wishlist/etc/frontend/di.xml b/app/code/Magento/Wishlist/etc/frontend/di.xml index f28e85fff0a15..c1c03802ba904 100644 --- a/app/code/Magento/Wishlist/etc/frontend/di.xml +++ b/app/code/Magento/Wishlist/etc/frontend/di.xml @@ -53,4 +53,7 @@ + + + diff --git a/app/code/Magento/Wishlist/i18n/en_US.csv b/app/code/Magento/Wishlist/i18n/en_US.csv index a9acce448c80c..7bcbd0751b7e9 100644 --- a/app/code/Magento/Wishlist/i18n/en_US.csv +++ b/app/code/Magento/Wishlist/i18n/en_US.csv @@ -101,7 +101,7 @@ Back,Back "Max Emails Allowed to be Sent","Max Emails Allowed to be Sent" "10 by default. Max - 10000","10 by default. Max - 10000" "Email Text Length Limit","Email Text Length Limit" -"255 by default","255 by default" +"255 by default. Max - 10000","255 by default. Max - 10000" "General Options","General Options" Enabled,Enabled "My Wish List Link","My Wish List Link" diff --git a/app/design/adminhtml/Magento/backend/Magento_Backend/web/css/source/module/main/_store-scope.less b/app/design/adminhtml/Magento/backend/Magento_Backend/web/css/source/module/main/_store-scope.less index cd73d4955d176..7c9f3975ef1b3 100644 --- a/app/design/adminhtml/Magento/backend/Magento_Backend/web/css/source/module/main/_store-scope.less +++ b/app/design/adminhtml/Magento/backend/Magento_Backend/web/css/source/module/main/_store-scope.less @@ -42,3 +42,11 @@ } } } + +.no-margin-top-tooltip { + .admin__legend { + .admin__field-tooltip { + margin-top: 0; + } + } +} diff --git a/app/design/adminhtml/Magento/backend/Magento_Banner/web/css/source/_module.less b/app/design/adminhtml/Magento/backend/Magento_Banner/web/css/source/_module.less index 2b0dade8a7386..d9e2cfdd66bf7 100644 --- a/app/design/adminhtml/Magento/backend/Magento_Banner/web/css/source/_module.less +++ b/app/design/adminhtml/Magento/backend/Magento_Banner/web/css/source/_module.less @@ -22,3 +22,17 @@ input[type='checkbox'].banner-content-checkbox { } } } + +.adminhtml-widget_instance-edit, +.adminhtml-banner-edit { + .admin__fieldset { + .admin__field-control { + .data-grid-actions-cell, + .data-grid-checkbox-cell-inner { + input[type='checkbox'] { + margin-top: 0; + } + } + } + } +} diff --git a/app/design/adminhtml/Magento/backend/etc/view.xml b/app/design/adminhtml/Magento/backend/etc/view.xml index 18c2d8f1b1722..621c18fc97cc8 100644 --- a/app/design/adminhtml/Magento/backend/etc/view.xml +++ b/app/design/adminhtml/Magento/backend/etc/view.xml @@ -24,7 +24,6 @@ Lib::mage/captcha.js - Lib::mage/captcha.min.js Lib::mage/common.js Lib::mage/cookies.js Lib::mage/dataPost.js @@ -46,7 +45,6 @@ Lib::mage/translate-inline-vde.js Lib::mage/webapi.js Lib::mage/zoom.js - Lib::mage/validation/dob-rule.js Lib::mage/validation/validation.js Lib::mage/adminhtml/varienLoader.js Lib::mage/adminhtml/tools.js @@ -57,12 +55,11 @@ Lib::jquery/jquery.parsequery.js Lib::jquery/jquery.mobile.custom.js Lib::jquery/jquery-ui.js - Lib::jquery/jquery-ui.min.js Lib::matchMedia.js Lib::requirejs/require.js Lib::requirejs/text.js - Lib::date-format-normalizer.js Lib::varien/js.js + Magento_Tinymce3::tiny_mce Lib::css Lib::lib Lib::prototype @@ -72,10 +69,5 @@ Lib::fotorama Lib::magnifier Lib::tiny_mce - Lib::tiny_mce/classes - Lib::tiny_mce/langs - Lib::tiny_mce/plugins - Lib::tiny_mce/themes - Lib::tiny_mce/utils diff --git a/app/design/adminhtml/Magento/backend/web/css/source/components/_modals_extend.less b/app/design/adminhtml/Magento/backend/web/css/source/components/_modals_extend.less index efc747e4d714a..72e9088f7cd34 100644 --- a/app/design/adminhtml/Magento/backend/web/css/source/components/_modals_extend.less +++ b/app/design/adminhtml/Magento/backend/web/css/source/components/_modals_extend.less @@ -33,6 +33,10 @@ // +.modals-overlay { + &:extend(.abs-modal-overlay all); +} + .modal-popup, .modal-slide { .action-close { diff --git a/app/design/adminhtml/Magento/backend/web/css/source/forms/_controls.less b/app/design/adminhtml/Magento/backend/web/css/source/forms/_controls.less index 4c32405f2c995..c6f39e8e8840d 100644 --- a/app/design/adminhtml/Magento/backend/web/css/source/forms/_controls.less +++ b/app/design/adminhtml/Magento/backend/web/css/source/forms/_controls.less @@ -175,6 +175,7 @@ option:empty { line-height: @field-control__line-height; padding-bottom: @field-control__padding-bottom; padding-top: @field-control__padding-top; + margin-left: @action__outer-indent; + [class*='admin__control-'] { margin-left: @action__outer-indent; diff --git a/app/design/adminhtml/Magento/backend/web/css/styles-old.less b/app/design/adminhtml/Magento/backend/web/css/styles-old.less index 53af8933343f1..44fca79c31be5 100644 --- a/app/design/adminhtml/Magento/backend/web/css/styles-old.less +++ b/app/design/adminhtml/Magento/backend/web/css/styles-old.less @@ -4060,6 +4060,16 @@ } } +.newsletter-template-preview { + height: 100%; + .cms-revision-preview { + height: 100%; + .preview_iframe { + height: calc(~'100% - 50px'); + } + } +} + .admin__scope-old { .buttons-set { margin: 0 0 15px; diff --git a/app/design/adminhtml/Magento/backend/web/js/theme.js b/app/design/adminhtml/Magento/backend/web/js/theme.js index 8e3b89dcf7e4e..39b364ea8553f 100644 --- a/app/design/adminhtml/Magento/backend/web/js/theme.js +++ b/app/design/adminhtml/Magento/backend/web/js/theme.js @@ -267,17 +267,18 @@ define('globalNavigation', [ if (subMenu.length) { e.preventDefault(); } - - menuItem.addClass('_show') - .siblings(menuItemSelector) - .removeClass('_show'); - - subMenu.attr('aria-expanded', 'true'); - closeBtn.on('click', close); - this.overlay.show(0).on('click', close); - this.menuLinks.last().off('blur'); + if ($(menuItem).hasClass('_show')) { + closeBtn.trigger('click'); + } else { + menuItem.addClass('_show') + .siblings(menuItemSelector) + .removeClass('_show'); + subMenu.attr('aria-expanded', 'true'); + this.overlay.show(0).on('click', close); + this.menuLinks.last().off('blur'); + } }, /** diff --git a/app/design/frontend/Magento/blank/Magento_AdvancedCheckout/web/css/source/_module.less b/app/design/frontend/Magento/blank/Magento_AdvancedCheckout/web/css/source/_module.less index 1db5dceea80aa..5fe3df4b0ccbe 100644 --- a/app/design/frontend/Magento/blank/Magento_AdvancedCheckout/web/css/source/_module.less +++ b/app/design/frontend/Magento/blank/Magento_AdvancedCheckout/web/css/source/_module.less @@ -53,12 +53,6 @@ } .block-content { - &:extend(.abs-add-clearfix-desktop all); - - .box { - &:extend(.abs-blocks-2columns all); - } - .actions-toolbar { clear: both; .lib-actions-toolbar( @@ -167,4 +161,16 @@ &:extend(.abs-add-clearfix-desktop all); } } + + .column { + .block-addbysku { + .block-content { + &:extend(.abs-add-clearfix-desktop all); + + .box { + &:extend(.abs-blocks-2columns all); + } + } + } + } } diff --git a/app/design/frontend/Magento/blank/Magento_Checkout/web/css/source/module/checkout/_estimated-total.less b/app/design/frontend/Magento/blank/Magento_Checkout/web/css/source/module/checkout/_estimated-total.less index 85c65bd0ff2d7..3caa80b7a28b6 100644 --- a/app/design/frontend/Magento/blank/Magento_Checkout/web/css/source/module/checkout/_estimated-total.less +++ b/app/design/frontend/Magento/blank/Magento_Checkout/web/css/source/module/checkout/_estimated-total.less @@ -15,7 +15,6 @@ .opc-estimated-wrapper { &:extend(.abs-add-clearfix all); - &:extend(.abs-no-display-desktop all); .lib-css(border-bottom, @border-width__base solid @color-gray80); margin: 0 0 15px; padding: 18px 15px; @@ -37,7 +36,7 @@ &:before { .lib-css(color, @button__color); } - + &:hover:before { .lib-css(color, @button__hover__color); } @@ -53,6 +52,6 @@ .media-width(@extremum, @break) when (@extremum = 'min') and (@break = @screen__m) { .opc-estimated-wrapper { - display: none; + &:extend(.abs-no-display-desktop all); } } diff --git a/app/design/frontend/Magento/blank/Magento_GiftMessage/web/css/source/_module.less b/app/design/frontend/Magento/blank/Magento_GiftMessage/web/css/source/_module.less index 70af2cbf611cc..6dfe5ef987aa5 100644 --- a/app/design/frontend/Magento/blank/Magento_GiftMessage/web/css/source/_module.less +++ b/app/design/frontend/Magento/blank/Magento_GiftMessage/web/css/source/_module.less @@ -18,7 +18,6 @@ & when (@media-common = true) { .gift-message { .field { - &:extend(.abs-clearfix all); margin-bottom: @indent__base; .label { diff --git a/app/design/frontend/Magento/blank/etc/view.xml b/app/design/frontend/Magento/blank/etc/view.xml index e742ce0a21cd1..5884699af15cd 100644 --- a/app/design/frontend/Magento/blank/etc/view.xml +++ b/app/design/frontend/Magento/blank/etc/view.xml @@ -262,12 +262,10 @@ Lib::jquery/jquery.min.js Lib::jquery/jquery-ui-1.9.2.js Lib::jquery/jquery.details.js - Lib::jquery/jquery.details.min.js Lib::jquery/jquery.hoverIntent.js Lib::jquery/colorpicker/js/colorpicker.js Lib::requirejs/require.js Lib::requirejs/text.js - Lib::date-format-normalizer.js Lib::legacy-build.min.js Lib::mage/captcha.js Lib::mage/dropdown_old.js @@ -292,6 +290,7 @@ Magento_Ui::templates/grid Magento_Ui::templates/dynamic-rows Magento_Swagger::swagger-ui + Magento_Tinymce3::tiny_mce Lib::modernizr Lib::tiny_mce Lib::varien diff --git a/app/design/frontend/Magento/blank/web/css/source/_extends.less b/app/design/frontend/Magento/blank/web/css/source/_extends.less index 74dfd48d87a87..5bdaa4c3c35a3 100644 --- a/app/design/frontend/Magento/blank/web/css/source/_extends.less +++ b/app/design/frontend/Magento/blank/web/css/source/_extends.less @@ -179,7 +179,7 @@ & when (@media-common = true) { .abs-block-title { margin-bottom: 15px; - + > strong { .lib-heading(h3); } @@ -194,7 +194,7 @@ .abs-account-blocks { .block-title { &:extend(.abs-block-title all); - + > .action { margin-left: 15px; } @@ -849,6 +849,51 @@ } } +// +// Account pages: title +// --------------------------------------------- + +& when (@media-common = true) { + .abs-account-title { + > strong, + > span { + .lib-font-size(22); + .lib-css(font-weight, @font-weight__light); + } + + .lib-css(border-bottom, 1px solid @border-color__base); + .lib-css(margin-bottom, @indent__m); + .lib-css(padding-bottom, @indent__s); + } +} + +// +// Ratings: vertical alignment +// --------------------------------------------- + +& when (@media-common = true) { + .abs-rating-summary { + .rating { + &-summary { + display: table-row; + } + + &-label { + display: table-cell; + padding-bottom: @indent__xs; + padding-right: @indent__m; + padding-top: 1px; + vertical-align: top; + } + + &-result { + display: table-cell; + vertical-align: top; + } + } + } +} + // // Add colon // --------------------------------------------- @@ -1245,7 +1290,7 @@ } // -// Shopping cart sidebar and checkout sidebar totals +// Mini Cart and checkout sidebar totals // --------------------------------------------- & when (@media-common = true) { diff --git a/app/design/frontend/Magento/luma/etc/view.xml b/app/design/frontend/Magento/luma/etc/view.xml index 7aa2e51481bd9..a2802b7e374f3 100644 --- a/app/design/frontend/Magento/luma/etc/view.xml +++ b/app/design/frontend/Magento/luma/etc/view.xml @@ -273,12 +273,10 @@ Lib::jquery/jquery.min.js Lib::jquery/jquery-ui-1.9.2.js Lib::jquery/jquery.details.js - Lib::jquery/jquery.details.min.js Lib::jquery/jquery.hoverIntent.js Lib::jquery/colorpicker/js/colorpicker.js Lib::requirejs/require.js Lib::requirejs/text.js - Lib::date-format-normalizer.js Lib::legacy-build.min.js Lib::mage/captcha.js Lib::mage/dropdown_old.js @@ -303,6 +301,7 @@ Magento_Ui::templates/grid Magento_Ui::templates/dynamic-rows Magento_Swagger::swagger-ui + Magento_Tinymce3::tiny_mce Lib::modernizr Lib::tiny_mce Lib::varien diff --git a/app/design/frontend/Magento/luma/web/css/source/_extends.less b/app/design/frontend/Magento/luma/web/css/source/_extends.less index 66732d7f68eec..ce86b690f6252 100644 --- a/app/design/frontend/Magento/luma/web/css/source/_extends.less +++ b/app/design/frontend/Magento/luma/web/css/source/_extends.less @@ -1212,7 +1212,7 @@ } // -// Forms: margin-bottom for small forms +// Ratings: vertical alignment // --------------------------------------------- & when (@media-common = true) { @@ -1713,7 +1713,7 @@ } // -// Shopping cart sidebar and checkout sidebar totals +// Mini Cart and checkout sidebar totals // --------------------------------------------- & when (@media-common = true) { diff --git a/app/etc/di.xml b/app/etc/di.xml index 1a74fd9d7f840..882d1be623988 100755 --- a/app/etc/di.xml +++ b/app/etc/di.xml @@ -47,6 +47,7 @@ + system/currency/installed @@ -1777,4 +1778,7 @@ type="Magento\Framework\Mail\MimeMessage" /> + + + diff --git a/app/etc/graphql/di.xml b/app/etc/graphql/di.xml new file mode 100644 index 0000000000000..aba60d00080ff --- /dev/null +++ b/app/etc/graphql/di.xml @@ -0,0 +1,10 @@ + + + + + diff --git a/bin-magento.yml b/bin-magento.yml deleted file mode 100644 index 99ce03c1c0316..0000000000000 --- a/bin-magento.yml +++ /dev/null @@ -1,8832 +0,0 @@ ---- -application: - name: Magento CLI - version: 2.3.2 -commands: -- name: help - usage: - - help [--format FORMAT] [--raw] [--] [] - description: Displays help for a command - help: |- - The help command displays help for a given command: - - php bin/magento help list - - You can also output the help in other formats by using the --format option: - - php bin/magento help --format=xml list - - To display the list of available commands, please use the list command. - definition: - arguments: - command_name: - name: command_name - is_required: false - is_array: false - description: The command name - default: help - options: - format: - name: "--format" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: The output format (txt, xml, json, or md) - default: txt - raw: - name: "--raw" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: To output raw command help - default: false - help: - name: "--help" - shortcut: "-h" - accept_value: false - is_value_required: false - is_multiple: false - description: Display this help message - default: false - quiet: - name: "--quiet" - shortcut: "-q" - accept_value: false - is_value_required: false - is_multiple: false - description: Do not output any message - default: false - verbose: - name: "--verbose" - shortcut: "-v|-vv|-vvv" - accept_value: false - is_value_required: false - is_multiple: false - description: 'Increase the verbosity of messages: 1 for normal output, 2 for - more verbose output and 3 for debug' - default: false - version: - name: "--version" - shortcut: "-V" - accept_value: false - is_value_required: false - is_multiple: false - description: Display this application version - default: false - ansi: - name: "--ansi" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Force ANSI output - default: false - no-ansi: - name: "--no-ansi" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Disable ANSI output - default: false - no-interaction: - name: "--no-interaction" - shortcut: "-n" - accept_value: false - is_value_required: false - is_multiple: false - description: Do not ask any interactive question - default: false - hidden: false -- name: list - usage: - - list [--raw] [--format FORMAT] [--] [] - description: Lists commands - help: |- - The list command lists all commands: - - php bin/magento list - - You can also display the commands for a specific namespace: - - php bin/magento list test - - You can also output the information in other formats by using the --format option: - - php bin/magento list --format=xml - - It's also possible to get raw list of commands (useful for embedding command runner): - - php bin/magento list --raw - definition: - arguments: - namespace: - name: namespace - is_required: false - is_array: false - description: The namespace name - default: - options: - raw: - name: "--raw" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: To output raw command list - default: false - format: - name: "--format" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: The output format (txt, xml, json, or md) - default: txt - hidden: false -- name: admin:user:create - usage: - - admin:user:create [--admin-user ADMIN-USER] [--admin-password ADMIN-PASSWORD] - [--admin-email ADMIN-EMAIL] [--admin-firstname ADMIN-FIRSTNAME] [--admin-lastname - ADMIN-LASTNAME] [--magento-init-params MAGENTO-INIT-PARAMS] - description: Creates an administrator - help: Creates an administrator - definition: - arguments: [] - options: - admin-user: - name: "--admin-user" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: "(Required) Admin user" - default: - admin-password: - name: "--admin-password" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: "(Required) Admin password" - default: - admin-email: - name: "--admin-email" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: "(Required) Admin email" - default: - admin-firstname: - name: "--admin-firstname" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: "(Required) Admin first name" - default: - admin-lastname: - name: "--admin-lastname" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: "(Required) Admin last name" - default: - magento-init-params: - name: "--magento-init-params" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: 'Add to any command to customize Magento initialization parameters - For example: "MAGE_MODE=developer&MAGE_DIRS[base][path]=/var/www/example.com&MAGE_DIRS[cache][path]=/var/tmp/cache"' - default: - help: - name: "--help" - shortcut: "-h" - accept_value: false - is_value_required: false - is_multiple: false - description: Display this help message - default: false - quiet: - name: "--quiet" - shortcut: "-q" - accept_value: false - is_value_required: false - is_multiple: false - description: Do not output any message - default: false - verbose: - name: "--verbose" - shortcut: "-v|-vv|-vvv" - accept_value: false - is_value_required: false - is_multiple: false - description: 'Increase the verbosity of messages: 1 for normal output, 2 for - more verbose output and 3 for debug' - default: false - version: - name: "--version" - shortcut: "-V" - accept_value: false - is_value_required: false - is_multiple: false - description: Display this application version - default: false - ansi: - name: "--ansi" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Force ANSI output - default: false - no-ansi: - name: "--no-ansi" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Disable ANSI output - default: false - no-interaction: - name: "--no-interaction" - shortcut: "-n" - accept_value: false - is_value_required: false - is_multiple: false - description: Do not ask any interactive question - default: false - hidden: false -- name: admin:user:unlock - usage: - - admin:user:unlock - description: Unlock Admin Account - help: |- - This command unlocks an admin account by its username. - To unlock: - bin/magento admin:user:unlock username - definition: - arguments: - username: - name: username - is_required: true - is_array: false - description: The admin username to unlock - default: - options: - help: - name: "--help" - shortcut: "-h" - accept_value: false - is_value_required: false - is_multiple: false - description: Display this help message - default: false - quiet: - name: "--quiet" - shortcut: "-q" - accept_value: false - is_value_required: false - is_multiple: false - description: Do not output any message - default: false - verbose: - name: "--verbose" - shortcut: "-v|-vv|-vvv" - accept_value: false - is_value_required: false - is_multiple: false - description: 'Increase the verbosity of messages: 1 for normal output, 2 for - more verbose output and 3 for debug' - default: false - version: - name: "--version" - shortcut: "-V" - accept_value: false - is_value_required: false - is_multiple: false - description: Display this application version - default: false - ansi: - name: "--ansi" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Force ANSI output - default: false - no-ansi: - name: "--no-ansi" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Disable ANSI output - default: false - no-interaction: - name: "--no-interaction" - shortcut: "-n" - accept_value: false - is_value_required: false - is_multiple: false - description: Do not ask any interactive question - default: false - hidden: false -- name: app:config:dump - usage: - - app:config:dump [...] - description: Create dump of application - help: Create dump of application - definition: - arguments: - config-types: - name: config-types - is_required: false - is_array: true - description: Space-separated list of config types or omit to dump all [scopes, - system, themes, i18n] - default: [] - options: - help: - name: "--help" - shortcut: "-h" - accept_value: false - is_value_required: false - is_multiple: false - description: Display this help message - default: false - quiet: - name: "--quiet" - shortcut: "-q" - accept_value: false - is_value_required: false - is_multiple: false - description: Do not output any message - default: false - verbose: - name: "--verbose" - shortcut: "-v|-vv|-vvv" - accept_value: false - is_value_required: false - is_multiple: false - description: 'Increase the verbosity of messages: 1 for normal output, 2 for - more verbose output and 3 for debug' - default: false - version: - name: "--version" - shortcut: "-V" - accept_value: false - is_value_required: false - is_multiple: false - description: Display this application version - default: false - ansi: - name: "--ansi" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Force ANSI output - default: false - no-ansi: - name: "--no-ansi" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Disable ANSI output - default: false - no-interaction: - name: "--no-interaction" - shortcut: "-n" - accept_value: false - is_value_required: false - is_multiple: false - description: Do not ask any interactive question - default: false - hidden: false -- name: app:config:import - usage: - - app:config:import - description: Import data from shared configuration files to appropriate data storage - help: Import data from shared configuration files to appropriate data storage - definition: - arguments: [] - options: - help: - name: "--help" - shortcut: "-h" - accept_value: false - is_value_required: false - is_multiple: false - description: Display this help message - default: false - quiet: - name: "--quiet" - shortcut: "-q" - accept_value: false - is_value_required: false - is_multiple: false - description: Do not output any message - default: false - verbose: - name: "--verbose" - shortcut: "-v|-vv|-vvv" - accept_value: false - is_value_required: false - is_multiple: false - description: 'Increase the verbosity of messages: 1 for normal output, 2 for - more verbose output and 3 for debug' - default: false - version: - name: "--version" - shortcut: "-V" - accept_value: false - is_value_required: false - is_multiple: false - description: Display this application version - default: false - ansi: - name: "--ansi" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Force ANSI output - default: false - no-ansi: - name: "--no-ansi" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Disable ANSI output - default: false - no-interaction: - name: "--no-interaction" - shortcut: "-n" - accept_value: false - is_value_required: false - is_multiple: false - description: Do not ask any interactive question - default: false - hidden: false -- name: app:config:status - usage: - - app:config:status - description: Checks if config propagation requires update - help: Checks if config propagation requires update - definition: - arguments: [] - options: - help: - name: "--help" - shortcut: "-h" - accept_value: false - is_value_required: false - is_multiple: false - description: Display this help message - default: false - quiet: - name: "--quiet" - shortcut: "-q" - accept_value: false - is_value_required: false - is_multiple: false - description: Do not output any message - default: false - verbose: - name: "--verbose" - shortcut: "-v|-vv|-vvv" - accept_value: false - is_value_required: false - is_multiple: false - description: 'Increase the verbosity of messages: 1 for normal output, 2 for - more verbose output and 3 for debug' - default: false - version: - name: "--version" - shortcut: "-V" - accept_value: false - is_value_required: false - is_multiple: false - description: Display this application version - default: false - ansi: - name: "--ansi" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Force ANSI output - default: false - no-ansi: - name: "--no-ansi" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Disable ANSI output - default: false - no-interaction: - name: "--no-interaction" - shortcut: "-n" - accept_value: false - is_value_required: false - is_multiple: false - description: Do not ask any interactive question - default: false - hidden: false -- name: cache:clean - usage: - - cache:clean [--bootstrap BOOTSTRAP] [--] [...] - description: Cleans cache type(s) - help: Cleans cache type(s) - definition: - arguments: - types: - name: types - is_required: false - is_array: true - description: Space-separated list of cache types or omit to apply to all cache - types. - default: [] - options: - bootstrap: - name: "--bootstrap" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: add or override parameters of the bootstrap - default: - help: - name: "--help" - shortcut: "-h" - accept_value: false - is_value_required: false - is_multiple: false - description: Display this help message - default: false - quiet: - name: "--quiet" - shortcut: "-q" - accept_value: false - is_value_required: false - is_multiple: false - description: Do not output any message - default: false - verbose: - name: "--verbose" - shortcut: "-v|-vv|-vvv" - accept_value: false - is_value_required: false - is_multiple: false - description: 'Increase the verbosity of messages: 1 for normal output, 2 for - more verbose output and 3 for debug' - default: false - version: - name: "--version" - shortcut: "-V" - accept_value: false - is_value_required: false - is_multiple: false - description: Display this application version - default: false - ansi: - name: "--ansi" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Force ANSI output - default: false - no-ansi: - name: "--no-ansi" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Disable ANSI output - default: false - no-interaction: - name: "--no-interaction" - shortcut: "-n" - accept_value: false - is_value_required: false - is_multiple: false - description: Do not ask any interactive question - default: false - hidden: false -- name: cache:disable - usage: - - cache:disable [--bootstrap BOOTSTRAP] [--] [...] - description: Disables cache type(s) - help: Disables cache type(s) - definition: - arguments: - types: - name: types - is_required: false - is_array: true - description: Space-separated list of cache types or omit to apply to all cache - types. - default: [] - options: - bootstrap: - name: "--bootstrap" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: add or override parameters of the bootstrap - default: - help: - name: "--help" - shortcut: "-h" - accept_value: false - is_value_required: false - is_multiple: false - description: Display this help message - default: false - quiet: - name: "--quiet" - shortcut: "-q" - accept_value: false - is_value_required: false - is_multiple: false - description: Do not output any message - default: false - verbose: - name: "--verbose" - shortcut: "-v|-vv|-vvv" - accept_value: false - is_value_required: false - is_multiple: false - description: 'Increase the verbosity of messages: 1 for normal output, 2 for - more verbose output and 3 for debug' - default: false - version: - name: "--version" - shortcut: "-V" - accept_value: false - is_value_required: false - is_multiple: false - description: Display this application version - default: false - ansi: - name: "--ansi" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Force ANSI output - default: false - no-ansi: - name: "--no-ansi" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Disable ANSI output - default: false - no-interaction: - name: "--no-interaction" - shortcut: "-n" - accept_value: false - is_value_required: false - is_multiple: false - description: Do not ask any interactive question - default: false - hidden: false -- name: cache:enable - usage: - - cache:enable [--bootstrap BOOTSTRAP] [--] [...] - description: Enables cache type(s) - help: Enables cache type(s) - definition: - arguments: - types: - name: types - is_required: false - is_array: true - description: Space-separated list of cache types or omit to apply to all cache - types. - default: [] - options: - bootstrap: - name: "--bootstrap" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: add or override parameters of the bootstrap - default: - help: - name: "--help" - shortcut: "-h" - accept_value: false - is_value_required: false - is_multiple: false - description: Display this help message - default: false - quiet: - name: "--quiet" - shortcut: "-q" - accept_value: false - is_value_required: false - is_multiple: false - description: Do not output any message - default: false - verbose: - name: "--verbose" - shortcut: "-v|-vv|-vvv" - accept_value: false - is_value_required: false - is_multiple: false - description: 'Increase the verbosity of messages: 1 for normal output, 2 for - more verbose output and 3 for debug' - default: false - version: - name: "--version" - shortcut: "-V" - accept_value: false - is_value_required: false - is_multiple: false - description: Display this application version - default: false - ansi: - name: "--ansi" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Force ANSI output - default: false - no-ansi: - name: "--no-ansi" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Disable ANSI output - default: false - no-interaction: - name: "--no-interaction" - shortcut: "-n" - accept_value: false - is_value_required: false - is_multiple: false - description: Do not ask any interactive question - default: false - hidden: false -- name: cache:flush - usage: - - cache:flush [--bootstrap BOOTSTRAP] [--] [...] - description: Flushes cache storage used by cache type(s) - help: Flushes cache storage used by cache type(s) - definition: - arguments: - types: - name: types - is_required: false - is_array: true - description: Space-separated list of cache types or omit to apply to all cache - types. - default: [] - options: - bootstrap: - name: "--bootstrap" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: add or override parameters of the bootstrap - default: - help: - name: "--help" - shortcut: "-h" - accept_value: false - is_value_required: false - is_multiple: false - description: Display this help message - default: false - quiet: - name: "--quiet" - shortcut: "-q" - accept_value: false - is_value_required: false - is_multiple: false - description: Do not output any message - default: false - verbose: - name: "--verbose" - shortcut: "-v|-vv|-vvv" - accept_value: false - is_value_required: false - is_multiple: false - description: 'Increase the verbosity of messages: 1 for normal output, 2 for - more verbose output and 3 for debug' - default: false - version: - name: "--version" - shortcut: "-V" - accept_value: false - is_value_required: false - is_multiple: false - description: Display this application version - default: false - ansi: - name: "--ansi" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Force ANSI output - default: false - no-ansi: - name: "--no-ansi" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Disable ANSI output - default: false - no-interaction: - name: "--no-interaction" - shortcut: "-n" - accept_value: false - is_value_required: false - is_multiple: false - description: Do not ask any interactive question - default: false - hidden: false -- name: cache:status - usage: - - cache:status [--bootstrap BOOTSTRAP] - description: Checks cache status - help: Checks cache status - definition: - arguments: [] - options: - bootstrap: - name: "--bootstrap" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: add or override parameters of the bootstrap - default: - help: - name: "--help" - shortcut: "-h" - accept_value: false - is_value_required: false - is_multiple: false - description: Display this help message - default: false - quiet: - name: "--quiet" - shortcut: "-q" - accept_value: false - is_value_required: false - is_multiple: false - description: Do not output any message - default: false - verbose: - name: "--verbose" - shortcut: "-v|-vv|-vvv" - accept_value: false - is_value_required: false - is_multiple: false - description: 'Increase the verbosity of messages: 1 for normal output, 2 for - more verbose output and 3 for debug' - default: false - version: - name: "--version" - shortcut: "-V" - accept_value: false - is_value_required: false - is_multiple: false - description: Display this application version - default: false - ansi: - name: "--ansi" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Force ANSI output - default: false - no-ansi: - name: "--no-ansi" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Disable ANSI output - default: false - no-interaction: - name: "--no-interaction" - shortcut: "-n" - accept_value: false - is_value_required: false - is_multiple: false - description: Do not ask any interactive question - default: false - hidden: false -- name: catalog:images:resize - usage: - - catalog:images:resize - description: Creates resized product images - help: Creates resized product images - definition: - arguments: [] - options: - help: - name: "--help" - shortcut: "-h" - accept_value: false - is_value_required: false - is_multiple: false - description: Display this help message - default: false - quiet: - name: "--quiet" - shortcut: "-q" - accept_value: false - is_value_required: false - is_multiple: false - description: Do not output any message - default: false - verbose: - name: "--verbose" - shortcut: "-v|-vv|-vvv" - accept_value: false - is_value_required: false - is_multiple: false - description: 'Increase the verbosity of messages: 1 for normal output, 2 for - more verbose output and 3 for debug' - default: false - version: - name: "--version" - shortcut: "-V" - accept_value: false - is_value_required: false - is_multiple: false - description: Display this application version - default: false - ansi: - name: "--ansi" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Force ANSI output - default: false - no-ansi: - name: "--no-ansi" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Disable ANSI output - default: false - no-interaction: - name: "--no-interaction" - shortcut: "-n" - accept_value: false - is_value_required: false - is_multiple: false - description: Do not ask any interactive question - default: false - hidden: false -- name: catalog:product:attributes:cleanup - usage: - - catalog:product:attributes:cleanup - description: Removes unused product attributes. - help: Removes unused product attributes. - definition: - arguments: [] - options: - help: - name: "--help" - shortcut: "-h" - accept_value: false - is_value_required: false - is_multiple: false - description: Display this help message - default: false - quiet: - name: "--quiet" - shortcut: "-q" - accept_value: false - is_value_required: false - is_multiple: false - description: Do not output any message - default: false - verbose: - name: "--verbose" - shortcut: "-v|-vv|-vvv" - accept_value: false - is_value_required: false - is_multiple: false - description: 'Increase the verbosity of messages: 1 for normal output, 2 for - more verbose output and 3 for debug' - default: false - version: - name: "--version" - shortcut: "-V" - accept_value: false - is_value_required: false - is_multiple: false - description: Display this application version - default: false - ansi: - name: "--ansi" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Force ANSI output - default: false - no-ansi: - name: "--no-ansi" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Disable ANSI output - default: false - no-interaction: - name: "--no-interaction" - shortcut: "-n" - accept_value: false - is_value_required: false - is_multiple: false - description: Do not ask any interactive question - default: false - hidden: false -- name: config:sensitive:set - usage: - - config:sensitive:set [-i|--interactive] [--scope [SCOPE]] [--scope-code [SCOPE-CODE]] - [--] [ []] - description: Set sensitive configuration values - help: Set sensitive configuration values - definition: - arguments: - path: - name: path - is_required: false - is_array: false - description: Configuration path for example group/section/field_name - default: - value: - name: value - is_required: false - is_array: false - description: Configuration value - default: - options: - interactive: - name: "--interactive" - shortcut: "-i" - accept_value: false - is_value_required: false - is_multiple: false - description: Enable interactive mode to set all sensitive variables - default: false - scope: - name: "--scope" - shortcut: '' - accept_value: true - is_value_required: false - is_multiple: false - description: Scope for configuration, if not set use 'default' - default: default - scope-code: - name: "--scope-code" - shortcut: '' - accept_value: true - is_value_required: false - is_multiple: false - description: Scope code for configuration, empty string by default - default: '' - help: - name: "--help" - shortcut: "-h" - accept_value: false - is_value_required: false - is_multiple: false - description: Display this help message - default: false - quiet: - name: "--quiet" - shortcut: "-q" - accept_value: false - is_value_required: false - is_multiple: false - description: Do not output any message - default: false - verbose: - name: "--verbose" - shortcut: "-v|-vv|-vvv" - accept_value: false - is_value_required: false - is_multiple: false - description: 'Increase the verbosity of messages: 1 for normal output, 2 for - more verbose output and 3 for debug' - default: false - version: - name: "--version" - shortcut: "-V" - accept_value: false - is_value_required: false - is_multiple: false - description: Display this application version - default: false - ansi: - name: "--ansi" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Force ANSI output - default: false - no-ansi: - name: "--no-ansi" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Disable ANSI output - default: false - no-interaction: - name: "--no-interaction" - shortcut: "-n" - accept_value: false - is_value_required: false - is_multiple: false - description: Do not ask any interactive question - default: false - hidden: false -- name: config:set - usage: - - config:set [--scope SCOPE] [--scope-code SCOPE-CODE] [-le|--lock-env] [-lc|--lock-config] - [-l|--lock] [--] - description: Change system configuration - help: Change system configuration - definition: - arguments: - path: - name: path - is_required: true - is_array: false - description: Configuration path in format section/group/field_name - default: - value: - name: value - is_required: true - is_array: false - description: Configuration value - default: - options: - scope: - name: "--scope" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: Configuration scope (default, website, or store) - default: default - scope-code: - name: "--scope-code" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: Scope code (required only if scope is not 'default') - default: - lock-env: - name: "--lock-env" - shortcut: "-le" - accept_value: false - is_value_required: false - is_multiple: false - description: Lock value which prevents modification in the Admin (will be - saved in app/etc/env.php) - default: false - lock-config: - name: "--lock-config" - shortcut: "-lc" - accept_value: false - is_value_required: false - is_multiple: false - description: Lock and share value with other installations, prevents modification - in the Admin (will be saved in app/etc/config.php) - default: false - lock: - name: "--lock" - shortcut: "-l" - accept_value: false - is_value_required: false - is_multiple: false - description: Deprecated, use the --lock-env option instead. - default: false - help: - name: "--help" - shortcut: "-h" - accept_value: false - is_value_required: false - is_multiple: false - description: Display this help message - default: false - quiet: - name: "--quiet" - shortcut: "-q" - accept_value: false - is_value_required: false - is_multiple: false - description: Do not output any message - default: false - verbose: - name: "--verbose" - shortcut: "-v|-vv|-vvv" - accept_value: false - is_value_required: false - is_multiple: false - description: 'Increase the verbosity of messages: 1 for normal output, 2 for - more verbose output and 3 for debug' - default: false - version: - name: "--version" - shortcut: "-V" - accept_value: false - is_value_required: false - is_multiple: false - description: Display this application version - default: false - ansi: - name: "--ansi" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Force ANSI output - default: false - no-ansi: - name: "--no-ansi" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Disable ANSI output - default: false - no-interaction: - name: "--no-interaction" - shortcut: "-n" - accept_value: false - is_value_required: false - is_multiple: false - description: Do not ask any interactive question - default: false - hidden: false -- name: config:show - usage: - - config:show [--scope [SCOPE]] [--scope-code [SCOPE-CODE]] [--] [] - description: Shows configuration value for given path. If path is not specified, - all saved values will be shown - help: Shows configuration value for given path. If path is not specified, all saved - values will be shown - definition: - arguments: - path: - name: path - is_required: false - is_array: false - description: Configuration path, for example section_id/group_id/field_id - default: - options: - scope: - name: "--scope" - shortcut: '' - accept_value: true - is_value_required: false - is_multiple: false - description: Scope for configuration, if not specified, then 'default' scope - will be used - default: default - scope-code: - name: "--scope-code" - shortcut: '' - accept_value: true - is_value_required: false - is_multiple: false - description: Scope code (required only if scope is not `default`) - default: '' - help: - name: "--help" - shortcut: "-h" - accept_value: false - is_value_required: false - is_multiple: false - description: Display this help message - default: false - quiet: - name: "--quiet" - shortcut: "-q" - accept_value: false - is_value_required: false - is_multiple: false - description: Do not output any message - default: false - verbose: - name: "--verbose" - shortcut: "-v|-vv|-vvv" - accept_value: false - is_value_required: false - is_multiple: false - description: 'Increase the verbosity of messages: 1 for normal output, 2 for - more verbose output and 3 for debug' - default: false - version: - name: "--version" - shortcut: "-V" - accept_value: false - is_value_required: false - is_multiple: false - description: Display this application version - default: false - ansi: - name: "--ansi" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Force ANSI output - default: false - no-ansi: - name: "--no-ansi" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Disable ANSI output - default: false - no-interaction: - name: "--no-interaction" - shortcut: "-n" - accept_value: false - is_value_required: false - is_multiple: false - description: Do not ask any interactive question - default: false - hidden: false -- name: cron:install - usage: - - cron:install [-f|--force] - description: Generates and installs crontab for current user - help: Generates and installs crontab for current user - definition: - arguments: [] - options: - force: - name: "--force" - shortcut: "-f" - accept_value: false - is_value_required: false - is_multiple: false - description: Force install tasks - default: false - help: - name: "--help" - shortcut: "-h" - accept_value: false - is_value_required: false - is_multiple: false - description: Display this help message - default: false - quiet: - name: "--quiet" - shortcut: "-q" - accept_value: false - is_value_required: false - is_multiple: false - description: Do not output any message - default: false - verbose: - name: "--verbose" - shortcut: "-v|-vv|-vvv" - accept_value: false - is_value_required: false - is_multiple: false - description: 'Increase the verbosity of messages: 1 for normal output, 2 for - more verbose output and 3 for debug' - default: false - version: - name: "--version" - shortcut: "-V" - accept_value: false - is_value_required: false - is_multiple: false - description: Display this application version - default: false - ansi: - name: "--ansi" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Force ANSI output - default: false - no-ansi: - name: "--no-ansi" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Disable ANSI output - default: false - no-interaction: - name: "--no-interaction" - shortcut: "-n" - accept_value: false - is_value_required: false - is_multiple: false - description: Do not ask any interactive question - default: false - hidden: false -- name: cron:remove - usage: - - cron:remove - description: Removes tasks from crontab - help: Removes tasks from crontab - definition: - arguments: [] - options: - help: - name: "--help" - shortcut: "-h" - accept_value: false - is_value_required: false - is_multiple: false - description: Display this help message - default: false - quiet: - name: "--quiet" - shortcut: "-q" - accept_value: false - is_value_required: false - is_multiple: false - description: Do not output any message - default: false - verbose: - name: "--verbose" - shortcut: "-v|-vv|-vvv" - accept_value: false - is_value_required: false - is_multiple: false - description: 'Increase the verbosity of messages: 1 for normal output, 2 for - more verbose output and 3 for debug' - default: false - version: - name: "--version" - shortcut: "-V" - accept_value: false - is_value_required: false - is_multiple: false - description: Display this application version - default: false - ansi: - name: "--ansi" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Force ANSI output - default: false - no-ansi: - name: "--no-ansi" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Disable ANSI output - default: false - no-interaction: - name: "--no-interaction" - shortcut: "-n" - accept_value: false - is_value_required: false - is_multiple: false - description: Do not ask any interactive question - default: false - hidden: false -- name: cron:run - usage: - - cron:run [--group GROUP] [--bootstrap BOOTSTRAP] - description: Runs jobs by schedule - help: Runs jobs by schedule - definition: - arguments: [] - options: - group: - name: "--group" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: Run jobs only from specified group - default: - bootstrap: - name: "--bootstrap" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: Add or override parameters of the bootstrap - default: - help: - name: "--help" - shortcut: "-h" - accept_value: false - is_value_required: false - is_multiple: false - description: Display this help message - default: false - quiet: - name: "--quiet" - shortcut: "-q" - accept_value: false - is_value_required: false - is_multiple: false - description: Do not output any message - default: false - verbose: - name: "--verbose" - shortcut: "-v|-vv|-vvv" - accept_value: false - is_value_required: false - is_multiple: false - description: 'Increase the verbosity of messages: 1 for normal output, 2 for - more verbose output and 3 for debug' - default: false - version: - name: "--version" - shortcut: "-V" - accept_value: false - is_value_required: false - is_multiple: false - description: Display this application version - default: false - ansi: - name: "--ansi" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Force ANSI output - default: false - no-ansi: - name: "--no-ansi" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Disable ANSI output - default: false - no-interaction: - name: "--no-interaction" - shortcut: "-n" - accept_value: false - is_value_required: false - is_multiple: false - description: Do not ask any interactive question - default: false - hidden: false -- name: customer:hash:upgrade - usage: - - customer:hash:upgrade - description: Upgrade customer's hash according to the latest algorithm - help: Upgrade customer's hash according to the latest algorithm - definition: - arguments: [] - options: - help: - name: "--help" - shortcut: "-h" - accept_value: false - is_value_required: false - is_multiple: false - description: Display this help message - default: false - quiet: - name: "--quiet" - shortcut: "-q" - accept_value: false - is_value_required: false - is_multiple: false - description: Do not output any message - default: false - verbose: - name: "--verbose" - shortcut: "-v|-vv|-vvv" - accept_value: false - is_value_required: false - is_multiple: false - description: 'Increase the verbosity of messages: 1 for normal output, 2 for - more verbose output and 3 for debug' - default: false - version: - name: "--version" - shortcut: "-V" - accept_value: false - is_value_required: false - is_multiple: false - description: Display this application version - default: false - ansi: - name: "--ansi" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Force ANSI output - default: false - no-ansi: - name: "--no-ansi" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Disable ANSI output - default: false - no-interaction: - name: "--no-interaction" - shortcut: "-n" - accept_value: false - is_value_required: false - is_multiple: false - description: Do not ask any interactive question - default: false - hidden: false -- name: deploy:mode:set - usage: - - deploy:mode:set [-s|--skip-compilation] [--] - description: Set application mode. - help: Set application mode. - definition: - arguments: - mode: - name: mode - is_required: true - is_array: false - description: The application mode to set. Available options are "developer" - or "production" - default: - options: - skip-compilation: - name: "--skip-compilation" - shortcut: "-s" - accept_value: false - is_value_required: false - is_multiple: false - description: Skips the clearing and regeneration of static content (generated - code, preprocessed CSS, and assets in pub/static/) - default: false - help: - name: "--help" - shortcut: "-h" - accept_value: false - is_value_required: false - is_multiple: false - description: Display this help message - default: false - quiet: - name: "--quiet" - shortcut: "-q" - accept_value: false - is_value_required: false - is_multiple: false - description: Do not output any message - default: false - verbose: - name: "--verbose" - shortcut: "-v|-vv|-vvv" - accept_value: false - is_value_required: false - is_multiple: false - description: 'Increase the verbosity of messages: 1 for normal output, 2 for - more verbose output and 3 for debug' - default: false - version: - name: "--version" - shortcut: "-V" - accept_value: false - is_value_required: false - is_multiple: false - description: Display this application version - default: false - ansi: - name: "--ansi" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Force ANSI output - default: false - no-ansi: - name: "--no-ansi" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Disable ANSI output - default: false - no-interaction: - name: "--no-interaction" - shortcut: "-n" - accept_value: false - is_value_required: false - is_multiple: false - description: Do not ask any interactive question - default: false - hidden: false -- name: deploy:mode:show - usage: - - deploy:mode:show - description: Displays current application mode. - help: Displays current application mode. - definition: - arguments: [] - options: - help: - name: "--help" - shortcut: "-h" - accept_value: false - is_value_required: false - is_multiple: false - description: Display this help message - default: false - quiet: - name: "--quiet" - shortcut: "-q" - accept_value: false - is_value_required: false - is_multiple: false - description: Do not output any message - default: false - verbose: - name: "--verbose" - shortcut: "-v|-vv|-vvv" - accept_value: false - is_value_required: false - is_multiple: false - description: 'Increase the verbosity of messages: 1 for normal output, 2 for - more verbose output and 3 for debug' - default: false - version: - name: "--version" - shortcut: "-V" - accept_value: false - is_value_required: false - is_multiple: false - description: Display this application version - default: false - ansi: - name: "--ansi" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Force ANSI output - default: false - no-ansi: - name: "--no-ansi" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Disable ANSI output - default: false - no-interaction: - name: "--no-interaction" - shortcut: "-n" - accept_value: false - is_value_required: false - is_multiple: false - description: Do not ask any interactive question - default: false - hidden: false -- name: dev:di:info - usage: - - dev:di:info - description: Provides information on Dependency Injection configuration for the - Command. - help: Provides information on Dependency Injection configuration for the Command. - definition: - arguments: - class: - name: class - is_required: true - is_array: false - description: Class name - default: - options: - help: - name: "--help" - shortcut: "-h" - accept_value: false - is_value_required: false - is_multiple: false - description: Display this help message - default: false - quiet: - name: "--quiet" - shortcut: "-q" - accept_value: false - is_value_required: false - is_multiple: false - description: Do not output any message - default: false - verbose: - name: "--verbose" - shortcut: "-v|-vv|-vvv" - accept_value: false - is_value_required: false - is_multiple: false - description: 'Increase the verbosity of messages: 1 for normal output, 2 for - more verbose output and 3 for debug' - default: false - version: - name: "--version" - shortcut: "-V" - accept_value: false - is_value_required: false - is_multiple: false - description: Display this application version - default: false - ansi: - name: "--ansi" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Force ANSI output - default: false - no-ansi: - name: "--no-ansi" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Disable ANSI output - default: false - no-interaction: - name: "--no-interaction" - shortcut: "-n" - accept_value: false - is_value_required: false - is_multiple: false - description: Do not ask any interactive question - default: false - hidden: false -- name: dev:profiler:disable - usage: - - dev:profiler:disable - description: Disable the profiler. - help: Disable the profiler. - definition: - arguments: [] - options: - help: - name: "--help" - shortcut: "-h" - accept_value: false - is_value_required: false - is_multiple: false - description: Display this help message - default: false - quiet: - name: "--quiet" - shortcut: "-q" - accept_value: false - is_value_required: false - is_multiple: false - description: Do not output any message - default: false - verbose: - name: "--verbose" - shortcut: "-v|-vv|-vvv" - accept_value: false - is_value_required: false - is_multiple: false - description: 'Increase the verbosity of messages: 1 for normal output, 2 for - more verbose output and 3 for debug' - default: false - version: - name: "--version" - shortcut: "-V" - accept_value: false - is_value_required: false - is_multiple: false - description: Display this application version - default: false - ansi: - name: "--ansi" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Force ANSI output - default: false - no-ansi: - name: "--no-ansi" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Disable ANSI output - default: false - no-interaction: - name: "--no-interaction" - shortcut: "-n" - accept_value: false - is_value_required: false - is_multiple: false - description: Do not ask any interactive question - default: false - hidden: false -- name: dev:profiler:enable - usage: - - dev:profiler:enable [] - description: Enable the profiler. - help: Enable the profiler. - definition: - arguments: - type: - name: type - is_required: false - is_array: false - description: Profiler type - default: - options: - help: - name: "--help" - shortcut: "-h" - accept_value: false - is_value_required: false - is_multiple: false - description: Display this help message - default: false - quiet: - name: "--quiet" - shortcut: "-q" - accept_value: false - is_value_required: false - is_multiple: false - description: Do not output any message - default: false - verbose: - name: "--verbose" - shortcut: "-v|-vv|-vvv" - accept_value: false - is_value_required: false - is_multiple: false - description: 'Increase the verbosity of messages: 1 for normal output, 2 for - more verbose output and 3 for debug' - default: false - version: - name: "--version" - shortcut: "-V" - accept_value: false - is_value_required: false - is_multiple: false - description: Display this application version - default: false - ansi: - name: "--ansi" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Force ANSI output - default: false - no-ansi: - name: "--no-ansi" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Disable ANSI output - default: false - no-interaction: - name: "--no-interaction" - shortcut: "-n" - accept_value: false - is_value_required: false - is_multiple: false - description: Do not ask any interactive question - default: false - hidden: false -- name: dev:query-log:disable - usage: - - dev:query-log:disable - description: Disable DB query logging - help: Disable DB query logging - definition: - arguments: [] - options: - help: - name: "--help" - shortcut: "-h" - accept_value: false - is_value_required: false - is_multiple: false - description: Display this help message - default: false - quiet: - name: "--quiet" - shortcut: "-q" - accept_value: false - is_value_required: false - is_multiple: false - description: Do not output any message - default: false - verbose: - name: "--verbose" - shortcut: "-v|-vv|-vvv" - accept_value: false - is_value_required: false - is_multiple: false - description: 'Increase the verbosity of messages: 1 for normal output, 2 for - more verbose output and 3 for debug' - default: false - version: - name: "--version" - shortcut: "-V" - accept_value: false - is_value_required: false - is_multiple: false - description: Display this application version - default: false - ansi: - name: "--ansi" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Force ANSI output - default: false - no-ansi: - name: "--no-ansi" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Disable ANSI output - default: false - no-interaction: - name: "--no-interaction" - shortcut: "-n" - accept_value: false - is_value_required: false - is_multiple: false - description: Do not ask any interactive question - default: false - hidden: false -- name: dev:query-log:enable - usage: - - dev:query-log:enable [--include-all-queries [INCLUDE-ALL-QUERIES]] [--query-time-threshold - [QUERY-TIME-THRESHOLD]] [--include-call-stack [INCLUDE-CALL-STACK]] - description: Enable DB query logging - help: Enable DB query logging - definition: - arguments: [] - options: - include-all-queries: - name: "--include-all-queries" - shortcut: '' - accept_value: true - is_value_required: false - is_multiple: false - description: Log all queries. [true|false] - default: 'true' - query-time-threshold: - name: "--query-time-threshold" - shortcut: '' - accept_value: true - is_value_required: false - is_multiple: false - description: Query time thresholds. - default: '0.001' - include-call-stack: - name: "--include-call-stack" - shortcut: '' - accept_value: true - is_value_required: false - is_multiple: false - description: Include call stack. [true|false] - default: 'true' - help: - name: "--help" - shortcut: "-h" - accept_value: false - is_value_required: false - is_multiple: false - description: Display this help message - default: false - quiet: - name: "--quiet" - shortcut: "-q" - accept_value: false - is_value_required: false - is_multiple: false - description: Do not output any message - default: false - verbose: - name: "--verbose" - shortcut: "-v|-vv|-vvv" - accept_value: false - is_value_required: false - is_multiple: false - description: 'Increase the verbosity of messages: 1 for normal output, 2 for - more verbose output and 3 for debug' - default: false - version: - name: "--version" - shortcut: "-V" - accept_value: false - is_value_required: false - is_multiple: false - description: Display this application version - default: false - ansi: - name: "--ansi" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Force ANSI output - default: false - no-ansi: - name: "--no-ansi" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Disable ANSI output - default: false - no-interaction: - name: "--no-interaction" - shortcut: "-n" - accept_value: false - is_value_required: false - is_multiple: false - description: Do not ask any interactive question - default: false - hidden: false -- name: dev:source-theme:deploy - usage: - - dev:source-theme:deploy [--type TYPE] [--locale LOCALE] [--area AREA] [--theme - THEME] [--] [...] - description: Collects and publishes source files for theme. - help: Collects and publishes source files for theme. - definition: - arguments: - file: - name: file - is_required: false - is_array: true - description: Files to pre-process (file should be specified without extension) - default: - - css/styles-m - - css/styles-l - options: - type: - name: "--type" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: 'Type of source files: [less]' - default: less - locale: - name: "--locale" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: 'Locale: [en_US]' - default: en_US - area: - name: "--area" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: 'Area: [frontend|adminhtml]' - default: frontend - theme: - name: "--theme" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: 'Theme: [Vendor/theme]' - default: Magento/luma - help: - name: "--help" - shortcut: "-h" - accept_value: false - is_value_required: false - is_multiple: false - description: Display this help message - default: false - quiet: - name: "--quiet" - shortcut: "-q" - accept_value: false - is_value_required: false - is_multiple: false - description: Do not output any message - default: false - verbose: - name: "--verbose" - shortcut: "-v|-vv|-vvv" - accept_value: false - is_value_required: false - is_multiple: false - description: 'Increase the verbosity of messages: 1 for normal output, 2 for - more verbose output and 3 for debug' - default: false - version: - name: "--version" - shortcut: "-V" - accept_value: false - is_value_required: false - is_multiple: false - description: Display this application version - default: false - ansi: - name: "--ansi" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Force ANSI output - default: false - no-ansi: - name: "--no-ansi" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Disable ANSI output - default: false - no-interaction: - name: "--no-interaction" - shortcut: "-n" - accept_value: false - is_value_required: false - is_multiple: false - description: Do not ask any interactive question - default: false - hidden: false -- name: dev:template-hints:disable - usage: - - dev:template-hints:disable - description: Disable frontend template hints. A cache flush might be required. - help: Disable frontend template hints. A cache flush might be required. - definition: - arguments: [] - options: - help: - name: "--help" - shortcut: "-h" - accept_value: false - is_value_required: false - is_multiple: false - description: Display this help message - default: false - quiet: - name: "--quiet" - shortcut: "-q" - accept_value: false - is_value_required: false - is_multiple: false - description: Do not output any message - default: false - verbose: - name: "--verbose" - shortcut: "-v|-vv|-vvv" - accept_value: false - is_value_required: false - is_multiple: false - description: 'Increase the verbosity of messages: 1 for normal output, 2 for - more verbose output and 3 for debug' - default: false - version: - name: "--version" - shortcut: "-V" - accept_value: false - is_value_required: false - is_multiple: false - description: Display this application version - default: false - ansi: - name: "--ansi" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Force ANSI output - default: false - no-ansi: - name: "--no-ansi" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Disable ANSI output - default: false - no-interaction: - name: "--no-interaction" - shortcut: "-n" - accept_value: false - is_value_required: false - is_multiple: false - description: Do not ask any interactive question - default: false - hidden: false -- name: dev:template-hints:enable - usage: - - dev:template-hints:enable - description: Enable frontend template hints. A cache flush might be required. - help: Enable frontend template hints. A cache flush might be required. - definition: - arguments: [] - options: - help: - name: "--help" - shortcut: "-h" - accept_value: false - is_value_required: false - is_multiple: false - description: Display this help message - default: false - quiet: - name: "--quiet" - shortcut: "-q" - accept_value: false - is_value_required: false - is_multiple: false - description: Do not output any message - default: false - verbose: - name: "--verbose" - shortcut: "-v|-vv|-vvv" - accept_value: false - is_value_required: false - is_multiple: false - description: 'Increase the verbosity of messages: 1 for normal output, 2 for - more verbose output and 3 for debug' - default: false - version: - name: "--version" - shortcut: "-V" - accept_value: false - is_value_required: false - is_multiple: false - description: Display this application version - default: false - ansi: - name: "--ansi" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Force ANSI output - default: false - no-ansi: - name: "--no-ansi" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Disable ANSI output - default: false - no-interaction: - name: "--no-interaction" - shortcut: "-n" - accept_value: false - is_value_required: false - is_multiple: false - description: Do not ask any interactive question - default: false - hidden: false -- name: dev:tests:run - usage: - - dev:tests:run [-c|--arguments ARGUMENTS] [--] [] - description: Runs tests - help: Runs tests - definition: - arguments: - type: - name: type - is_required: false - is_array: false - description: 'Type of test to run. Available types: all, unit, integration, - integration-all, static, static-all, integrity, legacy, default' - default: default - options: - arguments: - name: "--arguments" - shortcut: "-c" - accept_value: true - is_value_required: true - is_multiple: false - description: 'Additional arguments for PHPUnit. Example: "-c''--filter=MyTest''" - (no spaces)' - default: '' - help: - name: "--help" - shortcut: "-h" - accept_value: false - is_value_required: false - is_multiple: false - description: Display this help message - default: false - quiet: - name: "--quiet" - shortcut: "-q" - accept_value: false - is_value_required: false - is_multiple: false - description: Do not output any message - default: false - verbose: - name: "--verbose" - shortcut: "-v|-vv|-vvv" - accept_value: false - is_value_required: false - is_multiple: false - description: 'Increase the verbosity of messages: 1 for normal output, 2 for - more verbose output and 3 for debug' - default: false - version: - name: "--version" - shortcut: "-V" - accept_value: false - is_value_required: false - is_multiple: false - description: Display this application version - default: false - ansi: - name: "--ansi" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Force ANSI output - default: false - no-ansi: - name: "--no-ansi" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Disable ANSI output - default: false - no-interaction: - name: "--no-interaction" - shortcut: "-n" - accept_value: false - is_value_required: false - is_multiple: false - description: Do not ask any interactive question - default: false - hidden: false -- name: dev:urn-catalog:generate - usage: - - dev:urn-catalog:generate [--ide IDE] [--] - description: Generates the catalog of URNs to *.xsd mappings for the IDE to highlight - xml. - help: Generates the catalog of URNs to *.xsd mappings for the IDE to highlight xml. - definition: - arguments: - path: - name: path - is_required: true - is_array: false - description: Path to file to output the catalog. For PhpStorm use .idea/misc.xml - default: - options: - ide: - name: "--ide" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: 'Format in which catalog will be generated. Supported: [phpstorm]' - default: phpstorm - help: - name: "--help" - shortcut: "-h" - accept_value: false - is_value_required: false - is_multiple: false - description: Display this help message - default: false - quiet: - name: "--quiet" - shortcut: "-q" - accept_value: false - is_value_required: false - is_multiple: false - description: Do not output any message - default: false - verbose: - name: "--verbose" - shortcut: "-v|-vv|-vvv" - accept_value: false - is_value_required: false - is_multiple: false - description: 'Increase the verbosity of messages: 1 for normal output, 2 for - more verbose output and 3 for debug' - default: false - version: - name: "--version" - shortcut: "-V" - accept_value: false - is_value_required: false - is_multiple: false - description: Display this application version - default: false - ansi: - name: "--ansi" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Force ANSI output - default: false - no-ansi: - name: "--no-ansi" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Disable ANSI output - default: false - no-interaction: - name: "--no-interaction" - shortcut: "-n" - accept_value: false - is_value_required: false - is_multiple: false - description: Do not ask any interactive question - default: false - hidden: false -- name: dev:xml:convert - usage: - - dev:xml:convert [-o|--overwrite] [--] - description: Converts XML file using XSL style sheets - help: Converts XML file using XSL style sheets - definition: - arguments: - xml-file: - name: xml-file - is_required: true - is_array: false - description: Path to XML file that going to be transformed - default: - processor: - name: processor - is_required: true - is_array: false - description: Path to XSL style sheet that going to be applied to XML file - default: - options: - overwrite: - name: "--overwrite" - shortcut: "-o" - accept_value: false - is_value_required: false - is_multiple: false - description: Overwrite XML file - default: false - help: - name: "--help" - shortcut: "-h" - accept_value: false - is_value_required: false - is_multiple: false - description: Display this help message - default: false - quiet: - name: "--quiet" - shortcut: "-q" - accept_value: false - is_value_required: false - is_multiple: false - description: Do not output any message - default: false - verbose: - name: "--verbose" - shortcut: "-v|-vv|-vvv" - accept_value: false - is_value_required: false - is_multiple: false - description: 'Increase the verbosity of messages: 1 for normal output, 2 for - more verbose output and 3 for debug' - default: false - version: - name: "--version" - shortcut: "-V" - accept_value: false - is_value_required: false - is_multiple: false - description: Display this application version - default: false - ansi: - name: "--ansi" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Force ANSI output - default: false - no-ansi: - name: "--no-ansi" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Disable ANSI output - default: false - no-interaction: - name: "--no-interaction" - shortcut: "-n" - accept_value: false - is_value_required: false - is_multiple: false - description: Do not ask any interactive question - default: false - hidden: false -- name: encryption:payment-data:update - usage: - - encryption:payment-data:update - description: Re-encrypts encrypted credit card data with latest encryption cipher. - help: Re-encrypts encrypted credit card data with latest encryption cipher. - definition: - arguments: [] - options: - help: - name: "--help" - shortcut: "-h" - accept_value: false - is_value_required: false - is_multiple: false - description: Display this help message - default: false - quiet: - name: "--quiet" - shortcut: "-q" - accept_value: false - is_value_required: false - is_multiple: false - description: Do not output any message - default: false - verbose: - name: "--verbose" - shortcut: "-v|-vv|-vvv" - accept_value: false - is_value_required: false - is_multiple: false - description: 'Increase the verbosity of messages: 1 for normal output, 2 for - more verbose output and 3 for debug' - default: false - version: - name: "--version" - shortcut: "-V" - accept_value: false - is_value_required: false - is_multiple: false - description: Display this application version - default: false - ansi: - name: "--ansi" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Force ANSI output - default: false - no-ansi: - name: "--no-ansi" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Disable ANSI output - default: false - no-interaction: - name: "--no-interaction" - shortcut: "-n" - accept_value: false - is_value_required: false - is_multiple: false - description: Do not ask any interactive question - default: false - hidden: false -- name: i18n:collect-phrases - usage: - - i18n:collect-phrases [-o|--output OUTPUT] [-m|--magento] [--] [] - description: Discovers phrases in the codebase - help: Discovers phrases in the codebase - definition: - arguments: - directory: - name: directory - is_required: false - is_array: false - description: Directory path to parse. Not needed if --magento flag is set - default: - options: - output: - name: "--output" - shortcut: "-o" - accept_value: true - is_value_required: true - is_multiple: false - description: Path (including filename) to an output file. With no file specified, - defaults to stdout. - default: - magento: - name: "--magento" - shortcut: "-m" - accept_value: false - is_value_required: false - is_multiple: false - description: Use the --magento parameter to parse the current Magento codebase. - Omit the parameter if a directory is specified. - default: false - help: - name: "--help" - shortcut: "-h" - accept_value: false - is_value_required: false - is_multiple: false - description: Display this help message - default: false - quiet: - name: "--quiet" - shortcut: "-q" - accept_value: false - is_value_required: false - is_multiple: false - description: Do not output any message - default: false - verbose: - name: "--verbose" - shortcut: "-v|-vv|-vvv" - accept_value: false - is_value_required: false - is_multiple: false - description: 'Increase the verbosity of messages: 1 for normal output, 2 for - more verbose output and 3 for debug' - default: false - version: - name: "--version" - shortcut: "-V" - accept_value: false - is_value_required: false - is_multiple: false - description: Display this application version - default: false - ansi: - name: "--ansi" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Force ANSI output - default: false - no-ansi: - name: "--no-ansi" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Disable ANSI output - default: false - no-interaction: - name: "--no-interaction" - shortcut: "-n" - accept_value: false - is_value_required: false - is_multiple: false - description: Do not ask any interactive question - default: false - hidden: false -- name: i18n:pack - usage: - - i18n:pack [-m|--mode MODE] [-d|--allow-duplicates] [--] - description: Saves language package - help: Saves language package - definition: - arguments: - source: - name: source - is_required: true - is_array: false - description: Path to source dictionary file with translations - default: - locale: - name: locale - is_required: true - is_array: false - description: Target locale for dictionary, for example "de_DE" - default: - options: - mode: - name: "--mode" - shortcut: "-m" - accept_value: true - is_value_required: true - is_multiple: false - description: Save mode for dictionary - "replace" - replace language pack - by new one - "merge" - merge language packages, by default "replace" - default: replace - allow-duplicates: - name: "--allow-duplicates" - shortcut: "-d" - accept_value: false - is_value_required: false - is_multiple: false - description: Use the --allow-duplicates parameter to allow saving duplicates - of translate. Otherwise omit the parameter. - default: false - help: - name: "--help" - shortcut: "-h" - accept_value: false - is_value_required: false - is_multiple: false - description: Display this help message - default: false - quiet: - name: "--quiet" - shortcut: "-q" - accept_value: false - is_value_required: false - is_multiple: false - description: Do not output any message - default: false - verbose: - name: "--verbose" - shortcut: "-v|-vv|-vvv" - accept_value: false - is_value_required: false - is_multiple: false - description: 'Increase the verbosity of messages: 1 for normal output, 2 for - more verbose output and 3 for debug' - default: false - version: - name: "--version" - shortcut: "-V" - accept_value: false - is_value_required: false - is_multiple: false - description: Display this application version - default: false - ansi: - name: "--ansi" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Force ANSI output - default: false - no-ansi: - name: "--no-ansi" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Disable ANSI output - default: false - no-interaction: - name: "--no-interaction" - shortcut: "-n" - accept_value: false - is_value_required: false - is_multiple: false - description: Do not ask any interactive question - default: false - hidden: false -- name: i18n:uninstall - usage: - - i18n:uninstall [-b|--backup-code] [--] ... - description: Uninstalls language packages - help: Uninstalls language packages - definition: - arguments: - package: - name: package - is_required: true - is_array: true - description: Language package name - default: [] - options: - backup-code: - name: "--backup-code" - shortcut: "-b" - accept_value: false - is_value_required: false - is_multiple: false - description: Take code and configuration files backup (excluding temporary - files) - default: false - help: - name: "--help" - shortcut: "-h" - accept_value: false - is_value_required: false - is_multiple: false - description: Display this help message - default: false - quiet: - name: "--quiet" - shortcut: "-q" - accept_value: false - is_value_required: false - is_multiple: false - description: Do not output any message - default: false - verbose: - name: "--verbose" - shortcut: "-v|-vv|-vvv" - accept_value: false - is_value_required: false - is_multiple: false - description: 'Increase the verbosity of messages: 1 for normal output, 2 for - more verbose output and 3 for debug' - default: false - version: - name: "--version" - shortcut: "-V" - accept_value: false - is_value_required: false - is_multiple: false - description: Display this application version - default: false - ansi: - name: "--ansi" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Force ANSI output - default: false - no-ansi: - name: "--no-ansi" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Disable ANSI output - default: false - no-interaction: - name: "--no-interaction" - shortcut: "-n" - accept_value: false - is_value_required: false - is_multiple: false - description: Do not ask any interactive question - default: false - hidden: false -- name: indexer:info - usage: - - indexer:info - description: Shows allowed Indexers - help: Shows allowed Indexers - definition: - arguments: [] - options: - help: - name: "--help" - shortcut: "-h" - accept_value: false - is_value_required: false - is_multiple: false - description: Display this help message - default: false - quiet: - name: "--quiet" - shortcut: "-q" - accept_value: false - is_value_required: false - is_multiple: false - description: Do not output any message - default: false - verbose: - name: "--verbose" - shortcut: "-v|-vv|-vvv" - accept_value: false - is_value_required: false - is_multiple: false - description: 'Increase the verbosity of messages: 1 for normal output, 2 for - more verbose output and 3 for debug' - default: false - version: - name: "--version" - shortcut: "-V" - accept_value: false - is_value_required: false - is_multiple: false - description: Display this application version - default: false - ansi: - name: "--ansi" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Force ANSI output - default: false - no-ansi: - name: "--no-ansi" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Disable ANSI output - default: false - no-interaction: - name: "--no-interaction" - shortcut: "-n" - accept_value: false - is_value_required: false - is_multiple: false - description: Do not ask any interactive question - default: false - hidden: false -- name: indexer:reindex - usage: - - indexer:reindex [...] - description: Reindexes Data - help: Reindexes Data - definition: - arguments: - index: - name: index - is_required: false - is_array: true - description: Space-separated list of index types or omit to apply to all indexes. - default: [] - options: - help: - name: "--help" - shortcut: "-h" - accept_value: false - is_value_required: false - is_multiple: false - description: Display this help message - default: false - quiet: - name: "--quiet" - shortcut: "-q" - accept_value: false - is_value_required: false - is_multiple: false - description: Do not output any message - default: false - verbose: - name: "--verbose" - shortcut: "-v|-vv|-vvv" - accept_value: false - is_value_required: false - is_multiple: false - description: 'Increase the verbosity of messages: 1 for normal output, 2 for - more verbose output and 3 for debug' - default: false - version: - name: "--version" - shortcut: "-V" - accept_value: false - is_value_required: false - is_multiple: false - description: Display this application version - default: false - ansi: - name: "--ansi" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Force ANSI output - default: false - no-ansi: - name: "--no-ansi" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Disable ANSI output - default: false - no-interaction: - name: "--no-interaction" - shortcut: "-n" - accept_value: false - is_value_required: false - is_multiple: false - description: Do not ask any interactive question - default: false - hidden: false -- name: indexer:reset - usage: - - indexer:reset [...] - description: Resets indexer status to invalid - help: Resets indexer status to invalid - definition: - arguments: - index: - name: index - is_required: false - is_array: true - description: Space-separated list of index types or omit to apply to all indexes. - default: [] - options: - help: - name: "--help" - shortcut: "-h" - accept_value: false - is_value_required: false - is_multiple: false - description: Display this help message - default: false - quiet: - name: "--quiet" - shortcut: "-q" - accept_value: false - is_value_required: false - is_multiple: false - description: Do not output any message - default: false - verbose: - name: "--verbose" - shortcut: "-v|-vv|-vvv" - accept_value: false - is_value_required: false - is_multiple: false - description: 'Increase the verbosity of messages: 1 for normal output, 2 for - more verbose output and 3 for debug' - default: false - version: - name: "--version" - shortcut: "-V" - accept_value: false - is_value_required: false - is_multiple: false - description: Display this application version - default: false - ansi: - name: "--ansi" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Force ANSI output - default: false - no-ansi: - name: "--no-ansi" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Disable ANSI output - default: false - no-interaction: - name: "--no-interaction" - shortcut: "-n" - accept_value: false - is_value_required: false - is_multiple: false - description: Do not ask any interactive question - default: false - hidden: false -- name: indexer:set-dimensions-mode - usage: - - indexer:set-dimensions-mode [ []] - description: Set Indexer Dimensions Mode - help: Set Indexer Dimensions Mode - definition: - arguments: - indexer: - name: indexer - is_required: false - is_array: false - description: Indexer name [catalog_product_price] - default: - mode: - name: mode - is_required: false - is_array: false - description: 'Indexer dimension modes catalog_product_price none,website,customer_group,website_and_customer_group ' - default: - options: - help: - name: "--help" - shortcut: "-h" - accept_value: false - is_value_required: false - is_multiple: false - description: Display this help message - default: false - quiet: - name: "--quiet" - shortcut: "-q" - accept_value: false - is_value_required: false - is_multiple: false - description: Do not output any message - default: false - verbose: - name: "--verbose" - shortcut: "-v|-vv|-vvv" - accept_value: false - is_value_required: false - is_multiple: false - description: 'Increase the verbosity of messages: 1 for normal output, 2 for - more verbose output and 3 for debug' - default: false - version: - name: "--version" - shortcut: "-V" - accept_value: false - is_value_required: false - is_multiple: false - description: Display this application version - default: false - ansi: - name: "--ansi" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Force ANSI output - default: false - no-ansi: - name: "--no-ansi" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Disable ANSI output - default: false - no-interaction: - name: "--no-interaction" - shortcut: "-n" - accept_value: false - is_value_required: false - is_multiple: false - description: Do not ask any interactive question - default: false - hidden: false -- name: indexer:set-mode - usage: - - indexer:set-mode [ [...]] - description: Sets index mode type - help: Sets index mode type - definition: - arguments: - mode: - name: mode - is_required: false - is_array: false - description: Indexer mode type [realtime|schedule] - default: - index: - name: index - is_required: false - is_array: true - description: Space-separated list of index types or omit to apply to all indexes. - default: [] - options: - help: - name: "--help" - shortcut: "-h" - accept_value: false - is_value_required: false - is_multiple: false - description: Display this help message - default: false - quiet: - name: "--quiet" - shortcut: "-q" - accept_value: false - is_value_required: false - is_multiple: false - description: Do not output any message - default: false - verbose: - name: "--verbose" - shortcut: "-v|-vv|-vvv" - accept_value: false - is_value_required: false - is_multiple: false - description: 'Increase the verbosity of messages: 1 for normal output, 2 for - more verbose output and 3 for debug' - default: false - version: - name: "--version" - shortcut: "-V" - accept_value: false - is_value_required: false - is_multiple: false - description: Display this application version - default: false - ansi: - name: "--ansi" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Force ANSI output - default: false - no-ansi: - name: "--no-ansi" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Disable ANSI output - default: false - no-interaction: - name: "--no-interaction" - shortcut: "-n" - accept_value: false - is_value_required: false - is_multiple: false - description: Do not ask any interactive question - default: false - hidden: false -- name: indexer:show-dimensions-mode - usage: - - indexer:show-dimensions-mode [...] - description: Shows Indexer Dimension Mode - help: Shows Indexer Dimension Mode - definition: - arguments: - indexer: - name: indexer - is_required: false - is_array: true - description: Space-separated list of index types or omit to apply to all indexes - (catalog_product_price) - default: [] - options: - help: - name: "--help" - shortcut: "-h" - accept_value: false - is_value_required: false - is_multiple: false - description: Display this help message - default: false - quiet: - name: "--quiet" - shortcut: "-q" - accept_value: false - is_value_required: false - is_multiple: false - description: Do not output any message - default: false - verbose: - name: "--verbose" - shortcut: "-v|-vv|-vvv" - accept_value: false - is_value_required: false - is_multiple: false - description: 'Increase the verbosity of messages: 1 for normal output, 2 for - more verbose output and 3 for debug' - default: false - version: - name: "--version" - shortcut: "-V" - accept_value: false - is_value_required: false - is_multiple: false - description: Display this application version - default: false - ansi: - name: "--ansi" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Force ANSI output - default: false - no-ansi: - name: "--no-ansi" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Disable ANSI output - default: false - no-interaction: - name: "--no-interaction" - shortcut: "-n" - accept_value: false - is_value_required: false - is_multiple: false - description: Do not ask any interactive question - default: false - hidden: false -- name: indexer:show-mode - usage: - - indexer:show-mode [...] - description: Shows Index Mode - help: Shows Index Mode - definition: - arguments: - index: - name: index - is_required: false - is_array: true - description: Space-separated list of index types or omit to apply to all indexes. - default: [] - options: - help: - name: "--help" - shortcut: "-h" - accept_value: false - is_value_required: false - is_multiple: false - description: Display this help message - default: false - quiet: - name: "--quiet" - shortcut: "-q" - accept_value: false - is_value_required: false - is_multiple: false - description: Do not output any message - default: false - verbose: - name: "--verbose" - shortcut: "-v|-vv|-vvv" - accept_value: false - is_value_required: false - is_multiple: false - description: 'Increase the verbosity of messages: 1 for normal output, 2 for - more verbose output and 3 for debug' - default: false - version: - name: "--version" - shortcut: "-V" - accept_value: false - is_value_required: false - is_multiple: false - description: Display this application version - default: false - ansi: - name: "--ansi" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Force ANSI output - default: false - no-ansi: - name: "--no-ansi" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Disable ANSI output - default: false - no-interaction: - name: "--no-interaction" - shortcut: "-n" - accept_value: false - is_value_required: false - is_multiple: false - description: Do not ask any interactive question - default: false - hidden: false -- name: indexer:status - usage: - - indexer:status [...] - description: Shows status of Indexer - help: Shows status of Indexer - definition: - arguments: - index: - name: index - is_required: false - is_array: true - description: Space-separated list of index types or omit to apply to all indexes. - default: [] - options: - help: - name: "--help" - shortcut: "-h" - accept_value: false - is_value_required: false - is_multiple: false - description: Display this help message - default: false - quiet: - name: "--quiet" - shortcut: "-q" - accept_value: false - is_value_required: false - is_multiple: false - description: Do not output any message - default: false - verbose: - name: "--verbose" - shortcut: "-v|-vv|-vvv" - accept_value: false - is_value_required: false - is_multiple: false - description: 'Increase the verbosity of messages: 1 for normal output, 2 for - more verbose output and 3 for debug' - default: false - version: - name: "--version" - shortcut: "-V" - accept_value: false - is_value_required: false - is_multiple: false - description: Display this application version - default: false - ansi: - name: "--ansi" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Force ANSI output - default: false - no-ansi: - name: "--no-ansi" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Disable ANSI output - default: false - no-interaction: - name: "--no-interaction" - shortcut: "-n" - accept_value: false - is_value_required: false - is_multiple: false - description: Do not ask any interactive question - default: false - hidden: false -- name: info:adminuri - usage: - - info:adminuri - description: Displays the Magento Admin URI - help: Displays the Magento Admin URI - definition: - arguments: [] - options: - help: - name: "--help" - shortcut: "-h" - accept_value: false - is_value_required: false - is_multiple: false - description: Display this help message - default: false - quiet: - name: "--quiet" - shortcut: "-q" - accept_value: false - is_value_required: false - is_multiple: false - description: Do not output any message - default: false - verbose: - name: "--verbose" - shortcut: "-v|-vv|-vvv" - accept_value: false - is_value_required: false - is_multiple: false - description: 'Increase the verbosity of messages: 1 for normal output, 2 for - more verbose output and 3 for debug' - default: false - version: - name: "--version" - shortcut: "-V" - accept_value: false - is_value_required: false - is_multiple: false - description: Display this application version - default: false - ansi: - name: "--ansi" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Force ANSI output - default: false - no-ansi: - name: "--no-ansi" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Disable ANSI output - default: false - no-interaction: - name: "--no-interaction" - shortcut: "-n" - accept_value: false - is_value_required: false - is_multiple: false - description: Do not ask any interactive question - default: false - hidden: false -- name: info:backups:list - usage: - - info:backups:list - description: Prints list of available backup files - help: Prints list of available backup files - definition: - arguments: [] - options: - help: - name: "--help" - shortcut: "-h" - accept_value: false - is_value_required: false - is_multiple: false - description: Display this help message - default: false - quiet: - name: "--quiet" - shortcut: "-q" - accept_value: false - is_value_required: false - is_multiple: false - description: Do not output any message - default: false - verbose: - name: "--verbose" - shortcut: "-v|-vv|-vvv" - accept_value: false - is_value_required: false - is_multiple: false - description: 'Increase the verbosity of messages: 1 for normal output, 2 for - more verbose output and 3 for debug' - default: false - version: - name: "--version" - shortcut: "-V" - accept_value: false - is_value_required: false - is_multiple: false - description: Display this application version - default: false - ansi: - name: "--ansi" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Force ANSI output - default: false - no-ansi: - name: "--no-ansi" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Disable ANSI output - default: false - no-interaction: - name: "--no-interaction" - shortcut: "-n" - accept_value: false - is_value_required: false - is_multiple: false - description: Do not ask any interactive question - default: false - hidden: false -- name: info:currency:list - usage: - - info:currency:list - description: Displays the list of available currencies - help: Displays the list of available currencies - definition: - arguments: [] - options: - help: - name: "--help" - shortcut: "-h" - accept_value: false - is_value_required: false - is_multiple: false - description: Display this help message - default: false - quiet: - name: "--quiet" - shortcut: "-q" - accept_value: false - is_value_required: false - is_multiple: false - description: Do not output any message - default: false - verbose: - name: "--verbose" - shortcut: "-v|-vv|-vvv" - accept_value: false - is_value_required: false - is_multiple: false - description: 'Increase the verbosity of messages: 1 for normal output, 2 for - more verbose output and 3 for debug' - default: false - version: - name: "--version" - shortcut: "-V" - accept_value: false - is_value_required: false - is_multiple: false - description: Display this application version - default: false - ansi: - name: "--ansi" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Force ANSI output - default: false - no-ansi: - name: "--no-ansi" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Disable ANSI output - default: false - no-interaction: - name: "--no-interaction" - shortcut: "-n" - accept_value: false - is_value_required: false - is_multiple: false - description: Do not ask any interactive question - default: false - hidden: false -- name: info:dependencies:show-framework - usage: - - info:dependencies:show-framework [-o|--output OUTPUT] - description: Shows number of dependencies on Magento framework - help: Shows number of dependencies on Magento framework - definition: - arguments: [] - options: - output: - name: "--output" - shortcut: "-o" - accept_value: true - is_value_required: true - is_multiple: false - description: Report filename - default: framework-dependencies.csv - help: - name: "--help" - shortcut: "-h" - accept_value: false - is_value_required: false - is_multiple: false - description: Display this help message - default: false - quiet: - name: "--quiet" - shortcut: "-q" - accept_value: false - is_value_required: false - is_multiple: false - description: Do not output any message - default: false - verbose: - name: "--verbose" - shortcut: "-v|-vv|-vvv" - accept_value: false - is_value_required: false - is_multiple: false - description: 'Increase the verbosity of messages: 1 for normal output, 2 for - more verbose output and 3 for debug' - default: false - version: - name: "--version" - shortcut: "-V" - accept_value: false - is_value_required: false - is_multiple: false - description: Display this application version - default: false - ansi: - name: "--ansi" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Force ANSI output - default: false - no-ansi: - name: "--no-ansi" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Disable ANSI output - default: false - no-interaction: - name: "--no-interaction" - shortcut: "-n" - accept_value: false - is_value_required: false - is_multiple: false - description: Do not ask any interactive question - default: false - hidden: false -- name: info:dependencies:show-modules - usage: - - info:dependencies:show-modules [-o|--output OUTPUT] - description: Shows number of dependencies between modules - help: Shows number of dependencies between modules - definition: - arguments: [] - options: - output: - name: "--output" - shortcut: "-o" - accept_value: true - is_value_required: true - is_multiple: false - description: Report filename - default: modules-dependencies.csv - help: - name: "--help" - shortcut: "-h" - accept_value: false - is_value_required: false - is_multiple: false - description: Display this help message - default: false - quiet: - name: "--quiet" - shortcut: "-q" - accept_value: false - is_value_required: false - is_multiple: false - description: Do not output any message - default: false - verbose: - name: "--verbose" - shortcut: "-v|-vv|-vvv" - accept_value: false - is_value_required: false - is_multiple: false - description: 'Increase the verbosity of messages: 1 for normal output, 2 for - more verbose output and 3 for debug' - default: false - version: - name: "--version" - shortcut: "-V" - accept_value: false - is_value_required: false - is_multiple: false - description: Display this application version - default: false - ansi: - name: "--ansi" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Force ANSI output - default: false - no-ansi: - name: "--no-ansi" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Disable ANSI output - default: false - no-interaction: - name: "--no-interaction" - shortcut: "-n" - accept_value: false - is_value_required: false - is_multiple: false - description: Do not ask any interactive question - default: false - hidden: false -- name: info:dependencies:show-modules-circular - usage: - - info:dependencies:show-modules-circular [-o|--output OUTPUT] - description: Shows number of circular dependencies between modules - help: Shows number of circular dependencies between modules - definition: - arguments: [] - options: - output: - name: "--output" - shortcut: "-o" - accept_value: true - is_value_required: true - is_multiple: false - description: Report filename - default: modules-circular-dependencies.csv - help: - name: "--help" - shortcut: "-h" - accept_value: false - is_value_required: false - is_multiple: false - description: Display this help message - default: false - quiet: - name: "--quiet" - shortcut: "-q" - accept_value: false - is_value_required: false - is_multiple: false - description: Do not output any message - default: false - verbose: - name: "--verbose" - shortcut: "-v|-vv|-vvv" - accept_value: false - is_value_required: false - is_multiple: false - description: 'Increase the verbosity of messages: 1 for normal output, 2 for - more verbose output and 3 for debug' - default: false - version: - name: "--version" - shortcut: "-V" - accept_value: false - is_value_required: false - is_multiple: false - description: Display this application version - default: false - ansi: - name: "--ansi" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Force ANSI output - default: false - no-ansi: - name: "--no-ansi" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Disable ANSI output - default: false - no-interaction: - name: "--no-interaction" - shortcut: "-n" - accept_value: false - is_value_required: false - is_multiple: false - description: Do not ask any interactive question - default: false - hidden: false -- name: info:language:list - usage: - - info:language:list - description: Displays the list of available language locales - help: Displays the list of available language locales - definition: - arguments: [] - options: - help: - name: "--help" - shortcut: "-h" - accept_value: false - is_value_required: false - is_multiple: false - description: Display this help message - default: false - quiet: - name: "--quiet" - shortcut: "-q" - accept_value: false - is_value_required: false - is_multiple: false - description: Do not output any message - default: false - verbose: - name: "--verbose" - shortcut: "-v|-vv|-vvv" - accept_value: false - is_value_required: false - is_multiple: false - description: 'Increase the verbosity of messages: 1 for normal output, 2 for - more verbose output and 3 for debug' - default: false - version: - name: "--version" - shortcut: "-V" - accept_value: false - is_value_required: false - is_multiple: false - description: Display this application version - default: false - ansi: - name: "--ansi" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Force ANSI output - default: false - no-ansi: - name: "--no-ansi" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Disable ANSI output - default: false - no-interaction: - name: "--no-interaction" - shortcut: "-n" - accept_value: false - is_value_required: false - is_multiple: false - description: Do not ask any interactive question - default: false - hidden: false -- name: info:timezone:list - usage: - - info:timezone:list - description: Displays the list of available timezones - help: Displays the list of available timezones - definition: - arguments: [] - options: - help: - name: "--help" - shortcut: "-h" - accept_value: false - is_value_required: false - is_multiple: false - description: Display this help message - default: false - quiet: - name: "--quiet" - shortcut: "-q" - accept_value: false - is_value_required: false - is_multiple: false - description: Do not output any message - default: false - verbose: - name: "--verbose" - shortcut: "-v|-vv|-vvv" - accept_value: false - is_value_required: false - is_multiple: false - description: 'Increase the verbosity of messages: 1 for normal output, 2 for - more verbose output and 3 for debug' - default: false - version: - name: "--version" - shortcut: "-V" - accept_value: false - is_value_required: false - is_multiple: false - description: Display this application version - default: false - ansi: - name: "--ansi" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Force ANSI output - default: false - no-ansi: - name: "--no-ansi" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Disable ANSI output - default: false - no-interaction: - name: "--no-interaction" - shortcut: "-n" - accept_value: false - is_value_required: false - is_multiple: false - description: Do not ask any interactive question - default: false - hidden: false -- name: maintenance:allow-ips - usage: - - maintenance:allow-ips [--none] [--add] [--magento-init-params MAGENTO-INIT-PARAMS] - [--] [...] - description: Sets maintenance mode exempt IPs - help: Sets maintenance mode exempt IPs - definition: - arguments: - ip: - name: ip - is_required: false - is_array: true - description: Allowed IP addresses - default: [] - options: - none: - name: "--none" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Clear allowed IP addresses - default: false - add: - name: "--add" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Add the IP address to existing list - default: false - magento-init-params: - name: "--magento-init-params" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: 'Add to any command to customize Magento initialization parameters - For example: "MAGE_MODE=developer&MAGE_DIRS[base][path]=/var/www/example.com&MAGE_DIRS[cache][path]=/var/tmp/cache"' - default: - help: - name: "--help" - shortcut: "-h" - accept_value: false - is_value_required: false - is_multiple: false - description: Display this help message - default: false - quiet: - name: "--quiet" - shortcut: "-q" - accept_value: false - is_value_required: false - is_multiple: false - description: Do not output any message - default: false - verbose: - name: "--verbose" - shortcut: "-v|-vv|-vvv" - accept_value: false - is_value_required: false - is_multiple: false - description: 'Increase the verbosity of messages: 1 for normal output, 2 for - more verbose output and 3 for debug' - default: false - version: - name: "--version" - shortcut: "-V" - accept_value: false - is_value_required: false - is_multiple: false - description: Display this application version - default: false - ansi: - name: "--ansi" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Force ANSI output - default: false - no-ansi: - name: "--no-ansi" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Disable ANSI output - default: false - no-interaction: - name: "--no-interaction" - shortcut: "-n" - accept_value: false - is_value_required: false - is_multiple: false - description: Do not ask any interactive question - default: false - hidden: false -- name: maintenance:disable - usage: - - maintenance:disable [--ip IP] [--magento-init-params MAGENTO-INIT-PARAMS] - description: Disables maintenance mode - help: Disables maintenance mode - definition: - arguments: [] - options: - ip: - name: "--ip" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: true - description: Allowed IP addresses (use 'none' to clear allowed IP list) - default: [] - magento-init-params: - name: "--magento-init-params" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: 'Add to any command to customize Magento initialization parameters - For example: "MAGE_MODE=developer&MAGE_DIRS[base][path]=/var/www/example.com&MAGE_DIRS[cache][path]=/var/tmp/cache"' - default: - help: - name: "--help" - shortcut: "-h" - accept_value: false - is_value_required: false - is_multiple: false - description: Display this help message - default: false - quiet: - name: "--quiet" - shortcut: "-q" - accept_value: false - is_value_required: false - is_multiple: false - description: Do not output any message - default: false - verbose: - name: "--verbose" - shortcut: "-v|-vv|-vvv" - accept_value: false - is_value_required: false - is_multiple: false - description: 'Increase the verbosity of messages: 1 for normal output, 2 for - more verbose output and 3 for debug' - default: false - version: - name: "--version" - shortcut: "-V" - accept_value: false - is_value_required: false - is_multiple: false - description: Display this application version - default: false - ansi: - name: "--ansi" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Force ANSI output - default: false - no-ansi: - name: "--no-ansi" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Disable ANSI output - default: false - no-interaction: - name: "--no-interaction" - shortcut: "-n" - accept_value: false - is_value_required: false - is_multiple: false - description: Do not ask any interactive question - default: false - hidden: false -- name: maintenance:enable - usage: - - maintenance:enable [--ip IP] [--magento-init-params MAGENTO-INIT-PARAMS] - description: Enables maintenance mode - help: Enables maintenance mode - definition: - arguments: [] - options: - ip: - name: "--ip" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: true - description: Allowed IP addresses (use 'none' to clear allowed IP list) - default: [] - magento-init-params: - name: "--magento-init-params" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: 'Add to any command to customize Magento initialization parameters - For example: "MAGE_MODE=developer&MAGE_DIRS[base][path]=/var/www/example.com&MAGE_DIRS[cache][path]=/var/tmp/cache"' - default: - help: - name: "--help" - shortcut: "-h" - accept_value: false - is_value_required: false - is_multiple: false - description: Display this help message - default: false - quiet: - name: "--quiet" - shortcut: "-q" - accept_value: false - is_value_required: false - is_multiple: false - description: Do not output any message - default: false - verbose: - name: "--verbose" - shortcut: "-v|-vv|-vvv" - accept_value: false - is_value_required: false - is_multiple: false - description: 'Increase the verbosity of messages: 1 for normal output, 2 for - more verbose output and 3 for debug' - default: false - version: - name: "--version" - shortcut: "-V" - accept_value: false - is_value_required: false - is_multiple: false - description: Display this application version - default: false - ansi: - name: "--ansi" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Force ANSI output - default: false - no-ansi: - name: "--no-ansi" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Disable ANSI output - default: false - no-interaction: - name: "--no-interaction" - shortcut: "-n" - accept_value: false - is_value_required: false - is_multiple: false - description: Do not ask any interactive question - default: false - hidden: false -- name: maintenance:status - usage: - - maintenance:status [--magento-init-params MAGENTO-INIT-PARAMS] - description: Displays maintenance mode status - help: Displays maintenance mode status - definition: - arguments: [] - options: - magento-init-params: - name: "--magento-init-params" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: 'Add to any command to customize Magento initialization parameters - For example: "MAGE_MODE=developer&MAGE_DIRS[base][path]=/var/www/example.com&MAGE_DIRS[cache][path]=/var/tmp/cache"' - default: - help: - name: "--help" - shortcut: "-h" - accept_value: false - is_value_required: false - is_multiple: false - description: Display this help message - default: false - quiet: - name: "--quiet" - shortcut: "-q" - accept_value: false - is_value_required: false - is_multiple: false - description: Do not output any message - default: false - verbose: - name: "--verbose" - shortcut: "-v|-vv|-vvv" - accept_value: false - is_value_required: false - is_multiple: false - description: 'Increase the verbosity of messages: 1 for normal output, 2 for - more verbose output and 3 for debug' - default: false - version: - name: "--version" - shortcut: "-V" - accept_value: false - is_value_required: false - is_multiple: false - description: Display this application version - default: false - ansi: - name: "--ansi" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Force ANSI output - default: false - no-ansi: - name: "--no-ansi" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Disable ANSI output - default: false - no-interaction: - name: "--no-interaction" - shortcut: "-n" - accept_value: false - is_value_required: false - is_multiple: false - description: Do not ask any interactive question - default: false - hidden: false -- name: module:disable - usage: - - module:disable [-f|--force] [--all] [-c|--clear-static-content] [--magento-init-params - MAGENTO-INIT-PARAMS] [--] [...] - description: Disables specified modules - help: Disables specified modules - definition: - arguments: - module: - name: module - is_required: false - is_array: true - description: Name of the module - default: [] - options: - force: - name: "--force" - shortcut: "-f" - accept_value: false - is_value_required: false - is_multiple: false - description: Bypass dependencies check - default: false - all: - name: "--all" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Disable all modules - default: false - clear-static-content: - name: "--clear-static-content" - shortcut: "-c" - accept_value: false - is_value_required: false - is_multiple: false - description: Clear generated static view files. Necessary, if the module(s) - have static view files - default: false - magento-init-params: - name: "--magento-init-params" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: 'Add to any command to customize Magento initialization parameters - For example: "MAGE_MODE=developer&MAGE_DIRS[base][path]=/var/www/example.com&MAGE_DIRS[cache][path]=/var/tmp/cache"' - default: - help: - name: "--help" - shortcut: "-h" - accept_value: false - is_value_required: false - is_multiple: false - description: Display this help message - default: false - quiet: - name: "--quiet" - shortcut: "-q" - accept_value: false - is_value_required: false - is_multiple: false - description: Do not output any message - default: false - verbose: - name: "--verbose" - shortcut: "-v|-vv|-vvv" - accept_value: false - is_value_required: false - is_multiple: false - description: 'Increase the verbosity of messages: 1 for normal output, 2 for - more verbose output and 3 for debug' - default: false - version: - name: "--version" - shortcut: "-V" - accept_value: false - is_value_required: false - is_multiple: false - description: Display this application version - default: false - ansi: - name: "--ansi" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Force ANSI output - default: false - no-ansi: - name: "--no-ansi" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Disable ANSI output - default: false - no-interaction: - name: "--no-interaction" - shortcut: "-n" - accept_value: false - is_value_required: false - is_multiple: false - description: Do not ask any interactive question - default: false - hidden: false -- name: module:enable - usage: - - module:enable [-f|--force] [--all] [-c|--clear-static-content] [--magento-init-params - MAGENTO-INIT-PARAMS] [--] [...] - description: Enables specified modules - help: Enables specified modules - definition: - arguments: - module: - name: module - is_required: false - is_array: true - description: Name of the module - default: [] - options: - force: - name: "--force" - shortcut: "-f" - accept_value: false - is_value_required: false - is_multiple: false - description: Bypass dependencies check - default: false - all: - name: "--all" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Enable all modules - default: false - clear-static-content: - name: "--clear-static-content" - shortcut: "-c" - accept_value: false - is_value_required: false - is_multiple: false - description: Clear generated static view files. Necessary, if the module(s) - have static view files - default: false - magento-init-params: - name: "--magento-init-params" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: 'Add to any command to customize Magento initialization parameters - For example: "MAGE_MODE=developer&MAGE_DIRS[base][path]=/var/www/example.com&MAGE_DIRS[cache][path]=/var/tmp/cache"' - default: - help: - name: "--help" - shortcut: "-h" - accept_value: false - is_value_required: false - is_multiple: false - description: Display this help message - default: false - quiet: - name: "--quiet" - shortcut: "-q" - accept_value: false - is_value_required: false - is_multiple: false - description: Do not output any message - default: false - verbose: - name: "--verbose" - shortcut: "-v|-vv|-vvv" - accept_value: false - is_value_required: false - is_multiple: false - description: 'Increase the verbosity of messages: 1 for normal output, 2 for - more verbose output and 3 for debug' - default: false - version: - name: "--version" - shortcut: "-V" - accept_value: false - is_value_required: false - is_multiple: false - description: Display this application version - default: false - ansi: - name: "--ansi" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Force ANSI output - default: false - no-ansi: - name: "--no-ansi" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Disable ANSI output - default: false - no-interaction: - name: "--no-interaction" - shortcut: "-n" - accept_value: false - is_value_required: false - is_multiple: false - description: Do not ask any interactive question - default: false - hidden: false -- name: module:status - usage: - - module:status [--enabled] [--disabled] [--magento-init-params MAGENTO-INIT-PARAMS] - [--] [] - description: Displays status of modules - help: Displays status of modules - definition: - arguments: - module: - name: module - is_required: false - is_array: false - description: Optional module name - default: - options: - enabled: - name: "--enabled" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Print only enabled modules - default: false - disabled: - name: "--disabled" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Print only disabled modules - default: false - magento-init-params: - name: "--magento-init-params" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: 'Add to any command to customize Magento initialization parameters - For example: "MAGE_MODE=developer&MAGE_DIRS[base][path]=/var/www/example.com&MAGE_DIRS[cache][path]=/var/tmp/cache"' - default: - help: - name: "--help" - shortcut: "-h" - accept_value: false - is_value_required: false - is_multiple: false - description: Display this help message - default: false - quiet: - name: "--quiet" - shortcut: "-q" - accept_value: false - is_value_required: false - is_multiple: false - description: Do not output any message - default: false - verbose: - name: "--verbose" - shortcut: "-v|-vv|-vvv" - accept_value: false - is_value_required: false - is_multiple: false - description: 'Increase the verbosity of messages: 1 for normal output, 2 for - more verbose output and 3 for debug' - default: false - version: - name: "--version" - shortcut: "-V" - accept_value: false - is_value_required: false - is_multiple: false - description: Display this application version - default: false - ansi: - name: "--ansi" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Force ANSI output - default: false - no-ansi: - name: "--no-ansi" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Disable ANSI output - default: false - no-interaction: - name: "--no-interaction" - shortcut: "-n" - accept_value: false - is_value_required: false - is_multiple: false - description: Do not ask any interactive question - default: false - hidden: false -- name: module:uninstall - usage: - - module:uninstall [-r|--remove-data] [--backup-code] [--backup-media] [--backup-db] - [--non-composer] [-c|--clear-static-content] [--magento-init-params MAGENTO-INIT-PARAMS] - [--] ... - description: Uninstalls modules installed by composer - help: Uninstalls modules installed by composer - definition: - arguments: - module: - name: module - is_required: true - is_array: true - description: Name of the module - default: [] - options: - remove-data: - name: "--remove-data" - shortcut: "-r" - accept_value: false - is_value_required: false - is_multiple: false - description: Remove data installed by module(s) - default: false - backup-code: - name: "--backup-code" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Take code and configuration files backup (excluding temporary - files) - default: false - backup-media: - name: "--backup-media" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Take media backup - default: false - backup-db: - name: "--backup-db" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Take complete database backup - default: false - non-composer: - name: "--non-composer" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: All modules, that will be past here will be non composer based - default: false - clear-static-content: - name: "--clear-static-content" - shortcut: "-c" - accept_value: false - is_value_required: false - is_multiple: false - description: Clear generated static view files. Necessary, if the module(s) - have static view files - default: false - magento-init-params: - name: "--magento-init-params" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: 'Add to any command to customize Magento initialization parameters - For example: "MAGE_MODE=developer&MAGE_DIRS[base][path]=/var/www/example.com&MAGE_DIRS[cache][path]=/var/tmp/cache"' - default: - help: - name: "--help" - shortcut: "-h" - accept_value: false - is_value_required: false - is_multiple: false - description: Display this help message - default: false - quiet: - name: "--quiet" - shortcut: "-q" - accept_value: false - is_value_required: false - is_multiple: false - description: Do not output any message - default: false - verbose: - name: "--verbose" - shortcut: "-v|-vv|-vvv" - accept_value: false - is_value_required: false - is_multiple: false - description: 'Increase the verbosity of messages: 1 for normal output, 2 for - more verbose output and 3 for debug' - default: false - version: - name: "--version" - shortcut: "-V" - accept_value: false - is_value_required: false - is_multiple: false - description: Display this application version - default: false - ansi: - name: "--ansi" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Force ANSI output - default: false - no-ansi: - name: "--no-ansi" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Disable ANSI output - default: false - no-interaction: - name: "--no-interaction" - shortcut: "-n" - accept_value: false - is_value_required: false - is_multiple: false - description: Do not ask any interactive question - default: false - hidden: false -- name: newrelic:create:deploy-marker - usage: - - newrelic:create:deploy-marker [] - description: Check the deploy queue for entries and create an appropriate deploy - marker. - help: Check the deploy queue for entries and create an appropriate deploy marker. - definition: - arguments: - message: - name: message - is_required: true - is_array: false - description: Deploy Message? - default: - change_log: - name: change_log - is_required: true - is_array: false - description: Change Log? - default: - user: - name: user - is_required: false - is_array: false - description: Deployment User - default: - options: - help: - name: "--help" - shortcut: "-h" - accept_value: false - is_value_required: false - is_multiple: false - description: Display this help message - default: false - quiet: - name: "--quiet" - shortcut: "-q" - accept_value: false - is_value_required: false - is_multiple: false - description: Do not output any message - default: false - verbose: - name: "--verbose" - shortcut: "-v|-vv|-vvv" - accept_value: false - is_value_required: false - is_multiple: false - description: 'Increase the verbosity of messages: 1 for normal output, 2 for - more verbose output and 3 for debug' - default: false - version: - name: "--version" - shortcut: "-V" - accept_value: false - is_value_required: false - is_multiple: false - description: Display this application version - default: false - ansi: - name: "--ansi" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Force ANSI output - default: false - no-ansi: - name: "--no-ansi" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Disable ANSI output - default: false - no-interaction: - name: "--no-interaction" - shortcut: "-n" - accept_value: false - is_value_required: false - is_multiple: false - description: Do not ask any interactive question - default: false - hidden: false -- name: queue:consumers:list - usage: - - queue:consumers:list - description: List of MessageQueue consumers - help: This command shows list of MessageQueue consumers. - definition: - arguments: [] - options: - help: - name: "--help" - shortcut: "-h" - accept_value: false - is_value_required: false - is_multiple: false - description: Display this help message - default: false - quiet: - name: "--quiet" - shortcut: "-q" - accept_value: false - is_value_required: false - is_multiple: false - description: Do not output any message - default: false - verbose: - name: "--verbose" - shortcut: "-v|-vv|-vvv" - accept_value: false - is_value_required: false - is_multiple: false - description: 'Increase the verbosity of messages: 1 for normal output, 2 for - more verbose output and 3 for debug' - default: false - version: - name: "--version" - shortcut: "-V" - accept_value: false - is_value_required: false - is_multiple: false - description: Display this application version - default: false - ansi: - name: "--ansi" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Force ANSI output - default: false - no-ansi: - name: "--no-ansi" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Disable ANSI output - default: false - no-interaction: - name: "--no-interaction" - shortcut: "-n" - accept_value: false - is_value_required: false - is_multiple: false - description: Do not ask any interactive question - default: false - hidden: false -- name: queue:consumers:start - usage: - - queue:consumers:start [--max-messages MAX-MESSAGES] [--batch-size BATCH-SIZE] - [--area-code AREA-CODE] [--pid-file-path PID-FILE-PATH] [--] - description: Start MessageQueue consumer - help: |- - This command starts MessageQueue consumer by its name. - - To start consumer which will process all queued messages and terminate execution: - - bin/magento queue:consumers:start someConsumer - - To specify the number of messages which should be processed by consumer before its termination: - - bin/magento queue:consumers:start someConsumer --max-messages=50 - - To specify the number of messages per batch for the batch consumer: - - bin/magento queue:consumers:start someConsumer --batch-size=500 - - To specify the preferred area: - - bin/magento queue:consumers:start someConsumer --area-code='adminhtml' - - To save PID enter path: - - bin/magento queue:consumers:start someConsumer --pid-file-path='/var/someConsumer.pid' - definition: - arguments: - consumer: - name: consumer - is_required: true - is_array: false - description: The name of the consumer to be started. - default: - options: - max-messages: - name: "--max-messages" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: The number of messages to be processed by the consumer before - process termination. If not specified - terminate after processing all queued - messages. - default: - batch-size: - name: "--batch-size" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: The number of messages per batch. Applicable for the batch consumer - only. - default: - area-code: - name: "--area-code" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: The preferred area (global, adminhtml, etc...) default is global. - default: - pid-file-path: - name: "--pid-file-path" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: The file path for saving PID - default: - help: - name: "--help" - shortcut: "-h" - accept_value: false - is_value_required: false - is_multiple: false - description: Display this help message - default: false - quiet: - name: "--quiet" - shortcut: "-q" - accept_value: false - is_value_required: false - is_multiple: false - description: Do not output any message - default: false - verbose: - name: "--verbose" - shortcut: "-v|-vv|-vvv" - accept_value: false - is_value_required: false - is_multiple: false - description: 'Increase the verbosity of messages: 1 for normal output, 2 for - more verbose output and 3 for debug' - default: false - version: - name: "--version" - shortcut: "-V" - accept_value: false - is_value_required: false - is_multiple: false - description: Display this application version - default: false - ansi: - name: "--ansi" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Force ANSI output - default: false - no-ansi: - name: "--no-ansi" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Disable ANSI output - default: false - no-interaction: - name: "--no-interaction" - shortcut: "-n" - accept_value: false - is_value_required: false - is_multiple: false - description: Do not ask any interactive question - default: false - hidden: false -- name: sampledata:deploy - usage: - - sampledata:deploy [--no-update] - description: Deploy sample data modules for composer-based Magento installations - help: Deploy sample data modules for composer-based Magento installations - definition: - arguments: [] - options: - no-update: - name: "--no-update" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Update composer.json without executing composer update - default: false - help: - name: "--help" - shortcut: "-h" - accept_value: false - is_value_required: false - is_multiple: false - description: Display this help message - default: false - quiet: - name: "--quiet" - shortcut: "-q" - accept_value: false - is_value_required: false - is_multiple: false - description: Do not output any message - default: false - verbose: - name: "--verbose" - shortcut: "-v|-vv|-vvv" - accept_value: false - is_value_required: false - is_multiple: false - description: 'Increase the verbosity of messages: 1 for normal output, 2 for - more verbose output and 3 for debug' - default: false - version: - name: "--version" - shortcut: "-V" - accept_value: false - is_value_required: false - is_multiple: false - description: Display this application version - default: false - ansi: - name: "--ansi" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Force ANSI output - default: false - no-ansi: - name: "--no-ansi" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Disable ANSI output - default: false - no-interaction: - name: "--no-interaction" - shortcut: "-n" - accept_value: false - is_value_required: false - is_multiple: false - description: Do not ask any interactive question - default: false - hidden: false -- name: sampledata:remove - usage: - - sampledata:remove [--no-update] - description: Remove all sample data packages from composer.json - help: Remove all sample data packages from composer.json - definition: - arguments: [] - options: - no-update: - name: "--no-update" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Update composer.json without executing composer update - default: false - help: - name: "--help" - shortcut: "-h" - accept_value: false - is_value_required: false - is_multiple: false - description: Display this help message - default: false - quiet: - name: "--quiet" - shortcut: "-q" - accept_value: false - is_value_required: false - is_multiple: false - description: Do not output any message - default: false - verbose: - name: "--verbose" - shortcut: "-v|-vv|-vvv" - accept_value: false - is_value_required: false - is_multiple: false - description: 'Increase the verbosity of messages: 1 for normal output, 2 for - more verbose output and 3 for debug' - default: false - version: - name: "--version" - shortcut: "-V" - accept_value: false - is_value_required: false - is_multiple: false - description: Display this application version - default: false - ansi: - name: "--ansi" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Force ANSI output - default: false - no-ansi: - name: "--no-ansi" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Disable ANSI output - default: false - no-interaction: - name: "--no-interaction" - shortcut: "-n" - accept_value: false - is_value_required: false - is_multiple: false - description: Do not ask any interactive question - default: false - hidden: false -- name: sampledata:reset - usage: - - sampledata:reset - description: Reset all sample data modules for re-installation - help: Reset all sample data modules for re-installation - definition: - arguments: [] - options: - help: - name: "--help" - shortcut: "-h" - accept_value: false - is_value_required: false - is_multiple: false - description: Display this help message - default: false - quiet: - name: "--quiet" - shortcut: "-q" - accept_value: false - is_value_required: false - is_multiple: false - description: Do not output any message - default: false - verbose: - name: "--verbose" - shortcut: "-v|-vv|-vvv" - accept_value: false - is_value_required: false - is_multiple: false - description: 'Increase the verbosity of messages: 1 for normal output, 2 for - more verbose output and 3 for debug' - default: false - version: - name: "--version" - shortcut: "-V" - accept_value: false - is_value_required: false - is_multiple: false - description: Display this application version - default: false - ansi: - name: "--ansi" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Force ANSI output - default: false - no-ansi: - name: "--no-ansi" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Disable ANSI output - default: false - no-interaction: - name: "--no-interaction" - shortcut: "-n" - accept_value: false - is_value_required: false - is_multiple: false - description: Do not ask any interactive question - default: false - hidden: false -- name: setup:backup - usage: - - setup:backup [--code] [--media] [--db] [--magento-init-params MAGENTO-INIT-PARAMS] - description: Takes backup of Magento Application code base, media and database - help: Takes backup of Magento Application code base, media and database - definition: - arguments: [] - options: - code: - name: "--code" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Take code and configuration files backup (excluding temporary - files) - default: false - media: - name: "--media" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Take media backup - default: false - db: - name: "--db" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Take complete database backup - default: false - magento-init-params: - name: "--magento-init-params" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: 'Add to any command to customize Magento initialization parameters - For example: "MAGE_MODE=developer&MAGE_DIRS[base][path]=/var/www/example.com&MAGE_DIRS[cache][path]=/var/tmp/cache"' - default: - help: - name: "--help" - shortcut: "-h" - accept_value: false - is_value_required: false - is_multiple: false - description: Display this help message - default: false - quiet: - name: "--quiet" - shortcut: "-q" - accept_value: false - is_value_required: false - is_multiple: false - description: Do not output any message - default: false - verbose: - name: "--verbose" - shortcut: "-v|-vv|-vvv" - accept_value: false - is_value_required: false - is_multiple: false - description: 'Increase the verbosity of messages: 1 for normal output, 2 for - more verbose output and 3 for debug' - default: false - version: - name: "--version" - shortcut: "-V" - accept_value: false - is_value_required: false - is_multiple: false - description: Display this application version - default: false - ansi: - name: "--ansi" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Force ANSI output - default: false - no-ansi: - name: "--no-ansi" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Disable ANSI output - default: false - no-interaction: - name: "--no-interaction" - shortcut: "-n" - accept_value: false - is_value_required: false - is_multiple: false - description: Do not ask any interactive question - default: false - hidden: false -- name: setup:config:set - usage: - - setup:config:set [--amqp-host AMQP-HOST] [--amqp-port AMQP-PORT] [--amqp-user - AMQP-USER] [--amqp-password AMQP-PASSWORD] [--amqp-virtualhost AMQP-VIRTUALHOST] - [--amqp-ssl AMQP-SSL] [--amqp-ssl-options AMQP-SSL-OPTIONS] [--enable-debug-logging - ENABLE-DEBUG-LOGGING] [--enable-syslog-logging ENABLE-SYSLOG-LOGGING] [--backend-frontname - BACKEND-FRONTNAME] [--key KEY] [--db-host DB-HOST] [--db-name DB-NAME] [--db-user - DB-USER] [--db-engine DB-ENGINE] [--db-password DB-PASSWORD] [--db-prefix DB-PREFIX] - [--db-model DB-MODEL] [--db-init-statements DB-INIT-STATEMENTS] [-s|--skip-db-validation] - [--http-cache-hosts HTTP-CACHE-HOSTS] [--session-save SESSION-SAVE] [--session-save-redis-host - SESSION-SAVE-REDIS-HOST] [--session-save-redis-port SESSION-SAVE-REDIS-PORT] [--session-save-redis-password - SESSION-SAVE-REDIS-PASSWORD] [--session-save-redis-timeout SESSION-SAVE-REDIS-TIMEOUT] - [--session-save-redis-persistent-id SESSION-SAVE-REDIS-PERSISTENT-ID] [--session-save-redis-db - SESSION-SAVE-REDIS-DB] [--session-save-redis-compression-threshold SESSION-SAVE-REDIS-COMPRESSION-THRESHOLD] - [--session-save-redis-compression-lib SESSION-SAVE-REDIS-COMPRESSION-LIB] [--session-save-redis-log-level - SESSION-SAVE-REDIS-LOG-LEVEL] [--session-save-redis-max-concurrency SESSION-SAVE-REDIS-MAX-CONCURRENCY] - [--session-save-redis-break-after-frontend SESSION-SAVE-REDIS-BREAK-AFTER-FRONTEND] - [--session-save-redis-break-after-adminhtml SESSION-SAVE-REDIS-BREAK-AFTER-ADMINHTML] - [--session-save-redis-first-lifetime SESSION-SAVE-REDIS-FIRST-LIFETIME] [--session-save-redis-bot-first-lifetime - SESSION-SAVE-REDIS-BOT-FIRST-LIFETIME] [--session-save-redis-bot-lifetime SESSION-SAVE-REDIS-BOT-LIFETIME] - [--session-save-redis-disable-locking SESSION-SAVE-REDIS-DISABLE-LOCKING] [--session-save-redis-min-lifetime - SESSION-SAVE-REDIS-MIN-LIFETIME] [--session-save-redis-max-lifetime SESSION-SAVE-REDIS-MAX-LIFETIME] - [--session-save-redis-sentinel-master SESSION-SAVE-REDIS-SENTINEL-MASTER] [--session-save-redis-sentinel-servers - SESSION-SAVE-REDIS-SENTINEL-SERVERS] [--session-save-redis-sentinel-verify-master - SESSION-SAVE-REDIS-SENTINEL-VERIFY-MASTER] [--session-save-redis-sentinel-connect-retires - SESSION-SAVE-REDIS-SENTINEL-CONNECT-RETIRES] [--cache-backend CACHE-BACKEND] [--cache-backend-redis-server - CACHE-BACKEND-REDIS-SERVER] [--cache-backend-redis-db CACHE-BACKEND-REDIS-DB] - [--cache-backend-redis-port CACHE-BACKEND-REDIS-PORT] [--cache-backend-redis-password - CACHE-BACKEND-REDIS-PASSWORD] [--cache-backend-redis-compress-data CACHE-BACKEND-REDIS-COMPRESS-DATA] - [--cache-backend-redis-compression-lib CACHE-BACKEND-REDIS-COMPRESSION-LIB] [--cache-id-prefix - CACHE-ID-PREFIX] [--page-cache PAGE-CACHE] [--page-cache-redis-server PAGE-CACHE-REDIS-SERVER] - [--page-cache-redis-db PAGE-CACHE-REDIS-DB] [--page-cache-redis-port PAGE-CACHE-REDIS-PORT] - [--page-cache-redis-password PAGE-CACHE-REDIS-PASSWORD] [--page-cache-redis-compress-data - PAGE-CACHE-REDIS-COMPRESS-DATA] [--page-cache-redis-compression-lib PAGE-CACHE-REDIS-COMPRESSION-LIB] - [--page-cache-id-prefix PAGE-CACHE-ID-PREFIX] [--lock-provider LOCK-PROVIDER] - [--lock-db-prefix LOCK-DB-PREFIX] [--lock-zookeeper-host LOCK-ZOOKEEPER-HOST] - [--lock-zookeeper-path LOCK-ZOOKEEPER-PATH] [--lock-file-path LOCK-FILE-PATH] - [--magento-init-params MAGENTO-INIT-PARAMS] - description: Creates or modifies the deployment configuration - help: Creates or modifies the deployment configuration - definition: - arguments: [] - options: - amqp-host: - name: "--amqp-host" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: Amqp server host - default: '' - amqp-port: - name: "--amqp-port" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: Amqp server port - default: '5672' - amqp-user: - name: "--amqp-user" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: Amqp server username - default: '' - amqp-password: - name: "--amqp-password" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: Amqp server password - default: '' - amqp-virtualhost: - name: "--amqp-virtualhost" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: Amqp virtualhost - default: "/" - amqp-ssl: - name: "--amqp-ssl" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: Amqp SSL - default: '' - amqp-ssl-options: - name: "--amqp-ssl-options" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: Amqp SSL Options (JSON) - default: '' - enable-debug-logging: - name: "--enable-debug-logging" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: Enable debug logging - default: - enable-syslog-logging: - name: "--enable-syslog-logging" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: Enable syslog logging - default: - backend-frontname: - name: "--backend-frontname" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: Backend frontname (will be autogenerated if missing) - default: - key: - name: "--key" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: Encryption key - default: - db-host: - name: "--db-host" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: Database server host - default: - db-name: - name: "--db-name" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: Database name - default: - db-user: - name: "--db-user" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: Database server username - default: - db-engine: - name: "--db-engine" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: Database server engine - default: innodb - db-password: - name: "--db-password" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: Database server password - default: - db-prefix: - name: "--db-prefix" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: Database table prefix - default: - db-model: - name: "--db-model" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: Database type - default: mysql4 - db-init-statements: - name: "--db-init-statements" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: Database initial set of commands - default: SET NAMES utf8; - skip-db-validation: - name: "--skip-db-validation" - shortcut: "-s" - accept_value: false - is_value_required: false - is_multiple: false - description: If specified, then db connection validation will be skipped - default: false - http-cache-hosts: - name: "--http-cache-hosts" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: http Cache hosts - default: - session-save: - name: "--session-save" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: Session save handler - default: - session-save-redis-host: - name: "--session-save-redis-host" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: Fully qualified host name, IP address, or absolute path if using - UNIX sockets - default: - session-save-redis-port: - name: "--session-save-redis-port" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: Redis server listen port - default: - session-save-redis-password: - name: "--session-save-redis-password" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: Redis server password - default: - session-save-redis-timeout: - name: "--session-save-redis-timeout" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: Connection timeout, in seconds - default: - session-save-redis-persistent-id: - name: "--session-save-redis-persistent-id" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: Unique string to enable persistent connections - default: - session-save-redis-db: - name: "--session-save-redis-db" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: Redis database number - default: - session-save-redis-compression-threshold: - name: "--session-save-redis-compression-threshold" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: Redis compression threshold - default: - session-save-redis-compression-lib: - name: "--session-save-redis-compression-lib" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: Redis compression library. Values: gzip (default), lzf, lz4, - snappy - default: - session-save-redis-log-level: - name: "--session-save-redis-log-level" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: 'Redis log level. Values: 0 (least verbose) to 7 (most verbose)' - default: - session-save-redis-max-concurrency: - name: "--session-save-redis-max-concurrency" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: Maximum number of processes that can wait for a lock on one session - default: - session-save-redis-break-after-frontend: - name: "--session-save-redis-break-after-frontend" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: Number of seconds to wait before trying to break a lock for frontend - session - default: - session-save-redis-break-after-adminhtml: - name: "--session-save-redis-break-after-adminhtml" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: Number of seconds to wait before trying to break a lock for Admin - session - default: - session-save-redis-first-lifetime: - name: "--session-save-redis-first-lifetime" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: Lifetime, in seconds, of session for non-bots on the first write - (use 0 to disable) - default: - session-save-redis-bot-first-lifetime: - name: "--session-save-redis-bot-first-lifetime" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: Lifetime, in seconds, of session for bots on the first write - (use 0 to disable) - default: - session-save-redis-bot-lifetime: - name: "--session-save-redis-bot-lifetime" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: Lifetime of session for bots on subsequent writes (use 0 to disable) - default: - session-save-redis-disable-locking: - name: "--session-save-redis-disable-locking" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: Redis disable locking. Values: false (default), true - default: - session-save-redis-min-lifetime: - name: "--session-save-redis-min-lifetime" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: Redis min session lifetime, in seconds - default: - session-save-redis-max-lifetime: - name: "--session-save-redis-max-lifetime" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: Redis max session lifetime, in seconds - default: - session-save-redis-sentinel-master: - name: "--session-save-redis-sentinel-master" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: Redis Sentinel master - default: - session-save-redis-sentinel-servers: - name: "--session-save-redis-sentinel-servers" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: Redis Sentinel servers, comma separated - default: - session-save-redis-sentinel-verify-master: - name: "--session-save-redis-sentinel-verify-master" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: 'Redis Sentinel verify master. Values: false (default), true' - default: - session-save-redis-sentinel-connect-retires: - name: "--session-save-redis-sentinel-connect-retires" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: Redis Sentinel connect retries. - default: - cache-backend: - name: "--cache-backend" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: Default cache handler - default: - cache-backend-redis-server: - name: "--cache-backend-redis-server" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: Redis server - default: - cache-backend-redis-db: - name: "--cache-backend-redis-db" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: Database number for the cache - default: - cache-backend-redis-port: - name: "--cache-backend-redis-port" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: Redis server listen port - default: - cache-backend-redis-password: - name: "--cache-backend-redis-password" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: Redis server password - default: - cache-backend-redis-compress-data: - name: "--cache-backend-redis-compress-data" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: Set to 0 to disable compression (default is 1, enabled) - default: - cache-backend-redis-compression-lib: - name: "--cache-backend-redis-compression-lib" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: Compression lib to use [snappy,lzf,l4z,zstd,gzip] (leave blank - to determine automatically) - default: - cache-id-prefix: - name: "--cache-id-prefix" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: ID prefix for cache keys - default: - page-cache: - name: "--page-cache" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: Default cache handler - default: - page-cache-redis-server: - name: "--page-cache-redis-server" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: Redis server - default: - page-cache-redis-db: - name: "--page-cache-redis-db" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: Database number for the cache - default: - page-cache-redis-port: - name: "--page-cache-redis-port" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: Redis server listen port - default: - page-cache-redis-password: - name: "--page-cache-redis-password" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: Redis server password - default: - page-cache-redis-compress-data: - name: "--page-cache-redis-compress-data" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: Set to 1 to compress the full page cache (use 0 to disable) - default: - page-cache-redis-compression-lib: - name: "--page-cache-redis-compression-lib" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: Compression library to use [snappy,lzf,l4z,zstd,gzip] (leave - blank to determine automatically) - default: - page-cache-id-prefix: - name: "--page-cache-id-prefix" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: ID prefix for cache keys - default: - lock-provider: - name: "--lock-provider" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: Lock provider name - default: db - lock-db-prefix: - name: "--lock-db-prefix" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: Installation specific lock prefix to avoid lock conflicts - default: - lock-zookeeper-host: - name: "--lock-zookeeper-host" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: 'Host and port to connect to Zookeeper cluster. For example: - 127.0.0.1:2181' - default: - lock-zookeeper-path: - name: "--lock-zookeeper-path" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: 'The path where Zookeeper will save locks. The default path is: - /magento/locks' - default: - lock-file-path: - name: "--lock-file-path" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: The path where file locks will be saved. - default: - magento-init-params: - name: "--magento-init-params" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: 'Add to any command to customize Magento initialization parameters - For example: "MAGE_MODE=developer&MAGE_DIRS[base][path]=/var/www/example.com&MAGE_DIRS[cache][path]=/var/tmp/cache"' - default: - help: - name: "--help" - shortcut: "-h" - accept_value: false - is_value_required: false - is_multiple: false - description: Display this help message - default: false - quiet: - name: "--quiet" - shortcut: "-q" - accept_value: false - is_value_required: false - is_multiple: false - description: Do not output any message - default: false - verbose: - name: "--verbose" - shortcut: "-v|-vv|-vvv" - accept_value: false - is_value_required: false - is_multiple: false - description: 'Increase the verbosity of messages: 1 for normal output, 2 for - more verbose output and 3 for debug' - default: false - version: - name: "--version" - shortcut: "-V" - accept_value: false - is_value_required: false - is_multiple: false - description: Display this application version - default: false - ansi: - name: "--ansi" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Force ANSI output - default: false - no-ansi: - name: "--no-ansi" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Disable ANSI output - default: false - no-interaction: - name: "--no-interaction" - shortcut: "-n" - accept_value: false - is_value_required: false - is_multiple: false - description: Do not ask any interactive question - default: false - hidden: false -- name: setup:cron:run - usage: - - setup:cron:run [--magento-init-params MAGENTO-INIT-PARAMS] - description: Runs cron job scheduled for setup application - help: Runs cron job scheduled for setup application - definition: - arguments: [] - options: - magento-init-params: - name: "--magento-init-params" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: 'Add to any command to customize Magento initialization parameters - For example: "MAGE_MODE=developer&MAGE_DIRS[base][path]=/var/www/example.com&MAGE_DIRS[cache][path]=/var/tmp/cache"' - default: - help: - name: "--help" - shortcut: "-h" - accept_value: false - is_value_required: false - is_multiple: false - description: Display this help message - default: false - quiet: - name: "--quiet" - shortcut: "-q" - accept_value: false - is_value_required: false - is_multiple: false - description: Do not output any message - default: false - verbose: - name: "--verbose" - shortcut: "-v|-vv|-vvv" - accept_value: false - is_value_required: false - is_multiple: false - description: 'Increase the verbosity of messages: 1 for normal output, 2 for - more verbose output and 3 for debug' - default: false - version: - name: "--version" - shortcut: "-V" - accept_value: false - is_value_required: false - is_multiple: false - description: Display this application version - default: false - ansi: - name: "--ansi" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Force ANSI output - default: false - no-ansi: - name: "--no-ansi" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Disable ANSI output - default: false - no-interaction: - name: "--no-interaction" - shortcut: "-n" - accept_value: false - is_value_required: false - is_multiple: false - description: Do not ask any interactive question - default: false - hidden: false -- name: setup:db-data:upgrade - usage: - - setup:db-data:upgrade [--magento-init-params MAGENTO-INIT-PARAMS] - description: Installs and upgrades data in the DB - help: Installs and upgrades data in the DB - definition: - arguments: [] - options: - magento-init-params: - name: "--magento-init-params" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: 'Add to any command to customize Magento initialization parameters - For example: "MAGE_MODE=developer&MAGE_DIRS[base][path]=/var/www/example.com&MAGE_DIRS[cache][path]=/var/tmp/cache"' - default: - help: - name: "--help" - shortcut: "-h" - accept_value: false - is_value_required: false - is_multiple: false - description: Display this help message - default: false - quiet: - name: "--quiet" - shortcut: "-q" - accept_value: false - is_value_required: false - is_multiple: false - description: Do not output any message - default: false - verbose: - name: "--verbose" - shortcut: "-v|-vv|-vvv" - accept_value: false - is_value_required: false - is_multiple: false - description: 'Increase the verbosity of messages: 1 for normal output, 2 for - more verbose output and 3 for debug' - default: false - version: - name: "--version" - shortcut: "-V" - accept_value: false - is_value_required: false - is_multiple: false - description: Display this application version - default: false - ansi: - name: "--ansi" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Force ANSI output - default: false - no-ansi: - name: "--no-ansi" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Disable ANSI output - default: false - no-interaction: - name: "--no-interaction" - shortcut: "-n" - accept_value: false - is_value_required: false - is_multiple: false - description: Do not ask any interactive question - default: false - hidden: false -- name: setup:db-declaration:generate-patch - usage: - - setup:db-declaration:generate-patch [--revertable [REVERTABLE]] [--type [TYPE]] - [--] - description: Generate patch and put it in specific folder. - help: Generate patch and put it in specific folder. - definition: - arguments: - module: - name: module - is_required: true - is_array: false - description: Module name - default: - patch: - name: patch - is_required: true - is_array: false - description: Patch name - default: - options: - revertable: - name: "--revertable" - shortcut: '' - accept_value: true - is_value_required: false - is_multiple: false - description: Check whether patch is revertable or not. - default: false - type: - name: "--type" - shortcut: '' - accept_value: true - is_value_required: false - is_multiple: false - description: Find out what type of patch should be generated. - default: data - help: - name: "--help" - shortcut: "-h" - accept_value: false - is_value_required: false - is_multiple: false - description: Display this help message - default: false - quiet: - name: "--quiet" - shortcut: "-q" - accept_value: false - is_value_required: false - is_multiple: false - description: Do not output any message - default: false - verbose: - name: "--verbose" - shortcut: "-v|-vv|-vvv" - accept_value: false - is_value_required: false - is_multiple: false - description: 'Increase the verbosity of messages: 1 for normal output, 2 for - more verbose output and 3 for debug' - default: false - version: - name: "--version" - shortcut: "-V" - accept_value: false - is_value_required: false - is_multiple: false - description: Display this application version - default: false - ansi: - name: "--ansi" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Force ANSI output - default: false - no-ansi: - name: "--no-ansi" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Disable ANSI output - default: false - no-interaction: - name: "--no-interaction" - shortcut: "-n" - accept_value: false - is_value_required: false - is_multiple: false - description: Do not ask any interactive question - default: false - hidden: false -- name: setup:db-declaration:generate-whitelist - usage: - - setup:db-declaration:generate-whitelist [--module-name [MODULE-NAME]] - description: Generate whitelist of tables and columns that are allowed to be edited - by declaration installer - help: Generate whitelist of tables and columns that are allowed to be edited by - declaration installer - definition: - arguments: [] - options: - module-name: - name: "--module-name" - shortcut: '' - accept_value: true - is_value_required: false - is_multiple: false - description: Name of the module where whitelist will be generated - default: all - help: - name: "--help" - shortcut: "-h" - accept_value: false - is_value_required: false - is_multiple: false - description: Display this help message - default: false - quiet: - name: "--quiet" - shortcut: "-q" - accept_value: false - is_value_required: false - is_multiple: false - description: Do not output any message - default: false - verbose: - name: "--verbose" - shortcut: "-v|-vv|-vvv" - accept_value: false - is_value_required: false - is_multiple: false - description: 'Increase the verbosity of messages: 1 for normal output, 2 for - more verbose output and 3 for debug' - default: false - version: - name: "--version" - shortcut: "-V" - accept_value: false - is_value_required: false - is_multiple: false - description: Display this application version - default: false - ansi: - name: "--ansi" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Force ANSI output - default: false - no-ansi: - name: "--no-ansi" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Disable ANSI output - default: false - no-interaction: - name: "--no-interaction" - shortcut: "-n" - accept_value: false - is_value_required: false - is_multiple: false - description: Do not ask any interactive question - default: false - hidden: false -- name: setup:db-schema:upgrade - usage: - - setup:db-schema:upgrade [--convert-old-scripts [CONVERT-OLD-SCRIPTS]] [--magento-init-params - MAGENTO-INIT-PARAMS] - description: Installs and upgrades the DB schema - help: Installs and upgrades the DB schema - definition: - arguments: [] - options: - convert-old-scripts: - name: "--convert-old-scripts" - shortcut: '' - accept_value: true - is_value_required: false - is_multiple: false - description: Allows to convert old scripts (InstallSchema, UpgradeSchema) - to db_schema.xml format - default: false - magento-init-params: - name: "--magento-init-params" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: 'Add to any command to customize Magento initialization parameters - For example: "MAGE_MODE=developer&MAGE_DIRS[base][path]=/var/www/example.com&MAGE_DIRS[cache][path]=/var/tmp/cache"' - default: - help: - name: "--help" - shortcut: "-h" - accept_value: false - is_value_required: false - is_multiple: false - description: Display this help message - default: false - quiet: - name: "--quiet" - shortcut: "-q" - accept_value: false - is_value_required: false - is_multiple: false - description: Do not output any message - default: false - verbose: - name: "--verbose" - shortcut: "-v|-vv|-vvv" - accept_value: false - is_value_required: false - is_multiple: false - description: 'Increase the verbosity of messages: 1 for normal output, 2 for - more verbose output and 3 for debug' - default: false - version: - name: "--version" - shortcut: "-V" - accept_value: false - is_value_required: false - is_multiple: false - description: Display this application version - default: false - ansi: - name: "--ansi" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Force ANSI output - default: false - no-ansi: - name: "--no-ansi" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Disable ANSI output - default: false - no-interaction: - name: "--no-interaction" - shortcut: "-n" - accept_value: false - is_value_required: false - is_multiple: false - description: Do not ask any interactive question - default: false - hidden: false -- name: setup:db:status - usage: - - setup:db:status [--magento-init-params MAGENTO-INIT-PARAMS] - description: Checks if DB schema or data requires upgrade - help: Checks if DB schema or data requires upgrade - definition: - arguments: [] - options: - magento-init-params: - name: "--magento-init-params" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: 'Add to any command to customize Magento initialization parameters - For example: "MAGE_MODE=developer&MAGE_DIRS[base][path]=/var/www/example.com&MAGE_DIRS[cache][path]=/var/tmp/cache"' - default: - help: - name: "--help" - shortcut: "-h" - accept_value: false - is_value_required: false - is_multiple: false - description: Display this help message - default: false - quiet: - name: "--quiet" - shortcut: "-q" - accept_value: false - is_value_required: false - is_multiple: false - description: Do not output any message - default: false - verbose: - name: "--verbose" - shortcut: "-v|-vv|-vvv" - accept_value: false - is_value_required: false - is_multiple: false - description: 'Increase the verbosity of messages: 1 for normal output, 2 for - more verbose output and 3 for debug' - default: false - version: - name: "--version" - shortcut: "-V" - accept_value: false - is_value_required: false - is_multiple: false - description: Display this application version - default: false - ansi: - name: "--ansi" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Force ANSI output - default: false - no-ansi: - name: "--no-ansi" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Disable ANSI output - default: false - no-interaction: - name: "--no-interaction" - shortcut: "-n" - accept_value: false - is_value_required: false - is_multiple: false - description: Do not ask any interactive question - default: false - hidden: false -- name: setup:di:compile - usage: - - setup:di:compile - description: Generates DI configuration and all missing classes that can be auto-generated - help: Generates DI configuration and all missing classes that can be auto-generated - definition: - arguments: [] - options: - help: - name: "--help" - shortcut: "-h" - accept_value: false - is_value_required: false - is_multiple: false - description: Display this help message - default: false - quiet: - name: "--quiet" - shortcut: "-q" - accept_value: false - is_value_required: false - is_multiple: false - description: Do not output any message - default: false - verbose: - name: "--verbose" - shortcut: "-v|-vv|-vvv" - accept_value: false - is_value_required: false - is_multiple: false - description: 'Increase the verbosity of messages: 1 for normal output, 2 for - more verbose output and 3 for debug' - default: false - version: - name: "--version" - shortcut: "-V" - accept_value: false - is_value_required: false - is_multiple: false - description: Display this application version - default: false - ansi: - name: "--ansi" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Force ANSI output - default: false - no-ansi: - name: "--no-ansi" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Disable ANSI output - default: false - no-interaction: - name: "--no-interaction" - shortcut: "-n" - accept_value: false - is_value_required: false - is_multiple: false - description: Do not ask any interactive question - default: false - hidden: false -- name: setup:install - usage: - - setup:install [--amqp-host AMQP-HOST] [--amqp-port AMQP-PORT] [--amqp-user AMQP-USER] - [--amqp-password AMQP-PASSWORD] [--amqp-virtualhost AMQP-VIRTUALHOST] [--amqp-ssl - AMQP-SSL] [--amqp-ssl-options AMQP-SSL-OPTIONS] [--enable-debug-logging ENABLE-DEBUG-LOGGING] - [--enable-syslog-logging ENABLE-SYSLOG-LOGGING] [--backend-frontname BACKEND-FRONTNAME] - [--key KEY] [--db-host DB-HOST] [--db-name DB-NAME] [--db-user DB-USER] [--db-engine - DB-ENGINE] [--db-password DB-PASSWORD] [--db-prefix DB-PREFIX] [--db-model DB-MODEL] - [--db-init-statements DB-INIT-STATEMENTS] [-s|--skip-db-validation] [--http-cache-hosts - HTTP-CACHE-HOSTS] [--session-save SESSION-SAVE] [--session-save-redis-host SESSION-SAVE-REDIS-HOST] - [--session-save-redis-port SESSION-SAVE-REDIS-PORT] [--session-save-redis-password - SESSION-SAVE-REDIS-PASSWORD] [--session-save-redis-timeout SESSION-SAVE-REDIS-TIMEOUT] - [--session-save-redis-persistent-id SESSION-SAVE-REDIS-PERSISTENT-ID] [--session-save-redis-db - SESSION-SAVE-REDIS-DB] [--session-save-redis-compression-threshold SESSION-SAVE-REDIS-COMPRESSION-THRESHOLD] - [--session-save-redis-compression-lib SESSION-SAVE-REDIS-COMPRESSION-LIB] [--session-save-redis-log-level - SESSION-SAVE-REDIS-LOG-LEVEL] [--session-save-redis-max-concurrency SESSION-SAVE-REDIS-MAX-CONCURRENCY] - [--session-save-redis-break-after-frontend SESSION-SAVE-REDIS-BREAK-AFTER-FRONTEND] - [--session-save-redis-break-after-adminhtml SESSION-SAVE-REDIS-BREAK-AFTER-ADMINHTML] - [--session-save-redis-first-lifetime SESSION-SAVE-REDIS-FIRST-LIFETIME] [--session-save-redis-bot-first-lifetime - SESSION-SAVE-REDIS-BOT-FIRST-LIFETIME] [--session-save-redis-bot-lifetime SESSION-SAVE-REDIS-BOT-LIFETIME] - [--session-save-redis-disable-locking SESSION-SAVE-REDIS-DISABLE-LOCKING] [--session-save-redis-min-lifetime - SESSION-SAVE-REDIS-MIN-LIFETIME] [--session-save-redis-max-lifetime SESSION-SAVE-REDIS-MAX-LIFETIME] - [--session-save-redis-sentinel-master SESSION-SAVE-REDIS-SENTINEL-MASTER] [--session-save-redis-sentinel-servers - SESSION-SAVE-REDIS-SENTINEL-SERVERS] [--session-save-redis-sentinel-verify-master - SESSION-SAVE-REDIS-SENTINEL-VERIFY-MASTER] [--session-save-redis-sentinel-connect-retires - SESSION-SAVE-REDIS-SENTINEL-CONNECT-RETIRES] [--cache-backend CACHE-BACKEND] [--cache-backend-redis-server - CACHE-BACKEND-REDIS-SERVER] [--cache-backend-redis-db CACHE-BACKEND-REDIS-DB] - [--cache-backend-redis-port CACHE-BACKEND-REDIS-PORT] [--cache-backend-redis-password - CACHE-BACKEND-REDIS-PASSWORD] [--cache-backend-redis-compress-data CACHE-BACKEND-REDIS-COMPRESS-DATA] - [--cache-backend-redis-compression-lib CACHE-BACKEND-REDIS-COMPRESSION-LIB] [--cache-id-prefix - CACHE-ID-PREFIX] [--page-cache PAGE-CACHE] [--page-cache-redis-server PAGE-CACHE-REDIS-SERVER] - [--page-cache-redis-db PAGE-CACHE-REDIS-DB] [--page-cache-redis-port PAGE-CACHE-REDIS-PORT] - [--page-cache-redis-password PAGE-CACHE-REDIS-PASSWORD] [--page-cache-redis-compress-data - PAGE-CACHE-REDIS-COMPRESS-DATA] [--page-cache-redis-compression-lib PAGE-CACHE-REDIS-COMPRESSION-LIB] - [--page-cache-id-prefix PAGE-CACHE-ID-PREFIX] [--lock-provider LOCK-PROVIDER] - [--lock-db-prefix LOCK-DB-PREFIX] [--lock-zookeeper-host LOCK-ZOOKEEPER-HOST] - [--lock-zookeeper-path LOCK-ZOOKEEPER-PATH] [--lock-file-path LOCK-FILE-PATH] - [--base-url BASE-URL] [--language LANGUAGE] [--timezone TIMEZONE] [--currency - CURRENCY] [--use-rewrites USE-REWRITES] [--use-secure USE-SECURE] [--base-url-secure - BASE-URL-SECURE] [--use-secure-admin USE-SECURE-ADMIN] [--admin-use-security-key - ADMIN-USE-SECURITY-KEY] [--admin-user [ADMIN-USER]] [--admin-password [ADMIN-PASSWORD]] - [--admin-email [ADMIN-EMAIL]] [--admin-firstname [ADMIN-FIRSTNAME]] [--admin-lastname - [ADMIN-LASTNAME]] [--cleanup-database] [--sales-order-increment-prefix SALES-ORDER-INCREMENT-PREFIX] - [--use-sample-data] [--enable-modules [ENABLE-MODULES]] [--disable-modules [DISABLE-MODULES]] - [--convert-old-scripts [CONVERT-OLD-SCRIPTS]] [-i|--interactive] [--safe-mode - [SAFE-MODE]] [--data-restore [DATA-RESTORE]] [--dry-run [DRY-RUN]] [--magento-init-params - MAGENTO-INIT-PARAMS] - description: Installs the Magento application - help: Installs the Magento application - definition: - arguments: [] - options: - amqp-host: - name: "--amqp-host" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: Amqp server host - default: '' - amqp-port: - name: "--amqp-port" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: Amqp server port - default: '5672' - amqp-user: - name: "--amqp-user" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: Amqp server username - default: '' - amqp-password: - name: "--amqp-password" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: Amqp server password - default: '' - amqp-virtualhost: - name: "--amqp-virtualhost" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: Amqp virtualhost - default: "/" - amqp-ssl: - name: "--amqp-ssl" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: Amqp SSL - default: '' - amqp-ssl-options: - name: "--amqp-ssl-options" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: Amqp SSL Options (JSON) - default: '' - enable-debug-logging: - name: "--enable-debug-logging" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: Enable debug logging - default: - enable-syslog-logging: - name: "--enable-syslog-logging" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: Enable syslog logging - default: - backend-frontname: - name: "--backend-frontname" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: Backend frontname (will be autogenerated if missing) - default: - key: - name: "--key" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: Encryption key - default: - db-host: - name: "--db-host" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: Database server host - default: - db-name: - name: "--db-name" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: Database name - default: - db-user: - name: "--db-user" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: Database server username - default: - db-engine: - name: "--db-engine" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: Database server engine - default: innodb - db-password: - name: "--db-password" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: Database server password - default: - db-prefix: - name: "--db-prefix" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: Database table prefix - default: - db-model: - name: "--db-model" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: Database type - default: mysql4 - db-init-statements: - name: "--db-init-statements" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: Database initial set of commands - default: SET NAMES utf8; - skip-db-validation: - name: "--skip-db-validation" - shortcut: "-s" - accept_value: false - is_value_required: false - is_multiple: false - description: If specified, then db connection validation will be skipped - default: false - http-cache-hosts: - name: "--http-cache-hosts" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: http Cache hosts - default: - session-save: - name: "--session-save" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: Session save handler - default: - session-save-redis-host: - name: "--session-save-redis-host" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: Fully qualified host name, IP address, or absolute path if using - UNIX sockets - default: - session-save-redis-port: - name: "--session-save-redis-port" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: Redis server listen port - default: - session-save-redis-password: - name: "--session-save-redis-password" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: Redis server password - default: - session-save-redis-timeout: - name: "--session-save-redis-timeout" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: Connection timeout, in seconds - default: - session-save-redis-persistent-id: - name: "--session-save-redis-persistent-id" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: Unique string to enable persistent connections - default: - session-save-redis-db: - name: "--session-save-redis-db" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: Redis database number - default: - session-save-redis-compression-threshold: - name: "--session-save-redis-compression-threshold" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: Redis compression threshold - default: - session-save-redis-compression-lib: - name: "--session-save-redis-compression-lib" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: Redis compression library. Values: gzip (default), lzf, lz4, - snappy - default: - session-save-redis-log-level: - name: "--session-save-redis-log-level" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: 'Redis log level. Values: 0 (least verbose) to 7 (most verbose)' - default: - session-save-redis-max-concurrency: - name: "--session-save-redis-max-concurrency" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: Maximum number of processes that can wait for a lock on one session - default: - session-save-redis-break-after-frontend: - name: "--session-save-redis-break-after-frontend" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: Number of seconds to wait before trying to break a lock for frontend - session - default: - session-save-redis-break-after-adminhtml: - name: "--session-save-redis-break-after-adminhtml" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: Number of seconds to wait before trying to break a lock for Admin - session - default: - session-save-redis-first-lifetime: - name: "--session-save-redis-first-lifetime" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: Lifetime, in seconds, of session for non-bots on the first write - (use 0 to disable) - default: - session-save-redis-bot-first-lifetime: - name: "--session-save-redis-bot-first-lifetime" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: Lifetime, in seconds, of session for bots on the first write - (use 0 to disable) - default: - session-save-redis-bot-lifetime: - name: "--session-save-redis-bot-lifetime" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: Lifetime of session for bots on subsequent writes (use 0 to disable) - default: - session-save-redis-disable-locking: - name: "--session-save-redis-disable-locking" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: Redis disable locking. Values: false (default), true - default: - session-save-redis-min-lifetime: - name: "--session-save-redis-min-lifetime" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: Redis min session lifetime, in seconds - default: - session-save-redis-max-lifetime: - name: "--session-save-redis-max-lifetime" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: Redis max session lifetime, in seconds - default: - session-save-redis-sentinel-master: - name: "--session-save-redis-sentinel-master" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: Redis Sentinel master - default: - session-save-redis-sentinel-servers: - name: "--session-save-redis-sentinel-servers" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: Redis Sentinel servers, comma separated - default: - session-save-redis-sentinel-verify-master: - name: "--session-save-redis-sentinel-verify-master" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: 'Redis Sentinel verify master. Values: false (default), true' - default: - session-save-redis-sentinel-connect-retires: - name: "--session-save-redis-sentinel-connect-retires" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: Redis Sentinel connect retries. - default: - cache-backend: - name: "--cache-backend" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: Default cache handler - default: - cache-backend-redis-server: - name: "--cache-backend-redis-server" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: Redis server - default: - cache-backend-redis-db: - name: "--cache-backend-redis-db" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: Database number for the cache - default: - cache-backend-redis-port: - name: "--cache-backend-redis-port" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: Redis server listen port - default: - cache-backend-redis-password: - name: "--cache-backend-redis-password" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: Redis server password - default: - cache-backend-redis-compress-data: - name: "--cache-backend-redis-compress-data" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: Set to 0 to disable compression (default is 1, enabled) - default: - cache-backend-redis-compression-lib: - name: "--cache-backend-redis-compression-lib" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: Compression lib to use [snappy,lzf,l4z,zstd,gzip] (leave blank - to determine automatically) - default: - cache-id-prefix: - name: "--cache-id-prefix" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: ID prefix for cache keys - default: - page-cache: - name: "--page-cache" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: Default cache handler - default: - page-cache-redis-server: - name: "--page-cache-redis-server" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: Redis server - default: - page-cache-redis-db: - name: "--page-cache-redis-db" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: Database number for the cache - default: - page-cache-redis-port: - name: "--page-cache-redis-port" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: Redis server listen port - default: - page-cache-redis-password: - name: "--page-cache-redis-password" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: Redis server password - default: - page-cache-redis-compress-data: - name: "--page-cache-redis-compress-data" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: Set to 1 to compress the full page cache (use 0 to disable) - default: - page-cache-redis-compression-lib: - name: "--page-cache-redis-compression-lib" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: Compression library to use [snappy,lzf,l4z,zstd,gzip] (leave - blank to determine automatically) - default: - page-cache-id-prefix: - name: "--page-cache-id-prefix" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: ID prefix for cache keys - default: - lock-provider: - name: "--lock-provider" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: Lock provider name - default: db - lock-db-prefix: - name: "--lock-db-prefix" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: Installation specific lock prefix to avoid lock conflicts - default: - lock-zookeeper-host: - name: "--lock-zookeeper-host" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: 'Host and port to connect to Zookeeper cluster. For example: - 127.0.0.1:2181' - default: - lock-zookeeper-path: - name: "--lock-zookeeper-path" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: 'The path where Zookeeper will save locks. The default path is: - /magento/locks' - default: - lock-file-path: - name: "--lock-file-path" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: The path where file locks will be saved. - default: - base-url: - name: "--base-url" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: URL the store is supposed to be available at. Deprecated, use - config:set with path web/unsecure/base_url - default: - language: - name: "--language" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: Default language code. Deprecated, use config:set with path general/locale/code - default: - timezone: - name: "--timezone" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: Default time zone code. Deprecated, use config:set with path - general/locale/timezone - default: - currency: - name: "--currency" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: Default currency code. Deprecated, use config:set with path currency/options/base, - currency/options/default and currency/options/allow - default: - use-rewrites: - name: "--use-rewrites" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: Use rewrites. Deprecated, use config:set with path web/seo/use_rewrites - default: - use-secure: - name: "--use-secure" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: Use secure URLs. Enable this option only if SSL is available. - Deprecated, use config:set with path web/secure/use_in_frontend - default: - base-url-secure: - name: "--base-url-secure" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: Base URL for SSL connection. Deprecated, use config:set with - path web/secure/base_url - default: - use-secure-admin: - name: "--use-secure-admin" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: Run admin interface with SSL. Deprecated, use config:set with - path web/secure/use_in_adminhtml - default: - admin-use-security-key: - name: "--admin-use-security-key" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: Whether to use a "security key" feature in Magento Admin URLs - and forms. Deprecated, use config:set with path admin/security/use_form_key - default: - admin-user: - name: "--admin-user" - shortcut: '' - accept_value: true - is_value_required: false - is_multiple: false - description: Admin user - default: - admin-password: - name: "--admin-password" - shortcut: '' - accept_value: true - is_value_required: false - is_multiple: false - description: Admin password - default: - admin-email: - name: "--admin-email" - shortcut: '' - accept_value: true - is_value_required: false - is_multiple: false - description: Admin email - default: - admin-firstname: - name: "--admin-firstname" - shortcut: '' - accept_value: true - is_value_required: false - is_multiple: false - description: Admin first name - default: - admin-lastname: - name: "--admin-lastname" - shortcut: '' - accept_value: true - is_value_required: false - is_multiple: false - description: Admin last name - default: - cleanup-database: - name: "--cleanup-database" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Cleanup the database before installation - default: false - sales-order-increment-prefix: - name: "--sales-order-increment-prefix" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: Sales order number prefix - default: - use-sample-data: - name: "--use-sample-data" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Use sample data - default: false - enable-modules: - name: "--enable-modules" - shortcut: '' - accept_value: true - is_value_required: false - is_multiple: false - description: List of comma-separated module names. That must be included during - installation. Available magic param "all". - default: - disable-modules: - name: "--disable-modules" - shortcut: '' - accept_value: true - is_value_required: false - is_multiple: false - description: List of comma-separated module names. That must be avoided during - installation. Available magic param "all". - default: - convert-old-scripts: - name: "--convert-old-scripts" - shortcut: '' - accept_value: true - is_value_required: false - is_multiple: false - description: Allows to convert old scripts (InstallSchema, UpgradeSchema) - to db_schema.xml format - default: false - interactive: - name: "--interactive" - shortcut: "-i" - accept_value: false - is_value_required: false - is_multiple: false - description: Interactive Magento installation - default: false - safe-mode: - name: "--safe-mode" - shortcut: '' - accept_value: true - is_value_required: false - is_multiple: false - description: Safe installation of Magento with dumps on destructive operations, - like column removal - default: - data-restore: - name: "--data-restore" - shortcut: '' - accept_value: true - is_value_required: false - is_multiple: false - description: Restore removed data from dumps - default: - dry-run: - name: "--dry-run" - shortcut: '' - accept_value: true - is_value_required: false - is_multiple: false - description: Magento Installation will be run in dry-run mode - default: false - magento-init-params: - name: "--magento-init-params" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: 'Add to any command to customize Magento initialization parameters - For example: "MAGE_MODE=developer&MAGE_DIRS[base][path]=/var/www/example.com&MAGE_DIRS[cache][path]=/var/tmp/cache"' - default: - help: - name: "--help" - shortcut: "-h" - accept_value: false - is_value_required: false - is_multiple: false - description: Display this help message - default: false - quiet: - name: "--quiet" - shortcut: "-q" - accept_value: false - is_value_required: false - is_multiple: false - description: Do not output any message - default: false - verbose: - name: "--verbose" - shortcut: "-v|-vv|-vvv" - accept_value: false - is_value_required: false - is_multiple: false - description: 'Increase the verbosity of messages: 1 for normal output, 2 for - more verbose output and 3 for debug' - default: false - version: - name: "--version" - shortcut: "-V" - accept_value: false - is_value_required: false - is_multiple: false - description: Display this application version - default: false - ansi: - name: "--ansi" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Force ANSI output - default: false - no-ansi: - name: "--no-ansi" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Disable ANSI output - default: false - no-interaction: - name: "--no-interaction" - shortcut: "-n" - accept_value: false - is_value_required: false - is_multiple: false - description: Do not ask any interactive question - default: false - hidden: false -- name: setup:performance:generate-fixtures - usage: - - setup:performance:generate-fixtures [-s|--skip-reindex] [--] - description: Generates fixtures - help: Generates fixtures - definition: - arguments: - profile: - name: profile - is_required: true - is_array: false - description: Path to profile configuration file - default: - options: - skip-reindex: - name: "--skip-reindex" - shortcut: "-s" - accept_value: false - is_value_required: false - is_multiple: false - description: Skip reindex - default: false - help: - name: "--help" - shortcut: "-h" - accept_value: false - is_value_required: false - is_multiple: false - description: Display this help message - default: false - quiet: - name: "--quiet" - shortcut: "-q" - accept_value: false - is_value_required: false - is_multiple: false - description: Do not output any message - default: false - verbose: - name: "--verbose" - shortcut: "-v|-vv|-vvv" - accept_value: false - is_value_required: false - is_multiple: false - description: 'Increase the verbosity of messages: 1 for normal output, 2 for - more verbose output and 3 for debug' - default: false - version: - name: "--version" - shortcut: "-V" - accept_value: false - is_value_required: false - is_multiple: false - description: Display this application version - default: false - ansi: - name: "--ansi" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Force ANSI output - default: false - no-ansi: - name: "--no-ansi" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Disable ANSI output - default: false - no-interaction: - name: "--no-interaction" - shortcut: "-n" - accept_value: false - is_value_required: false - is_multiple: false - description: Do not ask any interactive question - default: false - hidden: false -- name: setup:rollback - usage: - - setup:rollback [-c|--code-file CODE-FILE] [-m|--media-file MEDIA-FILE] [-d|--db-file - DB-FILE] [--magento-init-params MAGENTO-INIT-PARAMS] - description: Rolls back Magento Application codebase, media and database - help: Rolls back Magento Application codebase, media and database - definition: - arguments: [] - options: - code-file: - name: "--code-file" - shortcut: "-c" - accept_value: true - is_value_required: true - is_multiple: false - description: Basename of the code backup file in var/backups - default: - media-file: - name: "--media-file" - shortcut: "-m" - accept_value: true - is_value_required: true - is_multiple: false - description: Basename of the media backup file in var/backups - default: - db-file: - name: "--db-file" - shortcut: "-d" - accept_value: true - is_value_required: true - is_multiple: false - description: Basename of the db backup file in var/backups - default: - magento-init-params: - name: "--magento-init-params" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: 'Add to any command to customize Magento initialization parameters - For example: "MAGE_MODE=developer&MAGE_DIRS[base][path]=/var/www/example.com&MAGE_DIRS[cache][path]=/var/tmp/cache"' - default: - help: - name: "--help" - shortcut: "-h" - accept_value: false - is_value_required: false - is_multiple: false - description: Display this help message - default: false - quiet: - name: "--quiet" - shortcut: "-q" - accept_value: false - is_value_required: false - is_multiple: false - description: Do not output any message - default: false - verbose: - name: "--verbose" - shortcut: "-v|-vv|-vvv" - accept_value: false - is_value_required: false - is_multiple: false - description: 'Increase the verbosity of messages: 1 for normal output, 2 for - more verbose output and 3 for debug' - default: false - version: - name: "--version" - shortcut: "-V" - accept_value: false - is_value_required: false - is_multiple: false - description: Display this application version - default: false - ansi: - name: "--ansi" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Force ANSI output - default: false - no-ansi: - name: "--no-ansi" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Disable ANSI output - default: false - no-interaction: - name: "--no-interaction" - shortcut: "-n" - accept_value: false - is_value_required: false - is_multiple: false - description: Do not ask any interactive question - default: false - hidden: false -- name: setup:static-content:deploy - usage: - - setup:static-content:deploy [-f|--force] [-s|--strategy [STRATEGY]] [-a|--area - [AREA]] [--exclude-area [EXCLUDE-AREA]] [-t|--theme [THEME]] [--exclude-theme - [EXCLUDE-THEME]] [-l|--language [LANGUAGE]] [--exclude-language [EXCLUDE-LANGUAGE]] - [-j|--jobs [JOBS]] [--max-execution-time [MAX-EXECUTION-TIME]] [--symlink-locale] - [--content-version CONTENT-VERSION] [--refresh-content-version-only] [--no-javascript] - [--no-css] [--no-less] [--no-images] [--no-fonts] [--no-html] [--no-misc] [--no-html-minify] - [--] [...] - description: Deploys static view files - help: Deploys static view files - definition: - arguments: - languages: - name: languages - is_required: false - is_array: true - description: Space-separated list of ISO-639 language codes for which to output - static view files. - default: [] - options: - force: - name: "--force" - shortcut: "-f" - accept_value: false - is_value_required: false - is_multiple: false - description: Deploy files in any mode. - default: false - strategy: - name: "--strategy" - shortcut: "-s" - accept_value: true - is_value_required: false - is_multiple: false - description: Deploy files using specified strategy. - default: quick - area: - name: "--area" - shortcut: "-a" - accept_value: true - is_value_required: false - is_multiple: true - description: Generate files only for the specified areas. - default: - - all - exclude-area: - name: "--exclude-area" - shortcut: '' - accept_value: true - is_value_required: false - is_multiple: true - description: Do not generate files for the specified areas. - default: - - none - theme: - name: "--theme" - shortcut: "-t" - accept_value: true - is_value_required: false - is_multiple: true - description: Generate static view files for only the specified themes. - default: - - all - exclude-theme: - name: "--exclude-theme" - shortcut: '' - accept_value: true - is_value_required: false - is_multiple: true - description: Do not generate files for the specified themes. - default: - - none - language: - name: "--language" - shortcut: "-l" - accept_value: true - is_value_required: false - is_multiple: true - description: Generate files only for the specified languages. - default: - - all - exclude-language: - name: "--exclude-language" - shortcut: '' - accept_value: true - is_value_required: false - is_multiple: true - description: Do not generate files for the specified languages. - default: - - none - jobs: - name: "--jobs" - shortcut: "-j" - accept_value: true - is_value_required: false - is_multiple: false - description: Enable parallel processing using the specified number of jobs. - default: 0 - max-execution-time: - name: "--max-execution-time" - shortcut: '' - accept_value: true - is_value_required: false - is_multiple: false - description: The maximum expected execution time of deployment static process - (in seconds). - default: 400 - symlink-locale: - name: "--symlink-locale" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Create symlinks for the files of those locales, which are passed - for deployment, but have no customizations. - default: false - content-version: - name: "--content-version" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: Custom version of static content can be used if running deployment - on multiple nodes to ensure that static content version is identical and - caching works properly. - default: - refresh-content-version-only: - name: "--refresh-content-version-only" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Refreshing the version of static content only can be used to - refresh static content in browser cache and CDN cache. - default: false - no-javascript: - name: "--no-javascript" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Do not deploy JavaScript files. - default: false - no-css: - name: "--no-css" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Do not deploy CSS files. - default: false - no-less: - name: "--no-less" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Do not deploy LESS files. - default: false - no-images: - name: "--no-images" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Do not deploy images. - default: false - no-fonts: - name: "--no-fonts" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Do not deploy font files. - default: false - no-html: - name: "--no-html" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Do not deploy HTML files. - default: false - no-misc: - name: "--no-misc" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Do not deploy files of other types (.md, .jbf, .csv, etc.). - default: false - no-html-minify: - name: "--no-html-minify" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Do not minify HTML files. - default: false - help: - name: "--help" - shortcut: "-h" - accept_value: false - is_value_required: false - is_multiple: false - description: Display this help message - default: false - quiet: - name: "--quiet" - shortcut: "-q" - accept_value: false - is_value_required: false - is_multiple: false - description: Do not output any message - default: false - verbose: - name: "--verbose" - shortcut: "-v|-vv|-vvv" - accept_value: false - is_value_required: false - is_multiple: false - description: 'Increase the verbosity of messages: 1 for normal output, 2 for - more verbose output and 3 for debug' - default: false - version: - name: "--version" - shortcut: "-V" - accept_value: false - is_value_required: false - is_multiple: false - description: Display this application version - default: false - ansi: - name: "--ansi" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Force ANSI output - default: false - no-ansi: - name: "--no-ansi" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Disable ANSI output - default: false - no-interaction: - name: "--no-interaction" - shortcut: "-n" - accept_value: false - is_value_required: false - is_multiple: false - description: Do not ask any interactive question - default: false - hidden: false -- name: setup:store-config:set - usage: - - setup:store-config:set [--base-url BASE-URL] [--language LANGUAGE] [--timezone - TIMEZONE] [--currency CURRENCY] [--use-rewrites USE-REWRITES] [--use-secure USE-SECURE] - [--base-url-secure BASE-URL-SECURE] [--use-secure-admin USE-SECURE-ADMIN] [--admin-use-security-key - ADMIN-USE-SECURITY-KEY] [--magento-init-params MAGENTO-INIT-PARAMS] - description: Installs the store configuration. Deprecated since 2.2.0. Use config:set - instead - help: Installs the store configuration. Deprecated since 2.2.0. Use config:set instead - definition: - arguments: [] - options: - base-url: - name: "--base-url" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: URL the store is supposed to be available at. Deprecated, use - config:set with path web/unsecure/base_url - default: - language: - name: "--language" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: Default language code. Deprecated, use config:set with path general/locale/code - default: - timezone: - name: "--timezone" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: Default time zone code. Deprecated, use config:set with path - general/locale/timezone - default: - currency: - name: "--currency" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: Default currency code. Deprecated, use config:set with path currency/options/base, - currency/options/default and currency/options/allow - default: - use-rewrites: - name: "--use-rewrites" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: Use rewrites. Deprecated, use config:set with path web/seo/use_rewrites - default: - use-secure: - name: "--use-secure" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: Use secure URLs. Enable this option only if SSL is available. - Deprecated, use config:set with path web/secure/use_in_frontend - default: - base-url-secure: - name: "--base-url-secure" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: Base URL for SSL connection. Deprecated, use config:set with - path web/secure/base_url - default: - use-secure-admin: - name: "--use-secure-admin" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: Run admin interface with SSL. Deprecated, use config:set with - path web/secure/use_in_adminhtml - default: - admin-use-security-key: - name: "--admin-use-security-key" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: Whether to use a "security key" feature in Magento Admin URLs - and forms. Deprecated, use config:set with path admin/security/use_form_key - default: - magento-init-params: - name: "--magento-init-params" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: 'Add to any command to customize Magento initialization parameters - For example: "MAGE_MODE=developer&MAGE_DIRS[base][path]=/var/www/example.com&MAGE_DIRS[cache][path]=/var/tmp/cache"' - default: - help: - name: "--help" - shortcut: "-h" - accept_value: false - is_value_required: false - is_multiple: false - description: Display this help message - default: false - quiet: - name: "--quiet" - shortcut: "-q" - accept_value: false - is_value_required: false - is_multiple: false - description: Do not output any message - default: false - verbose: - name: "--verbose" - shortcut: "-v|-vv|-vvv" - accept_value: false - is_value_required: false - is_multiple: false - description: 'Increase the verbosity of messages: 1 for normal output, 2 for - more verbose output and 3 for debug' - default: false - version: - name: "--version" - shortcut: "-V" - accept_value: false - is_value_required: false - is_multiple: false - description: Display this application version - default: false - ansi: - name: "--ansi" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Force ANSI output - default: false - no-ansi: - name: "--no-ansi" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Disable ANSI output - default: false - no-interaction: - name: "--no-interaction" - shortcut: "-n" - accept_value: false - is_value_required: false - is_multiple: false - description: Do not ask any interactive question - default: false - hidden: false -- name: setup:uninstall - usage: - - setup:uninstall [--magento-init-params MAGENTO-INIT-PARAMS] - description: Uninstalls the Magento application - help: Uninstalls the Magento application - definition: - arguments: [] - options: - magento-init-params: - name: "--magento-init-params" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: 'Add to any command to customize Magento initialization parameters - For example: "MAGE_MODE=developer&MAGE_DIRS[base][path]=/var/www/example.com&MAGE_DIRS[cache][path]=/var/tmp/cache"' - default: - help: - name: "--help" - shortcut: "-h" - accept_value: false - is_value_required: false - is_multiple: false - description: Display this help message - default: false - quiet: - name: "--quiet" - shortcut: "-q" - accept_value: false - is_value_required: false - is_multiple: false - description: Do not output any message - default: false - verbose: - name: "--verbose" - shortcut: "-v|-vv|-vvv" - accept_value: false - is_value_required: false - is_multiple: false - description: 'Increase the verbosity of messages: 1 for normal output, 2 for - more verbose output and 3 for debug' - default: false - version: - name: "--version" - shortcut: "-V" - accept_value: false - is_value_required: false - is_multiple: false - description: Display this application version - default: false - ansi: - name: "--ansi" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Force ANSI output - default: false - no-ansi: - name: "--no-ansi" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Disable ANSI output - default: false - no-interaction: - name: "--no-interaction" - shortcut: "-n" - accept_value: false - is_value_required: false - is_multiple: false - description: Do not ask any interactive question - default: false - hidden: false -- name: setup:upgrade - usage: - - setup:upgrade [--keep-generated] [--convert-old-scripts [CONVERT-OLD-SCRIPTS]] - [--safe-mode [SAFE-MODE]] [--data-restore [DATA-RESTORE]] [--dry-run [DRY-RUN]] - [--magento-init-params MAGENTO-INIT-PARAMS] - description: Upgrades the Magento application, DB data, and schema - help: Upgrades the Magento application, DB data, and schema - definition: - arguments: [] - options: - keep-generated: - name: "--keep-generated" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Prevents generated files from being deleted. We discourage using - this option except when deploying to production. Consult your system integrator - or administrator for more information. - default: false - convert-old-scripts: - name: "--convert-old-scripts" - shortcut: '' - accept_value: true - is_value_required: false - is_multiple: false - description: Allows to convert old scripts (InstallSchema, UpgradeSchema) - to db_schema.xml format - default: false - safe-mode: - name: "--safe-mode" - shortcut: '' - accept_value: true - is_value_required: false - is_multiple: false - description: Safe installation of Magento with dumps on destructive operations, - like column removal - default: - data-restore: - name: "--data-restore" - shortcut: '' - accept_value: true - is_value_required: false - is_multiple: false - description: Restore removed data from dumps - default: - dry-run: - name: "--dry-run" - shortcut: '' - accept_value: true - is_value_required: false - is_multiple: false - description: Magento Installation will be run in dry-run mode - default: false - magento-init-params: - name: "--magento-init-params" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: 'Add to any command to customize Magento initialization parameters - For example: "MAGE_MODE=developer&MAGE_DIRS[base][path]=/var/www/example.com&MAGE_DIRS[cache][path]=/var/tmp/cache"' - default: - help: - name: "--help" - shortcut: "-h" - accept_value: false - is_value_required: false - is_multiple: false - description: Display this help message - default: false - quiet: - name: "--quiet" - shortcut: "-q" - accept_value: false - is_value_required: false - is_multiple: false - description: Do not output any message - default: false - verbose: - name: "--verbose" - shortcut: "-v|-vv|-vvv" - accept_value: false - is_value_required: false - is_multiple: false - description: 'Increase the verbosity of messages: 1 for normal output, 2 for - more verbose output and 3 for debug' - default: false - version: - name: "--version" - shortcut: "-V" - accept_value: false - is_value_required: false - is_multiple: false - description: Display this application version - default: false - ansi: - name: "--ansi" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Force ANSI output - default: false - no-ansi: - name: "--no-ansi" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Disable ANSI output - default: false - no-interaction: - name: "--no-interaction" - shortcut: "-n" - accept_value: false - is_value_required: false - is_multiple: false - description: Do not ask any interactive question - default: false - hidden: false -- name: store:list - usage: - - store:list - description: Displays the list of stores - help: Displays the list of stores - definition: - arguments: [] - options: - help: - name: "--help" - shortcut: "-h" - accept_value: false - is_value_required: false - is_multiple: false - description: Display this help message - default: false - quiet: - name: "--quiet" - shortcut: "-q" - accept_value: false - is_value_required: false - is_multiple: false - description: Do not output any message - default: false - verbose: - name: "--verbose" - shortcut: "-v|-vv|-vvv" - accept_value: false - is_value_required: false - is_multiple: false - description: 'Increase the verbosity of messages: 1 for normal output, 2 for - more verbose output and 3 for debug' - default: false - version: - name: "--version" - shortcut: "-V" - accept_value: false - is_value_required: false - is_multiple: false - description: Display this application version - default: false - ansi: - name: "--ansi" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Force ANSI output - default: false - no-ansi: - name: "--no-ansi" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Disable ANSI output - default: false - no-interaction: - name: "--no-interaction" - shortcut: "-n" - accept_value: false - is_value_required: false - is_multiple: false - description: Do not ask any interactive question - default: false - hidden: false -- name: store:website:list - usage: - - store:website:list - description: Displays the list of websites - help: Displays the list of websites - definition: - arguments: [] - options: - help: - name: "--help" - shortcut: "-h" - accept_value: false - is_value_required: false - is_multiple: false - description: Display this help message - default: false - quiet: - name: "--quiet" - shortcut: "-q" - accept_value: false - is_value_required: false - is_multiple: false - description: Do not output any message - default: false - verbose: - name: "--verbose" - shortcut: "-v|-vv|-vvv" - accept_value: false - is_value_required: false - is_multiple: false - description: 'Increase the verbosity of messages: 1 for normal output, 2 for - more verbose output and 3 for debug' - default: false - version: - name: "--version" - shortcut: "-V" - accept_value: false - is_value_required: false - is_multiple: false - description: Display this application version - default: false - ansi: - name: "--ansi" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Force ANSI output - default: false - no-ansi: - name: "--no-ansi" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Disable ANSI output - default: false - no-interaction: - name: "--no-interaction" - shortcut: "-n" - accept_value: false - is_value_required: false - is_multiple: false - description: Do not ask any interactive question - default: false - hidden: false -- name: theme:uninstall - usage: - - theme:uninstall [--backup-code] [-c|--clear-static-content] [--] ... - description: Uninstalls theme - help: Uninstalls theme - definition: - arguments: - theme: - name: theme - is_required: true - is_array: true - description: Path of the theme. Theme path should be specified as full path - which is area/vendor/name. For example, frontend/Magento/blank - default: [] - options: - backup-code: - name: "--backup-code" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Take code backup (excluding temporary files) - default: false - clear-static-content: - name: "--clear-static-content" - shortcut: "-c" - accept_value: false - is_value_required: false - is_multiple: false - description: Clear generated static view files. - default: false - help: - name: "--help" - shortcut: "-h" - accept_value: false - is_value_required: false - is_multiple: false - description: Display this help message - default: false - quiet: - name: "--quiet" - shortcut: "-q" - accept_value: false - is_value_required: false - is_multiple: false - description: Do not output any message - default: false - verbose: - name: "--verbose" - shortcut: "-v|-vv|-vvv" - accept_value: false - is_value_required: false - is_multiple: false - description: 'Increase the verbosity of messages: 1 for normal output, 2 for - more verbose output and 3 for debug' - default: false - version: - name: "--version" - shortcut: "-V" - accept_value: false - is_value_required: false - is_multiple: false - description: Display this application version - default: false - ansi: - name: "--ansi" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Force ANSI output - default: false - no-ansi: - name: "--no-ansi" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Disable ANSI output - default: false - no-interaction: - name: "--no-interaction" - shortcut: "-n" - accept_value: false - is_value_required: false - is_multiple: false - description: Do not ask any interactive question - default: false - hidden: false -- name: varnish:vcl:generate - usage: - - varnish:vcl:generate [--access-list ACCESS-LIST] [--backend-host BACKEND-HOST] - [--backend-port BACKEND-PORT] [--export-version EXPORT-VERSION] [--grace-period - GRACE-PERIOD] [--output-file OUTPUT-FILE] - description: Generates Varnish VCL and echos it to the command line - help: Generates Varnish VCL and echos it to the command line - definition: - arguments: [] - options: - access-list: - name: "--access-list" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: true - description: IPs access list that can purge Varnish - default: - - localhost - backend-host: - name: "--backend-host" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: Host of the web backend - default: localhost - backend-port: - name: "--backend-port" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: Port of the web backend - default: 8080 - export-version: - name: "--export-version" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: The version of Varnish file - default: '4' - grace-period: - name: "--grace-period" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: Grace period in seconds - default: 300 - output-file: - name: "--output-file" - shortcut: '' - accept_value: true - is_value_required: true - is_multiple: false - description: Path to the file to write vcl - default: - help: - name: "--help" - shortcut: "-h" - accept_value: false - is_value_required: false - is_multiple: false - description: Display this help message - default: false - quiet: - name: "--quiet" - shortcut: "-q" - accept_value: false - is_value_required: false - is_multiple: false - description: Do not output any message - default: false - verbose: - name: "--verbose" - shortcut: "-v|-vv|-vvv" - accept_value: false - is_value_required: false - is_multiple: false - description: 'Increase the verbosity of messages: 1 for normal output, 2 for - more verbose output and 3 for debug' - default: false - version: - name: "--version" - shortcut: "-V" - accept_value: false - is_value_required: false - is_multiple: false - description: Display this application version - default: false - ansi: - name: "--ansi" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Force ANSI output - default: false - no-ansi: - name: "--no-ansi" - shortcut: '' - accept_value: false - is_value_required: false - is_multiple: false - description: Disable ANSI output - default: false - no-interaction: - name: "--no-interaction" - shortcut: "-n" - accept_value: false - is_value_required: false - is_multiple: false - description: Do not ask any interactive question - default: false - hidden: false -namespaces: -- id: _global - commands: - - help - - list -- id: admin - commands: - - admin:user:create - - admin:user:unlock -- id: app - commands: - - app:config:dump - - app:config:import - - app:config:status -- id: cache - commands: - - cache:clean - - cache:disable - - cache:enable - - cache:flush - - cache:status -- id: catalog - commands: - - catalog:images:resize - - catalog:product:attributes:cleanup -- id: config - commands: - - config:sensitive:set - - config:set - - config:show -- id: cron - commands: - - cron:install - - cron:remove - - cron:run -- id: customer - commands: - - customer:hash:upgrade -- id: deploy - commands: - - deploy:mode:set - - deploy:mode:show -- id: dev - commands: - - dev:di:info - - dev:profiler:disable - - dev:profiler:enable - - dev:query-log:disable - - dev:query-log:enable - - dev:source-theme:deploy - - dev:template-hints:disable - - dev:template-hints:enable - - dev:tests:run - - dev:urn-catalog:generate - - dev:xml:convert -- id: encryption - commands: - - encryption:payment-data:update -- id: i18n - commands: - - i18n:collect-phrases - - i18n:pack - - i18n:uninstall -- id: indexer - commands: - - indexer:info - - indexer:reindex - - indexer:reset - - indexer:set-dimensions-mode - - indexer:set-mode - - indexer:show-dimensions-mode - - indexer:show-mode - - indexer:status -- id: info - commands: - - info:adminuri - - info:backups:list - - info:currency:list - - info:dependencies:show-framework - - info:dependencies:show-modules - - info:dependencies:show-modules-circular - - info:language:list - - info:timezone:list -- id: maintenance - commands: - - maintenance:allow-ips - - maintenance:disable - - maintenance:enable - - maintenance:status -- id: module - commands: - - module:disable - - module:enable - - module:status - - module:uninstall -- id: newrelic - commands: - - newrelic:create:deploy-marker -- id: queue - commands: - - queue:consumers:list - - queue:consumers:start -- id: sampledata - commands: - - sampledata:deploy - - sampledata:remove - - sampledata:reset -- id: setup - commands: - - setup:backup - - setup:config:set - - setup:cron:run - - setup:db-data:upgrade - - setup:db-declaration:generate-patch - - setup:db-declaration:generate-whitelist - - setup:db-schema:upgrade - - setup:db:status - - setup:di:compile - - setup:install - - setup:performance:generate-fixtures - - setup:rollback - - setup:static-content:deploy - - setup:store-config:set - - setup:uninstall - - setup:upgrade -- id: store - commands: - - store:list - - store:website:list -- id: theme - commands: - - theme:uninstall -- id: varnish - commands: - - varnish:vcl:generate diff --git a/composer.json b/composer.json index 293cb06ef403c..4a179f480c9b0 100644 --- a/composer.json +++ b/composer.json @@ -52,7 +52,7 @@ "symfony/process": "~4.1.0|~4.2.0|~4.3.0", "tedivm/jshrink": "~1.3.0", "tubalmartin/cssmin": "4.1.1", - "webonyx/graphql-php": "^0.12.6", + "webonyx/graphql-php": "^0.13.8", "zendframework/zend-captcha": "^2.7.1", "zendframework/zend-code": "~3.3.0", "zendframework/zend-config": "^2.6.0", @@ -87,7 +87,7 @@ "allure-framework/allure-phpunit": "~1.2.0", "friendsofphp/php-cs-fixer": "~2.14.0", "lusitanian/oauth": "~0.8.10", - "magento/magento-coding-standard": "~3.0.0", + "magento/magento-coding-standard": "~4.0.0", "magento/magento2-functional-testing-framework": "2.4.3", "pdepend/pdepend": "2.5.2", "phpmd/phpmd": "@stable", @@ -147,6 +147,7 @@ "magento/module-currency-symbol": "*", "magento/module-customer": "*", "magento/module-customer-analytics": "*", + "magento/module-customer-downloadable-graph-ql": "*", "magento/module-customer-import-export": "*", "magento/module-deploy": "*", "magento/module-developer": "*", diff --git a/composer.lock b/composer.lock index f67eb50675314..cb4f029182219 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "a4299e3f4f0d4dd4915f37a5dde8e2ed", + "content-hash": "fe4a8dce06cfede9180e774e43149550", "packages": [ { "name": "braintree/braintree_php", @@ -2670,24 +2670,31 @@ }, { "name": "webonyx/graphql-php", - "version": "v0.12.6", + "version": "v0.13.8", "source": { "type": "git", "url": "https://github.com/webonyx/graphql-php.git", - "reference": "4c545e5ec4fc37f6eb36c19f5a0e7feaf5979c95" + "reference": "6829ae58f4c59121df1f86915fb9917a2ec595e8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/webonyx/graphql-php/zipball/4c545e5ec4fc37f6eb36c19f5a0e7feaf5979c95", - "reference": "4c545e5ec4fc37f6eb36c19f5a0e7feaf5979c95", + "url": "https://api.github.com/repos/webonyx/graphql-php/zipball/6829ae58f4c59121df1f86915fb9917a2ec595e8", + "reference": "6829ae58f4c59121df1f86915fb9917a2ec595e8", "shasum": "" }, "require": { + "ext-json": "*", "ext-mbstring": "*", - "php": ">=5.6" + "php": "^7.1||^8.0" }, "require-dev": { - "phpunit/phpunit": "^4.8", + "doctrine/coding-standard": "^6.0", + "phpbench/phpbench": "^0.14.0", + "phpstan/phpstan": "^0.11.4", + "phpstan/phpstan-phpunit": "^0.11.0", + "phpstan/phpstan-strict-rules": "^0.11.0", + "phpunit/phpcov": "^5.0", + "phpunit/phpunit": "^7.2", "psr/http-message": "^1.0", "react/promise": "2.*" }, @@ -2711,7 +2718,7 @@ "api", "graphql" ], - "time": "2018-09-02T14:59:54+00:00" + "time": "2019-08-25T10:32:47+00:00" }, { "name": "wikimedia/less.php", @@ -6784,16 +6791,16 @@ }, { "name": "magento/magento-coding-standard", - "version": "3", + "version": "4", "source": { "type": "git", "url": "https://github.com/magento/magento-coding-standard.git", - "reference": "73a7b7f3c00b02242f45f706571430735586f608" + "reference": "d24e0230a532e19941ed264f57db38fad5b1008a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/magento/magento-coding-standard/zipball/73a7b7f3c00b02242f45f706571430735586f608", - "reference": "73a7b7f3c00b02242f45f706571430735586f608", + "url": "https://api.github.com/repos/magento/magento-coding-standard/zipball/d24e0230a532e19941ed264f57db38fad5b1008a", + "reference": "d24e0230a532e19941ed264f57db38fad5b1008a", "shasum": "" }, "require": { @@ -6810,7 +6817,7 @@ "AFL-3.0" ], "description": "A set of Magento specific PHP CodeSniffer rules.", - "time": "2019-06-18T21:01:42+00:00" + "time": "2019-08-06T15:58:37+00:00" }, { "name": "magento/magento2-functional-testing-framework", diff --git a/dev/tests/acceptance/tests/_data/catalog_product_err_img.csv b/dev/tests/acceptance/tests/_data/catalog_product_err_img.csv new file mode 100644 index 0000000000000..97ac55e8e5a20 --- /dev/null +++ b/dev/tests/acceptance/tests/_data/catalog_product_err_img.csv @@ -0,0 +1,11 @@ +sku,store_view_code,attribute_set_code,product_type,categories,product_websites,name,description,short_description,weight,product_online,tax_class_name,visibility,price,special_price,special_price_from_date,special_price_to_date,url_key,additional_images +simple1,,Default,simple,,base,simple1,,,,1,Taxable Goods,"Catalog, Search",100,,,,simple1,test.jpg +simple2,,Default,simple,,base,simple2,,,,2,Taxable Goods,"Catalog, Search",101,,,,simple2,test.jpg +simple3,,Default,simple,,base,simple3,,,,3,Taxable Goods,"Catalog, Search",102,,,,simple3,test.jpg +simple4,,Default,simple,,base,simple4,,,,4,Taxable Goods,"Catalog, Search",103,,,,simple4,test.jpg +simple5,,Default,simple,,base,simple5,,,,5,Taxable Goods,"Catalog, Search",104,,,,simple5,test.jpg +simple6,,Default,simple,,base,simple6,,,,6,Taxable Goods,"Catalog, Search",105,,,,simple6,test.jpg +simple7,,Default,simple,,base,simple7,,,,7,Taxable Goods,"Catalog, Search",106,,,,simple7,test.jpg +simple8,,Default,simple,,base,simple8,,,,8,Taxable Goods,"Catalog, Search",107,,,,simple8,test.jpg +simple9,,Default,simple,,base,simple9,,,,9,Taxable Goods,"Catalog, Search",108,,,,simple9,test.jpg +simple10,,Default,simple,,base,simple10,,,,10,Taxable Goods,"Catalog, Search",109,,,,simple10,test.jpg diff --git a/dev/tests/acceptance/tests/_data/export_import_configurable_product.csv b/dev/tests/acceptance/tests/_data/export_import_configurable_product.csv new file mode 100644 index 0000000000000..97e63d06abe71 --- /dev/null +++ b/dev/tests/acceptance/tests/_data/export_import_configurable_product.csv @@ -0,0 +1,2 @@ +sku,store_view_code,attribute_set_code,product_type,categories,product_websites,name,description,short_description,weight,product_online,tax_class_name,visibility,price,special_price,special_price_from_date,special_price_to_date,url_key,meta_title,meta_keywords,meta_description,base_image,base_image_label,small_image,small_image_label,thumbnail_image,thumbnail_image_label,swatch_image,swatch_image_label,created_at,updated_at,new_from_date,new_to_date,display_product_options_in,map_price,msrp_price,map_enabled,gift_message_available,custom_design,custom_design_from,custom_design_to,custom_layout_update,page_layout,product_options_container,msrp_display_actual_price_type,country_of_manufacture,additional_attributes,qty,out_of_stock_qty,use_config_min_qty,is_qty_decimal,allow_backorders,use_config_backorders,min_cart_qty,use_config_min_sale_qty,max_cart_qty,use_config_max_sale_qty,is_in_stock,notify_on_stock_below,use_config_notify_stock_qty,manage_stock,use_config_manage_stock,use_config_qty_increments,qty_increments,use_config_enable_qty_inc,enable_qty_increments,is_decimal_divided,website_id,related_skus,related_position,crosssell_skus,crosssell_position,upsell_skus,upsell_position,additional_images,additional_image_labels,hide_from_product_page,custom_options,bundle_price_type,bundle_sku_type,bundle_price_view,bundle_weight_type,bundle_values,bundle_shipment_type,associated_skus,configurable_variations,configurable_variation_labels +api-configurable-export-import-product,,Default,configurable,Default Category/CategoryExportImport,base,API Configurable Export Import Product,,,2,1,Taxable Goods,"Catalog, Search",123,,,,api-configurable-export-import-product,,,,/m/a/magento-logo_1.png,Magento Logo,/m/a/magento-logo_1.png,Magento Logo,/m/a/magento-logo_1.png,Magento Logo,,,"7/26/19, 8:21 AM","7/26/19, 8:21 AM",,,Block after Info Column,,,,,,,,,,,Use config,,,0,0,1,0,0,1,1,1,0,1,1,,1,0,1,1,0,1,0,0,0,,,,,,,,,,,,,,,,,,"sku=api-simple-one-export-import,attribute=option1|sku=api-simple-two-export-import,attribute=option2",attribute=attributeExportImport diff --git a/dev/tests/api-functional/_files/Magento/TestModuleUps/Model/Carrier.php b/dev/tests/api-functional/_files/Magento/TestModuleUps/Model/Carrier.php new file mode 100644 index 0000000000000..b3c3c124cfe47 --- /dev/null +++ b/dev/tests/api-functional/_files/Magento/TestModuleUps/Model/Carrier.php @@ -0,0 +1,111 @@ +mockResponseLoader = $mockResponseLoader; + } + + /** + * @inheritdoc + */ + protected function _getCgiQuotes() + { + $responseBody = $this->mockResponseLoader->loadForRequest($this->_rawRequest->getDestCountry()); + return $this->_parseCgiResponse($responseBody); + } +} diff --git a/dev/tests/api-functional/_files/Magento/TestModuleUps/Model/MockResponseBodyLoader.php b/dev/tests/api-functional/_files/Magento/TestModuleUps/Model/MockResponseBodyLoader.php new file mode 100644 index 0000000000000..fe1750fa648f3 --- /dev/null +++ b/dev/tests/api-functional/_files/Magento/TestModuleUps/Model/MockResponseBodyLoader.php @@ -0,0 +1,63 @@ +moduleDirectory = $moduleDirectory; + $this->fileIo = $fileIo; + } + + /** + * Loads mock cgi response body for a given country + * + * @param string $country + * @return string + * @throws NotFoundException + */ + public function loadForRequest(string $country): string + { + $country = strtolower($country); + $moduleDir = $this->moduleDirectory->getDir('Magento_TestModuleUps'); + + $responsePath = sprintf(static::RESPONSE_FILE_PATTERN, $moduleDir, $country); + + if (!$this->fileIo->fileExists($responsePath)) { + throw new NotFoundException(__('%1 is not a valid destination country.', $country)); + } + + return $this->fileIo->read($responsePath); + } +} diff --git a/dev/tests/api-functional/_files/Magento/TestModuleUps/_files/mock_response_ca.txt b/dev/tests/api-functional/_files/Magento/TestModuleUps/_files/mock_response_ca.txt new file mode 100644 index 0000000000000..eca3e47a7e138 --- /dev/null +++ b/dev/tests/api-functional/_files/Magento/TestModuleUps/_files/mock_response_ca.txt @@ -0,0 +1,5 @@ +UPSOnLine4%XDM%90034%US%M4L 1V3%CA%081%1%138.17%0.00%138.17%-1% +4%XPR%90034%US%M4L 1V3%CA%081%1%95.07%0.00%95.07%12:00 P.M.% +4%WXS%90034%US%M4L 1V3%CA%481%1%93.99%0.00%93.99%-1% +4%XPD%90034%US%M4L 1V3%CA%071%1%85.85%0.00%85.85%-1% +4%STD%90034%US%M4L 1V3%CA%053%1%27.08%0.00%27.08%-1% \ No newline at end of file diff --git a/dev/tests/api-functional/_files/Magento/TestModuleUps/_files/mock_response_us.txt b/dev/tests/api-functional/_files/Magento/TestModuleUps/_files/mock_response_us.txt new file mode 100644 index 0000000000000..56f73dbd93a5a --- /dev/null +++ b/dev/tests/api-functional/_files/Magento/TestModuleUps/_files/mock_response_us.txt @@ -0,0 +1,5 @@ +UPSOnLine4%1DM%90034%US%75477%US%106%1%112.44%0.00%112.44%12:00 P.M.% +4%1DA%90034%US%75477%US%106%1%80.42%0.00%80.42%End of Day% +4%2DA%90034%US%75477%US%206%1%39.05%0.00%39.05%End of Day% +4%3DS%90034%US%75477%US%306%1%31.69%0.00%31.69%End of Day% +4%GND%90034%US%75477%US%006%1%15.61%0.00%15.61%End of Day% \ No newline at end of file diff --git a/dev/tests/api-functional/_files/Magento/TestModuleUps/etc/di.xml b/dev/tests/api-functional/_files/Magento/TestModuleUps/etc/di.xml new file mode 100644 index 0000000000000..28c2fa8e4d45f --- /dev/null +++ b/dev/tests/api-functional/_files/Magento/TestModuleUps/etc/di.xml @@ -0,0 +1,10 @@ + + + + + diff --git a/dev/tests/api-functional/_files/Magento/TestModuleUps/etc/module.xml b/dev/tests/api-functional/_files/Magento/TestModuleUps/etc/module.xml new file mode 100644 index 0000000000000..77d1d15f78d7d --- /dev/null +++ b/dev/tests/api-functional/_files/Magento/TestModuleUps/etc/module.xml @@ -0,0 +1,14 @@ + + + + + + + + + diff --git a/dev/tests/api-functional/_files/Magento/TestModuleUps/registration.php b/dev/tests/api-functional/_files/Magento/TestModuleUps/registration.php new file mode 100644 index 0000000000000..0668a2522e874 --- /dev/null +++ b/dev/tests/api-functional/_files/Magento/TestModuleUps/registration.php @@ -0,0 +1,13 @@ +getPath(ComponentRegistrar::MODULE, 'Magento_TestModuleUps') === null) { + ComponentRegistrar::register(ComponentRegistrar::MODULE, 'Magento_TestModuleUps', __DIR__); +} diff --git a/dev/tests/api-functional/testsuite/Magento/Catalog/Api/CategoryManagementTest.php b/dev/tests/api-functional/testsuite/Magento/Catalog/Api/CategoryManagementTest.php index a7be37530f966..bc3869df6a65b 100644 --- a/dev/tests/api-functional/testsuite/Magento/Catalog/Api/CategoryManagementTest.php +++ b/dev/tests/api-functional/testsuite/Magento/Catalog/Api/CategoryManagementTest.php @@ -9,8 +9,10 @@ use Magento\TestFramework\TestCase\WebapiAbstract; use Magento\TestFramework\Helper\Bootstrap; -use Magento\TestFramework\ObjectManager; +/** + * Tests CategoryManagement + */ class CategoryManagementTest extends WebapiAbstract { const RESOURCE_PATH = '/V1/categories'; @@ -18,10 +20,12 @@ class CategoryManagementTest extends WebapiAbstract const SERVICE_NAME = 'catalogCategoryManagementV1'; /** + * Tests getTree operation + * * @dataProvider treeDataProvider * @magentoApiDataFixture Magento/Catalog/_files/category_tree.php */ - public function testTree($rootCategoryId, $depth, $expectedLevel, $expectedId) + public function testTree($rootCategoryId, $depth, $expected) { $requestData = ['rootCategoryId' => $rootCategoryId, 'depth' => $depth]; $serviceInfo = [ @@ -36,24 +40,102 @@ public function testTree($rootCategoryId, $depth, $expectedLevel, $expectedId) ] ]; $result = $this->_webApiCall($serviceInfo, $requestData); - - for ($i = 0; $i < $expectedLevel; $i++) { - if (!array_key_exists(0, $result['children_data'])) { - $this->fail('Category "' . $result['name'] . '" doesn\'t have children but expected to have'); - } - $result = $result['children_data'][0]; - } - $this->assertEquals($expectedId, $result['id']); - $this->assertEmpty($result['children_data']); + $expected = array_replace_recursive($result, $expected); + $this->assertEquals($expected, $result); } - public function treeDataProvider() + /** + * @return array + */ + public function treeDataProvider(): array { return [ - [2, 100, 3, 402], - [2, null, 3, 402], - [400, 1, 1, 401], - [401, 0, 0, 401], + [ + 2, + 100, + [ + 'id' => 2, + 'name' => 'Default Category', + 'children_data' => [ + [ + 'id' => 400, + 'name' => 'Category 1', + 'children_data' => [ + [ + 'id' => 401, + 'name' => 'Category 1.1', + 'children_data' => [ + [ + 'id' => 402, + 'name' => 'Category 1.1.1', + 'children_data' => [ + + ] + ] + ] + ] + ] + ] + ] + ] + ], + [ + 2, + null, + [ + 'id' => 2, + 'name' => 'Default Category', + 'children_data' => [ + [ + 'id' => 400, + 'name' => 'Category 1', + 'children_data' => [ + [ + 'id' => 401, + 'name' => 'Category 1.1', + 'children_data' => [ + [ + 'id' => 402, + 'name' => 'Category 1.1.1', + 'children_data' => [ + + ] + ] + ] + ] + ] + ] + ] + ] + ], + [ + 400, + 1, + [ + 'id' => 400, + 'name' => 'Category 1', + 'children_data' => [ + [ + 'id' => 401, + 'name' => 'Category 1.1', + 'children_data' => [ + + ] + ] + ] + ] + ], + [ + 400, + 0, + [ + 'id' => 400, + 'name' => 'Category 1', + 'children_data' => [ + + ] + ] + ], ]; } diff --git a/dev/tests/api-functional/testsuite/Magento/Catalog/Api/ProductTierPriceManagementTest.php b/dev/tests/api-functional/testsuite/Magento/Catalog/Api/ProductTierPriceManagementTest.php index d5c035733942e..8531bb8a24159 100644 --- a/dev/tests/api-functional/testsuite/Magento/Catalog/Api/ProductTierPriceManagementTest.php +++ b/dev/tests/api-functional/testsuite/Magento/Catalog/Api/ProductTierPriceManagementTest.php @@ -9,6 +9,11 @@ use Magento\TestFramework\TestCase\WebapiAbstract; +/** + * Class ProductTierPriceManagementTest + * + * @package Magento\Catalog\Api + */ class ProductTierPriceManagementTest extends WebapiAbstract { const SERVICE_NAME = 'catalogProductTierPriceManagementV1'; @@ -34,15 +39,15 @@ public function testGetList($customerGroupId, $count, $value, $qty) ], ]; - $tearPriceList = $this->_webApiCall( + $tierPriceList = $this->_webApiCall( $serviceInfo, ['sku' => $productSku, 'customerGroupId' => $customerGroupId] ); - $this->assertCount($count, $tearPriceList); + $this->assertCount($count, $tierPriceList); if ($count) { - $this->assertEquals($value, $tearPriceList[0]['value']); - $this->assertEquals($qty, $tearPriceList[0]['qty']); + $this->assertEquals($value, $tierPriceList[0]['value']); + $this->assertEquals($qty, $tierPriceList[0]['qty']); } } diff --git a/dev/tests/api-functional/testsuite/Magento/Customer/Api/AddressMetadataTest.php b/dev/tests/api-functional/testsuite/Magento/Customer/Api/AddressMetadataTest.php index 748947825ba09..fbf131bf4deca 100644 --- a/dev/tests/api-functional/testsuite/Magento/Customer/Api/AddressMetadataTest.php +++ b/dev/tests/api-functional/testsuite/Magento/Customer/Api/AddressMetadataTest.php @@ -6,8 +6,11 @@ namespace Magento\Customer\Api; +use Magento\Config\Model\ResourceModel\Config; use Magento\Customer\Api\Data\AddressInterface as Address; use Magento\Customer\Model\Data\AttributeMetadata; +use Magento\Framework\App\Config\ReinitableConfigInterface; +use Magento\TestFramework\ObjectManager; use Magento\TestFramework\TestCase\WebapiAbstract; /** @@ -19,15 +22,40 @@ class AddressMetadataTest extends WebapiAbstract const SERVICE_VERSION = "V1"; const RESOURCE_PATH = "/V1/attributeMetadata/customerAddress"; + /** + * @var Config $config + */ + private $resourceConfig; + + /** + * @var ReinitableConfigInterface + */ + private $reinitConfig; + + /** + * @inheritdoc + */ + protected function setUp() + { + parent::setUp(); + + $objectManager = ObjectManager::getInstance(); + $this->resourceConfig = $objectManager->get(Config::class); + $this->reinitConfig = $objectManager->get(ReinitableConfigInterface::class); + } + /** * Test retrieval of attribute metadata for the address entity type. * * @param string $attributeCode The attribute code of the requested metadata. * @param array $expectedMetadata Expected entity metadata for the attribute code. * @dataProvider getAttributeMetadataDataProvider + * @magentoDbIsolation disabled */ - public function testGetAttributeMetadata($attributeCode, $expectedMetadata) + public function testGetAttributeMetadata($attributeCode, $configOptions, $expectedMetadata) { + $this->initConfig($configOptions); + $serviceInfo = [ 'rest' => [ 'resourcePath' => self::RESOURCE_PATH . "/attribute/$attributeCode", @@ -54,12 +82,14 @@ public function testGetAttributeMetadata($attributeCode, $expectedMetadata) * Data provider for testGetAttributeMetadata. * * @return array + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ public function getAttributeMetadataDataProvider() { return [ Address::POSTCODE => [ Address::POSTCODE, + [], [ AttributeMetadata::FRONTEND_INPUT => 'text', AttributeMetadata::INPUT_FILTER => '', @@ -83,7 +113,85 @@ public function getAttributeMetadataDataProvider() AttributeMetadata::IS_SEARCHABLE_IN_GRID => true, AttributeMetadata::ATTRIBUTE_CODE => 'postcode', ], - ] + ], + 'prefix' => [ + 'prefix', + [ + ['path' => 'customer/address/prefix_show', 'value' => 'opt'], + ['path' => 'customer/address/prefix_options', 'value' => 'prefA;prefB'] + ], + [ + AttributeMetadata::FRONTEND_INPUT => 'text', + AttributeMetadata::INPUT_FILTER => '', + AttributeMetadata::STORE_LABEL => 'Name Prefix', + AttributeMetadata::MULTILINE_COUNT => 0, + AttributeMetadata::VALIDATION_RULES => [], + AttributeMetadata::VISIBLE => false, + AttributeMetadata::REQUIRED => false, + AttributeMetadata::DATA_MODEL => '', + AttributeMetadata::OPTIONS => [ + [ + 'label' => 'prefA', + 'value' => 'prefA', + ], + [ + 'label' => 'prefB', + 'value' => 'prefB', + ], + ], + AttributeMetadata::FRONTEND_CLASS => '', + AttributeMetadata::USER_DEFINED => false, + AttributeMetadata::SORT_ORDER => 10, + AttributeMetadata::FRONTEND_LABEL => 'Name Prefix', + AttributeMetadata::NOTE => '', + AttributeMetadata::SYSTEM => false, + AttributeMetadata::BACKEND_TYPE => 'static', + AttributeMetadata::IS_USED_IN_GRID => false, + AttributeMetadata::IS_VISIBLE_IN_GRID => false, + AttributeMetadata::IS_FILTERABLE_IN_GRID => false, + AttributeMetadata::IS_SEARCHABLE_IN_GRID => false, + AttributeMetadata::ATTRIBUTE_CODE => 'prefix', + ], + ], + 'suffix' => [ + 'suffix', + [ + ['path' => 'customer/address/suffix_show', 'value' => 'opt'], + ['path' => 'customer/address/suffix_options', 'value' => 'suffA;suffB'] + ], + [ + AttributeMetadata::FRONTEND_INPUT => 'text', + AttributeMetadata::INPUT_FILTER => '', + AttributeMetadata::STORE_LABEL => 'Name Suffix', + AttributeMetadata::MULTILINE_COUNT => 0, + AttributeMetadata::VALIDATION_RULES => [], + AttributeMetadata::VISIBLE => false, + AttributeMetadata::REQUIRED => false, + AttributeMetadata::DATA_MODEL => '', + AttributeMetadata::OPTIONS => [ + [ + 'label' => 'suffA', + 'value' => 'suffA', + ], + [ + 'label' => 'suffB', + 'value' => 'suffB', + ], + ], + AttributeMetadata::FRONTEND_CLASS => '', + AttributeMetadata::USER_DEFINED => false, + AttributeMetadata::SORT_ORDER => 50, + AttributeMetadata::FRONTEND_LABEL => 'Name Suffix', + AttributeMetadata::NOTE => '', + AttributeMetadata::SYSTEM => false, + AttributeMetadata::BACKEND_TYPE => 'static', + AttributeMetadata::IS_USED_IN_GRID => false, + AttributeMetadata::IS_VISIBLE_IN_GRID => false, + AttributeMetadata::IS_FILTERABLE_IN_GRID => false, + AttributeMetadata::IS_SEARCHABLE_IN_GRID => false, + AttributeMetadata::ATTRIBUTE_CODE => 'suffix', + ], + ], ]; } @@ -106,7 +214,7 @@ public function testGetAllAttributesMetadata() $attributeMetadata = $this->_webApiCall($serviceInfo); $this->assertCount(19, $attributeMetadata); - $postcode = $this->getAttributeMetadataDataProvider()[Address::POSTCODE][1]; + $postcode = $this->getAttributeMetadataDataProvider()[Address::POSTCODE][2]; $validationResult = $this->checkMultipleAttributesValidationRules($postcode, $attributeMetadata); list($postcode, $attributeMetadata) = $validationResult; $this->assertContains($postcode, $attributeMetadata); @@ -187,7 +295,7 @@ public function getAttributesDataProvider() return [ [ 'customer_address_edit', - $attributeMetadata[Address::POSTCODE][1], + $attributeMetadata[Address::POSTCODE][2], ] ]; } @@ -200,6 +308,7 @@ public function getAttributesDataProvider() * @param array $actualResult * @return array * @SuppressWarnings(PHPMD.CyclomaticComplexity) + * phpcs:disable Generic.Metrics.NestingLevel */ public function checkValidationRules($expectedResult, $actualResult) { @@ -235,6 +344,7 @@ public function checkValidationRules($expectedResult, $actualResult) } return [$expectedResult, $actualResult]; } + //phpcs:enable /** * Check specific attribute validation rules in set of multiple attributes @@ -277,4 +387,19 @@ public static function tearDownAfterClass() $attribute->delete(); } } + + /** + * Set core config data. + * + * @param $configOptions + */ + private function initConfig(array $configOptions): void + { + if ($configOptions) { + foreach ($configOptions as $option) { + $this->resourceConfig->saveConfig($option['path'], $option['value']); + } + } + $this->reinitConfig->reinit(); + } } diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/AuthorizenetAcceptjs/Customer/SetPaymentMethodTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/AuthorizenetAcceptjs/Customer/SetPaymentMethodTest.php index ad454c67080e9..5f70cf4fd6687 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/AuthorizenetAcceptjs/Customer/SetPaymentMethodTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/AuthorizenetAcceptjs/Customer/SetPaymentMethodTest.php @@ -109,6 +109,113 @@ public function dataProviderTestPlaceOrder(): array ]; } + /** + * @magentoConfigFixture default_store carriers/flatrate/active 1 + * @magentoConfigFixture default_store payment/authorizenet_acceptjs/active 1 + * @magentoConfigFixture default_store payment/authorizenet_acceptjs/environment sandbox + * @magentoConfigFixture default_store payment/authorizenet_acceptjs/login def_login + * @magentoConfigFixture default_store payment/authorizenet_acceptjs/trans_key def_trans_key + * @magentoConfigFixture default_store payment/authorizenet_acceptjs/public_client_key def_public_client_key + * @magentoConfigFixture default_store payment/authorizenet_acceptjs/trans_signature_key def_trans_signature_key + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_billing_address.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_flatrate_shipping_method.php + * @dataProvider dataProviderSetPaymentInvalidInput + * @param \Closure $getMutationClosure + * @param string $expectedMessage + * @expectedException \Exception + */ + public function testSetPaymentInvalidInput(\Closure $getMutationClosure, string $expectedMessage) + { + $reservedOrderId = 'test_quote'; + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute($reservedOrderId); + + $setPaymentMutation = $getMutationClosure($maskedQuoteId); + + $this->expectExceptionMessage($expectedMessage); + $this->graphQlMutation($setPaymentMutation, [], '', $this->getHeaderMap()); + } + + /** + * Data provider for testSetPaymentInvalidInput + * + * @return array + */ + public function dataProviderSetPaymentInvalidInput(): array + { + return [ + [ + function (string $maskedQuoteId) { + return $this->getInvalidSetPaymentMutation($maskedQuoteId); + }, + 'Required parameter "authorizenet_acceptjs" for "payment_method" is missing.', + ], + [ + function (string $maskedQuoteId) { + return $this->getInvalidAcceptJsInput($maskedQuoteId); + }, + 'for "authorizenet_acceptjs" is missing.' + ], + ]; + } + + /** + * Get setPaymentMethodOnCart missing additional data property + * + * @param string $maskedQuoteId + * @return string + */ + private function getInvalidSetPaymentMutation(string $maskedQuoteId): string + { + return <<graphQlQuery($query); + $responseDataObject = new DataObject($response); + //Some sort of smoke testing + self::assertEquals( + 'Its a description of Test Category 1.2', + $responseDataObject->getData('category/children/0/children/1/description') + ); + self::assertEquals( + 'default-category', + $responseDataObject->getData('category/url_key') + ); + self::assertEquals( + [], + $responseDataObject->getData('category/children/0/available_sort_by') + ); + self::assertEquals( + 'name', + $responseDataObject->getData('category/children/0/default_sort_by') + ); + self::assertCount( + 7, + $responseDataObject->getData('category/children') + ); + self::assertCount( + 2, + $responseDataObject->getData('category/children/0/children') + ); + self::assertEquals( + 13, + $responseDataObject->getData('category/children/0/children/1/id') + ); + } + /** * @magentoApiDataFixture Magento/Catalog/_files/categories.php */ @@ -187,6 +261,25 @@ public function testGetDisabledCategory() $this->graphQlQuery($query); } + /** + * @magentoApiDataFixture Magento/Catalog/_files/categories.php + * @expectedException \Exception + * @expectedExceptionMessage Category doesn't exist + */ + public function testGetCategoryIdZero() + { + $categoryId = 0; + $query = <<graphQlQuery($query); + } + public function testNonExistentCategoryWithProductCount() { $query = << 3, 'items' => [ ['sku' => '12345'], - ['sku' => 'simple'], - ['sku' => 'simple-4'] + ['sku' => 'simple-4'], + ['sku' => 'simple'] ] ] ] @@ -419,8 +512,7 @@ private function assertBaseFields($product, $actualResponse) ['response_field' => 'attribute_set_id', 'expected_value' => $product->getAttributeSetId()], ['response_field' => 'created_at', 'expected_value' => $product->getCreatedAt()], ['response_field' => 'name', 'expected_value' => $product->getName()], - ['response_field' => 'price', 'expected_value' => - [ + ['response_field' => 'price', 'expected_value' => [ 'minimalPrice' => [ 'amount' => [ 'value' => $product->getPrice(), diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductAttributeOptionsTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductAttributeOptionsTest.php new file mode 100644 index 0000000000000..517a1c966b04d --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductAttributeOptionsTest.php @@ -0,0 +1,105 @@ +get(\Magento\Eav\Model\Config::class); + $attribute = $eavConfig->getAttribute('catalog_product', 'dropdown_attribute'); + /** @var AttributeOptionInterface[] $options */ + $options = $attribute->getOptions(); + array_shift($options); + $optionValues = []; + // phpcs:ignore Generic.CodeAnalysis.ForLoopWithTestFunctionCall + for ($i = 0; $i < count($options); $i++) { + $optionValues[] = $options[$i]->getValue(); + } + $query + = <<graphQlQuery($query); + + $expectedOptionArray = [ + [], // description attribute has no options + [ + [ + 'label' => 'Enabled', + 'value' => '1' + ], + [ + 'label' => 'Disabled', + 'value' => '2' + ] + ], + [ + [ + 'label' => 'Option 1', + 'value' => $optionValues[0] + ], + [ + 'label' => 'Option 2', + 'value' => $optionValues[1] + ], + [ + 'label' => 'Option 3', + 'value' => $optionValues[2] + ] + ] + ]; + + $this->assertNotEmpty($response['customAttributeMetadata']['items']); + $actualAttributes = $response['customAttributeMetadata']['items']; + + foreach ($expectedOptionArray as $index => $expectedOptions) { + $actualOption = $actualAttributes[$index]['attribute_options']; + $this->assertEquals($expectedOptions, $actualOption); + } + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductAttributeTypeTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductAttributeTypeTest.php index 063da7c11bf7f..a34d5e21704af 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductAttributeTypeTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductAttributeTypeTest.php @@ -50,7 +50,8 @@ public function testAttributeTypeResolver() { attribute_code attribute_type - entity_type + entity_type + input_type } } } @@ -71,7 +72,8 @@ public function testAttributeTypeResolver() \Magento\Catalog\Api\Data\ProductInterface::class ]; $attributeTypes = ['String', 'Int', 'Float','Boolean', 'Float']; - $this->assertAttributeType($attributeTypes, $expectedAttributeCodes, $entityType, $response); + $inputTypes = ['textarea', 'select', 'price', 'boolean', 'price']; + $this->assertAttributeType($attributeTypes, $expectedAttributeCodes, $entityType, $inputTypes, $response); } /** @@ -121,7 +123,8 @@ public function testComplexAttributeTypeResolver() { attribute_code attribute_type - entity_type + entity_type + input_type } } } @@ -154,7 +157,16 @@ public function testComplexAttributeTypeResolver() 'CustomerDataRegionInterface', 'ProductMediaGallery' ]; - $this->assertAttributeType($attributeTypes, $expectedAttributeCodes, $entityTypes, $response); + $inputTypes = [ + 'select', + 'multiselect', + 'select', + 'select', + 'text', + 'text', + 'gallery' + ]; + $this->assertAttributeType($attributeTypes, $expectedAttributeCodes, $entityTypes, $inputTypes, $response); } /** @@ -213,11 +225,17 @@ public function testUnDefinedAttributeType() * @param array $attributeTypes * @param array $expectedAttributeCodes * @param array $entityTypes + * @param array $inputTypes * @param array $actualResponse * @SuppressWarnings(PHPMD.UnusedLocalVariable) */ - private function assertAttributeType($attributeTypes, $expectedAttributeCodes, $entityTypes, $actualResponse) - { + private function assertAttributeType( + $attributeTypes, + $expectedAttributeCodes, + $entityTypes, + $inputTypes, + $actualResponse + ) { $attributeMetaDataItems = array_map(null, $actualResponse['customAttributeMetadata']['items'], $attributeTypes); foreach ($attributeMetaDataItems as $itemIndex => $itemArray) { @@ -225,8 +243,9 @@ private function assertAttributeType($attributeTypes, $expectedAttributeCodes, $ $attributeMetaDataItems[$itemIndex][0], [ "attribute_code" => $expectedAttributeCodes[$itemIndex], - "attribute_type" =>$attributeTypes[$itemIndex], - "entity_type" => $entityTypes[$itemIndex] + "attribute_type" => $attributeTypes[$itemIndex], + "entity_type" => $entityTypes[$itemIndex], + "input_type" => $inputTypes[$itemIndex] ] ); } diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductSearchTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductSearchTest.php index 4ce8ad8dab393..91f1795935f6a 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductSearchTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductSearchTest.php @@ -13,12 +13,16 @@ use Magento\Catalog\Api\ProductRepositoryInterface; use Magento\Catalog\Model\Category; use Magento\Catalog\Model\CategoryLinkManagement; -use Magento\Framework\EntityManager\MetadataPool; +use Magento\Eav\Model\Config; +use Magento\Indexer\Model\Indexer; use Magento\TestFramework\ObjectManager; use Magento\TestFramework\TestCase\GraphQlAbstract; use Magento\Catalog\Model\Product; use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Eav\Api\Data\AttributeOptionInterface; use Magento\Framework\DataObject; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Helper\CacheCleaner; /** * @SuppressWarnings(PHPMD.TooManyPublicMethods) @@ -28,8 +32,9 @@ class ProductSearchTest extends GraphQlAbstract { /** - * Verify that layered navigation filters are returned for product query + * Verify that layered navigation filters and aggregations are correct for product query * + * Filter products by an array of skus * @magentoApiDataFixture Magento/Catalog/_files/products_with_layered_navigation_attribute.php * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ @@ -40,7 +45,7 @@ public function testFilterLn() products ( filter: { sku: { - like:"simple%" + in:["simple1", "simple2"] } } pageSize: 4 @@ -72,9 +77,6 @@ public function testFilterLn() } } QUERY; - /** - * @var ProductRepositoryInterface $productRepository - */ $response = $this->graphQlQuery($query); $this->assertArrayHasKey( @@ -89,6 +91,809 @@ public function testFilterLn() ); } + /** + * Layered navigation for Configurable products with out of stock options + * Two configurable products each having two variations and one of the child products of one Configurable set to OOS + * + * @magentoApiDataFixture Magento/Catalog/_files/configurable_products_with_custom_attribute_layered_navigation.php + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + public function testLayeredNavigationForConfigurableProducts() + { + CacheCleaner::cleanAll(); + $attributeCode = 'test_configurable'; + + /** @var \Magento\Eav\Model\Config $eavConfig */ + $eavConfig = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get(Config::class); + $attribute = $eavConfig->getAttribute('catalog_product', $attributeCode); + /** @var AttributeOptionInterface[] $options */ + $options = $attribute->getOptions(); + array_shift($options); + $firstOption = $options[0]->getValue(); + $secondOption = $options[1]->getValue(); + $query = $this->getQueryProductsWithArrayOfCustomAttributes($attributeCode, $firstOption, $secondOption); + $this->reIndexAndCleanCache(); + $response = $this->graphQlQuery($query); + + $this->assertEquals(2, $response['products']['total_count']); + $this->assertNotEmpty($response['products']['aggregations']); + $this->assertNotEmpty($response['products']['filters'], 'Filters is empty'); + $this->assertCount(2, $response['products']['aggregations'], 'Aggregation count does not match'); + + // Custom attribute filter layer data + $this->assertResponseFields( + $response['products']['aggregations'][1], + [ + 'attribute_code' => $attribute->getAttributeCode(), + 'label'=> $attribute->getDefaultFrontendLabel(), + 'count'=> 2, + 'options' => [ + [ + 'label' => 'Option 1', + 'value' => $firstOption, + 'count' =>'2' + ], + [ + 'label' => 'Option 2', + 'value' => $secondOption, + 'count' =>'2' + ] + ], + ] + ); + } + + /** + * + * @return string + */ + private function getQueryProductsWithArrayOfCustomAttributes($attributeCode, $firstOption, $secondOption) : string + { + return <<getDefaultAttributeOptionValue($attributeCode); + $query = <<get(ProductRepositoryInterface::class); + $product1 = $productRepository->get('simple'); + $product2 = $productRepository->get('12345'); + $product3 = $productRepository->get('simple-4'); + $filteredProducts = [$product1, $product2, $product3 ]; + $countOfFilteredProducts = count($filteredProducts); + $this->reIndexAndCleanCache(); + $response = $this->graphQlQuery($query); + $this->assertEquals(3, $response['products']['total_count'], 'Number of products returned is incorrect'); + $this->assertTrue(count($response['products']['filters']) > 0, 'Product filters is not empty'); + $this->assertCount(3, $response['products']['aggregations'], 'Incorrect count of aggregations'); + + $productItemsInResponse = array_map(null, $response['products']['items'], $filteredProducts); + for ($itemIndex = 0; $itemIndex < $countOfFilteredProducts; $itemIndex++) { + $this->assertNotEmpty($productItemsInResponse[$itemIndex]); + //validate that correct products are returned + $this->assertResponseFields( + $productItemsInResponse[$itemIndex][0], + [ 'name' => $filteredProducts[$itemIndex]->getName(), + 'sku' => $filteredProducts[$itemIndex]->getSku() + ] + ); + } + + /** @var \Magento\Eav\Model\Config $eavConfig */ + $eavConfig = $objectManager->get(Config::class); + $attribute = $eavConfig->getAttribute('catalog_product', 'second_test_configurable'); + // Validate custom attribute filter layer data from aggregations + $this->assertResponseFields( + $response['products']['aggregations'][2], + [ + 'attribute_code' => $attribute->getAttributeCode(), + 'count'=> 1, + 'label'=> $attribute->getDefaultFrontendLabel(), + 'options' => [ + [ + 'label' => 'Option 3', + 'count' => 3, + 'value' => $optionValue + ], + ], + ] + ); + } + + /** + * @return void + */ + private function reIndexAndCleanCache() : void + { + $objectManager = Bootstrap::getObjectManager(); + $indexer = $objectManager->create(Indexer::class); + $indexer->load('catalogsearch_fulltext'); + $indexer->reindexAll(); + CacheCleaner::cleanAll(); + } + /** + * Filter products using an array of multi select custom attributes + * + * @magentoApiDataFixture Magento/Catalog/_files/products_with_layered_navigation_with_multiselect_attribute.php + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + public function testFilterProductsByMultiSelectCustomAttributes() + { + $objectManager = Bootstrap::getObjectManager(); + $this->reIndexAndCleanCache(); + $attributeCode = 'multiselect_attribute'; + /** @var \Magento\Eav\Model\Config $eavConfig */ + $eavConfig = $objectManager->get(\Magento\Eav\Model\Config::class); + $attribute = $eavConfig->getAttribute('catalog_product', $attributeCode); + /** @var AttributeOptionInterface[] $options */ + $options = $attribute->getOptions(); + array_shift($options); + $countOptions = count($options); + $optionValues = []; + for ($i = 0; $i < $countOptions; $i++) { + $optionValues[] = $options[$i]->getValue(); + } + $query = <<graphQlQuery($query); + $this->assertEquals(3, $response['products']['total_count']); + $this->assertNotEmpty($response['products']['filters']); + $this->assertNotEmpty($response['products']['aggregations']); + } + + /** + * Get the option value for the custom attribute to be used in the graphql query + * + * @param string $attributeCode + * @return string + */ + private function getDefaultAttributeOptionValue(string $attributeCode) : string + { + /** @var \Magento\Eav\Model\Config $eavConfig */ + $eavConfig = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get(\Magento\Eav\Model\Config::class); + $attribute = $eavConfig->getAttribute('catalog_product', $attributeCode); + /** @var AttributeOptionInterface[] $options */ + $options = $attribute->getOptions(); + array_shift($options); + $defaultOptionValue = $options[0]->getValue(); + return $defaultOptionValue; + } + + /** + * Full text search for Products and then filter the results by custom attribute ( sort is by defaulty by relevance) + * + * @magentoApiDataFixture Magento/Catalog/_files/products_with_layered_navigation_custom_attribute.php + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + public function testSearchAndFilterByCustomAttribute() + { + $this->reIndexAndCleanCache(); + $attribute_code = 'second_test_configurable'; + $optionValue = $this->getDefaultAttributeOptionValue($attribute_code); + + $query = <<graphQlQuery($query); + //Verify total count of the products returned + $this->assertEquals(3, $response['products']['total_count']); + $this->assertArrayHasKey('filters', $response['products']); + $this->assertCount(3, $response['products']['aggregations']); + $expectedFilterLayers = + [ + ['name' => 'Category', + 'request_var'=> 'cat' + ], + ['name' => 'Second Test Configurable', + 'request_var'=> 'second_test_configurable' + ] + ]; + $layers = array_map(null, $expectedFilterLayers, $response['products']['filters']); + + //Verify all the three layers from filters : Price, Category and Custom attribute layers + foreach ($layers as $layerIndex => $layerFilterData) { + $this->assertNotEmpty($layerFilterData); + $this->assertEquals( + $layers[$layerIndex][0]['name'], + $response['products']['filters'][$layerIndex]['name'], + 'Layer name does not match' + ); + $this->assertEquals( + $layers[$layerIndex][0]['request_var'], + $response['products']['filters'][$layerIndex]['request_var'], + 'request_var does not match' + ); + } + + // Validate the price layer of aggregations from the response + $this->assertResponseFields( + $response['products']['aggregations'][0], + [ + 'attribute_code' => 'price', + 'count'=> 2, + 'label'=> 'Price', + 'options' => [ + [ + 'count' => 2, + 'label' => '10-20', + 'value' => '10_20', + + ], + [ + 'count' => 1, + 'label' => '40-*', + 'value' => '40_*', + + ], + ], + ] + ); + // Validate the custom attribute layer of aggregations from the response + $this->assertResponseFields( + $response['products']['aggregations'][2], + [ + 'attribute_code' => $attribute_code, + 'count'=> 1, + 'label'=> 'Second Test Configurable', + 'options' => [ + [ + 'count' => 3, + 'label' => 'Option 3', + 'value' => $optionValue, + + ] + + ], + ] + ); + // 7 categories including the subcategories to which the items belong to , are returned + $this->assertCount(7, $response['products']['aggregations'][1]['options']); + unset($response['products']['aggregations'][1]['options']); + $this->assertResponseFields( + $response['products']['aggregations'][1], + [ + 'attribute_code' => 'category_id', + 'count'=> 7, + 'label'=> 'Category' + ] + ); + } + + /** + * Filter by category and custom attribute + * + * @magentoApiDataFixture Magento/Catalog/_files/products_with_layered_navigation_custom_attribute.php + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + public function testFilterByCategoryIdAndCustomAttribute() + { + $this->reIndexAndCleanCache(); + $categoryId = 13; + $optionValue = $this->getDefaultAttributeOptionValue('second_test_configurable'); + $query = <<graphQlQuery($query); + $this->assertEquals(2, $response['products']['total_count']); + /** @var ProductRepositoryInterface $productRepository */ + $productRepository = ObjectManager::getInstance()->get(ProductRepositoryInterface::class); + $product1 = $productRepository->get('simple'); + $product2 = $productRepository->get('simple-4'); + $filteredProducts = [$product1, $product2]; + $productItemsInResponse = array_map(null, $response['products']['items'], $filteredProducts); + //phpcs:ignore Generic.CodeAnalysis.ForLoopWithTestFunctionCall + for ($itemIndex = 0; $itemIndex < count($filteredProducts); $itemIndex++) { + $this->assertNotEmpty($productItemsInResponse[$itemIndex]); + //validate that correct products are returned + $this->assertResponseFields( + $productItemsInResponse[$itemIndex][0], + [ 'name' => $filteredProducts[$itemIndex]->getName(), + 'sku' => $filteredProducts[$itemIndex]->getSku() + ] + ); + } + $this->assertNotEmpty($response['products']['filters'], 'filters is empty'); + $this->assertNotEmpty($response['products']['aggregations'], 'Aggregations should not be empty'); + $this->assertCount(3, $response['products']['aggregations']); + + $actualCategoriesFromResponse = $response['products']['aggregations'][1]['options']; + + //Validate the number of categories/sub-categories that contain the products with the custom attribute + $this->assertCount(6, $actualCategoriesFromResponse); + + $expectedCategoryInAggregrations = + [ + [ + 'count' => 2, + 'label' => 'Category 1', + 'value'=> '3' + ], + [ + 'count'=> 1, + 'label' => 'Category 1.1', + 'value'=> '4' + + ], + [ + 'count'=> 1, + 'label' => 'Movable Position 2', + 'value'=> '10' + + ], + [ + 'count'=> 1, + 'label' => 'Movable Position 3', + 'value'=> '11' + ], + [ + 'count'=> 1, + 'label' => 'Category 12', + 'value'=> '12' + + ], + [ + 'count'=> 2, + 'label' => 'Category 1.2', + 'value'=> '13' + ], + ]; + $categoryInAggregations = array_map(null, $expectedCategoryInAggregrations, $actualCategoriesFromResponse); + +//Validate the categories and sub-categories data in the filter layer + foreach ($categoryInAggregations as $index => $categoryAggregationsData) { + $this->assertNotEmpty($categoryAggregationsData); + $this->assertEquals( + $categoryInAggregations[$index][0]['label'], + $actualCategoriesFromResponse[$index]['label'], + 'Category is incorrect' + ); + $this->assertEquals( + $categoryInAggregations[$index][0]['count'], + $actualCategoriesFromResponse[$index]['count'], + 'Products count in the category is incorrect' + ); + } + } + + /** + * Filter by exact match of product url key + * + * @magentoApiDataFixture Magento/Catalog/_files/categories.php + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + public function testFilterBySingleProductUrlKey() + { + /** @var ProductRepositoryInterface $productRepository */ + $productRepository = ObjectManager::getInstance()->get(ProductRepositoryInterface::class); + /** @var Product $product */ + $product = $productRepository->get('simple-4'); + $urlKey = $product->getUrlKey(); + + $query = <<graphQlQuery($query); + $this->assertEquals(1, $response['products']['total_count'], 'More than 1 product found'); + $this->assertCount(2, $response['products']['aggregations']); + $this->assertResponseFields( + $response['products']['items'][0], + [ + 'name' => $product->getName(), + 'sku' => $product->getSku(), + 'url_key'=> $product->getUrlKey() + ] + ); + $this->assertEquals('Price', $response['products']['aggregations'][0]['label']); + $this->assertEquals('Category', $response['products']['aggregations'][1]['label']); + //Disable the product + $product->setStatus(\Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_DISABLED); + $productRepository->save($product); + $query2 = <<graphQlQuery($query2); + $this->assertEquals(0, $response['products']['total_count'], 'Total count should be zero'); + $this->assertEmpty($response['products']['items']); + $this->assertEmpty($response['products']['aggregations']); + } + + /** + * Filter by multiple product url keys + * + * @magentoApiDataFixture Magento/Catalog/_files/categories.php + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + public function testFilterByMultipleProductUrlKeys() + { + /** @var ProductRepositoryInterface $productRepository */ + $productRepository = ObjectManager::getInstance()->get(ProductRepositoryInterface::class); + /** @var Product $product */ + $product1 = $productRepository->get('simple'); + $product2 = $productRepository->get('12345'); + $product3 = $productRepository->get('simple-4'); + $filteredProducts = [$product1, $product2, $product3]; + $urlKey =[]; + foreach ($filteredProducts as $product) { + $urlKey[] = $product->getUrlKey(); + } + + $query = <<graphQlQuery($query); + $this->assertEquals(3, $response['products']['total_count'], 'Total count is incorrect'); + $this->assertCount(2, $response['products']['aggregations']); + + $productItemsInResponse = array_map(null, $response['products']['items'], $filteredProducts); + //phpcs:ignore Generic.CodeAnalysis.ForLoopWithTestFunctionCall + for ($itemIndex = 0; $itemIndex < count($filteredProducts); $itemIndex++) { + $this->assertNotEmpty($productItemsInResponse[$itemIndex]); + //validate that correct products are returned + $this->assertResponseFields( + $productItemsInResponse[$itemIndex][0], + [ 'name' => $filteredProducts[$itemIndex]->getName(), + 'sku' => $filteredProducts[$itemIndex]->getSku(), + 'url_key'=> $filteredProducts[$itemIndex]->getUrlKey() + ] + ); + } + } + /** * Get array with expected data for layered navigation filters * @@ -157,8 +962,8 @@ private function getExpectedFiltersDataSet() private function assertFilters($response, $expectedFilters, $message = '') { $this->assertArrayHasKey('filters', $response['products'], 'Product has filters'); - $this->assertTrue(is_array(($response['products']['filters'])), 'Product filters is array'); - $this->assertTrue(count($response['products']['filters']) > 0, 'Product filters is not empty'); + $this->assertTrue(is_array(($response['products']['filters'])), 'Product filters is not array'); + $this->assertTrue(count($response['products']['filters']) > 0, 'Product filters is empty'); foreach ($expectedFilters as $expectedFilter) { $found = false; foreach ($response['products']['filters'] as $responseFilter) { @@ -175,12 +980,12 @@ private function assertFilters($response, $expectedFilters, $message = '') } /** - * Verify that items between the price range of 5 and 50 are returned after sorting name in DESC + * Verify product filtering using price range AND matching skus AND name sorted in DESC order * * @magentoApiDataFixture Magento/Catalog/_files/multiple_products.php * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ - public function testFilterProductsWithinSpecificPriceRangeSortedByNameDesc() + public function testFilterWithinSpecificPriceRangeSortedByNameDesc() { $query = <<assertEquals(4, $response['products']['page_info']['page_size']); } - /** - * Test a visible product with matching sku or name with special price - * - * Requesting for items that has a special price and price < $60, that are visible in Catalog, Search or Both which - * either has a sku like “simple” or name like “configurable”sorted by price in DESC - * - * @magentoApiDataFixture Magento/Catalog/_files/multiple_mixed_products_2.php - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) - */ - public function testFilterVisibleProductsWithMatchingSkuOrNameWithSpecialPrice() - { - $query - = <<get(ProductRepositoryInterface::class); - $product1 = $productRepository->get('simple1'); - $product2 = $productRepository->get('simple2'); - $filteredProducts = [$product2, $product1]; - - $response = $this->graphQlQuery($query); - $this->assertArrayHasKey('total_count', $response['products']); - $this->assertEquals(2, $response['products']['total_count']); - $this->assertProductItems($filteredProducts, $response); - } - /** * pageSize = total_count and current page = 2 * expected - error is thrown @@ -329,6 +1058,7 @@ public function testFilterVisibleProductsWithMatchingSkuOrNameWithSpecialPrice() public function testSearchWithFilterWithPageSizeEqualTotalCount() { + $query = <<get(ProductRepositoryInterface::class); - $product = $productRepository->get('simple2'); - + $product1 = $productRepository->get('grey_shorts'); + $product2 = $productRepository->get('white_shorts'); $response = $this->graphQlQuery($query); $this->assertEquals(2, $response['products']['total_count']); - $this->assertEquals(['page_size' => 1, 'current_page' => 2], $response['products']['page_info']); + $this->assertEquals(['page_size' => 2, 'current_page' => 1], $response['products']['page_info']); $this->assertEquals( - [['sku' => $product->getSku(), 'name' => $product->getName()]], + [ + ['sku' => $product1->getSku(), 'name' => $product1->getName()], + ['sku' => $product2->getSku(), 'name' => $product2->getName()] + ], $response['products']['items'] ); + $this->assertArrayHasKey('aggregations', $response['products']); + $this->assertCount(2, $response['products']['aggregations']); + $expectedAggregations =[ + [ + 'attribute_code' => 'price', + 'count' => 2, + 'label' => 'Price', + 'options' => [ + [ + 'label' => '10-20', + 'value' => '10_20', + 'count' => 1, + ], + [ + 'label' => '20-*', + 'value' => '20_*', + 'count' => 1, + ] + ] + ], + [ + 'attribute_code' => 'category_id', + 'count' => 1, + 'label' => 'Category', + 'options' => [ + [ + 'label' => 'Colorful Category', + 'value' => '330', + 'count' => 2, + ], + ], + ] + ]; + $this->assertEquals($expectedAggregations, $response['products']['aggregations']); } /** - * @magentoApiDataFixture Magento/Catalog/_files/product_in_multiple_categories.php + * @magentoApiDataFixture Magento/Catalog/_files/categories.php */ - public function testFilteringForProductInMultipleCategories() + public function testFilteringForProductsFromMultipleCategories() { - $productSku = 'simple333'; $query = <<graphQlQuery($query); /** @var ProductRepositoryInterface $productRepository */ - $productRepository = ObjectManager::getInstance()->get(ProductRepositoryInterface::class); - /** @var ProductInterface $product */ - $product = $productRepository->get('simple333'); - $categoryIds = $product->getCategoryIds(); - foreach ($categoryIds as $index => $value) { - $categoryIds[$index] = [ 'id' => (int)$value]; - } - $this->assertNotEmpty($response['products']['items'][0]['categories'], "Categories must not be empty"); - $this->assertNotNull($response['products']['items'][0]['categories'], "categories must not be null"); - $this->assertEquals($categoryIds, $response['products']['items'][0]['categories']); - /** @var MetadataPool $metaData */ - $metaData = ObjectManager::getInstance()->get(MetadataPool::class); - $linkField = $metaData->getMetadata(ProductInterface::class)->getLinkField(); - $assertionMap = [ - - ['response_field' => 'id', 'expected_value' => $product->getData($linkField)], - ['response_field' => 'sku', 'expected_value' => $product->getSku()], - ['response_field' => 'name', 'expected_value' => $product->getName()], - ['response_field' => 'attribute_set_id', 'expected_value' => $product->getAttributeSetId()] - ]; - $this->assertResponseFields($response['products']['items'][0], $assertionMap); + $this->assertEquals(3, $response['products']['total_count']); } /** + * Filter products by single category + * * @magentoApiDataFixture Magento/Catalog/_files/product_in_multiple_categories.php * @return void */ - public function testFilterProductsByCategoryIds() + public function testFilterProductsBySingleCategoryId() { $queryCategoryId = 333; $query @@ -619,7 +1380,7 @@ public function testFilterProductsByCategoryIds() QUERY; $response = $this->graphQlQuery($query); - + $this->assertEquals(2, $response['products']['total_count'], 'Incorrect count of products returned'); /** @var CategoryLinkManagement $productLinks */ $productLinks = ObjectManager::getInstance()->get(CategoryLinkManagement::class); /** @var CategoryRepositoryInterface $categoryRepository */ @@ -663,12 +1424,84 @@ public function testFilterProductsByCategoryIds() } /** - * Sorting by price in the DESC order from the filtered items with default pageSize + * Sorting the search results by relevance (DESC => most relevant) + * + * Search for products for a fuzzy match and checks if all matching results returned including + * results based on matching keywords from description + * + * @magentoApiDataFixture Magento/Catalog/_files/products_for_relevance_sorting.php + * @return void + */ + public function testSearchAndSortByRelevance() + { + $this->reIndexAndCleanCache(); + $search_term ="blue"; + $query + = <<graphQlQuery($query); + $this->assertEquals(3, $response['products']['total_count']); + $this->assertNotEmpty($response['products']['filters'], 'Filters should have the Category layer'); + $this->assertEquals('Colorful Category', $response['products']['filters'][0]['filter_items'][0]['label']); + $productsInResponse = ['Blue briefs','Navy Blue Striped Shoes','Grey shorts']; + $count = count($response['products']['items']); + for ($i = 0; $i < $count; $i++) { + $this->assertEquals($productsInResponse[$i], $response['products']['items'][$i]['name']); + } + $this->assertCount(2, $response['products']['aggregations']); + } + + /** + * Filtering for product with sku "equals" a specific value + * If pageSize and current page are not requested, default values are returned * * @magentoApiDataFixture Magento/Catalog/_files/multiple_mixed_products_2.php * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ - public function testQuerySortByPriceDESCWithDefaultPageSize() + public function testFilterByExactSkuAndSortByPriceDesc() { $query = <<get(ProductRepositoryInterface::class); $visibleProduct1 = $productRepository->get('simple1'); - $visibleProduct2 = $productRepository->get('simple2'); - $filteredProducts = [$visibleProduct2, $visibleProduct1]; + + $filteredProducts = [$visibleProduct1]; $response = $this->graphQlQuery($query); - $this->assertEquals(2, $response['products']['total_count']); + $this->assertEquals(1, $response['products']['total_count']); $this->assertProductItems($filteredProducts, $response); $this->assertEquals(20, $response['products']['page_info']['page_size']); $this->assertEquals(1, $response['products']['page_info']['current_page']); } - - /** - * @magentoApiDataFixture Magento/Catalog/_files/multiple_mixed_products_2.php - */ - public function testProductQueryUsingFromAndToFilterInput() - { - $query - = <<graphQlQuery($query); - $this->assertEquals(2, $response['products']['total_count']); - /** @var ProductRepositoryInterface $productRepository */ - $productRepository = ObjectManager::getInstance()->get(ProductRepositoryInterface::class); - $product1 = $productRepository->get('simple1'); - $product2 = $productRepository->get('simple2'); - $filteredProducts = [$product2, $product1]; - - $this->assertProductItemsWithMaximalAndMinimalPriceCheck($filteredProducts, $response); - } - /** - * @magentoApiDataFixture Magento/Catalog/_files/multiple_mixed_products_2.php + * Fuzzy search filtered for price and sorted by price and name + * + * @magentoApiDataFixture Magento/Catalog/_files/products_for_relevance_sorting.php */ public function testProductBasicFullTextSearchQuery() { - $textToSearch = 'Simple'; + $this->reIndexAndCleanCache(); + $textToSearch = 'blue'; $query =<<get(ProductRepositoryInterface::class); - $prod1 = $productRepository->get('simple1'); - + $prod1 = $productRepository->get('blue_briefs'); + $prod2 = $productRepository->get('grey_shorts'); + $prod3 = $productRepository->get('navy-striped-shoes'); $response = $this->graphQlQuery($query); - $this->assertEquals(1, $response['products']['total_count']); + $this->assertEquals(3, $response['products']['total_count']); - $filteredProducts = [$prod1]; + $filteredProducts = [$prod1, $prod2, $prod3]; $productItemsInResponse = array_map(null, $response['products']['items'], $filteredProducts); foreach ($productItemsInResponse as $itemIndex => $itemArray) { $this->assertNotEmpty($itemArray); @@ -839,7 +1636,7 @@ public function testProductBasicFullTextSearchQuery() 'price' => [ 'minimalPrice' => [ 'amount' => [ - 'value' => $filteredProducts[$itemIndex]->getSpecialPrice(), + 'value' => $filteredProducts[$itemIndex]->getPrice(), 'currency' => 'USD' ] ] @@ -850,24 +1647,43 @@ public function testProductBasicFullTextSearchQuery() } /** + * Filter products purely in a given price range + * + * @magentoApiDataFixture Magento/Catalog/_files/category.php * @magentoApiDataFixture Magento/Catalog/_files/multiple_mixed_products_2.php */ - public function testProductsThatMatchWithPricesFromList() + public function testFilterWithinASpecificPriceRangeSortedByPriceDESC() { + /** @var ProductRepositoryInterface $productRepository */ + $productRepository = ObjectManager::getInstance()->get(ProductRepositoryInterface::class); + + $prod1 = $productRepository->get('simple1'); + $prod2 = $productRepository->get('simple2'); + $filteredProducts = [$prod1, $prod2]; + /** @var \Magento\Catalog\Api\CategoryLinkManagementInterface $categoryLinkManagement */ + $categoryLinkManagement = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() + ->create(\Magento\Catalog\Api\CategoryLinkManagementInterface::class); + foreach ($filteredProducts as $product) { + $categoryLinkManagement->assignProductToCategories( + $product->getSku(), + [333] + ); + } + $query =<<graphQlQuery($query); $this->assertEquals(2, $response['products']['total_count']); - /** @var ProductRepositoryInterface $productRepository */ - $productRepository = ObjectManager::getInstance()->get(ProductRepositoryInterface::class); - - $prod1 = $productRepository->get('simple2'); - $prod2 = $productRepository->get('simple1'); - $filteredProducts = [$prod1, $prod2]; - $productItemsInResponse = array_map(null, $response['products']['items'], $filteredProducts); - foreach ($productItemsInResponse as $itemIndex => $itemArray) { - $this->assertNotEmpty($itemArray); - $this->assertResponseFields( - $productItemsInResponse[$itemIndex][0], - ['attribute_set_id' => $filteredProducts[$itemIndex]->getAttributeSetId(), - 'sku' => $filteredProducts[$itemIndex]->getSku(), - 'name' => $filteredProducts[$itemIndex]->getName(), - 'price' => [ - 'regularPrice' => [ - 'amount' => [ - 'value' => $filteredProducts[$itemIndex]->getPrice(), - 'currency' => 'USD' - ] - ] - ], - 'type_id' =>$filteredProducts[$itemIndex]->getTypeId(), - 'weight' => $filteredProducts[$itemIndex]->getWeight() - ] - ); + $this->assertProductItemsWithPriceCheck($filteredProducts, $response); + //verify that by default Price and category are the only layers available + $filterNames = ['Category', 'Price']; + $this->assertCount(2, $response['products']['filters'], 'Filter count does not match'); + $productCount = count($response['products']['filters']); + for ($i = 0; $i < $productCount; $i++) { + $this->assertEquals($filterNames[$i], $response['products']['filters'][$i]['name']); } } @@ -942,21 +1758,17 @@ public function testQueryFilterNoMatchingItems() { products( filter: - { - special_price:{lt:"15"} - price:{lt:"50"} - weight:{gt:"4"} - or: - { - sku:{like:"simple%"} - name:{like:"%simple%"} - } + { + price:{from:"50"} + + description:{match:"Description"} + } pageSize:2 currentPage:1 sort: { - sku:ASC + position:ASC } ) { @@ -1006,7 +1818,7 @@ public function testQueryPageOutOfBoundException() products( filter: { - price:{eq:"10"} + price:{to:"10"} } pageSize:2 currentPage:2 @@ -1053,6 +1865,7 @@ public function testQueryPageOutOfBoundException() } /** + * No filter or search arguments used * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ public function testQueryWithNoSearchOrFilterArgumentException() @@ -1098,7 +1911,7 @@ public function testFilterProductsThatAreOutOfStockWithConfigSettings() products( filter: { - sku:{like:"simple%"} + sku:{eq:"simple_visible_in_stock"} } pageSize:20 @@ -1151,7 +1964,7 @@ public function testInvalidCurrentPage() products ( filter: { sku: { - like:"simple%" + eq:"simple1" } } pageSize: 4 @@ -1180,7 +1993,7 @@ public function testInvalidPageSize() products ( filter: { sku: { - like:"simple%" + eq:"simple2" } } pageSize: 0 @@ -1204,8 +2017,8 @@ public function testInvalidPageSize() private function assertProductItems(array $filteredProducts, array $actualResponse) { $productItemsInResponse = array_map(null, $actualResponse['products']['items'], $filteredProducts); - // phpcs:ignore Generic.CodeAnalysis.ForLoopWithTestFunctionCall - for ($itemIndex = 0; $itemIndex < count($filteredProducts); $itemIndex++) { + $count = count($filteredProducts); + for ($itemIndex = 0; $itemIndex < $count; $itemIndex++) { $this->assertNotEmpty($productItemsInResponse[$itemIndex]); $this->assertResponseFields( $productItemsInResponse[$itemIndex][0], @@ -1227,7 +2040,7 @@ private function assertProductItems(array $filteredProducts, array $actualRespon } } - private function assertProductItemsWithMaximalAndMinimalPriceCheck(array $filteredProducts, array $actualResponse) + private function assertProductItemsWithPriceCheck(array $filteredProducts, array $actualResponse) { $productItemsInResponse = array_map(null, $actualResponse['products']['items'], $filteredProducts); @@ -1250,7 +2063,14 @@ private function assertProductItemsWithMaximalAndMinimalPriceCheck(array $filter 'value' => $filteredProducts[$itemIndex]->getSpecialPrice(), 'currency' => 'USD' ] - ] + ], + 'regularPrice' => [ + 'amount' => [ + 'value' => $filteredProducts[$itemIndex]->getPrice(), + 'currency' => 'USD' + ] + ] + ], 'type_id' =>$filteredProducts[$itemIndex]->getTypeId(), 'weight' => $filteredProducts[$itemIndex]->getWeight() diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductViewTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductViewTest.php index e11e2e8d108c2..5685fcdb25877 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductViewTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductViewTest.php @@ -592,7 +592,7 @@ public function testProductPrices() $secondProductSku = 'simple-156'; $query = <<assertResponseFields($value, $assertionMapValues); } else { + // phpcs:ignore Magento2.Performance.ForeachArrayMerge $assertionMap = array_merge( $assertionMap, [ @@ -823,7 +824,7 @@ private function assertOptions($product, $actualResponse) $valueKeyName = 'date_option'; $valueAssertionMap = []; } - + // phpcs:ignore Magento2.Performance.ForeachArrayMerge $valueAssertionMap = array_merge( $valueAssertionMap, [ @@ -980,7 +981,7 @@ public function testProductInAllAnchoredCategories() { $query = <<assertEquals('8', $response['storeConfig']['list_per_page_values']); $this->assertEquals(8, $response['storeConfig']['list_per_page']); $this->assertEquals('asc', $response['storeConfig']['catalog_default_sort_by']); + $this->assertEquals(2, $response['storeConfig']['root_category_id']); } } diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/ConfigurableProduct/AddConfigurableProductToCartTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/ConfigurableProduct/AddConfigurableProductToCartTest.php index 0e334999599b4..378d3e7dcd9aa 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/ConfigurableProduct/AddConfigurableProductToCartTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/ConfigurableProduct/AddConfigurableProductToCartTest.php @@ -67,6 +67,78 @@ public function testAddConfigurableProductToCart() self::assertArrayHasKey('value_label', $option); } + /** + * @magentoApiDataFixture Magento/ConfigurableProduct/_files/product_configurable.php + * @magentoApiDataFixture Magento/Checkout/_files/active_quote.php + */ + public function testAddMultipleConfigurableProductToCart() + { + $searchResponse = $this->graphQlQuery($this->getFetchProductQuery('configurable')); + $product = current($searchResponse['products']['items']); + + $quantityOne = 1; + $quantityTwo = 2; + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_order_1'); + $parentSku = $product['sku']; + $skuOne = 'simple_10'; + $skuTwo = 'simple_20'; + $valueIdOne = $product['configurable_options'][0]['values'][0]['value_index']; + + $query = <<graphQlMutation($query); + + $cartItems = $response['addConfigurableProductsToCart']['cart']['items']; + self::assertCount(2, $cartItems); + + foreach ($cartItems as $cartItem) { + if ($cartItem['configurable_options'][0]['value_id'] === $valueIdOne) { + self::assertEquals($quantityOne, $cartItem['quantity']); + } else { + self::assertEquals($quantityTwo, $cartItem['quantity']); + } + } + } + /** * @magentoApiDataFixture Magento/ConfigurableProduct/_files/product_configurable_sku.php * @magentoApiDataFixture Magento/Checkout/_files/active_quote.php @@ -92,6 +164,56 @@ public function testAddProductIfQuantityIsNotAvailable() $this->graphQlMutation($query); } + /** + * @magentoApiDataFixture Magento/ConfigurableProduct/_files/product_configurable_sku.php + * @magentoApiDataFixture Magento/Checkout/_files/active_quote.php + * @expectedException \Exception + * @expectedExceptionMessage Could not find a product with SKU "configurable_no_exist" + */ + public function testAddNonExistentConfigurableProductParentToCart() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_order_1'); + $parentSku = 'configurable_no_exist'; + $sku = 'simple_20'; + + $query = $this->getQuery( + $maskedQuoteId, + $parentSku, + $sku, + 2000 + ); + + $this->graphQlMutation($query); + } + + /** + * @magentoApiDataFixture Magento/ConfigurableProduct/_files/product_configurable_sku.php + * @magentoApiDataFixture Magento/Checkout/_files/active_quote.php + */ + public function testAddNonExistentConfigurableProductVariationToCart() + { + $searchResponse = $this->graphQlQuery($this->getFetchProductQuery('configurable')); + $product = current($searchResponse['products']['items']); + + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_order_1'); + $parentSku = $product['sku']; + $sku = 'simple_no_exist'; + + $query = $this->getQuery( + $maskedQuoteId, + $parentSku, + $sku, + 2000 + ); + + $this->expectException(\Exception::class); + $this->expectExceptionMessage( + 'Could not add the product with SKU configurable to the shopping cart: Could not find specified product.' + ); + + $this->graphQlMutation($query); + } + /** * @param string $maskedQuoteId * @param string $parentSku diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/ConfigurableProduct/RemoveConfigurableProductFromCartTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/ConfigurableProduct/RemoveConfigurableProductFromCartTest.php new file mode 100644 index 0000000000000..31308eaef5acc --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/ConfigurableProduct/RemoveConfigurableProductFromCartTest.php @@ -0,0 +1,114 @@ +getMaskedQuoteIdByReservedOrderId = $objectManager->get(GetMaskedQuoteIdByReservedOrderId::class); + $this->quoteFactory = $objectManager->get(QuoteFactory::class); + $this->quoteResource = $objectManager->get(QuoteResource::class); + $this->productRepository = $objectManager->get(ProductRepositoryInterface::class); + } + + /** + * @magentoApiDataFixture Magento/ConfigurableProduct/_files/quote_with_configurable_product.php + */ + public function testRemoveConfigurableProductFromCart() + { + $configurableOptionSku = 'simple_10'; + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_cart_with_configurable'); + $quoteItemId = $this->getQuoteItemIdBySku($configurableOptionSku); + $query = $this->getQuery($maskedQuoteId, $quoteItemId); + $response = $this->graphQlMutation($query); + + $this->assertArrayHasKey('cart', $response['removeItemFromCart']); + $this->assertArrayHasKey('items', $response['removeItemFromCart']['cart']); + $this->assertEquals(0, count($response['removeItemFromCart']['cart']['items'])); + } + + /** + * @param string $maskedQuoteId + * @param int $itemId + * @return string + */ + private function getQuery(string $maskedQuoteId, int $itemId): string + { + return <<quoteFactory->create(); + $this->quoteResource->load($quote, 'test_cart_with_configurable', 'reserved_order_id'); + /** @var Item $quoteItem */ + $quoteItemsCollection = $quote->getItemsCollection(); + foreach ($quoteItemsCollection->getItems() as $item) { + if ($item->getSku() == $sku) { + return (int)$item->getId(); + } + } + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/CreateCustomerAddressTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/CreateCustomerAddressTest.php index 47c4d3ad91cb6..203e9b5cb42e5 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/CreateCustomerAddressTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/CreateCustomerAddressTest.php @@ -303,8 +303,8 @@ public function invalidInputDataProvider() { return [ ['', 'Syntax Error: Expected Name, found )'], - ['input: ""', 'Expected type CustomerAddressInput!, found "".'], - ['input: "foo"', 'Expected type CustomerAddressInput!, found "foo".'] + ['input: ""', 'requires type CustomerAddressInput!, found "".'], + ['input: "foo"', 'requires type CustomerAddressInput!, found "foo".'] ]; } diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/CreateCustomerTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/CreateCustomerTest.php index fc51f57a83a76..c5714012f38c9 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/CreateCustomerTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/CreateCustomerTest.php @@ -139,7 +139,7 @@ public function testCreateCustomerIfInputDataIsEmpty() /** * @expectedException \Exception - * @expectedExceptionMessage The customer email is missing. Enter and try again. + * @expectedExceptionMessage Required parameters are missing: Email */ public function testCreateCustomerIfEmailMissed() { @@ -241,6 +241,72 @@ public function testCreateCustomerIfPassedAttributeDosNotExistsInCustomerInput() $this->graphQlMutation($query); } + /** + * @expectedException \Exception + * @expectedExceptionMessage Required parameters are missing: First Name + */ + public function testCreateCustomerIfNameEmpty() + { + $newEmail = 'customer_created' . rand(1, 2000000) . '@example.com'; + $newFirstname = ''; + $newLastname = 'Rowe'; + $currentPassword = 'test123#'; + $query = <<graphQlMutation($query); + } + + /** + * @magentoConfigFixture default_store newsletter/general/active 0 + */ + public function testCreateCustomerSubscribed() + { + $newFirstname = 'Richard'; + $newLastname = 'Rowe'; + $newEmail = 'new_customer@example.com'; + + $query = <<graphQlMutation($query); + + $this->assertEquals(false, $response['createCustomer']['customer']['is_subscribed']); + } + public function tearDown() { $newEmail = 'new_customer@example.com'; diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/UpdateCustomerAddressTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/UpdateCustomerAddressTest.php index f2e82398df49b..9840236dc9896 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/UpdateCustomerAddressTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/UpdateCustomerAddressTest.php @@ -306,8 +306,8 @@ public function invalidInputDataProvider() { return [ ['', '"input" value must be specified'], - ['input: ""', 'Expected type CustomerAddressInput, found ""'], - ['input: "foo"', 'Expected type CustomerAddressInput, found "foo"'] + ['input: ""', 'requires type CustomerAddressInput, found ""'], + ['input: "foo"', 'requires type CustomerAddressInput, found "foo"'] ]; } diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/UpdateCustomerTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/UpdateCustomerTest.php index 7b1f3a67a1193..178d10b3c35a4 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/UpdateCustomerTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/UpdateCustomerTest.php @@ -253,6 +253,8 @@ public function testUpdateEmailIfEmailAlreadyExists() $currentEmail = 'customer@example.com'; $currentPassword = 'password'; $existedEmail = 'customer_two@example.com'; + $firstname = 'Richard'; + $lastname = 'Rowe'; $query = <<graphQlMutation($query, [], '', $this->getCustomerAuthHeaders($currentEmail, $currentPassword)); } + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @expectedException \Exception + * @expectedExceptionMessage Required parameters are missing: First Name + */ + public function testEmptyCustomerName() + { + $currentEmail = 'customer@example.com'; + $currentPassword = 'password'; + + $query = <<graphQlMutation($query, [], '', $this->getCustomerAuthHeaders($currentEmail, $currentPassword)); + } + /** * @param string $email * @param string $password diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Downloadable/CustomerDownloadableProductsTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/CustomerDownloadableProduct/CustomerDownloadableProductTest.php similarity index 97% rename from dev/tests/api-functional/testsuite/Magento/GraphQl/Downloadable/CustomerDownloadableProductsTest.php rename to dev/tests/api-functional/testsuite/Magento/GraphQl/CustomerDownloadableProduct/CustomerDownloadableProductTest.php index 47fd8d66b92f1..6b8aad83edac7 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Downloadable/CustomerDownloadableProductsTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/CustomerDownloadableProduct/CustomerDownloadableProductTest.php @@ -5,7 +5,7 @@ */ declare(strict_types=1); -namespace Magento\GraphQl\Downloadable; +namespace Magento\GraphQl\CustomerDownloadableProduct; use Magento\Integration\Api\CustomerTokenServiceInterface; use Magento\TestFramework\Helper\Bootstrap; @@ -14,13 +14,12 @@ /** * Test retrieving of customer downloadable products. */ -class CustomerDownloadableProductsTest extends GraphQlAbstract +class CustomerDownloadableProductTest extends GraphQlAbstract { /** * @var CustomerTokenServiceInterface */ private $customerTokenService; - /** * @inheritdoc */ @@ -29,7 +28,6 @@ protected function setUp() $objectManager = Bootstrap::getObjectManager(); $this->customerTokenService = $objectManager->get(CustomerTokenServiceInterface::class); } - /** * @magentoApiDataFixture Magento/Customer/_files/customer.php * @magentoApiDataFixture Magento/Downloadable/_files/product_downloadable.php @@ -42,23 +40,17 @@ public function testCustomerDownloadableProducts() self::assertArrayHasKey('items', $response['customerDownloadableProducts']); self::assertCount(1, $response['customerDownloadableProducts']['items']); - self::assertArrayHasKey('date', $response['customerDownloadableProducts']['items'][0]); self::assertNotEmpty($response['customerDownloadableProducts']['items'][0]['date']); - self::assertArrayHasKey('download_url', $response['customerDownloadableProducts']['items'][0]); self::assertNotEmpty($response['customerDownloadableProducts']['items'][0]['download_url']); - self::assertArrayHasKey('order_increment_id', $response['customerDownloadableProducts']['items'][0]); self::assertNotEmpty($response['customerDownloadableProducts']['items'][0]['order_increment_id']); - self::assertArrayHasKey('remaining_downloads', $response['customerDownloadableProducts']['items'][0]); self::assertNotEmpty($response['customerDownloadableProducts']['items'][0]['remaining_downloads']); - self::assertArrayHasKey('status', $response['customerDownloadableProducts']['items'][0]); self::assertNotEmpty($response['customerDownloadableProducts']['items'][0]['status']); } - /** * @magentoApiDataFixture Magento/Customer/_files/customer.php * @magentoApiDataFixture Magento/Downloadable/_files/product_downloadable.php @@ -71,7 +63,6 @@ public function testGuestCannotAccessDownloadableProducts() { $this->graphQlQuery($this->getQuery()); } - /** * @magentoApiDataFixture Magento/Customer/_files/customer.php */ @@ -79,11 +70,9 @@ public function testCustomerHasNoOrders() { $query = $this->getQuery(); $response = $this->graphQlQuery($query, [], '', $this->getHeaderMap()); - self::assertArrayHasKey('items', $response['customerDownloadableProducts']); self::assertCount(0, $response['customerDownloadableProducts']['items']); } - /** * @return string */ @@ -103,7 +92,6 @@ private function getQuery(): string } QUERY; } - /** * @param string $username * @param string $password diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/DownloadableProduct/UpdateDownloadableCartItemsTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/DownloadableProduct/UpdateDownloadableCartItemsTest.php new file mode 100644 index 0000000000000..ae533252f14c0 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/DownloadableProduct/UpdateDownloadableCartItemsTest.php @@ -0,0 +1,202 @@ +objectManager = Bootstrap::getObjectManager(); + $this->getMaskedQuoteIdByReservedOrderId = $this->objectManager->get(GetMaskedQuoteIdByReservedOrderId::class); + $this->quoteFactory = $this->objectManager->get(QuoteFactory::class); + $this->productRepository = $this->objectManager->get(ProductRepositoryInterface::class); + $this->quoteResource = $this->objectManager->get(QuoteResource::class); + $this->getQuoteItemIdByReservedQuoteIdAndSku = $this->objectManager->get( + GetQuoteItemIdByReservedQuoteIdAndSku::class + ); + } + + /** + * Update a downloadable product into shopping cart when "Links can be purchased separately" is enabled + * + * @magentoApiDataFixture Magento/Checkout/_files/active_quote_with_downloadable_product.php + */ + public function testUpdateDownloadableCartItemQuantity() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_order_1'); + $sku = 'downloadable-product'; + $qty = 1; + $finalQty = $qty + 1; + $links = $this->getProductsLinks($sku); + $linkId = key($links); + + $query = <<graphQlMutation($query); + + self::assertArrayHasKey('items', $response['addDownloadableProductsToCart']['cart']); + self::assertCount(1, $response['addDownloadableProductsToCart']['cart']['items']); + self::assertEquals($finalQty, $response['addDownloadableProductsToCart']['cart']['items'][0]['quantity']); + self::assertEquals($sku, $response['addDownloadableProductsToCart']['cart']['items'][0]['product']['sku']); + } + + /** + * Update a downloadable product into shopping cart when "Links can be purchased separately" is enabled + * + * @magentoApiDataFixture Magento/Checkout/_files/active_quote_with_downloadable_product.php + */ + public function testRemoveCartItemIfQuantityIsZero() + { + $reservedOrderId = "test_order_1"; + $sku = "downloadable-product"; + + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute($reservedOrderId); + + /** @var Quote $quote */ + $quote = $this->quoteFactory->create(); + $this->quoteResource->load($quote, $reservedOrderId, 'reserved_order_id'); + $qty = 0; + + $itemId = 0; + /** @var Item $item */ + foreach ($quote->getAllItems() as $item) { + if ($item->getSku() == $sku) { + $itemId = $item->getId(); + } + } + + $query = <<graphQlMutation($query); + + self::assertArrayHasKey('updateCartItems', $response); + self::assertArrayHasKey('cart', $response['updateCartItems']); + + $responseCart = $response['updateCartItems']['cart']; + self::assertCount(0, $responseCart['items']); + } + + /** + * Function returns array of all product's links + * + * @param string $sku + * @return array + */ + private function getProductsLinks(string $sku) : array + { + $result = []; + $productRepository = $this->objectManager->get(ProductRepositoryInterface::class); + + $product = $productRepository->get($sku, false, null, true); + + foreach ($product->getDownloadableLinks() as $linkObject) { + $result[$linkObject->getLinkId()] = [ + 'title' => $linkObject->getTitle(), + 'link_type' => null, //deprecated field + 'price' => $linkObject->getPrice(), + ]; + } + + return $result; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/IntrospectionQueryTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/IntrospectionQueryTest.php index 60acb3a7a4d44..69bcc73dd27a1 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/IntrospectionQueryTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/IntrospectionQueryTest.php @@ -53,7 +53,135 @@ public function testIntrospectionQuery() defaultValue } QUERY; - $this->assertArrayHasKey('__schema', $this->graphQlQuery($query)); } + + /** + * Tests that Introspection Query with deprecated annotations on enum values, fields are read. + */ + public function testIntrospectionQueryWithDeprecatedAnnotationOnEnumAndFieldValues() + { + $query + = <<graphQlQuery($query); + $this->assertArrayHasKey('__schema', $response); + $schemaResponseFields = $response['__schema']['types']; + $enumValueReasonArray = $this->getEnumValueDeprecatedReason($schemaResponseFields); + $fieldsValueReasonArray = $this->getFieldsValueDeprecatedReason($schemaResponseFields); + $expectedOutput = require __DIR__ . '/_files/schema_response_sdl_deprecated_annotation.php'; + + // checking field values deprecated reason + $fieldDeprecatedReason = []; + $fieldsArray = $expectedOutput[0]['fields']; + foreach ($fieldsArray as $field) { + if ($field['isDeprecated'] === true) { + $fieldDeprecatedReason [] = $field['deprecationReason']; + } + } + $this->assertNotEmpty($fieldDeprecatedReason); + $this->assertContains( + 'Symbol was missed. Use `default_display_currency_code`.', + $fieldDeprecatedReason + ); + + $this->assertContains( + 'Symbol was missed. Use `default_display_currency_code`.', + $fieldsValueReasonArray + ); + + $this->assertNotEmpty( + array_intersect($fieldDeprecatedReason, $fieldsValueReasonArray) + ); + + // checking enum values deprecated reason + $enumValueDeprecatedReason = []; + $enumValuesArray = $expectedOutput[1]['enumValues']; + foreach ($enumValuesArray as $enumValue) { + if ($enumValue['isDeprecated'] === true) { + $enumValueDeprecatedReason [] = $enumValue['deprecationReason']; + } + } + $this->assertNotEmpty($enumValueDeprecatedReason); + $this->assertContains( + '`sample_url` serves to get the downloadable sample', + $enumValueDeprecatedReason + ); + $this->assertContains( + '`sample_url` serves to get the downloadable sample', + $enumValueReasonArray + ); + $this->assertNotEmpty( + array_intersect($enumValueDeprecatedReason, $enumValueReasonArray) + ); + } + + /** + * Get the enum values deprecated reasons from the schema + * + * @param array $schemaResponseFields + * @return array + */ + private function getEnumValueDeprecatedReason($schemaResponseFields): array + { + $enumValueReasonArray = []; + foreach ($schemaResponseFields as $schemaResponseField) { + if (!empty($schemaResponseField['enumValues'])) { + foreach ($schemaResponseField['enumValues'] as $enumValueDeprecationReasonArray) { + if (!empty($enumValueDeprecationReasonArray['deprecationReason'])) { + $enumValueReasonArray[] = $enumValueDeprecationReasonArray['deprecationReason']; + } + } + } + } + return $enumValueReasonArray; + } + + /** + * Get the fields values deprecated reasons from the schema + * + * @param array $schemaResponseFields + * @return array + */ + private function getFieldsValueDeprecatedReason($schemaResponseFields): array + { + $fieldsValueReasonArray = []; + foreach ($schemaResponseFields as $schemaResponseField) { + if (!empty($schemaResponseField['fields'])) { + foreach ($schemaResponseField['fields'] as $fieldsValueDeprecatedReasonArray) { + if (!empty($fieldsValueDeprecatedReasonArray['deprecationReason'])) { + $fieldsValueReasonArray[] = $fieldsValueDeprecatedReasonArray['deprecationReason']; + } + } + } + } + return $fieldsValueReasonArray; + } } diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/AddDownloadableProductWithCustomOptionsToCartTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/AddDownloadableProductWithCustomOptionsToCartTest.php new file mode 100644 index 0000000000000..fa7d1194c7f83 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/AddDownloadableProductWithCustomOptionsToCartTest.php @@ -0,0 +1,201 @@ +objectManager = Bootstrap::getObjectManager(); + $this->getMaskedQuoteIdByReservedOrderId = $this->objectManager->get(GetMaskedQuoteIdByReservedOrderId::class); + $this->getCustomOptionsValuesForQueryBySku = + $this->objectManager->get(GetCustomOptionsValuesForQueryBySku::class); + } + + /** + * @magentoApiDataFixture Magento/Downloadable/_files/product_downloadable_with_custom_options.php + * @magentoApiDataFixture Magento/Checkout/_files/active_quote.php + */ + public function testAddDownloadableProductWithOptions() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_order_1'); + + $sku = 'downloadable-product-with-purchased-separately-links'; + $qty = 1; + $links = $this->getProductsLinks($sku); + $linkId = key($links); + + $customOptionsValues = $this->getCustomOptionsValuesForQueryBySku->execute($sku); + /* Generate customizable options fragment for GraphQl request */ + $queryCustomizableOptionValues = preg_replace('/"([^"]+)"\s*:\s*/', '$1:', json_encode($customOptionsValues)); + $customizableOptions = "customizable_options: {$queryCustomizableOptionValues}"; + + $query = $this->getQuery($maskedQuoteId, $qty, $sku, $customizableOptions, $linkId); + + $response = $this->graphQlMutation($query); + self::assertArrayHasKey('items', $response['addDownloadableProductsToCart']['cart']); + self::assertCount($qty, $response['addDownloadableProductsToCart']['cart']); + $customizableOptionsOutput = + $response['addDownloadableProductsToCart']['cart']['items'][0]['customizable_options']; + $assignedOptionsCount = count($customOptionsValues); + for ($counter = 0; $counter < $assignedOptionsCount; $counter++) { + $expectedValues = $this->buildExpectedValuesArray($customOptionsValues[$counter]['value_string']); + self::assertEquals( + $expectedValues, + $customizableOptionsOutput[$counter]['values'] + ); + } + } + + /** + * @magentoApiDataFixture Magento/Downloadable/_files/product_downloadable_with_custom_options.php + * @magentoApiDataFixture Magento/Checkout/_files/active_quote.php + */ + public function testAddDownloadableProductWithMissedRequiredOptionsSet() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_order_1'); + + $sku = 'downloadable-product-with-purchased-separately-links'; + $qty = 1; + $links = $this->getProductsLinks($sku); + $linkId = key($links); + $customizableOptions = ''; + + $query = $this->getQuery($maskedQuoteId, $qty, $sku, $customizableOptions, $linkId); + + $this->expectException(\Exception::class); + $this->expectExceptionMessage( + 'The product\'s required option(s) weren\'t entered. Make sure the options are entered and try again.' + ); + + $this->graphQlMutation($query); + } + + /** + * Function returns array of all product's links + * + * @param string $sku + * @return array + */ + private function getProductsLinks(string $sku) : array + { + $result = []; + $productRepository = $this->objectManager->get(ProductRepositoryInterface::class); + + $product = $productRepository->get($sku, false, null, true); + + foreach ($product->getDownloadableLinks() as $linkObject) { + $result[$linkObject->getLinkId()] = [ + 'title' => $linkObject->getTitle(), + 'link_type' => null, //deprecated field + 'price' => $linkObject->getPrice(), + ]; + } + + return $result; + } + + /** + * Build the part of expected response. + * + * @param string $assignedValue + * @return array + */ + private function buildExpectedValuesArray(string $assignedValue) : array + { + $assignedOptionsArray = explode(',', trim($assignedValue, '[]')); + $expectedArray = []; + foreach ($assignedOptionsArray as $assignedOption) { + $expectedArray[] = ['value' => $assignedOption]; + } + return $expectedArray; + } + + /** + * Returns GraphQl query string + * + * @param string $maskedQuoteId + * @param int $qty + * @param string $sku + * @param string $customizableOptions + * @param $linkId + * @return string + */ + private function getQuery( + string $maskedQuoteId, + int $qty, + string $sku, + string $customizableOptions, + $linkId + ): string { + $query = <<getMaskedQuoteIdByReservedOrderId = $objectManager->get(GetMaskedQuoteIdByReservedOrderId::class); $this->productCustomOptionsRepository = $objectManager->get(ProductCustomOptionRepositoryInterface::class); + $this->getCustomOptionsValuesForQueryBySku = $objectManager->get(GetCustomOptionsValuesForQueryBySku::class); } /** @@ -49,7 +55,7 @@ public function testAddSimpleProductWithOptions() $quantity = 1; $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_order_1'); - $customOptionsValues = $this->getCustomOptionsValuesForQuery($sku); + $customOptionsValues = $this->getCustomOptionsValuesForQueryBySku->execute($sku); /* Generate customizable options fragment for GraphQl request */ $queryCustomizableOptionValues = preg_replace('/"([^"]+)"\s*:\s*/', '$1:', json_encode($customOptionsValues)); @@ -135,41 +141,6 @@ private function getQuery(string $maskedQuoteId, string $sku, float $quantity, s QUERY; } - /** - * Generate an array with test values for customizable options - * based on the option type - * - * @param string $sku - * @return array - */ - private function getCustomOptionsValuesForQuery(string $sku): array - { - $customOptions = $this->productCustomOptionsRepository->getList($sku); - $customOptionsValues = []; - - foreach ($customOptions as $customOption) { - $optionType = $customOption->getType(); - if ($optionType == 'field' || $optionType == 'area') { - $customOptionsValues[] = [ - 'id' => (int)$customOption->getOptionId(), - 'value_string' => 'test' - ]; - } elseif ($optionType == 'drop_down') { - $optionSelectValues = $customOption->getValues(); - $customOptionsValues[] = [ - 'id' => (int)$customOption->getOptionId(), - 'value_string' => reset($optionSelectValues)->getOptionTypeId() - ]; - } elseif ($optionType == 'multiple') { - $customOptionsValues[] = [ - 'id' => (int)$customOption->getOptionId(), - 'value_string' => '[' . implode(',', array_keys($customOption->getValues())) . ']' - ]; - } - } - return $customOptionsValues; - } - /** * Build the part of expected response. * diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/AddVirtualProductWithCustomOptionsToCartTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/AddVirtualProductWithCustomOptionsToCartTest.php index 7c49ab86120d8..a8088b0b46b87 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/AddVirtualProductWithCustomOptionsToCartTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/AddVirtualProductWithCustomOptionsToCartTest.php @@ -26,6 +26,11 @@ class AddVirtualProductWithCustomOptionsToCartTest extends GraphQlAbstract */ private $productCustomOptionsRepository; + /** + * @var GetCustomOptionsValuesForQueryBySku + */ + private $getCustomOptionsValuesForQueryBySku; + /** * @inheritdoc */ @@ -34,6 +39,7 @@ protected function setUp() $objectManager = Bootstrap::getObjectManager(); $this->getMaskedQuoteIdByReservedOrderId = $objectManager->get(GetMaskedQuoteIdByReservedOrderId::class); $this->productCustomOptionsRepository = $objectManager->get(ProductCustomOptionRepositoryInterface::class); + $this->getCustomOptionsValuesForQueryBySku = $objectManager->get(GetCustomOptionsValuesForQueryBySku::class); } /** @@ -49,7 +55,7 @@ public function testAddVirtualProductWithOptions() $quantity = 1; $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_order_1'); - $customOptionsValues = $this->getCustomOptionsValuesForQuery($sku); + $customOptionsValues = $this->getCustomOptionsValuesForQueryBySku->execute($sku); /* Generate customizable options fragment for GraphQl request */ $queryCustomizableOptionValues = preg_replace('/"([^"]+)"\s*:\s*/', '$1:', json_encode($customOptionsValues)); @@ -135,42 +141,6 @@ private function getQuery(string $maskedQuoteId, string $sku, float $quantity, s QUERY; } - /** - * Generate an array with test values for customizable options - * based on the option type - * - * @param string $sku - * @return array - */ - private function getCustomOptionsValuesForQuery(string $sku): array - { - $customOptions = $this->productCustomOptionsRepository->getList($sku); - $customOptionsValues = []; - - foreach ($customOptions as $customOption) { - $optionType = $customOption->getType(); - if ($optionType == 'field' || $optionType == 'area') { - $customOptionsValues[] = [ - 'id' => (int)$customOption->getOptionId(), - 'value_string' => 'test' - ]; - } elseif ($optionType == 'drop_down') { - $optionSelectValues = $customOption->getValues(); - $customOptionsValues[] = [ - 'id' => (int)$customOption->getOptionId(), - 'value_string' => reset($optionSelectValues)->getOptionTypeId() - ]; - } elseif ($optionType == 'multiple') { - $customOptionsValues[] = [ - 'id' => (int)$customOption->getOptionId(), - 'value_string' => '[' . implode(',', array_keys($customOption->getValues())) . ']' - ]; - } - } - - return $customOptionsValues; - } - /** * Build the part of expected response. * diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/CheckoutEndToEndTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/CheckoutEndToEndTest.php index 5a4cc88d69623..6a06b143d5fcf 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/CheckoutEndToEndTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/CheckoutEndToEndTest.php @@ -158,7 +158,7 @@ private function findProduct(): string products ( filter: { sku: { - like:"simple%" + eq:"simple1" } } pageSize: 1 diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/GetAvailableShippingMethodsTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/GetAvailableShippingMethodsTest.php index 02c22a0e902be..f950d35f54658 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/GetAvailableShippingMethodsTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/GetAvailableShippingMethodsTest.php @@ -62,10 +62,6 @@ public function testGetAvailableShippingMethods() 'value' => 10, 'currency' => 'USD', ], - 'base_amount' => [ - 'value' => 10, - 'currency' => 'USD', - ], 'carrier_code' => 'flatrate', 'carrier_title' => 'Flat Rate', 'error_message' => '', @@ -176,10 +172,6 @@ private function getQuery(string $maskedQuoteId): string value currency } - base_amount { - value - currency - } carrier_code carrier_title error_message diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/GetCartIsVirtualTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/GetCartIsVirtualTest.php new file mode 100644 index 0000000000000..cf72435a123bf --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/GetCartIsVirtualTest.php @@ -0,0 +1,119 @@ +getMaskedQuoteIdByReservedOrderId = $objectManager->get(GetMaskedQuoteIdByReservedOrderId::class); + $this->customerTokenService = $objectManager->get(CustomerTokenServiceInterface::class); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + */ + public function testGetCartIsNotVirtual() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + + $query = $this->getQuery($maskedQuoteId); + $response = $this->graphQlQuery($query, [], '', $this->getHeaderMap()); + + $this->assertArrayHasKey('cart', $response); + $this->assertArrayHasKey('is_virtual', $response['cart']); + $this->assertFalse($response['cart']['is_virtual']); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/Catalog/_files/product_virtual.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_virtual_product.php + */ + public function testGetMixedCartIsNotVirtual() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + + $query = $this->getQuery($maskedQuoteId); + $response = $this->graphQlQuery($query, [], '', $this->getHeaderMap()); + + $this->assertArrayHasKey('cart', $response); + $this->assertArrayHasKey('is_virtual', $response['cart']); + $this->assertFalse($response['cart']['is_virtual']); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/Catalog/_files/product_virtual.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_virtual_product.php + */ + public function testGetCartIsVirtual() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + + $query = $this->getQuery($maskedQuoteId); + $response = $this->graphQlQuery($query, [], '', $this->getHeaderMap()); + + $this->assertArrayHasKey('cart', $response); + $this->assertArrayHasKey('is_virtual', $response['cart']); + $this->assertTrue($response['cart']['is_virtual']); + } + + /** + * @param string $maskedQuoteId + * @return string + */ + private function getQuery(string $maskedQuoteId): string + { + return <<customerTokenService->createCustomerAccessToken($username, $password); + $headerMap = ['Authorization' => 'Bearer ' . $customerToken]; + return $headerMap; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/GetSelectedShippingMethodTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/GetSelectedShippingMethodTest.php index 9bb36bf8f0929..f5700d27fea7a 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/GetSelectedShippingMethodTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/GetSelectedShippingMethodTest.php @@ -78,14 +78,6 @@ public function testGetSelectedShippingMethod() self::assertEquals(10, $amount['value']); self::assertArrayHasKey('currency', $amount); self::assertEquals('USD', $amount['currency']); - - self::assertArrayHasKey('base_amount', $shippingAddress['selected_shipping_method']); - $baseAmount = $shippingAddress['selected_shipping_method']['base_amount']; - - self::assertArrayHasKey('value', $baseAmount); - self::assertEquals(10, $baseAmount['value']); - self::assertArrayHasKey('currency', $baseAmount); - self::assertEquals('USD', $baseAmount['currency']); } /** @@ -108,18 +100,7 @@ public function testGetSelectedShippingMethodBeforeSet() $shippingAddress = current($response['cart']['shipping_addresses']); self::assertArrayHasKey('selected_shipping_method', $shippingAddress); - - self::assertArrayHasKey('carrier_code', $shippingAddress['selected_shipping_method']); - self::assertNull($shippingAddress['selected_shipping_method']['carrier_code']); - - self::assertArrayHasKey('method_code', $shippingAddress['selected_shipping_method']); - self::assertNull($shippingAddress['selected_shipping_method']['method_code']); - - self::assertArrayHasKey('carrier_title', $shippingAddress['selected_shipping_method']); - self::assertNull($shippingAddress['selected_shipping_method']['carrier_title']); - - self::assertArrayHasKey('method_title', $shippingAddress['selected_shipping_method']); - self::assertNull($shippingAddress['selected_shipping_method']['method_title']); + self::assertNull($shippingAddress['selected_shipping_method']); } /** @@ -182,13 +163,7 @@ public function testGetGetSelectedShippingMethodIfShippingMethodIsNotSet() $shippingAddress = current($response['cart']['shipping_addresses']); self::assertArrayHasKey('selected_shipping_method', $shippingAddress); - - self::assertNull($shippingAddress['selected_shipping_method']['carrier_code']); - self::assertNull($shippingAddress['selected_shipping_method']['method_code']); - self::assertNull($shippingAddress['selected_shipping_method']['carrier_title']); - self::assertNull($shippingAddress['selected_shipping_method']['method_title']); - self::assertNull($shippingAddress['selected_shipping_method']['amount']); - self::assertNull($shippingAddress['selected_shipping_method']['base_amount']); + self::assertNull($shippingAddress['selected_shipping_method']); } /** @@ -240,10 +215,6 @@ private function getQuery(string $maskedQuoteId): string value currency } - base_amount { - value - currency - } } } } diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/GetSpecifiedShippingAddressTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/GetSpecifiedShippingAddressTest.php index de3c384e65783..2023603a21eed 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/GetSpecifiedShippingAddressTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/GetSpecifiedShippingAddressTest.php @@ -92,28 +92,7 @@ public function testGetSpecifiedShippingAddressIfShippingAddressIsNotSet() self::assertArrayHasKey('cart', $response); self::assertArrayHasKey('shipping_addresses', $response['cart']); - $expectedShippingAddressData = [ - 'firstname' => null, - 'lastname' => null, - 'company' => null, - 'street' => [ - '' - ], - 'city' => null, - 'region' => [ - 'code' => null, - 'label' => null, - ], - 'postcode' => null, - 'country' => [ - 'code' => null, - 'label' => null, - ], - 'telephone' => null, - '__typename' => 'ShippingCartAddress', - 'customer_notes' => null, - ]; - self::assertEquals($expectedShippingAddressData, current($response['cart']['shipping_addresses'])); + self::assertEquals([], $response['cart']['shipping_addresses']); } /** diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/SetOfflineShippingMethodsOnCartTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/SetOfflineShippingMethodsOnCartTest.php index dcb5539fb3d49..fa8a7da092f5e 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/SetOfflineShippingMethodsOnCartTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/SetOfflineShippingMethodsOnCartTest.php @@ -53,7 +53,6 @@ protected function setUp() * @param string $carrierTitle * @param string $methodTitle * @param array $amount - * @param array $baseAmount * @throws \Magento\Framework\Exception\NoSuchEntityException * @dataProvider offlineShippingMethodDataProvider */ @@ -62,8 +61,7 @@ public function testSetOfflineShippingMethod( string $methodCode, string $carrierTitle, string $methodTitle, - array $amount, - array $baseAmount + array $amount ) { $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); @@ -96,9 +94,6 @@ public function testSetOfflineShippingMethod( self::assertArrayHasKey('amount', $shippingAddress['selected_shipping_method']); self::assertEquals($amount, $shippingAddress['selected_shipping_method']['amount']); - - self::assertArrayHasKey('base_amount', $shippingAddress['selected_shipping_method']); - self::assertEquals($baseAmount, $shippingAddress['selected_shipping_method']['base_amount']); } /** @@ -113,7 +108,6 @@ public function offlineShippingMethodDataProvider(): array 'Flat Rate', 'Fixed', ['value' => 10, 'currency' => 'USD'], - ['value' => 10, 'currency' => 'USD'], ], 'tablerate_bestway' => [ 'tablerate', @@ -121,7 +115,6 @@ public function offlineShippingMethodDataProvider(): array 'Best Way', 'Table Rate', ['value' => 10, 'currency' => 'USD'], - ['value' => 10, 'currency' => 'USD'], ], 'freeshipping_freeshipping' => [ 'freeshipping', @@ -129,7 +122,6 @@ public function offlineShippingMethodDataProvider(): array 'Free Shipping', 'Free', ['value' => 0, 'currency' => 'USD'], - ['value' => 0, 'currency' => 'USD'], ], ]; } @@ -166,10 +158,6 @@ private function getQuery( value currency } - base_amount { - value - currency - } } } } diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/SetPaymentMethodAndPlaceOrderTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/SetPaymentMethodAndPlaceOrderTest.php index 12fb356904224..192c10a67aa6b 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/SetPaymentMethodAndPlaceOrderTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/SetPaymentMethodAndPlaceOrderTest.php @@ -78,9 +78,14 @@ public function testSetPaymentOnCartWithSimpleProduct() $query = $this->getQuery($maskedQuoteId, $methodCode); $response = $this->graphQlMutation($query, [], '', $this->getHeaderMap()); - self::assertArrayHasKey('setPaymentMethodAndPlaceOrder', $response); - self::assertArrayHasKey('order', $response['setPaymentMethodAndPlaceOrder']); - self::assertArrayHasKey('order_id', $response['setPaymentMethodAndPlaceOrder']['order']); + self::assertArrayHasKey('setPaymentMethodOnCart', $response); + self::assertArrayHasKey('cart', $response['setPaymentMethodOnCart']); + self::assertArrayHasKey('selected_payment_method', $response['setPaymentMethodOnCart']['cart']); + self::assertArrayHasKey('code', $response['setPaymentMethodOnCart']['cart']['selected_payment_method']); + self::assertEquals($methodCode, $response['setPaymentMethodOnCart']['cart']['selected_payment_method']['code']); + + self::assertArrayHasKey('order', $response['placeOrder']); + self::assertArrayHasKey('order_id', $response['placeOrder']['order']); } /** @@ -116,9 +121,14 @@ public function testSetPaymentOnCartWithVirtualProduct() $query = $this->getQuery($maskedQuoteId, $methodCode); $response = $this->graphQlMutation($query, [], '', $this->getHeaderMap()); - self::assertArrayHasKey('setPaymentMethodAndPlaceOrder', $response); - self::assertArrayHasKey('order', $response['setPaymentMethodAndPlaceOrder']); - self::assertArrayHasKey('order_id', $response['setPaymentMethodAndPlaceOrder']['order']); + self::assertArrayHasKey('setPaymentMethodOnCart', $response); + self::assertArrayHasKey('cart', $response['setPaymentMethodOnCart']); + self::assertArrayHasKey('selected_payment_method', $response['setPaymentMethodOnCart']['cart']); + self::assertArrayHasKey('code', $response['setPaymentMethodOnCart']['cart']['selected_payment_method']); + self::assertEquals($methodCode, $response['setPaymentMethodOnCart']['cart']['selected_payment_method']['code']); + + self::assertArrayHasKey('order', $response['placeOrder']); + self::assertArrayHasKey('order_id', $response['placeOrder']['order']); } /** @@ -224,12 +234,25 @@ private function getQuery( ) : string { return << 'postcode', 'expected_value' => '887766'], ['response_field' => 'telephone', 'expected_value' => '88776655'], ['response_field' => 'country', 'expected_value' => ['code' => 'US', 'label' => 'US']], - ['response_field' => '__typename', 'expected_value' => 'ShippingCartAddress'] + ['response_field' => '__typename', 'expected_value' => 'ShippingCartAddress'], + ['response_field' => 'customer_notes', 'expected_value' => 'Test note'] ]; $this->assertResponseFields($shippingAddressResponse, $assertionMap); diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/SetShippingMethodsOnCartTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/SetShippingMethodsOnCartTest.php index 8197db7e7fef1..278f21f30b72d 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/SetShippingMethodsOnCartTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/SetShippingMethodsOnCartTest.php @@ -85,14 +85,6 @@ public function testSetShippingMethodOnCartWithSimpleProduct() self::assertEquals(10, $amount['value']); self::assertArrayHasKey('currency', $amount); self::assertEquals('USD', $amount['currency']); - - self::assertArrayHasKey('base_amount', $shippingAddress['selected_shipping_method']); - $baseAmount = $shippingAddress['selected_shipping_method']['base_amount']; - - self::assertArrayHasKey('value', $baseAmount); - self::assertEquals(10, $baseAmount['value']); - self::assertArrayHasKey('currency', $baseAmount); - self::assertEquals('USD', $baseAmount['currency']); } /** @@ -379,10 +371,6 @@ private function getQuery( value currency } - base_amount { - value - currency - } } } } diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/GetCartTotalQuantityTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/GetCartTotalQuantityTest.php new file mode 100644 index 0000000000000..aa7cb16ec1296 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/GetCartTotalQuantityTest.php @@ -0,0 +1,64 @@ +getMaskedQuoteIdByReservedOrderId = $objectManager->get(GetMaskedQuoteIdByReservedOrderId::class); + } + + /** + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + */ + public function testGetTotalQuantity() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + + $query = $this->getQuery($maskedQuoteId); + + $response = $this->graphQlQuery($query); + + self::assertArrayHasKey('cart', $response); + $cart = $response['cart']; + self::assertArrayHasKey('total_quantity', $cart); + self::assertEquals(2, $cart['total_quantity']); + } + + /** + * Create cart query + * + * @param string $maskedQuoteId + * @return string + */ + private function getQuery(string $maskedQuoteId): string + { + return <<productCustomOptionRepository = $productCustomOptionRepository; + } + + /** + * Returns array of custom options for the product + * + * @param string $sku + * @return array + */ + public function execute(string $sku): array + { + $customOptions = $this->productCustomOptionRepository->getList($sku); + $customOptionsValues = []; + + foreach ($customOptions as $customOption) { + $optionType = $customOption->getType(); + if ($optionType == 'field' || $optionType == 'area') { + $customOptionsValues[] = [ + 'id' => (int)$customOption->getOptionId(), + 'value_string' => 'test' + ]; + } elseif ($optionType == 'drop_down') { + $optionSelectValues = $customOption->getValues(); + $customOptionsValues[] = [ + 'id' => (int)$customOption->getOptionId(), + 'value_string' => reset($optionSelectValues)->getOptionTypeId() + ]; + } elseif ($optionType == 'multiple') { + $customOptionsValues[] = [ + 'id' => (int)$customOption->getOptionId(), + 'value_string' => '[' . implode(',', array_keys($customOption->getValues())) . ']' + ]; + } + } + + return $customOptionsValues; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/CheckoutEndToEndTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/CheckoutEndToEndTest.php index ed5aa9303d875..95308a350c953 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/CheckoutEndToEndTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/CheckoutEndToEndTest.php @@ -95,7 +95,7 @@ private function findProduct(): string products ( filter: { sku: { - like:"simple%" + eq:"simple1" } } pageSize: 1 diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/GetAvailableShippingMethodsTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/GetAvailableShippingMethodsTest.php index 5d90d26d4983c..81222a84435c8 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/GetAvailableShippingMethodsTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/GetAvailableShippingMethodsTest.php @@ -54,10 +54,6 @@ public function testGetAvailableShippingMethods() 'value' => 10, 'currency' => 'USD', ], - 'base_amount' => [ - 'value' => 10, - 'currency' => 'USD', - ], 'carrier_code' => 'flatrate', 'carrier_title' => 'Flat Rate', 'error_message' => '', @@ -144,10 +140,6 @@ private function getQuery(string $maskedQuoteId): string value currency } - base_amount { - value - currency - } carrier_code carrier_title error_message diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/GetCartIsVirtualTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/GetCartIsVirtualTest.php new file mode 100644 index 0000000000000..79fe2273184b2 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/GetCartIsVirtualTest.php @@ -0,0 +1,97 @@ +getMaskedQuoteIdByReservedOrderId = $objectManager->get(GetMaskedQuoteIdByReservedOrderId::class); + } + + /** + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + */ + public function testGetCartIsNotVirtual() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + + $query = $this->getQuery($maskedQuoteId); + $response = $this->graphQlQuery($query); + + $this->assertArrayHasKey('cart', $response); + $this->assertArrayHasKey('is_virtual', $response['cart']); + $this->assertFalse($response['cart']['is_virtual']); + } + + /** + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/Catalog/_files/product_virtual.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_virtual_product.php + */ + public function testGetMixedCartIsNotVirtual() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + + $query = $this->getQuery($maskedQuoteId); + $response = $this->graphQlQuery($query); + + $this->assertArrayHasKey('cart', $response); + $this->assertArrayHasKey('is_virtual', $response['cart']); + $this->assertFalse($response['cart']['is_virtual']); + } + + /** + * @magentoApiDataFixture Magento/Catalog/_files/product_virtual.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_virtual_product.php + */ + public function testGetCartIsVirtual() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + + $query = $this->getQuery($maskedQuoteId); + $response = $this->graphQlQuery($query); + + $this->assertArrayHasKey('cart', $response); + $this->assertArrayHasKey('is_virtual', $response['cart']); + $this->assertTrue($response['cart']['is_virtual']); + } + + /** + * @param string $maskedQuoteId + * @return string + */ + private function getQuery(string $maskedQuoteId): string + { + return << null, - 'lastname' => null, - 'company' => null, - 'street' => [ - '' - ], - 'city' => null, - 'region' => [ - 'code' => null, - 'label' => null, - ], - 'postcode' => null, - 'country' => [ - 'code' => null, - 'label' => null, - ], - 'telephone' => null, - '__typename' => 'ShippingCartAddress', - ]; - self::assertEquals($expectedShippingAddressData, current($response['cart']['shipping_addresses'])); + self::assertEquals([], $response['cart']['shipping_addresses']); } /** diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/SetOfflineShippingMethodsOnCartTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/SetOfflineShippingMethodsOnCartTest.php index bf3abe557ef82..921335e9e2082 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/SetOfflineShippingMethodsOnCartTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/SetOfflineShippingMethodsOnCartTest.php @@ -45,7 +45,6 @@ protected function setUp() * @param string $carrierTitle * @param string $methodTitle * @param array $amount - * @param array $baseAmount * @throws \Magento\Framework\Exception\NoSuchEntityException * @dataProvider offlineShippingMethodDataProvider */ @@ -54,8 +53,7 @@ public function testSetOfflineShippingMethod( string $methodCode, string $carrierTitle, string $methodTitle, - array $amount, - array $baseAmount + array $amount ) { $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); @@ -88,9 +86,6 @@ public function testSetOfflineShippingMethod( self::assertArrayHasKey('amount', $shippingAddress['selected_shipping_method']); self::assertEquals($amount, $shippingAddress['selected_shipping_method']['amount']); - - self::assertArrayHasKey('base_amount', $shippingAddress['selected_shipping_method']); - self::assertEquals($baseAmount, $shippingAddress['selected_shipping_method']['base_amount']); } /** @@ -105,7 +100,6 @@ public function offlineShippingMethodDataProvider(): array 'Flat Rate', 'Fixed', ['value' => 10, 'currency' => 'USD'], - ['value' => 10, 'currency' => 'USD'], ], 'tablerate_bestway' => [ 'tablerate', @@ -113,7 +107,6 @@ public function offlineShippingMethodDataProvider(): array 'Best Way', 'Table Rate', ['value' => 10, 'currency' => 'USD'], - ['value' => 10, 'currency' => 'USD'], ], 'freeshipping_freeshipping' => [ 'freeshipping', @@ -121,7 +114,6 @@ public function offlineShippingMethodDataProvider(): array 'Free Shipping', 'Free', ['value' => 0, 'currency' => 'USD'], - ['value' => 0, 'currency' => 'USD'], ], ]; } @@ -158,10 +150,6 @@ private function getQuery( value currency } - base_amount { - value - currency - } } } } diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/SetShippingAddressOnCartTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/SetShippingAddressOnCartTest.php index 537c8f09a0a98..0351a4f58a8e0 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/SetShippingAddressOnCartTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/SetShippingAddressOnCartTest.php @@ -55,6 +55,7 @@ public function testSetNewShippingAddressOnCartWithSimpleProduct() telephone: "88776655" save_in_address_book: false } + customer_notes: "Test note" } ] } @@ -73,6 +74,7 @@ public function testSetNewShippingAddressOnCartWithSimpleProduct() label } __typename + customer_notes } } } @@ -527,7 +529,8 @@ private function assertNewShippingAddressFields(array $shippingAddressResponse): ['response_field' => 'postcode', 'expected_value' => '887766'], ['response_field' => 'telephone', 'expected_value' => '88776655'], ['response_field' => 'country', 'expected_value' => ['code' => 'US', 'label' => 'US']], - ['response_field' => '__typename', 'expected_value' => 'ShippingCartAddress'] + ['response_field' => '__typename', 'expected_value' => 'ShippingCartAddress'], + ['response_field' => 'customer_notes', 'expected_value' => 'Test note'] ]; $this->assertResponseFields($shippingAddressResponse, $assertionMap); diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/SetShippingMethodsOnCartTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/SetShippingMethodsOnCartTest.php index 946c66a809389..117aedf59b5a5 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/SetShippingMethodsOnCartTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/SetShippingMethodsOnCartTest.php @@ -77,14 +77,6 @@ public function testSetShippingMethodOnCartWithSimpleProduct() self::assertEquals(10, $amount['value']); self::assertArrayHasKey('currency', $amount); self::assertEquals('USD', $amount['currency']); - - self::assertArrayHasKey('base_amount', $shippingAddress['selected_shipping_method']); - $baseAmount = $shippingAddress['selected_shipping_method']['base_amount']; - - self::assertArrayHasKey('value', $baseAmount); - self::assertEquals(10, $baseAmount['value']); - self::assertArrayHasKey('currency', $baseAmount); - self::assertEquals('USD', $baseAmount['currency']); } /** @@ -390,10 +382,6 @@ private function getQuery( value currency } - base_amount { - value - currency - } } } } diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Swatches/ProductSearchTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Swatches/ProductSearchTest.php index f0397c51c4660..8ba8b534cfe5c 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Swatches/ProductSearchTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Swatches/ProductSearchTest.php @@ -30,7 +30,7 @@ public function testFilterLn() products ( filter:{ sku:{ - like:"%simple%" + in:["simple1", "simple2", "simple3"] } } pageSize: 4 diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/VariablesSupportQueryTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/VariablesSupportQueryTest.php index 7448b165fc234..3221026871bc8 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/VariablesSupportQueryTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/VariablesSupportQueryTest.php @@ -33,7 +33,7 @@ public function testQueryObjectVariablesSupport() $query = <<<'QUERY' -query GetProductsQuery($pageSize: Int, $filterInput: ProductFilterInput, $priceSort: SortEnum) { +query GetProductsQuery($pageSize: Int, $filterInput: ProductAttributeFilterInput, $priceSort: SortEnum) { products( pageSize: $pageSize filter: $filterInput @@ -58,8 +58,8 @@ public function testQueryObjectVariablesSupport() 'pageSize' => 1, 'priceSort' => 'ASC', 'filterInput' => [ - 'min_price' => [ - 'gt' => 150, + 'price' => [ + 'from' => 150, ], ], ]; diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/_files/schema_response_sdl_deprecated_annotation.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/_files/schema_response_sdl_deprecated_annotation.php new file mode 100644 index 0000000000000..c3a150fcd75ed --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/_files/schema_response_sdl_deprecated_annotation.php @@ -0,0 +1,88 @@ + 'OBJECT', + 'name'=> 'Currency', + 'description'=> '', + 'fields'=> [ + [ + 'name'=> 'available_currency_codes', + 'description'=> null, + 'isDeprecated'=> false, + 'deprecationReason'=> null + ], + [ + 'name'=> 'base_currency_code', + 'description'=> null, + 'isDeprecated'=> false, + 'deprecationReason'=> null + ], + + [ + 'name'=> 'base_currency_symbol', + 'description'=> null, + 'isDeprecated'=> false, + 'deprecationReason'=> null + ], + [ + 'name'=> 'default_display_currecy_code', + 'description'=> null, + 'isDeprecated'=> true, + 'deprecationReason'=> 'Symbol was missed. Use `default_display_currency_code`.' + ], + [ + 'name'=> 'default_display_currecy_symbol', + 'description'=> null, + 'isDeprecated'=> true, + 'deprecationReason'=> 'Symbol was missed. Use `default_display_currency_code`.' + ], + [ + 'name'=> 'default_display_currency_code', + 'description'=> null, + 'isDeprecated'=> false, + 'deprecationReason'=> null + ], + [ + 'name'=> 'default_display_currency_symbol', + 'description'=> null, + 'isDeprecated'=> false, + 'deprecationReason'=> null + ], + [ + 'name'=> 'exchange_rates', + 'description'=> null, + 'isDeprecated'=> false, + 'deprecationReason'=> null + ], + + ], + 'enumValues'=> null + ], + [ + 'kind' => 'ENUM', + 'name' => 'DownloadableFileTypeEnum', + 'description' => '', + 'fields' => null, + 'enumValues' => [ + [ + 'name' => 'FILE', + 'description' => '', + 'isDeprecated' => true, + 'deprecationReason' => 'sample_url` serves to get the downloadable sample' + ], + [ + 'name' => 'URL', + 'description' => '', + 'isDeprecated' => true, + 'deprecationReason' => '`sample_url` serves to get the downloadable sample' + ] + ], + 'possibleTypes' => null + ], +]; diff --git a/dev/tests/api-functional/testsuite/Magento/Sales/Service/V1/OrderUpdateTest.php b/dev/tests/api-functional/testsuite/Magento/Sales/Service/V1/OrderUpdateTest.php new file mode 100644 index 0000000000000..d4bfbfc177390 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/Sales/Service/V1/OrderUpdateTest.php @@ -0,0 +1,402 @@ +objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + } + + /** + * Check order increment id after updating via webapi + * + * @magentoApiDataFixture Magento/Sales/_files/order.php + */ + public function testOrderUpdate() + { + /** @var Order $order */ + $order = $this->objectManager->get(Order::class)->loadByIncrementId(self::ORDER_INCREMENT_ID); + $serviceInfo = [ + 'rest' => [ + 'resourcePath' => self::RESOURCE_PATH, + 'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_POST, + ], + 'soap' => [ + 'service' => self::SERVICE_NAME, + 'serviceVersion' => self::SERVICE_VERSION, + 'operation' => self::SERVICE_NAME . 'save', + ], + ]; + $result = $this->_webApiCall($serviceInfo, ['entity' => $this->getOrderData($order)]); + $this->assertGreaterThan(1, count($result)); + /** @var Order $actualOrder */ + $actualOrder = $this->objectManager->get(Order::class)->load($order->getId()); + $this->assertEquals( + $order->getData(OrderInterface::INCREMENT_ID), + $actualOrder->getData(OrderInterface::INCREMENT_ID) + ); + + //Ship the order and check increment id. + $serviceInfo = [ + 'rest' => [ + 'resourcePath' => '/V1/order/' . $order->getId() . '/ship', + 'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_POST, + ], + 'soap' => [ + 'service' => 'salesShipOrderV1', + 'serviceVersion' => self::SERVICE_VERSION, + 'operation' => 'salesShipOrderV1' . 'execute', + ], + ]; + $shipmentId = $this->_webApiCall($serviceInfo, $this->getDataForShipment($order)); + $this->assertNotEmpty($shipmentId); + $actualOrder = $this->objectManager->get(Order::class)->load($order->getId()); + $this->assertEquals( + $order->getData(OrderInterface::INCREMENT_ID), + $actualOrder->getData(OrderInterface::INCREMENT_ID) + ); + + //Invoice the order and check increment id. + $serviceInfo = [ + 'rest' => [ + 'resourcePath' => '/V1/invoices', + 'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_POST, + ], + 'soap' => [ + 'service' => 'salesInvoiceRepositoryV1', + 'serviceVersion' => self::SERVICE_VERSION, + 'operation' => 'salesInvoiceRepositoryV1' . 'save', + ], + ]; + + $result = $this->_webApiCall($serviceInfo, ['entity' => $this->getDataForInvoice($order)]); + $this->assertNotEmpty($result); + $actualOrder = $this->objectManager->get(Order::class)->load($order->getId()); + $this->assertEquals( + $order->getData(OrderInterface::INCREMENT_ID), + $actualOrder->getData(OrderInterface::INCREMENT_ID) + ); + + //Create creditmemo for the order and check increment id. + $serviceInfo = [ + 'rest' => [ + 'resourcePath' => '/V1/creditmemo', + 'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_POST, + ], + 'soap' => [ + 'service' => 'salesCreditmemoRepositoryV1', + 'serviceVersion' => self::SERVICE_VERSION, + 'operation' => 'salesCreditmemoRepositoryV1' . 'save', + ], + ]; + $result = $this->_webApiCall($serviceInfo, ['entity' => $this->getDataForCreditmemo($order)]); + $this->assertNotEmpty($result); + $actualOrder = $this->objectManager->get(Order::class)->load($order->getId()); + $this->assertEquals( + $order->getData(OrderInterface::INCREMENT_ID), + $actualOrder->getData(OrderInterface::INCREMENT_ID) + ); + } + + /** + * Check order increment id after updating via webapi + * + * @magentoApiDataFixture Magento/Sales/_files/order.php + */ + public function testOrderStatusUpdate() + { + /** @var Order $order */ + $order = $this->objectManager->get(Order::class) + ->loadByIncrementId(self::ORDER_INCREMENT_ID); + + $entityData = $this->getOrderData($order); + $entityData[OrderInterface::STATE] = 'complete'; + $entityData[OrderInterface::STATUS] = 'complete'; + + $requestData = ['entity' => $entityData]; + + $serviceInfo = [ + 'rest' => [ + 'resourcePath' => '/V1/orders', + 'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_POST, + ], + 'soap' => [ + 'service' => self::SERVICE_NAME, + 'serviceVersion' => self::SERVICE_VERSION, + 'operation' => self::SERVICE_NAME . 'save', + ], + ]; + $result = $this->_webApiCall($serviceInfo, $requestData); + $this->assertGreaterThan(1, count($result)); + + /** @var Order $actualOrder */ + $actualOrder = $this->objectManager->get(Order::class)->load($order->getId()); + $this->assertEquals( + $order->getData(OrderInterface::INCREMENT_ID), + $actualOrder->getData(OrderInterface::INCREMENT_ID) + ); + } + + /** + * Prepare order data for request + * + * @param Order $order + * @return array + */ + private function getOrderData(Order $order) + { + if (TESTS_WEB_API_ADAPTER == self::ADAPTER_SOAP) { + $entityData = $order->getData(); + unset($entityData[OrderInterface::INCREMENT_ID]); + $entityData[OrderInterface::STATE] = 'processing'; + $entityData[OrderInterface::STATUS] = 'processing'; + + $orderData = $order->getData(); + $orderData['billing_address'] = $order->getBillingAddress()->getData(); + $orderData['billing_address']['street'] = ['Street']; + + $orderItems = []; + foreach ($order->getItems() as $item) { + $orderItems[] = $item->getData(); + } + $orderData['items'] = $orderItems; + + $shippingAddress = $order->getShippingAddress()->getData(); + $orderData['extension_attributes']['shipping_assignments'] = + [ + [ + 'shipping' => [ + 'address' => $shippingAddress, + 'method' => 'flatrate_flatrate' + ], + 'items' => $order->getItems(), + 'stock_id' => null, + ] + ]; + } else { + $orderData = [ + OrderInterface::ENTITY_ID => $order->getId(), + OrderInterface::STATE => 'processing', + OrderInterface::STATUS => 'processing' + ]; + } + return $orderData; + } + + /** + * Get data for invoice from order. + * + * @param Order $order + * @return array + */ + private function getDataForInvoice(Order $order): array + { + $orderItems = $order->getAllItems(); + return [ + 'order_id' => $order->getId(), + 'base_currency_code' => null, + 'base_discount_amount' => null, + 'base_grand_total' => null, + 'base_discount_tax_compensation_amount' => null, + 'base_shipping_amount' => null, + 'base_shipping_discount_tax_compensation_amnt' => null, + 'base_shipping_incl_tax' => null, + 'base_shipping_tax_amount' => null, + 'base_subtotal' => null, + 'base_subtotal_incl_tax' => null, + 'base_tax_amount' => null, + 'base_total_refunded' => null, + 'base_to_global_rate' => null, + 'base_to_order_rate' => null, + 'billing_address_id' => null, + 'can_void_flag' => null, + 'created_at' => null, + 'discount_amount' => null, + 'discount_description' => null, + 'email_sent' => null, + 'entity_id' => null, + 'global_currency_code' => null, + 'grand_total' => null, + 'discount_tax_compensation_amount' => null, + 'increment_id' => null, + 'is_used_for_refund' => null, + 'order_currency_code' => null, + 'shipping_address_id' => null, + 'shipping_amount' => null, + 'shipping_discount_tax_compensation_amount' => null, + 'shipping_incl_tax' => null, + 'shipping_tax_amount' => null, + 'state' => null, + 'store_currency_code' => null, + 'store_id' => null, + 'store_to_base_rate' => null, + 'store_to_order_rate' => null, + 'subtotal' => null, + 'subtotal_incl_tax' => null, + 'tax_amount' => null, + 'total_qty' => '1', + 'transaction_id' => null, + 'updated_at' => null, + 'items' => [ + [ + 'orderItemId' => $orderItems[0]->getId(), + 'qty' => 2, + 'additionalData' => null, + 'baseCost' => null, + 'baseDiscountAmount' => null, + 'baseDiscountTaxCompensationAmount' => null, + 'basePrice' => null, + 'basePriceInclTax' => null, + 'baseRowTotal' => null, + 'baseRowTotalInclTax' => null, + 'baseTaxAmount' => null, + 'description' => null, + 'discountAmount' => null, + 'discountTaxCompensationAmount' => null, + 'name' => null, + 'entity_id' => null, + 'parentId' => null, + 'price' => null, + 'priceInclTax' => null, + 'productId' => null, + 'rowTotal' => null, + 'rowTotalInclTax' => null, + 'sku' => 'sku' . uniqid(), + 'taxAmount' => null, + ], + ], + ]; + } + + /** + * Get data for creditmemo. + * + * @param Order $order + * @return array + */ + private function getDataForCreditmemo(Order $order): array + { + $orderItem = current($order->getAllItems()); + $items = [ + $orderItem->getId() => ['order_item_id' => $orderItem->getId(), 'qty' => $orderItem->getQtyInvoiced()], + ]; + return [ + 'adjustment' => null, + 'adjustment_negative' => null, + 'adjustment_positive' => null, + 'base_adjustment' => null, + 'base_adjustment_negative' => null, + 'base_adjustment_positive' => null, + 'base_currency_code' => null, + 'base_discount_amount' => null, + 'base_grand_total' => null, + 'base_discount_tax_compensation_amount' => null, + 'base_shipping_amount' => null, + 'base_shipping_discount_tax_compensation_amnt' => null, + 'base_shipping_incl_tax' => null, + 'base_shipping_tax_amount' => null, + 'base_subtotal' => null, + 'base_subtotal_incl_tax' => null, + 'base_tax_amount' => null, + 'base_to_global_rate' => null, + 'base_to_order_rate' => null, + 'billing_address_id' => null, + 'created_at' => null, + 'creditmemo_status' => null, + 'discount_amount' => null, + 'discount_description' => null, + 'email_sent' => null, + 'entity_id' => null, + 'global_currency_code' => null, + 'grand_total' => null, + 'discount_tax_compensation_amount' => null, + 'increment_id' => null, + 'invoice_id' => null, + 'order_currency_code' => null, + 'order_id' => $order->getId(), + 'shipping_address_id' => null, + 'shipping_amount' => null, + 'shipping_discount_tax_compensation_amount' => null, + 'shipping_incl_tax' => null, + 'shipping_tax_amount' => null, + 'state' => null, + 'store_currency_code' => null, + 'store_id' => null, + 'store_to_base_rate' => null, + 'store_to_order_rate' => null, + 'subtotal' => null, + 'subtotal_incl_tax' => null, + 'tax_amount' => null, + 'transaction_id' => null, + 'updated_at' => null, + 'items' => $items, + ]; + } + + /** + * Get data for shipment. + * + * @param Order $order + * @return array + */ + private function getDataForShipment(Order $order): array + { + $requestShipData = [ + 'orderId' => $order->getId(), + 'items' => [], + 'comment' => [ + 'comment' => 'Test Comment', + 'is_visible_on_front' => 1, + ], + 'tracks' => [ + [ + 'track_number' => 'TEST_TRACK_0001', + 'title' => 'Simple shipment track', + 'carrier_code' => 'UPS' + ] + ] + ]; + + foreach ($order->getAllItems() as $item) { + if ($item->getProductType() == 'simple') { + $requestShipData['items'][] = [ + 'order_item_id' => $item->getItemId(), + 'qty' => $item->getQtyOrdered(), + ]; + break; + } + } + + return $requestShipData; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/WebapiAsync/Model/AsyncScheduleMultiStoreTest.php b/dev/tests/api-functional/testsuite/Magento/WebapiAsync/Model/AsyncScheduleMultiStoreTest.php index a58bb6b14d069..b1f86739786c2 100644 --- a/dev/tests/api-functional/testsuite/Magento/WebapiAsync/Model/AsyncScheduleMultiStoreTest.php +++ b/dev/tests/api-functional/testsuite/Magento/WebapiAsync/Model/AsyncScheduleMultiStoreTest.php @@ -243,7 +243,6 @@ private function clearProducts() foreach ($this->skus as $sku) { $this->productRepository->deleteById($sku); } - // phpcs:ignore Magento2.Exceptions.ThrowCatch } catch (\Exception $e) { throw $e; //nothing to delete diff --git a/dev/tests/functional/lib/Magento/Mtf/Util/Protocol/CurlTransport/BackendDecorator.php b/dev/tests/functional/lib/Magento/Mtf/Util/Protocol/CurlTransport/BackendDecorator.php index a9a082e2c0027..5999b52141f05 100644 --- a/dev/tests/functional/lib/Magento/Mtf/Util/Protocol/CurlTransport/BackendDecorator.php +++ b/dev/tests/functional/lib/Magento/Mtf/Util/Protocol/CurlTransport/BackendDecorator.php @@ -109,7 +109,6 @@ protected function authorize() $isAuthorized = true; $_ENV['app_backend_url'] = $url; break; - // phpcs:ignore Magento2.Exceptions.ThrowCatch } catch (\Exception $e) { continue; } diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/Block/Adminhtml/Product/ProductForm.xml b/dev/tests/functional/tests/app/Magento/Catalog/Test/Block/Adminhtml/Product/ProductForm.xml index 028dfc6d109ea..525e6b47374a0 100644 --- a/dev/tests/functional/tests/app/Magento/Catalog/Test/Block/Adminhtml/Product/ProductForm.xml +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/Block/Adminhtml/Product/ProductForm.xml @@ -31,7 +31,7 @@ - fieldset[data-index="quantity_and_stock_status_qty"] [name="product[quantity_and_stock_status][qty]"] + fieldset[data-index="container_quantity_and_stock_status_qty"] [name="product[quantity_and_stock_status][qty]"] [data-index="quantity_and_stock_status"] [name="product[quantity_and_stock_status][is_in_stock]"] diff --git a/dev/tests/functional/tests/app/Magento/Integration/Test/Constraint/AssertUrlValidationErrorGenerated.php b/dev/tests/functional/tests/app/Magento/Integration/Test/Constraint/AssertUrlValidationErrorGenerated.php new file mode 100644 index 0000000000000..2fabecd2a920c --- /dev/null +++ b/dev/tests/functional/tests/app/Magento/Integration/Test/Constraint/AssertUrlValidationErrorGenerated.php @@ -0,0 +1,53 @@ +getIntegrationForm()->getJsErrors("integration_info"); + $urlJsError = false; + foreach ($errors as $error) { + if (strpos($error, 'Please enter a valid URL.') !== false) { + $urlJsError = true; + break; + } + } + Assert::assertTrue( + $urlJsError, + 'Failed to validate callback url (' . $integration->getEndpoint() . ') when saving integration.' + ); + } + + /** + * Returns a string representation of successful assertion. + * + * @return string + */ + public function toString() + { + return 'Callback url is properly validated when saving integration.'; + } +} diff --git a/dev/tests/functional/tests/app/Magento/Integration/Test/TestCase/CreateIntegrationEntityTest.xml b/dev/tests/functional/tests/app/Magento/Integration/Test/TestCase/CreateIntegrationEntityTest.xml index 711de1ac31bb5..e9d1a94dbbd90 100644 --- a/dev/tests/functional/tests/app/Magento/Integration/Test/TestCase/CreateIntegrationEntityTest.xml +++ b/dev/tests/functional/tests/app/Magento/Integration/Test/TestCase/CreateIntegrationEntityTest.xml @@ -43,10 +43,8 @@ <script>alert('XSS')</script> %current_password% All - - - - + + <IMG SRC=javascript:alert('XSS-%isolation%')> @@ -55,10 +53,8 @@ <IMG SRC=javascript:alert('XSS')> %current_password% All - - - - + + name-%isolation%' OR 'a'='a @@ -67,10 +63,8 @@ link' OR 'a'='a %current_password% All - - - - + + name-%isolation%" OR "a"="a @@ -79,10 +73,8 @@ link" OR "a"="a %current_password% All - - - - + + name-%isolation%" OR 'a"='a @@ -91,10 +83,8 @@ link" OR 'a"='a %current_password% All - - - - + + Integration%isolation% diff --git a/dev/tests/functional/tests/app/Magento/Msrp/Test/Constraint/AssertProductEditPageAdvancedPricingFields.php b/dev/tests/functional/tests/app/Magento/Msrp/Test/Constraint/AssertProductEditPageAdvancedPricingFields.php index d1b56bcf5c0f8..2901ff86dd5e7 100644 --- a/dev/tests/functional/tests/app/Magento/Msrp/Test/Constraint/AssertProductEditPageAdvancedPricingFields.php +++ b/dev/tests/functional/tests/app/Magento/Msrp/Test/Constraint/AssertProductEditPageAdvancedPricingFields.php @@ -11,16 +11,16 @@ use Magento\Mtf\Fixture\FixtureInterface; /** - * Check "Manufacturer's Suggested Retail Price" field on "Advanced pricing" page. + * Check "Minimum Advertised Price" field on "Advanced pricing" page. */ class AssertProductEditPageAdvancedPricingFields extends AbstractConstraint { /** - * Title of "Manufacturer's Suggested Retail Price" field. + * Title of "Minimum Advertised Price" field. * * @var string */ - private $manufacturerFieldTitle = 'Manufacturer\'s Suggested Retail Price'; + private $manufacturerFieldTitle = 'Minimum Advertised Price'; /** * @param CatalogProductEdit $catalogProductEdit @@ -35,7 +35,7 @@ public function processAssert(CatalogProductEdit $catalogProductEdit, FixtureInt \PHPUnit\Framework\Assert::assertTrue( $advancedPricing->checkField($this->manufacturerFieldTitle), - '"Manufacturer\'s Suggested Retail Price" field is not correct.' + '"Minimum Advertised Price" field is not correct.' ); } @@ -46,6 +46,6 @@ public function processAssert(CatalogProductEdit $catalogProductEdit, FixtureInt */ public function toString() { - return '"Manufacturer\'s Suggested Retail Price" field is correct.'; + return '"Minimum Advertised Price" field is correct.'; } } diff --git a/dev/tests/functional/tests/app/Magento/Paypal/Test/TestCase/ReorderUsingVaultTest.xml b/dev/tests/functional/tests/app/Magento/Paypal/Test/TestCase/ReorderUsingVaultTest.xml index 7c646d2c05cc6..fe2ff170b0e0a 100644 --- a/dev/tests/functional/tests/app/Magento/Paypal/Test/TestCase/ReorderUsingVaultTest.xml +++ b/dev/tests/functional/tests/app/Magento/Paypal/Test/TestCase/ReorderUsingVaultTest.xml @@ -24,9 +24,14 @@ payflowpro, payflowpro_use_vault Processing test_type:3rd_party_test, severity:S1 + + Visa + xxxx-1111 + + diff --git a/dev/tests/functional/tests/app/Magento/Sales/Test/Block/Adminhtml/Invoice/Grid.php b/dev/tests/functional/tests/app/Magento/Sales/Test/Block/Adminhtml/Invoice/Grid.php index 4d37ebe95a7ec..a5c172da3a992 100644 --- a/dev/tests/functional/tests/app/Magento/Sales/Test/Block/Adminhtml/Invoice/Grid.php +++ b/dev/tests/functional/tests/app/Magento/Sales/Test/Block/Adminhtml/Invoice/Grid.php @@ -24,10 +24,10 @@ class Grid extends \Magento\Ui\Test\Block\Adminhtml\DataGrid 'selector' => 'input[name="order_increment_id"]', ], 'grand_total_from' => [ - 'selector' => 'input[name="grand_total[from]"]', + 'selector' => 'input[name="base_grand_total[from]"]', ], 'grand_total_to' => [ - 'selector' => 'input[name="grand_total[to]"]', + 'selector' => 'input[name="base_grand_total[to]"]', ], ]; diff --git a/dev/tests/functional/tests/app/Magento/Sales/Test/TestStep/SubmitOrderStep.php b/dev/tests/functional/tests/app/Magento/Sales/Test/TestStep/SubmitOrderStep.php index 0ba457fd3b668..c9c6a00829a5c 100644 --- a/dev/tests/functional/tests/app/Magento/Sales/Test/TestStep/SubmitOrderStep.php +++ b/dev/tests/functional/tests/app/Magento/Sales/Test/TestStep/SubmitOrderStep.php @@ -114,7 +114,7 @@ public function run() $orderData = $this->order !== null ? $this->order->getData() : []; $order = $this->fixtureFactory->createByCode( 'orderInjectable', - ['data' => array_merge($data, $orderData)] + ['data' => array_merge($orderData, $data)] ); return ['orderId' => $orderId, 'order' => $order]; diff --git a/dev/tests/integration/_files/Magento/TestModuleQuoteTotalsObserver/Model/Config.php b/dev/tests/integration/_files/Magento/TestModuleQuoteTotalsObserver/Model/Config.php new file mode 100644 index 0000000000000..7eca15cc5d94a --- /dev/null +++ b/dev/tests/integration/_files/Magento/TestModuleQuoteTotalsObserver/Model/Config.php @@ -0,0 +1,28 @@ +active = true; + } + + public function disableObserver() + { + $this->active = false; + } + + public function isActive() + { + return $this->active; + } +} diff --git a/dev/tests/integration/_files/Magento/TestModuleQuoteTotalsObserver/Observer/AfterCollectTotals.php b/dev/tests/integration/_files/Magento/TestModuleQuoteTotalsObserver/Observer/AfterCollectTotals.php new file mode 100644 index 0000000000000..7cc6504dbfb75 --- /dev/null +++ b/dev/tests/integration/_files/Magento/TestModuleQuoteTotalsObserver/Observer/AfterCollectTotals.php @@ -0,0 +1,49 @@ +config = $config; + $this->session = $messageManager; + } + + /** + * @param Observer $observer + * @return void + */ + public function execute(\Magento\Framework\Event\Observer $observer) + { + $observer->getEvent(); + if ($this->config->isActive()) { + $this->session->getQuote(); + } + } +} diff --git a/dev/tests/integration/_files/Magento/TestModuleQuoteTotalsObserver/etc/events.xml b/dev/tests/integration/_files/Magento/TestModuleQuoteTotalsObserver/etc/events.xml new file mode 100644 index 0000000000000..91dd450e1934f --- /dev/null +++ b/dev/tests/integration/_files/Magento/TestModuleQuoteTotalsObserver/etc/events.xml @@ -0,0 +1,12 @@ + + + + + + + diff --git a/dev/tests/integration/_files/Magento/TestModuleQuoteTotalsObserver/etc/module.xml b/dev/tests/integration/_files/Magento/TestModuleQuoteTotalsObserver/etc/module.xml new file mode 100644 index 0000000000000..a24489ba74173 --- /dev/null +++ b/dev/tests/integration/_files/Magento/TestModuleQuoteTotalsObserver/etc/module.xml @@ -0,0 +1,11 @@ + + + + + + diff --git a/dev/tests/integration/_files/Magento/TestModuleQuoteTotalsObserver/registration.php b/dev/tests/integration/_files/Magento/TestModuleQuoteTotalsObserver/registration.php new file mode 100644 index 0000000000000..61e2553951b62 --- /dev/null +++ b/dev/tests/integration/_files/Magento/TestModuleQuoteTotalsObserver/registration.php @@ -0,0 +1,12 @@ +getPath(ComponentRegistrar::MODULE, 'Magento_TestModuleQuoteTotalsObserver') === null) { + ComponentRegistrar::register(ComponentRegistrar::MODULE, 'Magento_TestModuleQuoteTotalsObserver', __DIR__); +} diff --git a/dev/tests/integration/framework/Magento/TestFramework/Catalog/Model/Indexer/Category/Product/Action/Full.php b/dev/tests/integration/framework/Magento/TestFramework/Catalog/Model/Indexer/Category/Product/Action/Full.php new file mode 100644 index 0000000000000..120ad5840b16e --- /dev/null +++ b/dev/tests/integration/framework/Magento/TestFramework/Catalog/Model/Indexer/Category/Product/Action/Full.php @@ -0,0 +1,21 @@ + 0) { return; } + + $objectManager = Bootstrap::getInstance()->getObjectManager(); + $cache = $objectManager->get(CacheInterface::class); + $serializer = $objectManager->get(SerializerInterface::class); + $cachedProperties = $cache->load(self::CACHE_NAME); + + if ($cachedProperties) { + self::$backupStaticVariables = $serializer->unserialize($cachedProperties); + return; + } + + unset($cachedProperties, $objectManager); + $classFiles = array_filter( Files::init()->getPhpFiles( Files::INCLUDE_APP_CODE @@ -156,12 +186,14 @@ public static function backupStaticVariables() ), function ($classFile) { return StaticProperties::_isClassInCleanableFolders($classFile) + // phpcs:ignore Magento2.Functions.DiscouragedFunction && strpos(file_get_contents($classFile), ' static ') > 0; } ); $namespacePattern = '/namespace [a-zA-Z0-9\\\\]+;/'; $classPattern = '/\nclass [a-zA-Z0-9_]+/'; foreach ($classFiles as $classFile) { + // phpcs:ignore Magento2.Functions.DiscouragedFunction $code = file_get_contents($classFile); preg_match($namespacePattern, $code, $namespace); preg_match($classPattern, $code, $class); @@ -187,17 +219,16 @@ function ($classFile) { } } } + + $cache->save($serializer->serialize(self::$backupStaticVariables), self::CACHE_NAME); } /** * Handler for 'startTestSuite' event - * */ public function startTestSuite() { - if (empty(self::$backupStaticVariables)) { - self::backupStaticVariables(); - } + self::backupStaticVariables(); } /** diff --git a/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/Fixture/full_order_with_capture.php b/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/Fixture/full_order_with_capture.php index 4ef5a4dd14c08..0b1e8196ef007 100644 --- a/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/Fixture/full_order_with_capture.php +++ b/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/Fixture/full_order_with_capture.php @@ -7,13 +7,16 @@ declare(strict_types=1); use Magento\AuthorizenetAcceptjs\Gateway\Config; +use Magento\Sales\Api\InvoiceRepositoryInterface; use Magento\Sales\Model\Order\Payment; use Magento\Sales\Model\OrderRepository; +use Magento\Sales\Model\Service\InvoiceService; use Magento\TestFramework\Helper\Bootstrap; use Magento\Sales\Api\TransactionRepositoryInterface; use Magento\Sales\Model\Order\Payment\Transaction; use Magento\Sales\Model\Order\Payment\Transaction\BuilderInterface as TransactionBuilder; +// phpcs:ignore Magento2.Security.IncludeFile.FoundIncludeFile $order = include __DIR__ . '/../_files/full_order.php'; $objectManager = Bootstrap::getObjectManager(); @@ -24,11 +27,32 @@ $payment->setAuthorizationTransaction(false); $payment->setParentTransactionId(4321); - /** @var OrderRepository $orderRepo */ $orderRepo = $objectManager->get(OrderRepository::class); $orderRepo->save($order); +/** @var InvoiceService $invoiceService */ +$invoiceService = $objectManager->get(InvoiceService::class); +$invoice = $invoiceService->prepareInvoice($order); +$invoice->setIncrementId('100000001'); +$invoice->register(); + +/** @var InvoiceRepositoryInterface $invoiceRepository */ +$invoiceRepository = $objectManager->get(InvoiceRepositoryInterface::class); +$invoice = $invoiceRepository->save($invoice); + + +/** @var \Magento\Sales\Model\Order\CreditmemoFactory $creditmemoFactory */ +$creditmemoFactory = $objectManager->get(\Magento\Sales\Model\Order\CreditmemoFactory::class); +$creditmemo = $creditmemoFactory->createByInvoice($invoice, $invoice->getData()); +$creditmemo->setOrder($order); +$creditmemo->setState(Magento\Sales\Model\Order\Creditmemo::STATE_OPEN); +$creditmemo->setIncrementId('100000001'); + +/** @var \Magento\Sales\Api\CreditmemoRepositoryInterface $creditmemoRepository */ +$creditmemoRepository = $objectManager->get(\Magento\Sales\Api\CreditmemoRepositoryInterface::class); +$creditmemoRepository->save($creditmemo); + /** @var TransactionBuilder $transactionBuilder */ $transactionBuilder = $objectManager->create(TransactionBuilder::class); $transactionAuthorize = $transactionBuilder->setPayment($payment) diff --git a/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/Gateway/Command/RefundSettledCommandTest.php b/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/Gateway/Command/RefundSettledCommandTest.php index 6e06d749f3906..0206ecd6b876b 100644 --- a/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/Gateway/Command/RefundSettledCommandTest.php +++ b/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/Gateway/Command/RefundSettledCommandTest.php @@ -10,7 +10,10 @@ use Magento\AuthorizenetAcceptjs\Gateway\AbstractTest; use Magento\Payment\Gateway\Command\CommandPoolInterface; +use Magento\Sales\Api\Data\CreditmemoInterface; +use Magento\Sales\Model\Order; use Magento\Sales\Model\Order\Payment; +use Magento\Sales\Model\ResourceModel\Order\Creditmemo\CollectionFactory as CreditmemoCollectionFactory; class RefundSettledCommandTest extends AbstractTest { @@ -29,6 +32,7 @@ public function testRefundSettledCommand() $order = $this->getOrderWithIncrementId('100000001'); $payment = $order->getPayment(); + $payment->setCreditmemo($this->getCreditmemo($order)); $paymentDO = $this->paymentFactory->create($payment); @@ -41,13 +45,35 @@ public function testRefundSettledCommand() $this->responseMock->method('getBody') ->willReturn(json_encode($response)); - $command->execute([ - 'payment' => $paymentDO, - 'amount' => 100.00 - ]); + $command->execute( + [ + 'payment' => $paymentDO, + 'amount' => 100.00 + ] + ); /** @var Payment $payment */ $this->assertTrue($payment->getIsTransactionClosed()); $this->assertSame('5678', $payment->getTransactionId()); } + + /** + * Retrieve creditmemo from order. + * + * @param Order $order + * @return CreditmemoInterface + */ + private function getCreditmemo(Order $order): CreditmemoInterface + { + /** @var \Magento\Sales\Model\ResourceModel\Order\Creditmemo\Collection $creditMemoCollection */ + $creditMemoCollection = $this->objectManager->create(CreditmemoCollectionFactory::class)->create(); + + /** @var CreditmemoInterface $creditMemo */ + $creditMemo = $creditMemoCollection + ->setOrderFilter($order) + ->setPageSize(1) + ->getFirstItem(); + + return $creditMemo; + } } diff --git a/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/_files/full_order.php b/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/_files/full_order.php index cac7c38971ae5..420d0f55cf34e 100644 --- a/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/_files/full_order.php +++ b/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/_files/full_order.php @@ -41,12 +41,14 @@ ->setMetaDescription('meta description') ->setVisibility(Visibility::VISIBILITY_BOTH) ->setStatus(Status::STATUS_ENABLED) - ->setStockData([ - 'use_config_manage_stock' => 1, - 'qty' => 100, - 'is_qty_decimal' => 0, - 'is_in_stock' => 1, - ])->setCanSaveCustomOptions(true) + ->setStockData( + [ + 'use_config_manage_stock' => 1, + 'qty' => 100, + 'is_qty_decimal' => 0, + 'is_in_stock' => 1, + ] + )->setCanSaveCustomOptions(true) ->setHasOptions(false); /** @var ProductRepositoryInterface $productRepository */ @@ -65,6 +67,7 @@ ->setLastname('Doe') ->setShippingMethod('flatrate_flatrate'); +/** @var Payment $payment */ $payment = $objectManager->create(Payment::class); $payment->setAdditionalInformation('ccLast4', '1111'); $payment->setAdditionalInformation('opaqueDataDescriptor', 'mydescriptor'); @@ -116,6 +119,7 @@ ->setShippingAddress($shippingAddress) ->setShippingDescription('Flat Rate - Fixed') ->setShippingAmount(10) + ->setBaseShippingAmount(10) ->setStoreId(1) ->addItem($orderItem1) ->addItem($orderItem2) diff --git a/dev/tests/integration/testsuite/Magento/Braintree/Controller/Paypal/PlaceOrderTest.php b/dev/tests/integration/testsuite/Magento/Braintree/Controller/Paypal/PlaceOrderTest.php index 3c4fcdd1fc58c..756e5bae36e28 100644 --- a/dev/tests/integration/testsuite/Magento/Braintree/Controller/Paypal/PlaceOrderTest.php +++ b/dev/tests/integration/testsuite/Magento/Braintree/Controller/Paypal/PlaceOrderTest.php @@ -76,7 +76,6 @@ protected function tearDown() /** * Tests a negative scenario for a place order flow when exception throws after placing an order. * - * * @magentoAppArea frontend * @magentoAppIsolation enabled * @magentoDataFixture Magento/Braintree/Fixtures/paypal_quote.php @@ -113,6 +112,43 @@ public function testExecuteWithFailedOrder() self::assertEquals('canceled', $order->getState()); } + /** + * Tests a negative scenario for a place order flow when exception throws before order creation. + * + * @magentoAppArea frontend + * @magentoAppIsolation enabled + * @magentoDataFixture Magento/Braintree/Fixtures/paypal_quote.php + */ + public function testExecuteWithFailedQuoteValidation() + { + $reservedOrderId = null; + $quote = $this->getQuote('test01'); + $quote->setReservedOrderId($reservedOrderId); + + $this->session->method('getQuote') + ->willReturn($quote); + $this->session->method('getQuoteId') + ->willReturn($quote->getId()); + + $this->adapter->method('sale') + ->willReturn($this->getTransactionStub('authorized')); + $this->adapter->method('void') + ->willReturn($this->getTransactionStub('voided')); + + // emulates an error after placing the order + $this->session->method('setLastOrderStatus') + ->willThrowException(new \Exception('Test Exception')); + + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); + $this->dispatch('braintree/paypal/placeOrder'); + + self::assertRedirect(self::stringContains('checkout/cart')); + self::assertSessionMessages( + self::equalTo(['The order #' . $reservedOrderId . ' cannot be processed.']), + MessageInterface::TYPE_ERROR + ); + } + /** * Gets quote by reserved order ID. * diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/Indexer/Category/Product/Action/FullTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/Indexer/Category/Product/Action/FullTest.php new file mode 100644 index 0000000000000..54d717747d046 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/Indexer/Category/Product/Action/FullTest.php @@ -0,0 +1,71 @@ +objectManager = Bootstrap::getObjectManager(); + $preferenceObject = $this->objectManager->get(PreferenceObject::class); + $this->objectManager->addSharedInstance($preferenceObject, OriginObject::class); + $this->interceptor = $this->objectManager->get(OriginObject::class); + $this->pluginList = $this->objectManager->get(PluginListInterface::class); + } + + /** + * @inheritDoc + */ + protected function tearDown() + { + $this->objectManager->removeSharedInstance(OriginObject::class); + } + + /** + * Test possibility to add object preference + */ + public function testPreference() + { + $interceptorClassName = get_class($this->interceptor); + + // Check interceptor class name + $this->assertEquals($interceptorClassName, PreferenceObject::class . '\Interceptor'); + + //check that there are no fatal errors + $this->pluginList->getNext($interceptorClassName, 'execute'); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/Indexer/Product/Flat/Action/RelationTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/Indexer/Product/Flat/Action/RelationTest.php index 85bcd54767e36..72f1d330ee7d6 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Model/Indexer/Product/Flat/Action/RelationTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/Indexer/Product/Flat/Action/RelationTest.php @@ -122,6 +122,7 @@ protected function tearDown() */ public function testExecute() : void { + $this->markTestSkipped('MC-19675'); try { $this->indexer->execute(); } catch (\Magento\Framework\Exception\LocalizedException $e) { diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/ResourceModel/Attribute/Entity/AttributeTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/ResourceModel/Attribute/Entity/AttributeTest.php new file mode 100644 index 0000000000000..8ecf3da8e1aae --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/ResourceModel/Attribute/Entity/AttributeTest.php @@ -0,0 +1,144 @@ +objectManager = Bootstrap::getObjectManager(); + $this->productRepository = $this->objectManager->get(ProductRepositoryInterface::class); + $this->attributeRepository = $this->objectManager->get(AttributeRepository::class); + $this->model = $this->objectManager->get(Attribute::class); + } + + /** + * Test to Clear selected option in entities after remove + */ + public function testClearSelectedOptionInEntities() + { + $dropdownAttribute = $this->loadAttribute('dropdown_attribute'); + $dropdownOption = array_keys($dropdownAttribute->getOptions())[1]; + + $multiplyAttribute = $this->loadAttribute('multiselect_attribute'); + $multiplyOptions = array_keys($multiplyAttribute->getOptions()); + $multiplySelectedOptions = implode(',', $multiplyOptions); + $multiplyOptionToRemove = $multiplyOptions[1]; + unset($multiplyOptions[1]); + $multiplyOptionsExpected = implode(',', $multiplyOptions); + + $product = $this->loadProduct('simple'); + $product->setData('dropdown_attribute', $dropdownOption); + $product->setData('multiselect_attribute', $multiplySelectedOptions); + $this->productRepository->save($product); + + $product = $this->loadProduct('simple'); + $this->assertEquals( + $dropdownOption, + $product->getData('dropdown_attribute'), + 'The dropdown attribute is not selected' + ); + $this->assertEquals( + $multiplySelectedOptions, + $product->getData('multiselect_attribute'), + 'The multiselect attribute is not selected' + ); + + $this->removeAttributeOption($dropdownAttribute, $dropdownOption); + $this->removeAttributeOption($multiplyAttribute, $multiplyOptionToRemove); + + $product = $this->loadProduct('simple'); + $this->assertEmpty($product->getData('dropdown_attribute')); + $this->assertEquals($multiplyOptionsExpected, $product->getData('multiselect_attribute')); + } + + /** + * Remove option from attribute + * + * @param Attribute $attribute + * @param int $optionId + */ + private function removeAttributeOption(Attribute $attribute, int $optionId): void + { + $removalMarker = [ + 'option' => [ + 'value' => [$optionId => []], + 'delete' => [$optionId => '1'], + ], + ]; + $attribute->addData($removalMarker); + $attribute->save($attribute); + } + + /** + * Load product by sku + * + * @param string $sku + * @return Product + */ + private function loadProduct(string $sku): Product + { + return $this->productRepository->get($sku, true, null, true); + } + + /** + * Load attrubute by code + * + * @param string $attributeCode + * @return Attribute + */ + private function loadAttribute(string $attributeCode): Attribute + { + /** @var Attribute $attribute */ + $attribute = $this->objectManager->create(Attribute::class); + $attribute->loadByCode(4, $attributeCode); + + return $attribute; + } +} diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/ResourceModel/ProductTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/ResourceModel/ProductTest.php old mode 100644 new mode 100755 index 476f01eb277df..e218c508b7d3e --- a/dev/tests/integration/testsuite/Magento/Catalog/Model/ResourceModel/ProductTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/ResourceModel/ProductTest.php @@ -9,7 +9,17 @@ use Magento\Framework\ObjectManagerInterface; use Magento\TestFramework\Helper\Bootstrap; use PHPUnit\Framework\TestCase; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\Exception\CouldNotSaveException; +use Magento\Framework\Exception\InputException; +use Magento\Framework\Exception\StateException; +/** + * Tests product resource model + * + * @see \Magento\Catalog\Model\ResourceModel\Product + * @see \Magento\Catalog\Model\ResourceModel\AbstractResource + */ class ProductTest extends TestCase { /** @@ -53,6 +63,87 @@ public function testGetAttributeRawValue() self::assertEquals($product->getName(), $actual); } + /** + * @magentoAppArea adminhtml + * @magentoDataFixture Magento/Catalog/_files/product_simple_with_custom_store_scope_attribute.php + * @throws NoSuchEntityException + * @throws CouldNotSaveException + * @throws InputException + * @throws StateException + */ + public function testGetAttributeRawValueGetDefault() + { + $product = $this->productRepository->get('simple_with_store_scoped_custom_attribute', true, 0, true); + $product->setCustomAttribute('store_scoped_attribute_code', 'default_value'); + $this->productRepository->save($product); + + $actual = $this->model->getAttributeRawValue($product->getId(), 'store_scoped_attribute_code', 1); + $this->assertEquals('default_value', $actual); + } + + /** + * @magentoAppArea adminhtml + * @magentoDataFixture Magento/Catalog/_files/product_simple_with_custom_store_scope_attribute.php + * @throws NoSuchEntityException + * @throws CouldNotSaveException + * @throws InputException + * @throws StateException + */ + public function testGetAttributeRawValueGetStoreSpecificValueNoDefault() + { + $product = $this->productRepository->get('simple_with_store_scoped_custom_attribute', true, 0, true); + $product->setCustomAttribute('store_scoped_attribute_code', null); + $this->productRepository->save($product); + + $product = $this->productRepository->get('simple_with_store_scoped_custom_attribute', true, 1, true); + $product->setCustomAttribute('store_scoped_attribute_code', 'store_value'); + $this->productRepository->save($product); + + $actual = $this->model->getAttributeRawValue($product->getId(), 'store_scoped_attribute_code', 1); + $this->assertEquals('store_value', $actual); + } + + /** + * @magentoAppArea adminhtml + * @magentoDataFixture Magento/Catalog/_files/product_simple_with_custom_store_scope_attribute.php + * @throws NoSuchEntityException + * @throws CouldNotSaveException + * @throws InputException + * @throws StateException + */ + public function testGetAttributeRawValueGetStoreSpecificValueWithDefault() + { + $product = $this->productRepository->get('simple_with_store_scoped_custom_attribute', true, 0, true); + $product->setCustomAttribute('store_scoped_attribute_code', 'default_value'); + $this->productRepository->save($product); + + $product = $this->productRepository->get('simple_with_store_scoped_custom_attribute', true, 1, true); + $product->setCustomAttribute('store_scoped_attribute_code', 'store_value'); + $this->productRepository->save($product); + + $actual = $this->model->getAttributeRawValue($product->getId(), 'store_scoped_attribute_code', 1); + $this->assertEquals('store_value', $actual); + } + + /** + * @magentoAppArea adminhtml + * @magentoDataFixture Magento/Catalog/_files/product_simple_with_custom_store_scope_attribute.php + * @throws NoSuchEntityException + * @throws CouldNotSaveException + * @throws InputException + * @throws StateException + * @throws NoSuchEntityException + */ + public function testGetAttributeRawValueGetStoreValueFallbackToDefault() + { + $product = $this->productRepository->get('simple_with_store_scoped_custom_attribute', true, 0, true); + $product->setCustomAttribute('store_scoped_attribute_code', 'default_value'); + $this->productRepository->save($product); + + $actual = $this->model->getAttributeRawValue($product->getId(), 'store_scoped_attribute_code', 1); + $this->assertEquals('default_value', $actual); + } + /** * @magentoAppArea adminhtml * @magentoDataFixture Magento/Catalog/_files/product_special_price.php diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/attribute_set_based_on_default_set.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/attribute_set_based_on_default_set.php new file mode 100644 index 0000000000000..929b88367dd78 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/attribute_set_based_on_default_set.php @@ -0,0 +1,26 @@ +create(\Magento\Eav\Model\Entity\Attribute\Set::class); + +$entityType = $objectManager->create(\Magento\Eav\Model\Entity\Type::class)->loadByCode('catalog_product'); +$defaultSetId = $objectManager->create(\Magento\Catalog\Model\Product::class)->getDefaultAttributeSetid(); + +$data = [ + 'attribute_set_name' => 'second_attribute_set', + 'entity_type_id' => $entityType->getId(), + 'sort_order' => 200, +]; + +$attributeSet->setData($data); +$attributeSet->validate(); +$attributeSet->save(); +$attributeSet->initFromSkeleton($defaultSetId); +$attributeSet->save(); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/configurable_products_with_custom_attribute_layered_navigation.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/configurable_products_with_custom_attribute_layered_navigation.php new file mode 100644 index 0000000000000..27564d486c808 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/configurable_products_with_custom_attribute_layered_navigation.php @@ -0,0 +1,38 @@ +get(\Magento\Eav\Model\Config::class); + +/** @var $attribute \Magento\Catalog\Model\ResourceModel\Eav\Attribute */ +$attribute = $eavConfig->getAttribute('catalog_product', 'test_configurable'); + +$eavConfig->clear(); + +$attribute->setIsSearchable(1) + ->setIsVisibleInAdvancedSearch(1) + ->setIsFilterable(true) + ->setIsFilterableInSearch(true) + ->setIsVisibleOnFront(1); + +/** @var AttributeRepositoryInterface $attributeRepository */ +$attributeRepository = Bootstrap::getObjectManager()->create(AttributeRepositoryInterface::class); +$attributeRepository->save($attribute); +CacheCleaner::cleanAll(); +/** @var \Magento\Indexer\Model\Indexer\Collection $indexerCollection */ +$indexerCollection = Bootstrap::getObjectManager()->get(\Magento\Indexer\Model\Indexer\Collection::class); +$indexerCollection->load(); +/** @var \Magento\Indexer\Model\Indexer $indexer */ +foreach ($indexerCollection->getItems() as $indexer) { + $indexer->reindexAll(); +} diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/configurable_products_with_custom_attribute_layered_navigation_rollback.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/configurable_products_with_custom_attribute_layered_navigation_rollback.php new file mode 100644 index 0000000000000..49e2a8e88a1ac --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/configurable_products_with_custom_attribute_layered_navigation_rollback.php @@ -0,0 +1,9 @@ + 0, 'frontend_label' => ['Drop-Down Attribute'], 'backend_type' => 'varchar', - 'backend_model' => \Magento\Eav\Model\Entity\Attribute\Backend\ArrayBackend::class, 'option' => [ 'value' => [ 'option_1' => ['Option 1'], diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/dropdown_attribute_rollback.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/dropdown_attribute_rollback.php new file mode 100644 index 0000000000000..0ed7317762056 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/dropdown_attribute_rollback.php @@ -0,0 +1,18 @@ +get('Magento\Framework\Registry'); +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); +/** @var $attribute \Magento\Catalog\Model\ResourceModel\Eav\Attribute */ +$attribute = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( + 'Magento\Catalog\Model\ResourceModel\Eav\Attribute' +); +$attribute->load('dropdown_attribute', 'attribute_code'); +$attribute->delete(); + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_with_custom_store_scope_attribute.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_with_custom_store_scope_attribute.php new file mode 100755 index 0000000000000..183d531f947e8 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_with_custom_store_scope_attribute.php @@ -0,0 +1,72 @@ +get(ProductRepositoryInterface::class); +/** @var ProductFactory $productFactory */ +$productFactory = $objectManager->get(ProductFactory::class); +/** @var ProductAttributeRepositoryInterface $attributeRepository */ +$attributeRepository = $objectManager->get(ProductAttributeRepositoryInterface::class); + + +/** @var $installer CategorySetup */ +$installer = $objectManager->create(CategorySetup::class); +$entityModel = $objectManager->create(Entity::class); +$attributeSetId = $installer->getAttributeSetId(Product::ENTITY, 'Default'); +$entityTypeId = $entityModel->setType(Product::ENTITY) + ->getTypeId(); +$groupId = $installer->getDefaultAttributeGroupId($entityTypeId, $attributeSetId); + +/** @var ProductAttributeInterface $attribute */ +$attribute = $objectManager->create(ProductAttributeInterface::class); + +$attribute->setAttributeCode('store_scoped_attribute_code') + ->setEntityTypeId($entityTypeId) + ->setIsVisible(true) + ->setFrontendInput('text') + ->setIsFilterable(1) + ->setIsUserDefined(1) + ->setUsedInProductListing(1) + ->setBackendType('varchar') + ->setIsUsedInGrid(1) + ->setIsVisibleInGrid(1) + ->setIsFilterableInGrid(1) + ->setFrontendLabel('nobody cares') + ->setAttributeGroupId($groupId) + ->setAttributeSetId(4); + +$attributeRepository->save($attribute); + +$product = $productFactory->create() + ->setTypeId(Type::TYPE_SIMPLE) + ->setAttributeSetId(4) + ->setName('Simple With Store Scoped Custom Attribute') + ->setSku('simple_with_store_scoped_custom_attribute') + ->setPrice(100) + ->setVisibility(1) + ->setStockData( + [ + 'use_config_manage_stock' => 1, + 'qty' => 100, + 'is_in_stock' => 1, + ] + ) + ->setStatus(1); +$product->setCustomAttribute('store_scoped_attribute_code', 'default_value'); +$productRepository->save($product); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_with_custom_store_scope_attribute_rollback.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_with_custom_store_scope_attribute_rollback.php new file mode 100755 index 0000000000000..54c832dd6a6ff --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_with_custom_store_scope_attribute_rollback.php @@ -0,0 +1,41 @@ +get(ProductRepositoryInterface::class); +/** @var ProductAttributeRepositoryInterface $attributeRepository */ +$attributeRepository = $objectManager->get(ProductAttributeRepositoryInterface::class); +/** @var Registry $registry */ +$registry = $objectManager->get(Registry::class); + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); + +try { + /** @var \Magento\Catalog\Api\Data\ProductInterface $product */ + $product = $productRepository->get('simple_with_store_scoped_custom_attribute'); + $productRepository->delete($product); +} catch (NoSuchEntityException $e) { +} + +try { + /** @var \Magento\Catalog\Api\Data\ProductAttributeInterface $attribute */ + $attribute = $attributeRepository->get('store_scoped_attribute_code'); + $attributeRepository->delete($attribute); +} catch (NoSuchEntityException $e) { +} + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/products_for_relevance_sorting.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_for_relevance_sorting.php new file mode 100644 index 0000000000000..85b3146fc7ec0 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_for_relevance_sorting.php @@ -0,0 +1,112 @@ +get(ProductRepositoryInterface::class); +$categoryLinkRepository = $objectManager->create( + \Magento\Catalog\Api\CategoryLinkRepositoryInterface::class, + [ + 'productRepository' => $productRepository + ] +); +$categoryLinkManagement = $objectManager->create( + \Magento\Catalog\Api\CategoryLinkManagementInterface::class, + [ + 'productRepository' => $productRepository, + 'categoryLinkRepository' => $categoryLinkRepository + ] +); +$category = $objectManager->create(\Magento\Catalog\Model\Category::class); +$category->isObjectNew(true); +$category->setId( + 330 +)->setCreatedAt( + '2019-08-27 11:05:07' +)->setName( + 'Colorful Category' +)->setParentId( + 2 +)->setPath( + '1/2/330' +)->setLevel( + 2 +)->setAvailableSortBy( + ['position', 'name'] +)->setDefaultSortBy( + 'name' +)->setIsActive( + true +)->setPosition( + 1 +)->save(); + +$defaultAttributeSet = $objectManager->get(Magento\Eav\Model\Config::class) + ->getEntityType('catalog_product') + ->getDefaultAttributeSetId(); + +/** @var $product \Magento\Catalog\Model\Product */ +$product = $objectManager->create(\Magento\Catalog\Model\Product::class); +$product->isObjectNew(true); +$product->setTypeId(\Magento\Catalog\Model\Product\Type::TYPE_SIMPLE) + ->setAttributeSetId($defaultAttributeSet) + ->setStoreId(1) + ->setWebsiteIds([1]) + ->setName('Navy Blue Striped Shoes') + ->setSku('navy-striped-shoes') + ->setPrice(40) + ->setWeight(8) + ->setDescription('blue striped flip flops at one') + ->setMetaTitle('navy blue colored shoes meta title') + ->setMetaKeyword('blue, navy, striped , women, kids') + ->setMetaDescription('blue shoes women kids meta description') + ->setStockData(['use_config_manage_stock' => 0]) + ->setVisibility(\Magento\Catalog\Model\Product\Visibility::VISIBILITY_BOTH) + ->setStatus(\Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED) + ->save(); + +/** @var $product \Magento\Catalog\Model\Product */ +$product = $objectManager->create(\Magento\Catalog\Model\Product::class); +$product->isObjectNew(true); +$product->setTypeId(\Magento\Catalog\Model\Product\Type::TYPE_SIMPLE) + ->setAttributeSetId($defaultAttributeSet) + ->setStoreId(1) + ->setWebsiteIds([1]) + ->setName('light green Shoes') + ->setSku('light-green-shoes') + ->setPrice(40) + ->setWeight(8) + ->setDescription('green polka dots shoes one') + ->setMetaTitle('light green shoes meta title') + ->setMetaKeyword('light, green , women, kids') + ->setMetaDescription('shoes women kids meta description') + ->setStockData(['use_config_manage_stock' => 0]) + ->setVisibility(\Magento\Catalog\Model\Product\Visibility::VISIBILITY_BOTH) + ->setStatus(\Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED) + ->save(); + +/** @var \Magento\Catalog\Model\Product $greyProduct */ +$greyProduct = $productRepository->get('grey_shorts'); +$greyProduct->setDescription('Description with Blue lines'); +$productRepository->save($greyProduct); + +$skus = ['green_socks', 'white_shorts','red_trousers','blue_briefs','grey_shorts', + 'navy-striped-shoes', 'light-green-shoes']; + +/** @var \Magento\Catalog\Api\CategoryLinkManagementInterface $categoryLinkManagement */ +$categoryLinkManagement = $objectManager->create(\Magento\Catalog\Api\CategoryLinkManagementInterface::class); +foreach ($skus as $sku) { + $categoryLinkManagement->assignProductToCategories( + $sku, + [330] + ); +} diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/products_for_relevance_sorting_rollback.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_for_relevance_sorting_rollback.php new file mode 100644 index 0000000000000..5a1dd30c6b492 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_for_relevance_sorting_rollback.php @@ -0,0 +1,35 @@ +get(\Magento\Framework\Registry::class); +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); + +/** @var $category \Magento\Catalog\Model\Category */ +$category = $objectManager->create(\Magento\Catalog\Model\Category::class); +$category->load(330); +if ($category->getId()) { + $category->delete(); +} +// Remove products +/** @var \Magento\Catalog\Api\ProductRepositoryInterface $productRepository */ +$productRepository = $objectManager->create(\Magento\Catalog\Api\ProductRepositoryInterface::class); +$productsToDelete = ['green_socks', 'white_shorts','red_trousers','blue_briefs', + 'grey_shorts', 'navy-striped-shoes','light-green-shoes']; + +foreach ($productsToDelete as $sku) { + try { + $product = $productRepository->get($sku, false, null, true); + $productRepository->delete($product); + } catch (\Magento\Framework\Exception\NoSuchEntityException $e) { + //Product already removed + } +} + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_layered_navigation_attribute.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_layered_navigation_attribute.php index 48c47c9988d59..7bee46bc2078f 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_layered_navigation_attribute.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_layered_navigation_attribute.php @@ -36,7 +36,7 @@ 'is_unique' => 0, 'is_required' => 0, 'is_searchable' => 1, - 'is_visible_in_advanced_search' => 0, + 'is_visible_in_advanced_search' => 1, 'is_comparable' => 1, 'is_filterable' => 1, 'is_filterable_in_search' => 1, diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_layered_navigation_custom_attribute.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_layered_navigation_custom_attribute.php new file mode 100644 index 0000000000000..72336c48410d5 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_layered_navigation_custom_attribute.php @@ -0,0 +1,152 @@ +get(\Magento\Eav\Model\Config::class); +$attribute = $eavConfig->getAttribute('catalog_product', 'test_configurable'); + +$eavConfig->clear(); + +$attribute1 = $eavConfig->getAttribute('catalog_product', ' second_test_configurable'); +$eavConfig->clear(); + +/** @var $installer \Magento\Catalog\Setup\CategorySetup */ +$installer = Bootstrap::getObjectManager()->create(\Magento\Catalog\Setup\CategorySetup::class); + +if (!$attribute->getId()) { + + /** @var $attribute \Magento\Catalog\Model\ResourceModel\Eav\Attribute */ + $attribute = Bootstrap::getObjectManager()->create( + \Magento\Catalog\Model\ResourceModel\Eav\Attribute::class + ); + + /** @var AttributeRepositoryInterface $attributeRepository */ + $attributeRepository = Bootstrap::getObjectManager()->create(AttributeRepositoryInterface::class); + + $attribute->setData( + [ + 'attribute_code' => 'test_configurable', + 'entity_type_id' => $installer->getEntityTypeId('catalog_product'), + 'is_global' => 1, + 'is_user_defined' => 1, + 'frontend_input' => 'select', + 'is_unique' => 0, + 'is_required' => 0, + 'is_searchable' => 1, + 'is_visible_in_advanced_search' => 1, + 'is_comparable' => 1, + 'is_filterable' => 1, + 'is_filterable_in_search' => 1, + 'is_used_for_promo_rules' => 0, + 'is_html_allowed_on_front' => 1, + 'is_visible_on_front' => 1, + 'used_in_product_listing' => 1, + 'used_for_sort_by' => 1, + 'frontend_label' => ['Test Configurable'], + 'backend_type' => 'int', + 'option' => [ + 'value' => ['option_0' => ['Option 1'], 'option_1' => ['Option 2']], + 'order' => ['option_0' => 1, 'option_1' => 2], + ], + 'default_value' => 'option_0' + ] + ); + + $attributeRepository->save($attribute); + + /* Assign attribute to attribute set */ + $installer->addAttributeToGroup('catalog_product', 'Default', 'General', $attribute->getId()); +} +// create a second attribute +if (!$attribute1->getId()) { + + /** @var $attribute1 \Magento\Catalog\Model\ResourceModel\Eav\Attribute */ + $attribute1 = Bootstrap::getObjectManager()->create( + \Magento\Catalog\Model\ResourceModel\Eav\Attribute::class + ); + + /** @var AttributeRepositoryInterface $attributeRepository */ + $attributeRepository = Bootstrap::getObjectManager()->create(AttributeRepositoryInterface::class); + + $attribute1->setData( + [ + 'attribute_code' => 'second_test_configurable', + 'entity_type_id' => $installer->getEntityTypeId('catalog_product'), + 'is_global' => 1, + 'is_user_defined' => 1, + 'frontend_input' => 'select', + 'is_unique' => 0, + 'is_required' => 0, + 'is_searchable' => 1, + 'is_visible_in_advanced_search' => 1, + 'is_comparable' => 1, + 'is_filterable' => 1, + 'is_filterable_in_search' => 1, + 'is_used_for_promo_rules' => 0, + 'is_html_allowed_on_front' => 1, + 'is_visible_on_front' => 1, + 'used_in_product_listing' => 1, + 'used_for_sort_by' => 1, + 'frontend_label' => ['Second Test Configurable'], + 'backend_type' => 'int', + 'option' => [ + 'value' => ['option_0' => ['Option 3'], 'option_1' => ['Option 4']], + 'order' => ['option_0' => 1, 'option_1' => 2], + ], + 'default' => ['option_0'] + ] + ); + + $attributeRepository->save($attribute1); + + /* Assign attribute to attribute set */ + $installer->addAttributeToGroup( + 'catalog_product', + $attributeSet->getId(), + $attributeSet->getDefaultGroupId(), + $attribute1->getId() + ); +} + +$eavConfig->clear(); + +/** @var \Magento\Framework\ObjectManagerInterface $objectManager */ +$objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + +/** @var $productRepository \Magento\Catalog\Api\ProductRepositoryInterface */ +$productRepository = $objectManager->get(\Magento\Catalog\Api\ProductRepositoryInterface::class); +$productsWithNewAttributeSet = ['simple', '12345', 'simple-4']; + +foreach ($productsWithNewAttributeSet as $sku) { + try { + $product = $productRepository->get($sku, false, null, true); + $product->setAttributeSetId($attributeSet->getId()); + $product->setStockData( + ['use_config_manage_stock' => 1, + 'qty' => 50, + 'is_qty_decimal' => 0, + 'is_in_stock' => 1] + ); + $productRepository->save($product); + } catch (\Magento\Framework\Exception\NoSuchEntityException $e) { + + } +} +/** @var \Magento\Indexer\Model\Indexer\Collection $indexerCollection */ +$indexerCollection = Bootstrap::getObjectManager()->get(\Magento\Indexer\Model\Indexer\Collection::class); +$indexerCollection->load(); +/** @var \Magento\Indexer\Model\Indexer $indexer */ +foreach ($indexerCollection->getItems() as $indexer) { + $indexer->reindexAll(); +} diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_layered_navigation_custom_attribute_rollback.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_layered_navigation_custom_attribute_rollback.php new file mode 100644 index 0000000000000..5cababbc988c7 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_layered_navigation_custom_attribute_rollback.php @@ -0,0 +1,46 @@ +get(\Magento\Eav\Model\Config::class); +$attributesToDelete = ['test_configurable', 'second_test_configurable']; +/** @var AttributeRepositoryInterface $attributeRepository */ +$attributeRepository = Bootstrap::getObjectManager()->get(AttributeRepositoryInterface::class); + +foreach ($attributesToDelete as $attributeCode) { + /** @var \Magento\Eav\Api\Data\AttributeInterface $attribute */ + $attribute = $attributeRepository->get('catalog_product', $attributeCode); + $attributeRepository->delete($attribute); +} +/** @var $product \Magento\Catalog\Model\Product */ +$objectManager = Bootstrap::getObjectManager(); + +$entityType = $objectManager->create(\Magento\Eav\Model\Entity\Type::class)->loadByCode('catalog_product'); + +// remove attribute set + +/** @var \Magento\Eav\Model\ResourceModel\Entity\Attribute\Set\Collection $attributeSetCollection */ +$attributeSetCollection = $objectManager->create( + \Magento\Eav\Model\ResourceModel\Entity\Attribute\Set\Collection::class +); +$attributeSetCollection->addFilter('attribute_set_name', 'second_attribute_set'); +$attributeSetCollection->addFilter('entity_type_id', $entityType->getId()); +$attributeSetCollection->setOrder('attribute_set_id'); // descending is default value +$attributeSetCollection->setPageSize(1); +$attributeSetCollection->load(); + +/** @var \Magento\Eav\Model\Entity\Attribute\Set $attributeSet */ +$attributeSet = $attributeSetCollection->fetchItem(); +$attributeSet->delete(); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_layered_navigation_with_multiselect_attribute.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_layered_navigation_with_multiselect_attribute.php new file mode 100644 index 0000000000000..7d4f22e154030 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_layered_navigation_with_multiselect_attribute.php @@ -0,0 +1,98 @@ +create( + \Magento\Catalog\Setup\CategorySetup::class +); + +/** @var $options \Magento\Eav\Model\ResourceModel\Entity\Attribute\Option\Collection */ +$options = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( + \Magento\Eav\Model\ResourceModel\Entity\Attribute\Option\Collection::class +); +$eavConfig = Bootstrap::getObjectManager()->get(\Magento\Eav\Model\Config::class); + +/** @var $attribute \Magento\Catalog\Model\ResourceModel\Eav\Attribute */ +$attribute = $eavConfig->getAttribute('catalog_product', 'multiselect_attribute'); + +$eavConfig->clear(); +$attribute->setIsSearchable(1) + ->setIsVisibleInAdvancedSearch(1) + ->setIsFilterable(true) + ->setIsFilterableInSearch(true) + ->setIsVisibleOnFront(1); +/** @var AttributeRepositoryInterface $attributeRepository */ +$attributeRepository = Bootstrap::getObjectManager()->create(AttributeRepositoryInterface::class); +$attributeRepository->save($attribute); + +$options->setAttributeFilter($attribute->getId()); +$optionIds = $options->getAllIds(); + +/** @var \Magento\Catalog\Api\ProductRepositoryInterface $productRepository */ +$productRepository = Bootstrap::getObjectManager()->get(\Magento\Catalog\Api\ProductRepositoryInterface::class); + +/** @var $product \Magento\Catalog\Model\Product */ +$product = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(\Magento\Catalog\Model\Product::class); +$product->setTypeId(\Magento\Catalog\Model\Product\Type::TYPE_SIMPLE) + ->setId($optionIds[0] * 10) + ->setAttributeSetId($installer->getAttributeSetId('catalog_product', 'Default')) + ->setWebsiteIds([1]) + ->setName('With Multiselect 1 and 2') + ->setSku('simple_ms_1') + ->setPrice(10) + ->setDescription('Hello " &" Bring the water bottle when you can!') + ->setVisibility(\Magento\Catalog\Model\Product\Visibility::VISIBILITY_BOTH) + ->setMultiselectAttribute([$optionIds[1],$optionIds[2]]) + ->setStatus(\Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED) + ->setStockData(['use_config_manage_stock' => 1, 'qty' => 100, 'is_qty_decimal' => 0, 'is_in_stock' => 1]); +$productRepository->save($product); + +$product = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(\Magento\Catalog\Model\Product::class); +$product->setTypeId(\Magento\Catalog\Model\Product\Type::TYPE_SIMPLE) + ->setId($optionIds[1] * 10) + ->setAttributeSetId($installer->getAttributeSetId('catalog_product', 'Default')) + ->setWebsiteIds([1]) + ->setName('With Multiselect 2 and 3') + ->setSku('simple_ms_2') + ->setPrice(10) + ->setVisibility(\Magento\Catalog\Model\Product\Visibility::VISIBILITY_BOTH) + ->setMultiselectAttribute([$optionIds[2], $optionIds[3]]) + ->setStatus(\Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED) + ->setStockData(['use_config_manage_stock' => 1, 'qty' => 100, 'is_qty_decimal' => 0, 'is_in_stock' => 1]); +$productRepository->save($product); + +$product = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(\Magento\Catalog\Model\Product::class); +$product->setTypeId(\Magento\Catalog\Model\Product\Type::TYPE_SIMPLE) + ->setId($optionIds[2] * 10) + ->setAttributeSetId($installer->getAttributeSetId('catalog_product', 'Default')) + ->setWebsiteIds([1]) + ->setName('With Multiselect 1 and 3') + ->setSku('simple_ms_2') + ->setPrice(10) + ->setVisibility(\Magento\Catalog\Model\Product\Visibility::VISIBILITY_BOTH) + ->setMultiselectAttribute([$optionIds[2], $optionIds[3]]) + ->setStatus(\Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED) + ->setStockData(['use_config_manage_stock' => 1, 'qty' => 100, 'is_qty_decimal' => 0, 'is_in_stock' => 1]); + +$productRepository->save($product); + +/** @var \Magento\Indexer\Model\Indexer\Collection $indexerCollection */ +$indexerCollection = Bootstrap::getObjectManager()->get(\Magento\Indexer\Model\Indexer\Collection::class); +$indexerCollection->load(); +/** @var \Magento\Indexer\Model\Indexer $indexer */ +foreach ($indexerCollection->getItems() as $indexer) { + $indexer->reindexAll(); +} diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_layered_navigation_with_multiselect_attribute_rollback.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_layered_navigation_with_multiselect_attribute_rollback.php new file mode 100644 index 0000000000000..eb8201f04e6cc --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_layered_navigation_with_multiselect_attribute_rollback.php @@ -0,0 +1,32 @@ +get('Magento\Framework\Registry'); +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); + +/** @var $productCollection \Magento\Catalog\Model\ResourceModel\Product */ +$productCollection = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() + ->create('Magento\Catalog\Model\Product') + ->getCollection(); + +foreach ($productCollection as $product) { + $product->delete(); +} + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); + +\Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get(IndexerRegistry::class) + ->get(Magento\CatalogInventory\Model\Indexer\Stock\Processor::INDEXER_ID) + ->reindexAll(); diff --git a/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/ProductTest.php b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/ProductTest.php index c9a08c7f8a11d..1b33cd695d06e 100644 --- a/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/ProductTest.php +++ b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/ProductTest.php @@ -25,6 +25,7 @@ use Magento\Framework\Registry; use Magento\ImportExport\Model\Import; use Magento\Store\Model\Store; +use Magento\UrlRewrite\Model\ResourceModel\UrlRewriteCollection; use Psr\Log\LoggerInterface; use Magento\ImportExport\Model\Import\Source\Csv; @@ -375,8 +376,8 @@ public function testSaveCustomOptions(string $importFile, string $sku, int $expe /** * Tests adding of custom options with multiple store views * - * @magentoDataFixture Magento/Store/_files/second_store.php - * @magentoDataFixture Magento/Catalog/_files/product_simple.php + * @magentoConfigFixture current_store catalog/price/scope 1 + * @magentoDataFixture Magento/Store/_files/core_second_third_fixturestore.php * @magentoAppIsolation enabled */ public function testSaveCustomOptionsWithMultipleStoreViews() @@ -387,7 +388,7 @@ public function testSaveCustomOptionsWithMultipleStoreViews() $storeCodes = [ 'admin', 'default', - 'fixture_second_store', + 'secondstore', ]; /** @var \Magento\Store\Model\StoreManagerInterface $storeManager */ $importFile = 'product_with_custom_options_and_multiple_store_views.csv'; @@ -626,6 +627,7 @@ function ($input) { explode(',', $optionData) ) ); + // phpcs:ignore Magento2.Performance.ForeachArrayMerge $option = array_merge(...$option); if (!empty($option['type']) && !empty($option['name'])) { @@ -692,12 +694,14 @@ protected function mergeWithExistingData( } } else { $existingOptionId = array_search($optionKey, $expectedOptions); + // phpcs:ignore Magento2.Performance.ForeachArrayMerge $expectedData[$existingOptionId] = array_merge( $this->getOptionData($option), $expectedData[$existingOptionId] ); if ($optionValues) { foreach ($optionValues as $optionKey => $optionValue) { + // phpcs:ignore Magento2.Performance.ForeachArrayMerge $expectedValues[$existingOptionId][$optionKey] = array_merge( $optionValue, $expectedValues[$existingOptionId][$optionKey] @@ -842,6 +846,58 @@ public function testSaveMediaImage() $this->assertEquals('Additional Image Label Two', $additionalImageTwoItem->getLabel()); } + /** + * Test that new images should be added after the existing ones. + * + * @magentoDataFixture mediaImportImageFixture + * @magentoAppIsolation enabled + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + public function testNewImagesShouldBeAddedAfterExistingOnes() + { + $this->importDataForMediaTest('import_media.csv'); + + $product = $this->getProductBySku('simple_new'); + + $items = array_values($product->getMediaGalleryImages()->getItems()); + + $images = [ + ['file' => '/m/a/magento_image.jpg', 'label' => 'Image Label'], + ['file' => '/m/a/magento_small_image.jpg', 'label' => 'Small Image Label'], + ['file' => '/m/a/magento_thumbnail.jpg', 'label' => 'Thumbnail Label'], + ['file' => '/m/a/magento_additional_image_one.jpg', 'label' => 'Additional Image Label One'], + ['file' => '/m/a/magento_additional_image_two.jpg', 'label' => 'Additional Image Label Two'], + ]; + + $this->assertCount(5, $items); + $this->assertEquals( + $images, + array_map( + function (\Magento\Framework\DataObject $item) { + return $item->toArray(['file', 'label']); + }, + $items + ) + ); + + $this->importDataForMediaTest('import_media_additional_images.csv'); + $product->cleanModelCache(); + $product = $this->getProductBySku('simple_new'); + $items = array_values($product->getMediaGalleryImages()->getItems()); + $images[] = ['file' => '/m/a/magento_additional_image_three.jpg', 'label' => '']; + $images[] = ['file' => '/m/a/magento_additional_image_four.jpg', 'label' => '']; + $this->assertCount(7, $items); + $this->assertEquals( + $images, + array_map( + function (\Magento\Framework\DataObject $item) { + return $item->toArray(['file', 'label']); + }, + $items + ) + ); + } + /** * Test that errors occurred during importing images are logged. * @@ -892,6 +948,14 @@ public static function mediaImportImageFixture() 'source' => __DIR__ . '/_files/magento_additional_image_two.jpg', 'dest' => $dirPath . '/magento_additional_image_two.jpg', ], + [ + 'source' => __DIR__ . '/_files/magento_additional_image_three.jpg', + 'dest' => $dirPath . '/magento_additional_image_three.jpg', + ], + [ + 'source' => __DIR__ . '/_files/magento_additional_image_four.jpg', + 'dest' => $dirPath . '/magento_additional_image_four.jpg', + ], ]; foreach ($items as $item) { @@ -1647,6 +1711,63 @@ public function testImportWithNonExistingImage() } } + /** + * @magentoDataFixture Magento/Catalog/_files/product_without_options.php + * @magentoDbIsolation enabled + * @magentoAppIsolation enabled + */ + public function testUpdateUrlRewritesOnImport() + { + $filesystem = $this->objectManager->create(\Magento\Framework\Filesystem::class); + + $directory = $filesystem->getDirectoryWrite(DirectoryList::ROOT); + + $source = $this->objectManager->create( + \Magento\ImportExport\Model\Import\Source\Csv::class, + [ + 'file' => __DIR__ . '/_files/products_to_import_with_category.csv', + 'directory' => $directory + ] + ); + $errors = $this->_model->setParameters( + [ + 'behavior' => \Magento\ImportExport\Model\Import::BEHAVIOR_APPEND, + 'entity' => \Magento\Catalog\Model\Product::ENTITY + ] + )->setSource( + $source + )->validateData(); + + $this->assertTrue($errors->getErrorsCount() == 0); + + $this->_model->importData(); + + /** @var \Magento\Catalog\Model\Product $product */ + $product = $this->objectManager->create(\Magento\Catalog\Model\ProductRepository::class)->get('simple'); + + $repUrlRewriteCol = $this->objectManager->create( + UrlRewriteCollection::class + ); + + /** @var UrlRewriteCollection $collUrlRewrite */ + $collUrlRewrite = $repUrlRewriteCol->addFieldToSelect(['request_path']) + ->addFieldToFilter('entity_id', ['eq'=> $product->getEntityId()]) + ->addFieldToFilter('entity_type', ['eq'=> 'product']) + ->load(); + + $this->assertCount(2, $collUrlRewrite); + + $this->assertEquals( + sprintf('%s.html', $product->getUrlKey()), + $collUrlRewrite->getFirstItem()->getRequestPath() + ); + + $this->assertContains( + sprintf('men/tops/%s.html', $product->getUrlKey()), + $collUrlRewrite->getLastItem()->getRequestPath() + ); + } + /** * @magentoDataFixture Magento/Catalog/_files/product_simple_with_url_key.php * @magentoDbIsolation enabled @@ -2023,7 +2144,17 @@ private function importDataForMediaTest(string $fileName, int $expectedErrors = $this->assertTrue($errors->getErrorsCount() == 0); $this->_model->importData(); - $this->assertTrue($this->_model->getErrorAggregator()->getErrorsCount() == $expectedErrors); + $this->assertEquals( + $expectedErrors, + $this->_model->getErrorAggregator()->getErrorsCount(), + array_reduce( + $this->_model->getErrorAggregator()->getAllErrors(), + function ($output, $error) { + return "$output\n{$error->getErrorMessage()}"; + }, + '' + ) + ); } /** diff --git a/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/_files/import_media_additional_images.csv b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/_files/import_media_additional_images.csv new file mode 100644 index 0000000000000..fa2fbe9062bba --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/_files/import_media_additional_images.csv @@ -0,0 +1,2 @@ +sku,additional_images +simple_new,"magento_additional_image_three.jpg,magento_additional_image_four.jpg" \ No newline at end of file diff --git a/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/_files/magento_additional_image_four.jpg b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/_files/magento_additional_image_four.jpg new file mode 100644 index 0000000000000..0816984ee3606 Binary files /dev/null and b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/_files/magento_additional_image_four.jpg differ diff --git a/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/_files/magento_additional_image_three.jpg b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/_files/magento_additional_image_three.jpg new file mode 100644 index 0000000000000..0816984ee3606 Binary files /dev/null and b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/_files/magento_additional_image_three.jpg differ diff --git a/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/_files/product_with_custom_options_and_multiple_store_views.csv b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/_files/product_with_custom_options_and_multiple_store_views.csv index f068365258546..d4c4130e946a4 100644 --- a/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/_files/product_with_custom_options_and_multiple_store_views.csv +++ b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/_files/product_with_custom_options_and_multiple_store_views.csv @@ -1,4 +1,4 @@ sku,website_code,store_view_code,attribute_set_code,product_type,name,description,short_description,weight,product_online,visibility,product_websites,categories,price,special_price,special_price_from_date,special_price_to_date,tax_class_name,url_key,meta_title,meta_keywords,meta_description,base_image,base_image_label,small_image,small_image_label,thumbnail_image,thumbnail_image_label,additional_images,additional_image_labels,configurable_variation_labels,configurable_variations,bundle_price_type,bundle_sku_type,bundle_weight_type,bundle_values,downloadble_samples,downloadble_links,associated_skus,related_skus,crosssell_skus,upsell_skus,custom_options,additional_attributes,manage_stock,is_in_stock,qty,out_of_stock_qty,is_qty_decimal,allow_backorders,min_cart_qty,max_cart_qty,notify_on_stock_below,qty_increments,enable_qty_increments,is_decimal_divided,new_from_date,new_to_date,gift_message_available,created_at,updated_at,custom_design,custom_design_from,custom_design_to,custom_layout_update,page_layout,product_options_container,msrp_price,msrp_display_actual_price_type,map_enabled -simple,base,,Default,simple,New Product,,,9,1,"Catalog, Search",base,,10,,,,Taxable Goods,new-product,,,,,,,,,,,,,,,,,,,,,,,,"name=Test Select,type=drop_down,required=1,price=3,option_title=Select Option 1,sku=3-1-select|name=Test Select,type=drop_down,required=1,price=3,option_title=Select Option 2,sku=3-2-select|name=Test Field Title,type=field,required=1,sku=1-text,price=0,price_type=fixed,max_characters=10|name=Test Date and Time Title,type=date_time,required=1,price=2,sku=2-date|name=Test Checkbox,type=checkbox,required=1,price=3,option_title=Checkbox Option 1,sku=4-1-select|name=Test Checkbox,type=checkbox,required=1,price=3,option_title=Checkbox Option 2,sku=4-2-select|name=Test Radio,type=radio,required=1,price=3,option_title=Radio Option 1,sku=5-1-radio|name=Test Radio,type=radio,required=1,price=3,option_title=Radio Option 2,sku=5-2-radio",,1,1,999,0,0,0,1,10000,1,1,0,0,,,,,,,,,,,Block after Info Column,,, -simple,,default,Default,simple,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,"name=Test Select_default,type=drop_down,option_title=Select Option 1_default|name=Test Select_default,type=drop_down,option_title=Select Option 2_default|name=Test Field Title_default,type=field|name=Test Date and Time Title_default,type=date_time|name=Test Checkbox_default,type=checkbox,option_title=Checkbox Option 1_default|name=Test Checkbox_default,type=checkbox,option_title=Checkbox Option 2_default|name=Test Radio_default,type=radio,option_title=Radio Option 1_default|name=Test Radio_default,type=radio,option_title=Radio Option 2_default",,,,,,,,,,,,,,,,,,,,,,,,,,, -simple,,fixture_second_store,Default,simple,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,"name=Test Select_fixture_second_store,type=drop_down,price=1,option_title=Select Option 1_fixture_second_store|name=Test Select_fixture_second_store,type=drop_down,option_title=Select Option 2_fixture_second_store|name=Test Field Title_fixture_second_store,type=field|name=Test Date and Time Title_fixture_second_store,type=date_time|name=Test Checkbox_second_store,type=checkbox,option_title=Checkbox Option 1_second_store|name=Test Checkbox_second_store,type=checkbox,option_title=Checkbox Option 2_second_store|name=Test Radio_fixture_second_store,type=radio,option_title=Radio Option 1_fixture_second_store|name=Test Radio_fixture_second_store,type=radio,option_title=Radio Option 2_fixture_second_store",,,,,,,,,,,,,,,,,,,,,,,,,,, +simple,base,,Default,simple,New Product,,,9,1,"Catalog, Search","base,secondwebsite",,10,,,,Taxable Goods,new-product,,,,,,,,,,,,,,,,,,,,,,,,"name=Test Field Title,type=field,required=1,sku=1-text,price=100|name=Test Date and Time Title,type=date_time,required=1,sku=2-date,price=200|name=Test Select,type=drop_down,required=1,sku=3-1-select,price=310,option_title=Select Option 1|name=Test Select,type=drop_down,required=1,sku=3-2-select,price=320,option_title=Select Option 2|name=Test Checkbox,type=checkbox,required=1,sku=4-1-select,price=410,option_title=Checkbox Option 1|name=Test Checkbox,type=checkbox,required=1,sku=4-2-select,price=420,option_title=Checkbox Option 2|name=Test Radio,type=radio,required=1,sku=5-1-radio,price=510,option_title=Radio Option 1|name=Test Radio,type=radio,required=1,sku=5-2-radio,price=520,option_title=Radio Option 2",,1,1,999,0,0,0,1,10000,1,1,0,0,,,,,,,,,,,Block after Info Column,,, +simple,,default,Default,simple,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,"name=Test Field Title_default,type=field,sku=1-text|name=Test Date and Time Title_default,type=date_time,sku=2-date|name=Test Select_default,type=drop_down,sku=3-1-select,option_title=Select Option 1_default|name=Test Select_default,type=drop_down,sku=3-2-select,option_title=Select Option 2_default|name=Test Checkbox_default,type=checkbox,sku=4-1-select,option_title=Checkbox Option 1_default|name=Test Checkbox_default,type=checkbox,sku=4-2-select,option_title=Checkbox Option 2_default|name=Test Radio_default,type=radio,sku=5-1-radio,option_title=Radio Option 1_default|name=Test Radio_default,type=radio,sku=5-2-radio,option_title=Radio Option 2_default",,,,,,,,,,,,,,,,,,,,,,,,,,, +simple,,secondstore,Default,simple,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,"name=Test Field Title_fixture_second_store,type=field,sku=1-text,price=101|name=Test Date and Time Title_fixture_second_store,type=date_time,sku=2-date,price=201|name=Test Select_fixture_second_store,type=drop_down,sku=3-1-select,price=311,option_title=Select Option 1_fixture_second_store|name=Test Select_fixture_second_store,type=drop_down,sku=3-2-select,price=321,option_title=Select Option 2_fixture_second_store|name=Test Checkbox_second_store,type=checkbox,sku=4-1-select,price=411,option_title=Checkbox Option 1_second_store|name=Test Checkbox_second_store,type=checkbox,sku=4-2-select,price=421,option_title=Checkbox Option 2_second_store|name=Test Radio_fixture_second_store,type=radio,sku=5-1-radio,price=511,option_title=Radio Option 1_fixture_second_store|name=Test Radio_fixture_second_store,type=radio,sku=5-2-radio,price=521,option_title=Radio Option 2_fixture_second_store",,,,,,,,,,,,,,,,,,,,,,,,,,, diff --git a/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/_files/products_to_import_with_category.csv b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/_files/products_to_import_with_category.csv new file mode 100644 index 0000000000000..fec3f049737e9 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/_files/products_to_import_with_category.csv @@ -0,0 +1,2 @@ +sku,store_view_code,attribute_set_code,product_type,categories +simple,default,Default,simple,Default Category/Men/Tops \ No newline at end of file diff --git a/dev/tests/integration/testsuite/Magento/CatalogSearch/Model/Indexer/Fulltext/Action/FullTest.php b/dev/tests/integration/testsuite/Magento/CatalogSearch/Model/Indexer/Fulltext/Action/FullTest.php index 137a3845b1efa..916af235edbd8 100644 --- a/dev/tests/integration/testsuite/Magento/CatalogSearch/Model/Indexer/Fulltext/Action/FullTest.php +++ b/dev/tests/integration/testsuite/Magento/CatalogSearch/Model/Indexer/Fulltext/Action/FullTest.php @@ -82,37 +82,45 @@ private function getExpectedIndexData() $taxClassId = $attributeRepository ->get(\Magento\Customer\Api\Data\GroupInterface::TAX_CLASS_ID) ->getAttributeId(); + $urlKeyId = $attributeRepository + ->get(\Magento\Catalog\Api\Data\ProductAttributeInterface::CODE_SEO_FIELD_URL_KEY) + ->getAttributeId(); return [ 'configurable' => [ $skuId => 'configurable', $configurableId => 'Option 2', $nameId => 'Configurable Product | Configurable OptionOption 2', $taxClassId => 'Taxable Goods | Taxable Goods', - $statusId => 'Enabled | Enabled' + $statusId => 'Enabled | Enabled', + $urlKeyId => 'configurable-product | configurable-optionoption-2' ], 'index_enabled' => [ $skuId => 'index_enabled', $nameId => 'index enabled', $taxClassId => 'Taxable Goods', - $statusId => 'Enabled' + $statusId => 'Enabled', + $urlKeyId => 'index-enabled' ], 'index_visible_search' => [ $skuId => 'index_visible_search', $nameId => 'index visible search', $taxClassId => 'Taxable Goods', - $statusId => 'Enabled' + $statusId => 'Enabled', + $urlKeyId => 'index-visible-search' ], 'index_visible_category' => [ $skuId => 'index_visible_category', $nameId => 'index visible category', $taxClassId => 'Taxable Goods', - $statusId => 'Enabled' + $statusId => 'Enabled', + $urlKeyId => 'index-visible-category' ], 'index_visible_both' => [ $skuId => 'index_visible_both', $nameId => 'index visible both', $taxClassId => 'Taxable Goods', - $statusId => 'Enabled' + $statusId => 'Enabled', + $urlKeyId => 'index-visible-both' ] ]; } diff --git a/dev/tests/integration/testsuite/Magento/CatalogSearch/_files/indexer_fulltext.php b/dev/tests/integration/testsuite/Magento/CatalogSearch/_files/indexer_fulltext.php index 2ed7c1a45360d..0e5987f8326a5 100644 --- a/dev/tests/integration/testsuite/Magento/CatalogSearch/_files/indexer_fulltext.php +++ b/dev/tests/integration/testsuite/Magento/CatalogSearch/_files/indexer_fulltext.php @@ -14,6 +14,7 @@ ->setWebsiteIds([1]) ->setName('Simple Product Apple') ->setSku('fulltext-1') + ->setUrlKey('fulltext-1') ->setPrice(10) ->setMetaTitle('first meta title') ->setMetaKeyword('first meta keyword') @@ -30,6 +31,7 @@ ->setWebsiteIds([1]) ->setName('Simple Product Banana') ->setSku('fulltext-2') + ->setUrlKey('fulltext-2') ->setPrice(20) ->setMetaTitle('second meta title') ->setMetaKeyword('second meta keyword') @@ -46,6 +48,7 @@ ->setWebsiteIds([1]) ->setName('Simple Product Orange') ->setSku('fulltext-3') + ->setUrlKey('fulltext-3') ->setPrice(20) ->setMetaTitle('third meta title') ->setMetaKeyword('third meta keyword') @@ -62,6 +65,7 @@ ->setWebsiteIds([1]) ->setName('Simple Product Papaya') ->setSku('fulltext-4') + ->setUrlKey('fulltext-4') ->setPrice(20) ->setMetaTitle('fourth meta title') ->setMetaKeyword('fourth meta keyword') @@ -78,6 +82,7 @@ ->setWebsiteIds([1]) ->setName('Simple Product Cherry') ->setSku('fulltext-5') + ->setUrlKey('fulltext-5') ->setPrice(20) ->setMetaTitle('fifth meta title') ->setMetaKeyword('fifth meta keyword') diff --git a/dev/tests/integration/testsuite/Magento/Checkout/Controller/Cart/UpdateItemQtyTest.php b/dev/tests/integration/testsuite/Magento/Checkout/Controller/Cart/UpdateItemQtyTest.php index 4c653ab9ae33f..8d6c2625daadc 100644 --- a/dev/tests/integration/testsuite/Magento/Checkout/Controller/Cart/UpdateItemQtyTest.php +++ b/dev/tests/integration/testsuite/Magento/Checkout/Controller/Cart/UpdateItemQtyTest.php @@ -10,6 +10,7 @@ use Magento\Catalog\Model\Product; use Magento\Checkout\Model\Session; use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Framework\App\Request\Http as HttpRequest; use Magento\Framework\Data\Form\FormKey; use Magento\Framework\Serialize\Serializer\Json; @@ -81,6 +82,7 @@ public function testExecute($requestQuantity, $expectedResponse) ]; } + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); $this->getRequest()->setPostValue($request); $this->dispatch('checkout/cart/updateItemQty'); $response = $this->getResponse()->getBody(); diff --git a/dev/tests/integration/testsuite/Magento/Checkout/_files/active_quote_with_downloadable_product.php b/dev/tests/integration/testsuite/Magento/Checkout/_files/active_quote_with_downloadable_product.php new file mode 100644 index 0000000000000..de888f0f5a629 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Checkout/_files/active_quote_with_downloadable_product.php @@ -0,0 +1,40 @@ +create(\Magento\Catalog\Api\ProductRepositoryInterface::class); +/** @var $product \Magento\Catalog\Model\Product */ +$product = $productRepository->get('downloadable-product'); + +/** @var $linkCollection \Magento\Downloadable\Model\ResourceModel\Link\Collection */ +$linkCollection = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( + \Magento\Downloadable\Model\Link::class +)->getCollection()->addProductToFilter( + $product->getId() +)->addTitleToResult( + $product->getStoreId() +)->addPriceToResult( + $product->getStore()->getWebsiteId() +); + +/** @var $link \Magento\Downloadable\Model\Link */ +$link = $linkCollection->getFirstItem(); + +$requestInfo = new \Magento\Framework\DataObject(['qty' => 1, 'links' => [$link->getId()]]); + +/** @var $cart \Magento\Checkout\Model\Cart */ +$cart = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(\Magento\Checkout\Model\Cart::class); +$cart->setQuote($quote); +$cart->addProduct($product, $requestInfo); +$cart->save(); + +/** @var $objectManager \Magento\TestFramework\ObjectManager */ +$objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); +$objectManager->removeSharedInstance(\Magento\Checkout\Model\Session::class); diff --git a/dev/tests/integration/testsuite/Magento/Checkout/_files/active_quote_with_downloadable_product_rollback.php b/dev/tests/integration/testsuite/Magento/Checkout/_files/active_quote_with_downloadable_product_rollback.php new file mode 100644 index 0000000000000..e1ea3356938b3 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Checkout/_files/active_quote_with_downloadable_product_rollback.php @@ -0,0 +1,8 @@ +create(\Magento\Cms\Model\Page::class); +$page->setTitle('First test page') + ->setIdentifier('page1') + ->setStores([1]) + ->setIsActive(1) + ->setPageLayout('1column') + ->save(); + +/** @var $page \Magento\Cms\Model\Page */ +$page = $objectManager->create(\Magento\Cms\Model\Page::class); +$page->setTitle('Second test page') + ->setIdentifier('page1') + ->setStores([$store->getId()]) + ->setIsActive(1) + ->setPageLayout('1column') + ->save(); diff --git a/dev/tests/integration/testsuite/Magento/Cms/_files/two_cms_page_with_same_url_for_different_stores_rollback.php b/dev/tests/integration/testsuite/Magento/Cms/_files/two_cms_page_with_same_url_for_different_stores_rollback.php new file mode 100644 index 0000000000000..ec7a422805172 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Cms/_files/two_cms_page_with_same_url_for_different_stores_rollback.php @@ -0,0 +1,29 @@ +get(PageRepositoryInterface::class); + +/** @var SearchCriteriaBuilder $searchCriteriaBuilder */ +$searchCriteriaBuilder = $objectManager->get(SearchCriteriaBuilder::class); +$searchCriteria = $searchCriteriaBuilder->addFilter(PageInterface::IDENTIFIER, 'page1') + ->create(); +$result = $pageRepository->getList($searchCriteria); + +foreach ($result->getItems() as $item) { + $pageRepository->delete($item); +} + +// phpcs:ignore Magento2.Security.IncludeFile +require __DIR__ . '/../../../Magento/Store/_files/second_store_rollback.php'; diff --git a/dev/tests/integration/testsuite/Magento/CmsUrlRewrite/Plugin/Cms/Model/Store/ViewTest.php b/dev/tests/integration/testsuite/Magento/CmsUrlRewrite/Plugin/Cms/Model/Store/ViewTest.php new file mode 100644 index 0000000000000..422cc2958e988 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/CmsUrlRewrite/Plugin/Cms/Model/Store/ViewTest.php @@ -0,0 +1,108 @@ +objectManager = Bootstrap::getObjectManager(); + $this->urlFinder = $this->objectManager->create(UrlFinderInterface::class); + $this->storeFactory = $this->objectManager->create(StoreFactory::class); + } + + /** + * Test of replacing cms page url rewrites on create and delete store + * + * @magentoDataFixture Magento/Cms/_files/pages.php + */ + public function testUrlRewritesChangesAfterStoreSave() + { + $storeId = $this->createStore(); + $this->assertUrlRewritesCount($storeId, 1); + $this->deleteStore($storeId); + $this->assertUrlRewritesCount($storeId, 0); + } + + /** + * Assert url rewrites count by store id + * + * @param int $storeId + * @param int $expectedCount + */ + private function assertUrlRewritesCount(int $storeId, int $expectedCount): void + { + $data = [ + UrlRewrite::REQUEST_PATH => 'page100', + UrlRewrite::STORE_ID => $storeId + ]; + $urlRewrites = $this->urlFinder->findAllByData($data); + $this->assertCount($expectedCount, $urlRewrites); + } + + /** + * Create test store + * + * @return int + */ + private function createStore(): int + { + $store = $this->storeFactory->create(); + $store->setCode('test_' . random_int(0, 999)) + ->setName('Test Store') + ->unsId() + ->save(); + + return (int)$store->getId(); + } + + /** + * Delete test store + * + * @param int $storeId + * @return void + */ + private function deleteStore(int $storeId): void + { + $store = $this->storeFactory->create(); + $store->load($storeId); + if ($store !== null) { + $store->delete(); + } + } +} diff --git a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/quote_with_configurable_product.php b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/quote_with_configurable_product.php index 150ce3b3108e5..7f108623f02f2 100644 --- a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/quote_with_configurable_product.php +++ b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/quote_with_configurable_product.php @@ -40,3 +40,11 @@ /** @var $objectManager \Magento\TestFramework\ObjectManager */ $objectManager = Bootstrap::getObjectManager(); $objectManager->removeSharedInstance(\Magento\Checkout\Model\Session::class); + +/** @var \Magento\Quote\Model\QuoteIdMask $quoteIdMask */ +$quoteIdMask = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() + ->create(\Magento\Quote\Model\QuoteIdMaskFactory::class) + ->create(); +$quoteIdMask->setQuoteId($cart->getQuote()->getId()); +$quoteIdMask->setDataChanges(true); +$quoteIdMask->save(); diff --git a/dev/tests/integration/testsuite/Magento/Customer/Controller/Adminhtml/Index/MassSubscribeTest.php b/dev/tests/integration/testsuite/Magento/Customer/Controller/Adminhtml/Index/MassSubscribeTest.php index c2fc7b1b58756..1669063fb4f76 100644 --- a/dev/tests/integration/testsuite/Magento/Customer/Controller/Adminhtml/Index/MassSubscribeTest.php +++ b/dev/tests/integration/testsuite/Magento/Customer/Controller/Adminhtml/Index/MassSubscribeTest.php @@ -8,6 +8,7 @@ namespace Magento\Customer\Controller\Adminhtml\Index; use Magento\Backend\Model\Session; +use Magento\Framework\App\Request\Http as HttpRequest; use Magento\Framework\Message\MessageInterface; use Magento\Newsletter\Model\Subscriber; use Magento\Newsletter\Model\SubscriberFactory; @@ -75,7 +76,8 @@ public function testMassSubscriberAction() ], 'namespace' => 'customer_listing', ]; - $this->getRequest()->setParams($params); + $this->getRequest()->setParams($params) + ->setMethod(HttpRequest::METHOD_POST); $this->dispatch('backend/customer/index/massSubscribe'); @@ -109,7 +111,8 @@ public function testMassSubscriberActionNoSelection() 'namespace' => 'customer_listing' ]; - $this->getRequest()->setParams($params); + $this->getRequest()->setParams($params) + ->setMethod(HttpRequest::METHOD_POST); $this->dispatch('backend/customer/index/massSubscribe'); $this->assertRedirect($this->stringStartsWith($this->baseControllerUrl)); diff --git a/dev/tests/integration/testsuite/Magento/Downloadable/_files/product_downloadable_with_custom_options.php b/dev/tests/integration/testsuite/Magento/Downloadable/_files/product_downloadable_with_custom_options.php new file mode 100644 index 0000000000000..b5528dd27ee7c --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Downloadable/_files/product_downloadable_with_custom_options.php @@ -0,0 +1,100 @@ +setCanSaveCustomOptions(true); +$product->setHasOptions(true); + +$options = [ + [ + 'title' => 'test_option_code_1', + 'type' => 'field', + 'is_require' => true, + 'sort_order' => 1, + 'price' => -10.0, + 'price_type' => 'fixed', + 'sku' => 'sku1', + 'max_characters' => 10, + ], + [ + 'title' => 'area option', + 'type' => 'area', + 'is_require' => true, + 'sort_order' => 2, + 'price' => 20.0, + 'price_type' => 'percent', + 'sku' => 'sku2', + 'max_characters' => 20 + ], + [ + 'title' => 'drop_down option', + 'type' => 'drop_down', + 'is_require' => false, + 'sort_order' => 4, + 'values' => [ + [ + 'title' => 'drop_down option 1', + 'price' => 10, + 'price_type' => 'fixed', + 'sku' => 'drop_down option 1 sku', + 'sort_order' => 1, + ], + [ + 'title' => 'drop_down option 2', + 'price' => 20, + 'price_type' => 'fixed', + 'sku' => 'drop_down option 2 sku', + 'sort_order' => 2, + ], + ], + ], + [ + 'title' => 'multiple option', + 'type' => 'multiple', + 'is_require' => false, + 'sort_order' => 5, + 'values' => [ + [ + 'title' => 'multiple option 1', + 'price' => 10, + 'price_type' => 'fixed', + 'sku' => 'multiple option 1 sku', + 'sort_order' => 1, + ], + [ + 'title' => 'multiple option 2', + 'price' => 20, + 'price_type' => 'fixed', + 'sku' => 'multiple option 2 sku', + 'sort_order' => 2, + ], + ], + ] +]; + +$customOptions = []; + +/** @var \Magento\Catalog\Api\Data\ProductCustomOptionInterfaceFactory $customOptionFactory */ +$customOptionFactory = Bootstrap::getObjectManager() + ->get(\Magento\Catalog\Api\Data\ProductCustomOptionInterfaceFactory::class); + +foreach ($options as $option) { + /** @var \Magento\Catalog\Api\Data\ProductCustomOptionInterface $customOption */ + $customOption = $customOptionFactory->create(['data' => $option]); + $customOption->setProductSku($product->getSku()); + + $customOptions[] = $customOption; +} + +$product->setOptions($customOptions); + +/** @var \Magento\Catalog\Api\ProductRepositoryInterface $productRepositoryFactory */ +$productRepositoryFactory = Bootstrap::getObjectManager() + ->create(\Magento\Catalog\Api\ProductRepositoryInterface::class); +$productRepositoryFactory->save($product); diff --git a/dev/tests/integration/testsuite/Magento/Downloadable/_files/product_downloadable_with_custom_options_rollback.php b/dev/tests/integration/testsuite/Magento/Downloadable/_files/product_downloadable_with_custom_options_rollback.php new file mode 100644 index 0000000000000..4eef2a941f65f --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Downloadable/_files/product_downloadable_with_custom_options_rollback.php @@ -0,0 +1,8 @@ +operation = $objectManager->get(AddOptionToAttribute::class); + /** @var ModuleDataSetupInterface $setup */ + $this->setup = $objectManager->get(ModuleDataSetupInterface::class); + /** @var AttributeRepositoryInterface attrRepo */ + $this->attrRepo = $objectManager->get(AttributeRepositoryInterface::class); + /** @var EavSetup $eavSetup */ + $this->eavSetup = $objectManager->get(EavSetupFactory::class) + ->create(['setup' => $this->setup]); + $this->attributeId = $this->eavSetup->getAttributeId(Product::ENTITY, 'zzz'); + } + + /** + * @param bool $fetchPairs + * + * @return array + */ + private function getAttributeOptions($fetchPairs = true): array + { + $optionTable = $this->setup->getTable('eav_attribute_option'); + $optionValueTable = $this->setup->getTable('eav_attribute_option_value'); + + $select = $this->setup + ->getConnection() + ->select() + ->from(['o' => $optionTable]) + ->reset('columns') + ->columns('sort_order') + ->join(['ov' => $optionValueTable], 'o.option_id = ov.option_id', 'value') + ->where(AttributeInterface::ATTRIBUTE_ID . ' = ?', $this->attributeId) + ->where('store_id = 0'); + + return $fetchPairs + ? $this->setup->getConnection()->fetchPairs($select) + : $this->setup->getConnection()->fetchAll($select); + } + + /** + * @magentoDataFixture Magento/Eav/_files/attribute_with_options.php + */ + public function testAddNewOptions() + { + $optionsBefore = $this->getAttributeOptions(false); + $this->operation->execute( + [ + 'values' => ['new1', 'new2'], + 'attribute_id' => $this->attributeId + ] + ); + $optionsAfter = $this->getAttributeOptions(false); + $this->assertEquals(count($optionsBefore) + 2, count($optionsAfter)); + $this->assertArraySubset($optionsBefore, $optionsAfter); + } + + /** + * @magentoDataFixture Magento/Eav/_files/attribute_with_options.php + */ + public function testAddExistingOptionsWithTheSameSortOrder() + { + $optionsBefore = $this->getAttributeOptions(); + $this->operation->execute( + [ + 'values' => ['Black', 'White'], + 'attribute_id' => $this->attributeId + ] + ); + $optionsAfter = $this->getAttributeOptions(); + $this->assertEquals(count($optionsBefore), count($optionsAfter)); + $this->assertArraySubset($optionsBefore, $optionsAfter); + } + + /** + * @magentoDataFixture Magento/Eav/_files/attribute_with_options.php + */ + public function testAddExistingOptionsWithDifferentSortOrder() + { + $optionsBefore = $this->getAttributeOptions(); + $this->operation->execute( + [ + 'values' => [666 => 'White', 777 => 'Black'], + 'attribute_id' => $this->attributeId + ] + ); + $optionsAfter = $this->getAttributeOptions(); + $this->assertSameSize($optionsBefore, array_intersect($optionsBefore, $optionsAfter)); + $this->assertEquals($optionsAfter[777], $optionsBefore[0]); + $this->assertEquals($optionsAfter[666], $optionsBefore[1]); + } + + /** + * @magentoDataFixture Magento/Eav/_files/attribute_with_options.php + */ + public function testAddMixedOptions() + { + $sizeBefore = count($this->getAttributeOptions()); + $this->operation->execute( + [ + 'values' => [666 => 'Black', 'NewOption'], + 'attribute_id' => $this->attributeId + ] + ); + $updatedOptions = $this->getAttributeOptions(); + $this->assertEquals(count($updatedOptions), $sizeBefore + 1); + $this->assertEquals($updatedOptions[666], 'Black'); + $this->assertEquals($updatedOptions[667], 'NewOption'); + } + + /** + * @magentoDataFixture Magento/Eav/_files/attribute_with_options.php + */ + public function testAddNewOption() + { + $sizeBefore = count($this->getAttributeOptions()); + + $this->operation->execute( + [ + 'attribute_id' => $this->attributeId, + 'order' => [0 => 13], + 'value' => [ + [ + 0 => 'NewOption', + ], + ], + ] + ); + $updatedOptions = $this->getAttributeOptions(); + $this->assertEquals(count($updatedOptions), $sizeBefore + 1); + $this->assertEquals($updatedOptions[13], 'NewOption'); + } + + /** + * @magentoDataFixture Magento/Eav/_files/attribute_with_options.php + */ + public function testDeleteOption() + { + $optionsBefore = $this->getAttributeOptions(); + $options = $this->attrRepo->get(Product::ENTITY, $this->attributeId)->getOptions(); + /** @var AttributeOptionInterface $optionToDelete */ + $optionToDelete = end($options); + $this->operation->execute( + [ + 'attribute_id' => $this->attributeId, + 'delete' => [$optionToDelete->getValue() => true], + 'value' => [ + $optionToDelete->getValue() => null, + ], + ] + ); + $updatedOptions = $this->getAttributeOptions(); + $this->assertArraySubset($updatedOptions, $optionsBefore); + $this->assertEquals(count($updatedOptions), count($optionsBefore) - 1); + } + + /** + * @magentoDataFixture Magento/Eav/_files/attribute_with_options.php + */ + public function testUpdateOption() + { + $optionsBefore = $this->getAttributeOptions(); + $this->operation->execute( + [ + 'attribute_id' => $this->attributeId, + 'value' => [ + 0 => ['updatedValue'], + ], + ] + ); + $optionsAfter = $this->getAttributeOptions(); + $this->assertEquals($optionsAfter[0], 'updatedValue'); + $this->assertSame(array_slice($optionsBefore, 1), array_slice($optionsAfter, 1)); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Eav/_files/attribute_with_options.php b/dev/tests/integration/testsuite/Magento/Eav/_files/attribute_with_options.php new file mode 100644 index 0000000000000..a53bdc68e6b1a --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Eav/_files/attribute_with_options.php @@ -0,0 +1,46 @@ +get(ModuleDataSetupInterface::class); +/** @var EavSetup $eavSetup */ +$eavSetup = $objectManager->get(EavSetupFactory::class) + ->create(['setup' => $setup]); +$eavSetup->addAttribute( + \Magento\Catalog\Model\Product::ENTITY, + 'zzz', + [ + 'type' => 'int', + 'backend' => '', + 'frontend' => '', + 'label' => 'zzz', + 'input' => 'select', + 'class' => '', + 'source' => '', + 'global' => 1, + 'visible' => true, + 'required' => true, + 'user_defined' => true, + 'default' => null, + 'searchable' => false, + 'filterable' => false, + 'comparable' => false, + 'visible_on_front' => false, + 'used_in_product_listing' => false, + 'unique' => true, + 'apply_to' => '', + 'system' => 1, + 'group' => 'General', + 'option' => ['values' => ["Black", "White", "Red", "Brown", "zzz", "Metallic"]] + ] +); diff --git a/dev/tests/integration/testsuite/Magento/Eav/_files/attribute_with_options_rollback.php b/dev/tests/integration/testsuite/Magento/Eav/_files/attribute_with_options_rollback.php new file mode 100644 index 0000000000000..5b26403c797ec --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Eav/_files/attribute_with_options_rollback.php @@ -0,0 +1,20 @@ +get(ModuleDataSetupInterface::class); +/** @var EavSetup $eavSetup */ +$eavSetup = $objectManager->get(EavSetupFactory::class) + ->create(['setup' => $setup]); +$eavSetup->removeAttribute(Product::ENTITY, 'zzz'); diff --git a/dev/tests/integration/testsuite/Magento/Email/Model/Template/FilterTest.php b/dev/tests/integration/testsuite/Magento/Email/Model/Template/FilterTest.php index 7e16115ed2fef..5b354dce5062f 100644 --- a/dev/tests/integration/testsuite/Magento/Email/Model/Template/FilterTest.php +++ b/dev/tests/integration/testsuite/Magento/Email/Model/Template/FilterTest.php @@ -9,6 +9,7 @@ use Magento\Framework\App\State; use Magento\Framework\App\TemplateTypesInterface; use Magento\Framework\Phrase; +use Magento\Framework\View\Asset\ContentProcessorInterface; use Magento\Setup\Module\I18n\Locale; use Magento\Theme\Block\Html\Footer; @@ -267,7 +268,7 @@ public function cssDirectiveDataProvider() 'Empty or missing file' => [ TemplateTypesInterface::TYPE_HTML, 'file="css/non-existent-file.css"', - '/* Contents of the specified CSS file could not be loaded or is empty */' + '/*' . PHP_EOL . ContentProcessorInterface::ERROR_MESSAGE_PREFIX . 'LESS file is empty: ', ], 'File with compilation error results in error message' => [ TemplateTypesInterface::TYPE_HTML, @@ -407,10 +408,12 @@ public function inlinecssDirectiveThrowsExceptionWhenMissingParameterDataProvide protected function setUpDesignParams() { $themeCode = 'Vendor_EmailTest/custom_theme'; - $this->model->setDesignParams([ - 'area' => Area::AREA_FRONTEND, - 'theme' => $themeCode, - 'locale' => Locale::DEFAULT_SYSTEM_LOCALE, - ]); + $this->model->setDesignParams( + [ + 'area' => Area::AREA_FRONTEND, + 'theme' => $themeCode, + 'locale' => Locale::DEFAULT_SYSTEM_LOCALE, + ] + ); } } diff --git a/dev/tests/integration/testsuite/Magento/Framework/Error/ProcessorTest.php b/dev/tests/integration/testsuite/Magento/Framework/Error/ProcessorTest.php index 515e1a898dfac..0ed7bd4440be7 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/Error/ProcessorTest.php +++ b/dev/tests/integration/testsuite/Magento/Framework/Error/ProcessorTest.php @@ -5,52 +5,136 @@ */ namespace Magento\Framework\Error; +use Magento\TestFramework\Helper\Bootstrap; + require_once __DIR__ . '/../../../../../../../pub/errors/processor.php'; class ProcessorTest extends \PHPUnit\Framework\TestCase { - /** @var \Magento\Framework\Error\Processor */ + /** + * @var Processor + */ private $processor; - public function setUp() + /** + * @inheritdoc + */ + protected function setUp() { $this->processor = $this->createProcessor(); } - public function tearDown() + /** + * {@inheritdoc} + * @throws \Exception + */ + protected function tearDown() { - if ($this->processor->reportId) { - unlink($this->processor->_reportDir . '/' . $this->processor->reportId); - } + $reportDir = $this->processor->_reportDir; + $this->removeDirRecursively($reportDir); } - public function testSaveAndLoadReport() - { + /** + * @param int $logReportDirNestingLevel + * @param int $logReportDirNestingLevelChanged + * @param string $exceptionMessage + * @dataProvider dataProviderSaveAndLoadReport + */ + public function testSaveAndLoadReport( + int $logReportDirNestingLevel, + int $logReportDirNestingLevelChanged, + string $exceptionMessage + ) { + $_ENV['MAGE_ERROR_REPORT_DIR_NESTING_LEVEL'] = $logReportDirNestingLevel; $reportData = [ - 0 => 'exceptionMessage', + 0 => $exceptionMessage, 1 => 'exceptionTrace', 'script_name' => 'processor.php' ]; + $reportData['report_id'] = hash('sha256', implode('', $reportData)); $expectedReportData = array_merge($reportData, ['url' => '']); - $this->processor = $this->createProcessor(); - $this->processor->saveReport($reportData); - if (!$this->processor->reportId) { + $processor = $this->createProcessor(); + $processor->saveReport($reportData); + $reportId = $processor->reportId; + if (!$reportId) { $this->fail("Failed to generate report id"); } - $this->assertFileExists($this->processor->_reportDir . '/' . $this->processor->reportId); - $this->assertEquals($expectedReportData, $this->processor->reportData); + $this->assertEquals($expectedReportData, $processor->reportData); + $_ENV['MAGE_ERROR_REPORT_DIR_NESTING_LEVEL'] = $logReportDirNestingLevelChanged; + $processor = $this->createProcessor(); + $processor->loadReport($reportId); + $this->assertEquals($expectedReportData, $processor->reportData, "File contents of report don't match"); + } + + /** + * Data Provider for testSaveAndLoadReport + * + * @return array + */ + public function dataProviderSaveAndLoadReport(): array + { + return [ + [ + 'logReportDirNestingLevel' => 0, + 'logReportDirNestingLevelChanged' => 0, + 'exceptionMessage' => '$exceptionMessage 0', + ], + [ + 'logReportDirNestingLevel' => 1, + 'logReportDirNestingLevelChanged' => 1, + 'exceptionMessage' => '$exceptionMessage 1', + ], + [ + 'logReportDirNestingLevel' => 2, + 'logReportDirNestingLevelChanged' => 2, + 'exceptionMessage' => '$exceptionMessage 2', + ], + [ + 'logReportDirNestingLevel' => 3, + 'logReportDirNestingLevelChanged' => 23, + 'exceptionMessage' => '$exceptionMessage 2', + ], + [ + 'logReportDirNestingLevel' => 32, + 'logReportDirNestingLevelChanged' => 32, + 'exceptionMessage' => '$exceptionMessage 3', + ], + [ + 'logReportDirNestingLevel' => 100, + 'logReportDirNestingLevelChanged' => 100, + 'exceptionMessage' => '$exceptionMessage 100', + ], + ]; + } - $loadProcessor = $this->createProcessor(); - $loadProcessor->loadReport($this->processor->reportId); - $this->assertEquals($expectedReportData, $loadProcessor->reportData, "File contents of report don't match"); + /** + * @return Processor + */ + private function createProcessor(): Processor + { + return Bootstrap::getObjectManager()->create(Processor::class); } /** - * @return \Magento\Framework\Error\Processor + * Remove dir recursively + * + * @param string $dir + * @param int $i + * @return bool + * @throws \Exception */ - private function createProcessor() + private function removeDirRecursively(string $dir, int $i = 0): bool { - return \Magento\TestFramework\Helper\Bootstrap::getObjectManager() - ->create(\Magento\Framework\Error\Processor::class); + if ($i >= 100) { + throw new \Exception('Emergency exit from recursion'); + } + $files = array_diff(scandir($dir), ['.', '..']); + foreach ($files as $file) { + $i++; + (is_dir("$dir/$file")) + ? $this->removeDirRecursively("$dir/$file", $i) + : unlink("$dir/$file"); + } + return rmdir($dir); } } diff --git a/dev/tests/integration/testsuite/Magento/Framework/GraphQl/Config/GraphQlReaderTest.php b/dev/tests/integration/testsuite/Magento/Framework/GraphQl/Config/GraphQlReaderTest.php index af8d1c7af134b..2ffce44b32cfe 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/GraphQl/Config/GraphQlReaderTest.php +++ b/dev/tests/integration/testsuite/Magento/Framework/GraphQl/Config/GraphQlReaderTest.php @@ -200,11 +200,14 @@ enumValues(includeDeprecated: true) { $sortFields = ['inputFields', 'fields']; foreach ($sortFields as $sortField) { isset($searchTerm[$sortField]) && is_array($searchTerm[$sortField]) - ? usort($searchTerm[$sortField], function ($a, $b) { - $cmpField = 'name'; - return isset($a[$cmpField]) && isset($b[$cmpField]) + ? usort( + $searchTerm[$sortField], + function ($a, $b) { + $cmpField = 'name'; + return isset($a[$cmpField]) && isset($b[$cmpField]) ? strcmp($a[$cmpField], $b[$cmpField]) : 0; - }) : null; + } + ) : null; } $this->assertTrue( diff --git a/dev/tests/integration/testsuite/Magento/Framework/MessageQueue/UseCase/QueueTestCaseAbstract.php b/dev/tests/integration/testsuite/Magento/Framework/MessageQueue/UseCase/QueueTestCaseAbstract.php index bda232c7fb9c4..e38eccc73597e 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/MessageQueue/UseCase/QueueTestCaseAbstract.php +++ b/dev/tests/integration/testsuite/Magento/Framework/MessageQueue/UseCase/QueueTestCaseAbstract.php @@ -45,18 +45,24 @@ class QueueTestCaseAbstract extends \PHPUnit\Framework\TestCase /** * @var PublisherConsumerController */ - private $publisherConsumerController; + protected $publisherConsumerController; + /** + * @inheritdoc + */ protected function setUp() { $this->objectManager = Bootstrap::getObjectManager(); $this->logFilePath = TESTS_TEMP_DIR . "/MessageQueueTestLog.txt"; - $this->publisherConsumerController = $this->objectManager->create(PublisherConsumerController::class, [ - 'consumers' => $this->consumers, - 'logFilePath' => $this->logFilePath, - 'maxMessages' => $this->maxMessages, - 'appInitParams' => \Magento\TestFramework\Helper\Bootstrap::getInstance()->getAppInitParams() - ]); + $this->publisherConsumerController = $this->objectManager->create( + PublisherConsumerController::class, + [ + 'consumers' => $this->consumers, + 'logFilePath' => $this->logFilePath, + 'maxMessages' => $this->maxMessages, + 'appInitParams' => \Magento\TestFramework\Helper\Bootstrap::getInstance()->getAppInitParams() + ] + ); try { $this->publisherConsumerController->initialize(); @@ -70,6 +76,9 @@ protected function setUp() $this->publisher = $this->publisherConsumerController->getPublisher(); } + /** + * @inheritdoc + */ protected function tearDown() { $this->publisherConsumerController->stopConsumers(); @@ -85,25 +94,35 @@ protected function waitForAsynchronousResult($expectedLinesCount, $logFilePath) { try { //$expectedLinesCount, $logFilePath - $this->publisherConsumerController->waitForAsynchronousResult([$this, 'checkLogsExists'], [ - $expectedLinesCount, $logFilePath - ]); + $this->publisherConsumerController->waitForAsynchronousResult( + [$this, 'checkLogsExists'], + [$expectedLinesCount, $logFilePath] + ); } catch (PreconditionFailedException $e) { $this->fail($e->getMessage()); } } + /** + * Checks that logs exist + * + * @param int $expectedLinesCount + * @return bool + */ public function checkLogsExists($expectedLinesCount) { + //phpcs:ignore Magento2.Functions.DiscouragedFunction $actualCount = file_exists($this->logFilePath) ? count(file($this->logFilePath)) : 0; return $expectedLinesCount === $actualCount; } /** * Workaround for https://bugs.php.net/bug.php?id=72286 + * phpcs:disable Magento2.Functions.StaticFunction */ public static function tearDownAfterClass() { + // phpcs:enable Magento2.Functions.StaticFunction if (version_compare(phpversion(), '7') == -1) { $closeConnection = new \ReflectionMethod(\Magento\Amqp\Model\Config::class, 'closeConnection'); $closeConnection->setAccessible(true); diff --git a/dev/tests/integration/testsuite/Magento/Framework/MessageQueue/UseCase/WaitAndNotWaitMessagesTest.php b/dev/tests/integration/testsuite/Magento/Framework/MessageQueue/UseCase/WaitAndNotWaitMessagesTest.php new file mode 100644 index 0000000000000..ba5809b6634c2 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Framework/MessageQueue/UseCase/WaitAndNotWaitMessagesTest.php @@ -0,0 +1,150 @@ +msgObject = $this->objectManager->create(AsyncTestData::class); + $this->reader = $this->objectManager->get(FileReader::class); + $this->filesystem = $this->objectManager->get(Filesystem::class); + $this->config = $this->loadConfig(); + } + + /** + * Check if consumers wait for messages from the queue + */ + public function testWaitForMessages() + { + $this->assertArraySubset(['queue' => ['consumers_wait_for_messages' => 1]], $this->config); + + foreach ($this->messages as $message) { + $this->publishMessage($message); + } + + $this->waitForAsynchronousResult(count($this->messages), $this->logFilePath); + + foreach ($this->messages as $item) { + $this->assertContains($item, file_get_contents($this->logFilePath)); + } + + $this->publishMessage('message4'); + $this->waitForAsynchronousResult(count($this->messages) + 1, $this->logFilePath); + $this->assertContains('message4', file_get_contents($this->logFilePath)); + } + + /** + * Check if consumers do not wait for messages from the queue and die + */ + public function testNotWaitForMessages(): void + { + $this->publisherConsumerController->stopConsumers(); + + $config = $this->config; + $config['queue']['consumers_wait_for_messages'] = 0; + $this->writeConfig($config); + + $this->assertArraySubset(['queue' => ['consumers_wait_for_messages' => 0]], $this->loadConfig()); + foreach ($this->messages as $message) { + $this->publishMessage($message); + } + + $this->publisherConsumerController->startConsumers(); + $this->waitForAsynchronousResult(count($this->messages), $this->logFilePath); + + foreach ($this->messages as $item) { + $this->assertContains($item, file_get_contents($this->logFilePath)); + } + + // Checks that consumers do not wait 4th message and die + $this->assertArraySubset( + ['mixed.sync.and.async.queue.consumer' => []], + $this->publisherConsumerController->getConsumersProcessIds() + ); + } + + /** + * @param string $message + */ + private function publishMessage(string $message): void + { + $this->msgObject->setValue($message); + $this->msgObject->setTextFilePath($this->logFilePath); + $this->publisher->publish('multi.topic.queue.topic.c', $this->msgObject); + } + + /** + * @return array + */ + private function loadConfig(): array + { + return $this->reader->load(ConfigFilePool::APP_ENV); + } + + /** + * @param array $config + */ + private function writeConfig(array $config): void + { + $writer = $this->objectManager->get(Writer::class); + $writer->saveConfig([ConfigFilePool::APP_ENV => $config], true); + } + + /** + * @inheritdoc + */ + protected function tearDown() + { + parent::tearDown(); + $this->writeConfig($this->config); + } +} diff --git a/dev/tests/integration/testsuite/Magento/GraphQl/Controller/GraphQlControllerTest.php b/dev/tests/integration/testsuite/Magento/GraphQl/Controller/GraphQlControllerTest.php index d0d746812ec44..2fd388d9db3f5 100644 --- a/dev/tests/integration/testsuite/Magento/GraphQl/Controller/GraphQlControllerTest.php +++ b/dev/tests/integration/testsuite/Magento/GraphQl/Controller/GraphQlControllerTest.php @@ -167,7 +167,7 @@ public function testDispatchGetWithParameterizedVariables() : void /** @var ProductInterface $product */ $product = $productRepository->get('simple1'); $query = <<assertEquals( \Magento\Framework\GraphQl\Exception\GraphQlInputException::EXCEPTION_CATEGORY, - $error['category'] + $error['extensions']['category'] ); if (isset($error['message'])) { $this->assertEquals($error['message'], 'Invalid entity_type specified: invalid'); diff --git a/dev/tests/integration/testsuite/Magento/GroupedProduct/Pricing/Price/FinalPriceTest.php b/dev/tests/integration/testsuite/Magento/GroupedProduct/Pricing/Price/FinalPriceTest.php index 8b9fb9f0969ae..b5d9cb89356f6 100644 --- a/dev/tests/integration/testsuite/Magento/GroupedProduct/Pricing/Price/FinalPriceTest.php +++ b/dev/tests/integration/testsuite/Magento/GroupedProduct/Pricing/Price/FinalPriceTest.php @@ -9,6 +9,11 @@ use Magento\Catalog\Api\Data\ProductTierPriceInterface; use Magento\TestFramework\Helper\Bootstrap; +/** + * Class FinalPriceTest + * + * @package Magento\GroupedProduct\Pricing\Price + */ class FinalPriceTest extends \PHPUnit\Framework\TestCase { /** @@ -29,7 +34,7 @@ public function testFinalPrice() * @magentoDataFixture Magento/GroupedProduct/_files/product_grouped.php * @magentoAppIsolation enabled */ - public function testFinalPriceWithTearPrice() + public function testFinalPriceWithTierPrice() { $productRepository = Bootstrap::getObjectManager() ->get(\Magento\Catalog\Api\ProductRepositoryInterface::class); @@ -41,9 +46,11 @@ public function testFinalPriceWithTearPrice() /** @var $simpleProduct \Magento\Catalog\Api\Data\ProductInterface */ $simpleProduct = $productRepository->get('simple'); - $simpleProduct->setTierPrices([ - $tierPrice - ]); + $simpleProduct->setTierPrices( + [ + $tierPrice + ] + ); $productRepository->save($simpleProduct); /** @var $product \Magento\Catalog\Model\Product */ diff --git a/dev/tests/integration/testsuite/Magento/InstantPurchase/_files/fake_payment_token.php b/dev/tests/integration/testsuite/Magento/InstantPurchase/_files/fake_payment_token.php index ad2fc8f836168..276fb77a22ea2 100644 --- a/dev/tests/integration/testsuite/Magento/InstantPurchase/_files/fake_payment_token.php +++ b/dev/tests/integration/testsuite/Magento/InstantPurchase/_files/fake_payment_token.php @@ -19,4 +19,6 @@ $token->setIsVisible(true); $token->setCreatedAt(strtotime('-1 day')); $token->setExpiresAt(strtotime('+1 day')); +$tokenDetails = ['cc_last4' => '1111', 'cc_exp_year' => '2020', 'cc_exp_month' => '01', 'cc_type' => 'VI']; +$token->setTokenDetails(json_encode($tokenDetails)); $repository->save($token); diff --git a/dev/tests/integration/testsuite/Magento/MediaStorage/Helper/File/Storage/DatabaseTest.php b/dev/tests/integration/testsuite/Magento/MediaStorage/Helper/File/Storage/DatabaseTest.php index 92a3ee4181122..a132f97b07e6e 100644 --- a/dev/tests/integration/testsuite/Magento/MediaStorage/Helper/File/Storage/DatabaseTest.php +++ b/dev/tests/integration/testsuite/Magento/MediaStorage/Helper/File/Storage/DatabaseTest.php @@ -3,6 +3,8 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\MediaStorage\Helper\File\Storage; use Magento\Framework\ObjectManagerInterface; @@ -50,7 +52,10 @@ protected function setUp() /** * test for \Magento\MediaStorage\Model\File\Storage\Database::deleteFolder() * + * @magentoDbIsolation enabled * @magentoDataFixture Magento/MediaStorage/_files/database_mode.php + * @magentoConfigFixture current_store system/media_storage_configuration/media_storage 1 + * @magentoConfigFixture current_store system/media_storage_configuration/media_database default_setup */ public function testDeleteFolder() { diff --git a/dev/tests/integration/testsuite/Magento/MediaStorage/_files/database_mode.php b/dev/tests/integration/testsuite/Magento/MediaStorage/_files/database_mode.php index 1330fe8ea6af7..ef92d6668be4a 100644 --- a/dev/tests/integration/testsuite/Magento/MediaStorage/_files/database_mode.php +++ b/dev/tests/integration/testsuite/Magento/MediaStorage/_files/database_mode.php @@ -12,6 +12,7 @@ $database = $objectManager->get(\Magento\MediaStorage\Helper\File\Storage\Database::class); $database->getStorageDatabaseModel()->init(); +/** @var Magento\Framework\App\Config\ConfigResource\ConfigInterface $config */ $config = $objectManager->get(Magento\Framework\App\Config\ConfigResource\ConfigInterface::class); $config->saveConfig('system/media_storage_configuration/media_storage', '1'); $config->saveConfig('system/media_storage_configuration/media_database', 'default_setup'); diff --git a/dev/tests/integration/testsuite/Magento/MediaStorage/_files/database_mode_rollback.php b/dev/tests/integration/testsuite/Magento/MediaStorage/_files/database_mode_rollback.php new file mode 100644 index 0000000000000..b226a8de54976 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/MediaStorage/_files/database_mode_rollback.php @@ -0,0 +1,15 @@ +get(Magento\Framework\App\Config\ConfigResource\ConfigInterface::class); +$config->deleteConfig('system/media_storage_configuration/media_storage'); +$config->deleteConfig('system/media_storage_configuration/media_database'); +$objectManager->get(Magento\Framework\App\Config\ReinitableConfigInterface::class)->reinit(); diff --git a/dev/tests/integration/testsuite/Magento/Multishipping/Fixtures/quote_with_configurable_product.php b/dev/tests/integration/testsuite/Magento/Multishipping/Fixtures/quote_with_configurable_product.php new file mode 100644 index 0000000000000..2a472371fd19f --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Multishipping/Fixtures/quote_with_configurable_product.php @@ -0,0 +1,145 @@ +getById(10); +$product->setStockData(['use_config_manage_stock' => 1, 'qty' => 4, 'is_qty_decimal' => 0, 'is_in_stock' => 1]); +$productRepository->save($product); + +/** @var Quote $quote */ +$quote = $objectManager->create(Quote::class); +$request = $objectManager->create(DataObject::class); + +/** @var Config $eavConfig */ +$eavConfig = $objectManager->get(Config::class); +/** @var $attribute */ +$attribute = $eavConfig->getAttribute('catalog_product', 'test_configurable'); + +$request->setData( + [ + 'product_id' => $productRepository->get('configurable')->getId(), + 'selected_configurable_option' => '1', + 'super_attribute' => [ + $attribute->getAttributeId() => $attribute->getOptions()[1]->getValue() + ], + 'qty' => '2' + ] +); + +$quote->setStoreId(1) + ->setIsActive( + true + )->setIsMultiShipping( + 1 + )->setReservedOrderId( + 'test_order_with_configurable_product' + )->setCustomerEmail( + 'store@example.com' + )->addProduct( + $productRepository->get('configurable'), + $request + ); + +/** @var PaymentInterface $payment */ +$payment = $objectManager->create(PaymentInterface::class); +$payment->setMethod('checkmo'); +$quote->setPayment($payment); + +$addressList = [ + [ + 'firstname' => 'Jonh', + 'lastname' => 'Doe', + 'telephone' => '0333-233-221', + 'street' => ['Main Division 1'], + 'city' => 'Culver City', + 'region' => 'CA', + 'postcode' => 90800, + 'country_id' => 'US', + 'email' => 'store@example.com', + 'address_type' => 'shipping', + ], + [ + 'firstname' => 'Antoni', + 'lastname' => 'Holmes', + 'telephone' => '0333-233-221', + 'street' => ['Second Division 2'], + 'city' => 'Denver', + 'region' => 'CO', + 'postcode' => 80203, + 'country_id' => 'US', + 'email' => 'customer002@shipping.test', + 'address_type' => 'shipping' + ], +]; + +$methodCode = 'flatrate_flatrate'; +foreach ($addressList as $data) { + /** @var Rate $rate */ + $rate = $objectManager->create(Rate::class); + $rate->setCode($methodCode) + ->setPrice(5.00); + + $address = $objectManager->create(AddressInterface::class, ['data' => $data]); + $address->setShippingMethod($methodCode) + ->addShippingRate($rate) + ->setShippingAmount(5.00) + ->setBaseShippingAmount(5.00); + + $quote->addAddress($address); +} + +/** @var CartRepositoryInterface $quoteRepository */ +$quoteRepository = $objectManager->get(CartRepositoryInterface::class); +$quoteRepository->save($quote); + +$items = $quote->getAllItems(); + +foreach ($quote->getAllShippingAddresses() as $address) { + foreach ($items as $item) { + $item->setQty(1); + $address->setTotalQty(1); + $address->addItem($item); + }; +} + +$billingAddressData = [ + 'firstname' => 'Jonh', + 'lastname' => 'Doe', + 'telephone' => '0333-233-221', + 'street' => ['Third Division 1'], + 'city' => 'New York', + 'region' => 'NY', + 'postcode' => 10029, + 'country_id' => 'US', + 'email' => 'store@example.com', + 'address_type' => 'billing', +]; + +/** @var AddressInterface $address */ +$billingAddress = $objectManager->create(AddressInterface::class, ['data' => $billingAddressData]); +$quote->setBillingAddress($billingAddress); +$quote->collectTotals(); +$quoteRepository->save($quote); + +/** @var Session $session */ +$session = $objectManager->get(Session::class); +$session->setQuoteId($quote->getId()); diff --git a/dev/tests/integration/testsuite/Magento/Multishipping/Fixtures/quote_with_configurable_product_rollback.php b/dev/tests/integration/testsuite/Magento/Multishipping/Fixtures/quote_with_configurable_product_rollback.php new file mode 100644 index 0000000000000..578fcd9855de3 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Multishipping/Fixtures/quote_with_configurable_product_rollback.php @@ -0,0 +1,17 @@ +create(Quote::class); +$quote->load('test_order_with_configurable_product', 'reserved_order_id')->delete(); diff --git a/dev/tests/integration/testsuite/Magento/Multishipping/Model/Checkout/Type/MultishippingTest.php b/dev/tests/integration/testsuite/Magento/Multishipping/Model/Checkout/Type/MultishippingTest.php index b4222c0d0189d..ef9e65156c0b5 100644 --- a/dev/tests/integration/testsuite/Magento/Multishipping/Model/Checkout/Type/MultishippingTest.php +++ b/dev/tests/integration/testsuite/Magento/Multishipping/Model/Checkout/Type/MultishippingTest.php @@ -361,6 +361,42 @@ public function testCreateOrdersWithSomeFailedOrders() ); } + /** + * Check product parent item id in order item + * + * @magentoDataFixture Magento/Multishipping/Fixtures/quote_with_configurable_product.php + */ + public function testCreateOrdersWithConfigurableProduct() + { + $quote = $this->getQuote('test_order_with_configurable_product'); + /** @var CheckoutSession $session */ + $session = $this->objectManager->get(CheckoutSession::class); + $session->replaceQuote($quote); + + $this->model->createOrders(); + + $quoteItemParentIds = []; + foreach ($quote->getAllItems() as $quoteItem) { + $quoteItemParentIds[] = $quoteItem->getParentItemId(); + } + + $orderList = $this->getOrderList((int)$quote->getId()); + $firstOrder = array_shift($orderList); + $secondOrder = array_shift($orderList); + + $firstOrderItemParentsIds = []; + foreach ($firstOrder->getItems() as $orderItem) { + $firstOrderItemParentsIds[] = $orderItem->getParentItemId(); + } + $secondOrderItemParentsIds = []; + foreach ($secondOrder->getItems() as $orderItem) { + $secondOrderItemParentsIds[] = $orderItem->getParentItemId(); + } + + $this->assertNotNull($firstOrderItemParentsIds[1]); + $this->assertNotNull($secondOrderItemParentsIds[1]); + } + /** * Returns order service mock with successful place on first call and exceptions on other calls. * diff --git a/dev/tests/integration/testsuite/Magento/Paypal/Model/Express/CheckoutTest.php b/dev/tests/integration/testsuite/Magento/Paypal/Model/Express/CheckoutTest.php index 3f7f8719fd587..8d6e4dbf30ae5 100644 --- a/dev/tests/integration/testsuite/Magento/Paypal/Model/Express/CheckoutTest.php +++ b/dev/tests/integration/testsuite/Magento/Paypal/Model/Express/CheckoutTest.php @@ -278,7 +278,6 @@ public function testReturnFromPaypal() $this->assertTrue((bool)$shippingAddress->getSameAsBilling()); $this->assertNull($shippingAddress->getPrefix()); $this->assertNull($shippingAddress->getMiddlename()); - $this->assertNull($shippingAddress->getLastname()); $this->assertNull($shippingAddress->getSuffix()); $this->assertTrue($shippingAddress->getShouldIgnoreValidation()); $this->assertContains('exported', $shippingAddress->getFirstname()); diff --git a/dev/tests/integration/testsuite/Magento/PaypalGraphQl/Model/Resolver/Guest/PaypalExpressTokenTest.php b/dev/tests/integration/testsuite/Magento/PaypalGraphQl/Model/Resolver/Guest/PaypalExpressTokenTest.php index 3c7bd4a8c0bd3..24c64bce54438 100644 --- a/dev/tests/integration/testsuite/Magento/PaypalGraphQl/Model/Resolver/Guest/PaypalExpressTokenTest.php +++ b/dev/tests/integration/testsuite/Magento/PaypalGraphQl/Model/Resolver/Guest/PaypalExpressTokenTest.php @@ -136,7 +136,7 @@ public function testResolveWithPaypalError($paymentMethod): void $this->assertArrayHasKey('errors', $responseData); $actualError = $responseData['errors'][0]; $this->assertEquals($expectedExceptionMessage, $actualError['message']); - $this->assertEquals(GraphQlInputException::EXCEPTION_CATEGORY, $actualError['category']); + $this->assertEquals(GraphQlInputException::EXCEPTION_CATEGORY, $actualError['extensions']['category']); } /** @@ -173,7 +173,7 @@ public function testResolveWithInvalidRedirectUrl($paymentMethod): void $this->assertArrayHasKey('errors', $responseData); $actualError = $responseData['errors'][0]; $this->assertEquals($expectedExceptionMessage, $actualError['message']); - $this->assertEquals(GraphQlInputException::EXCEPTION_CATEGORY, $actualError['category']); + $this->assertEquals(GraphQlInputException::EXCEPTION_CATEGORY, $actualError['extensions']['category']); } /** diff --git a/dev/tests/integration/testsuite/Magento/PaypalGraphQl/Model/Resolver/Guest/PaypalPayflowProTokenExceptionTest.php b/dev/tests/integration/testsuite/Magento/PaypalGraphQl/Model/Resolver/Guest/PaypalPayflowProTokenExceptionTest.php index b5de225b7abbc..25a9a01c4c4c7 100644 --- a/dev/tests/integration/testsuite/Magento/PaypalGraphQl/Model/Resolver/Guest/PaypalPayflowProTokenExceptionTest.php +++ b/dev/tests/integration/testsuite/Magento/PaypalGraphQl/Model/Resolver/Guest/PaypalPayflowProTokenExceptionTest.php @@ -73,6 +73,6 @@ public function testResolveWithPaypalError(): void $this->assertArrayHasKey('errors', $responseData); $actualError = $responseData['errors'][0]; $this->assertEquals($expectedExceptionMessage, $actualError['message']); - $this->assertEquals(GraphQlInputException::EXCEPTION_CATEGORY, $actualError['category']); + $this->assertEquals(GraphQlInputException::EXCEPTION_CATEGORY, $actualError['extensions']['category']); } } diff --git a/dev/tests/integration/testsuite/Magento/PaypalGraphQl/Model/Resolver/Guest/PaypalPayflowProTokenTest.php b/dev/tests/integration/testsuite/Magento/PaypalGraphQl/Model/Resolver/Guest/PaypalPayflowProTokenTest.php index df7f80ccd35a8..248f5d297be32 100644 --- a/dev/tests/integration/testsuite/Magento/PaypalGraphQl/Model/Resolver/Guest/PaypalPayflowProTokenTest.php +++ b/dev/tests/integration/testsuite/Magento/PaypalGraphQl/Model/Resolver/Guest/PaypalPayflowProTokenTest.php @@ -130,6 +130,6 @@ public function testResolveWithInvalidRedirectUrl(): void $this->assertArrayHasKey('errors', $responseData); $actualError = $responseData['errors'][0]; $this->assertEquals($expectedExceptionMessage, $actualError['message']); - $this->assertEquals(GraphQlInputException::EXCEPTION_CATEGORY, $actualError['category']); + $this->assertEquals(GraphQlInputException::EXCEPTION_CATEGORY, $actualError['extensions']['category']); } } diff --git a/dev/tests/integration/testsuite/Magento/PaypalGraphQl/Model/Resolver/Guest/PlaceOrderWithHostedProTest.php b/dev/tests/integration/testsuite/Magento/PaypalGraphQl/Model/Resolver/Guest/PlaceOrderWithHostedProTest.php index 0920aa2eb36b8..a8136fda73c09 100644 --- a/dev/tests/integration/testsuite/Magento/PaypalGraphQl/Model/Resolver/Guest/PlaceOrderWithHostedProTest.php +++ b/dev/tests/integration/testsuite/Magento/PaypalGraphQl/Model/Resolver/Guest/PlaceOrderWithHostedProTest.php @@ -206,7 +206,7 @@ public function testOrderWithHostedProDeclined(): void $this->assertArrayHasKey('errors', $responseData); $actualError = $responseData['errors'][0]; $this->assertEquals($expectedExceptionMessage, $actualError['message']); - $this->assertEquals(GraphQlInputException::EXCEPTION_CATEGORY, $actualError['category']); + $this->assertEquals(GraphQlInputException::EXCEPTION_CATEGORY, $actualError['extensions']['category']); } /** @@ -258,6 +258,6 @@ public function testSetPaymentMethodInvalidUrls() $this->assertArrayHasKey('errors', $responseData); $actualError = $responseData['errors'][0]; $this->assertEquals($expectedExceptionMessage, $actualError['message']); - $this->assertEquals(GraphQlInputException::EXCEPTION_CATEGORY, $actualError['category']); + $this->assertEquals(GraphQlInputException::EXCEPTION_CATEGORY, $actualError['extensions']['category']); } } diff --git a/dev/tests/integration/testsuite/Magento/PaypalGraphQl/Model/Resolver/Guest/PlaceOrderWithPayflowLinkTest.php b/dev/tests/integration/testsuite/Magento/PaypalGraphQl/Model/Resolver/Guest/PlaceOrderWithPayflowLinkTest.php index c2ec5e6bddde3..f4fe3e7e60fd8 100644 --- a/dev/tests/integration/testsuite/Magento/PaypalGraphQl/Model/Resolver/Guest/PlaceOrderWithPayflowLinkTest.php +++ b/dev/tests/integration/testsuite/Magento/PaypalGraphQl/Model/Resolver/Guest/PlaceOrderWithPayflowLinkTest.php @@ -273,6 +273,6 @@ public function testResolveWithPayflowLinkDeclined(): void $expectedExceptionMessage, $actualError['message'] ); - $this->assertEquals(GraphQlInputException::EXCEPTION_CATEGORY, $actualError['category']); + $this->assertEquals(GraphQlInputException::EXCEPTION_CATEGORY, $actualError['extensions']['category']); } } diff --git a/dev/tests/integration/testsuite/Magento/PaypalGraphQl/Model/Resolver/Guest/PlaceOrderWithPaymentsAdvancedTest.php b/dev/tests/integration/testsuite/Magento/PaypalGraphQl/Model/Resolver/Guest/PlaceOrderWithPaymentsAdvancedTest.php index 7ef5db4a2ddab..a40a56be5faee 100644 --- a/dev/tests/integration/testsuite/Magento/PaypalGraphQl/Model/Resolver/Guest/PlaceOrderWithPaymentsAdvancedTest.php +++ b/dev/tests/integration/testsuite/Magento/PaypalGraphQl/Model/Resolver/Guest/PlaceOrderWithPaymentsAdvancedTest.php @@ -178,7 +178,7 @@ public function testResolvePaymentsAdvancedWithInvalidUrl(): void $this->assertArrayHasKey('errors', $responseData); $actualError = $responseData['errors'][0]; $this->assertEquals($expectedExceptionMessage, $actualError['message']); - $this->assertEquals(GraphQlInputException::EXCEPTION_CATEGORY, $actualError['category']); + $this->assertEquals(GraphQlInputException::EXCEPTION_CATEGORY, $actualError['extensions']['category']); } /** @@ -230,7 +230,7 @@ public function testResolveWithPaymentAdvancedDeclined(): void $this->assertArrayHasKey('errors', $responseData); $actualError = $responseData['errors'][0]; $this->assertEquals($expectedExceptionMessage, $actualError['message']); - $this->assertEquals(GraphQlInputException::EXCEPTION_CATEGORY, $actualError['category']); + $this->assertEquals(GraphQlInputException::EXCEPTION_CATEGORY, $actualError['extensions']['category']); } /** diff --git a/dev/tests/integration/testsuite/Magento/PaypalGraphQl/Model/Resolver/Guest/SetPaymentMethodAsPayflowLinkTest.php b/dev/tests/integration/testsuite/Magento/PaypalGraphQl/Model/Resolver/Guest/SetPaymentMethodAsPayflowLinkTest.php index 7c23ec08af652..224159348ada4 100644 --- a/dev/tests/integration/testsuite/Magento/PaypalGraphQl/Model/Resolver/Guest/SetPaymentMethodAsPayflowLinkTest.php +++ b/dev/tests/integration/testsuite/Magento/PaypalGraphQl/Model/Resolver/Guest/SetPaymentMethodAsPayflowLinkTest.php @@ -168,6 +168,6 @@ public function testInvalidUrl(): void $expectedExceptionMessage = "Invalid Url."; $actualError = $responseData['errors'][0]; $this->assertEquals($expectedExceptionMessage, $actualError['message']); - $this->assertEquals(GraphQlInputException::EXCEPTION_CATEGORY, $actualError['category']); + $this->assertEquals(GraphQlInputException::EXCEPTION_CATEGORY, $actualError['extensions']['category']); } } diff --git a/dev/tests/integration/testsuite/Magento/Quote/Model/QuoteInfiniteLoopTest.php b/dev/tests/integration/testsuite/Magento/Quote/Model/QuoteInfiniteLoopTest.php new file mode 100644 index 0000000000000..2b0bc8f4d1d05 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Quote/Model/QuoteInfiniteLoopTest.php @@ -0,0 +1,138 @@ +objectManager = Bootstrap::getObjectManager(); + $this->config = $this->objectManager->get(\Magento\TestModuleQuoteTotalsObserver\Model\Config::class); + $this->config->disableObserver(); + } + + /** + * @inheritdoc + */ + protected function tearDown() + { + $this->config->disableObserver(); + $this->objectManager->removeSharedInstance(\Magento\Checkout\Model\Session::class); + } + + /** + * @dataProvider getLoadQuoteParametersProvider + * + * @param $triggerRecollect + * @param $observerEnabled + * @return void + */ + public function testLoadQuoteSuccessfully($triggerRecollect, $observerEnabled): void + { + $originalQuote = $this->generateQuote($triggerRecollect); + $quoteId = $originalQuote->getId(); + + $this->assertGreaterThan(0, $quoteId, "The quote should have a database id"); + $this->assertEquals( + $triggerRecollect, + $originalQuote->getTriggerRecollect(), + "trigger_recollect failed to be set" + ); + + if ($observerEnabled) { + $this->config->enableObserver(); + } + + /** @var $session \Magento\Checkout\Model\Session */ + $this->objectManager->removeSharedInstance(\Magento\Checkout\Model\Session::class); + $session = $this->objectManager->get(\Magento\Checkout\Model\Session::class); + $session->setQuoteId($quoteId); + + $quote = $session->getQuote(); + $this->assertEquals($quoteId, $quote->getId(), "The loaded quote should have the same ID as the initial quote"); + $this->assertEquals(0, $quote->getTriggerRecollect(), "trigger_recollect should be unset after a quote reload"); + } + + /** + * @return array + */ + public function getLoadQuoteParametersProvider() + { + return [ + [0, false], + [0, true], + [1, false], + //[1, true], this combination of trigger recollect and third party code causes the loop, tested separately + ]; + } + + /** + * @expectedException \LogicException + * @expectedExceptionMessage Infinite loop detected, review the trace for the looping path + * + * @return void + */ + public function testLoadQuoteWithTriggerRecollectInfiniteLoop(): void + { + $originalQuote = $this->generateQuote(); + $quoteId = $originalQuote->getId(); + + $this->assertGreaterThan(0, $quoteId, "The quote should have a database id"); + $this->assertEquals(1, $originalQuote->getTriggerRecollect(), "The quote has trigger_recollect set"); + + // Enable an observer which gets the quote from the session + // The observer hooks into part of the collect totals process for an easy demonstration of the loop. + $this->config->enableObserver(); + + /** @var $session \Magento\Checkout\Model\Session */ + $this->objectManager->removeSharedInstance(\Magento\Checkout\Model\Session::class); + $session = $this->objectManager->get(\Magento\Checkout\Model\Session::class); + $session->setQuoteId($quoteId); + $session->getQuote(); + } + + /** + * Generate a quote with trigger_recollect and save it in the database. + * + * @param int $triggerRecollect + * @return Quote + */ + private function generateQuote($triggerRecollect = 1) + { + //Fully init a quote with standard quote session procedure + /** @var $session \Magento\Checkout\Model\Session */ + $session = $this->objectManager->create(\Magento\Checkout\Model\Session::class); + $session->setQuoteId(null); + $quote = $session->getQuote(); + $quote->setTriggerRecollect($triggerRecollect); + + /** @var \Magento\Quote\Api\CartRepositoryInterface $quoteRepository */ + $quoteRepository = $this->objectManager->create('\Magento\Quote\Api\CartRepositoryInterface'); + $quoteRepository->save($quote); + return $quote; + } +} diff --git a/dev/tests/integration/testsuite/Magento/Sales/_files/order.php b/dev/tests/integration/testsuite/Magento/Sales/_files/order.php index 6b9cf3bc613ce..9ea85aae56cbb 100644 --- a/dev/tests/integration/testsuite/Magento/Sales/_files/order.php +++ b/dev/tests/integration/testsuite/Magento/Sales/_files/order.php @@ -45,7 +45,8 @@ ->setPrice($product->getPrice()) ->setRowTotal($product->getPrice()) ->setProductType('simple') - ->setName($product->getName()); + ->setName($product->getName()) + ->setSku($product->getSku()); /** @var Order $order */ $order = $objectManager->create(Order::class); diff --git a/dev/tests/integration/testsuite/Magento/Sales/_files/quote.php b/dev/tests/integration/testsuite/Magento/Sales/_files/quote.php index c99e7de47f3ea..5be2fcefbde26 100644 --- a/dev/tests/integration/testsuite/Magento/Sales/_files/quote.php +++ b/dev/tests/integration/testsuite/Magento/Sales/_files/quote.php @@ -56,7 +56,7 @@ $quote->collectTotals(); $quoteRepository = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() - ->create(\Magento\Quote\Api\CartRepositoryInterface::class); + ->get(\Magento\Quote\Api\CartRepositoryInterface::class); $quoteRepository->save($quote); /** @var \Magento\Quote\Model\QuoteIdMask $quoteIdMask */ diff --git a/dev/tests/integration/testsuite/Magento/SalesRule/Block/Adminhtml/Promo/Quote/Edit/Tab/Coupons/GridTest.php b/dev/tests/integration/testsuite/Magento/SalesRule/Block/Adminhtml/Promo/Quote/Edit/Tab/Coupons/GridTest.php new file mode 100644 index 0000000000000..c5b45e662db7b --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/SalesRule/Block/Adminhtml/Promo/Quote/Edit/Tab/Coupons/GridTest.php @@ -0,0 +1,151 @@ +resourceConnection = Bootstrap::getObjectManager()->get(ResourceConnection::class); + $this->registry = Bootstrap::getObjectManager()->get(Registry::class); + + $this->initSalesRule(); + $this->prepareLayout(); + } + + /** + * @inheritdoc + */ + protected function tearDown() + { + parent::tearDown(); + + $this->registry->unregister(RegistryConstants::CURRENT_SALES_RULE); + } + + /** + * Check if mass action block exists + */ + public function testMassActionBlockExists() + { + $this->assertNotFalse( + $this->getMassActionBlock(), + 'Mass action block does not exist in the grid, or it name was changed.' + ); + } + + /** + * Check if function returns correct result + */ + public function testMassActionBlockContainsCorrectIdList() + { + $this->assertEquals( + implode(',', $this->getCouponsIdList()), + $this->getMassActionBlock()->getGridIdsJson(), + 'Function returns incorrect result.' + ); + } + + /** + * Retrieve mass action block + * + * @return bool|MassActionBlock + */ + private function getMassActionBlock() + { + /** @var Grid $grid */ + $grid = $this->layout->getBlock('sales_rule_quote_edit_tab_coupons_grid'); + + return $grid->getMassactionBlock(); + } + + /** + * Prepare layout blocks + */ + private function prepareLayout() + { + $this->layout = Bootstrap::getObjectManager()->create(LayoutInterface::class); + $this->layout->getUpdate()->load('sales_rule_promo_quote_couponsgrid'); + $this->layout->generateXml(); + $this->layout->generateElements(); + + $grid = $this->layout->getBlock('sales_rule_quote_edit_tab_coupons_grid'); + $grid->toHtml(); + } + + /** + * Init current sales rule + */ + private function initSalesRule() + { + /** @var RuleCollection $collection */ + $collection = Bootstrap::getObjectManager()->create(RuleCollection::class); + $collection->addFieldToFilter('name', 'Rule with coupon list'); + $this->salesRule = $collection->getFirstItem(); + $this->registry->register(RegistryConstants::CURRENT_SALES_RULE, $this->salesRule); + } + + /** + * Retrieve id list of coupons + * + * @return array + */ + private function getCouponsIdList(): array + { + $select = $this->resourceConnection->getConnection() + ->select() + ->from($this->resourceConnection->getTableName('salesrule_coupon')) + ->columns(['coupon_id']) + ->where('rule_id=?', $this->salesRule->getId()); + + return $this->resourceConnection->getConnection()->fetchCol($select); + } +} diff --git a/dev/tests/integration/testsuite/Magento/SalesRule/Controller/Adminhtml/Promo/Quote/NewActionHtmlTest.php b/dev/tests/integration/testsuite/Magento/SalesRule/Controller/Adminhtml/Promo/Quote/NewActionHtmlTest.php new file mode 100644 index 0000000000000..82f1c53d8f161 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/SalesRule/Controller/Adminhtml/Promo/Quote/NewActionHtmlTest.php @@ -0,0 +1,81 @@ +prepareRequest(); + $this->dispatch($this->uri); + $html = $this->getResponse() + ->getBody(); + $this->assertContains($this->formName, $html); + } + + /** + * @inheritdoc + */ + public function testAclHasAccess() + { + $this->prepareRequest(); + parent::testAclHasAccess(); + } + + /** + * @inheritdoc + */ + public function testAclNoAccess() + { + $this->prepareRequest(); + parent::testAclNoAccess(); + } + + /** + * Prepare request + * + * @return void + */ + private function prepareRequest(): void + { + $this->getRequest()->setParams( + [ + 'id' => 1, + 'form_namespace' => $this->formName, + 'type' => 'Magento\SalesRule\Model\Rule\Condition\Product|quote_item_price', + ] + )->setMethod('POST'); + } +} diff --git a/dev/tests/integration/testsuite/Magento/SalesRule/Model/Observer/AssignCouponDataAfterOrderCustomerAssignTest.php b/dev/tests/integration/testsuite/Magento/SalesRule/Model/Observer/AssignCouponDataAfterOrderCustomerAssignTest.php new file mode 100644 index 0000000000000..9eaca30e4bd5d --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/SalesRule/Model/Observer/AssignCouponDataAfterOrderCustomerAssignTest.php @@ -0,0 +1,292 @@ +objectManager = Bootstrap::getObjectManager(); + $this->eventManager = $this->createMock(\Magento\Framework\Event\ManagerInterface::class); + $this->orderRepository = $this->objectManager->get(\Magento\Sales\Model\OrderRepository::class); + $this->delegateCustomerService = $this->objectManager->get(Order\OrderCustomerDelegate::class); + $this->customerRepository = $this->objectManager->get(\Magento\Customer\Api\CustomerRepositoryInterface::class); + $this->ruleCustomerFactory = $this->objectManager->get(\Magento\SalesRule\Model\Rule\CustomerFactory::class); + $this->assignCouponToCustomerObserver = $this->objectManager->get( + \Magento\SalesRule\Observer\AssignCouponDataAfterOrderCustomerAssignObserver::class + ); + + $this->salesRule = $this->prepareSalesRule(); + $this->coupon = $this->attachSalesruleCoupon($this->salesRule); + $this->order = $this->makeOrderWithCouponAsGuest($this->coupon); + $this->delegateOrderToBeAssigned($this->order); + $this->customer = $this->registerNewCustomer(); + $this->order->setCustomerId($this->customer->getId()); + } + + /** + * @inheritdoc + */ + protected function tearDown() + { + $this->salesRule = null; + $this->customer = null; + $this->coupon = null; + $this->order = null; + } + + /** + * @magentoDataFixture Magento/Sales/_files/order.php + */ + public function testCouponDataHasBeenAssignedTest() + { + $ruleCustomer = $this->getSalesruleCustomerUsage($this->customer, $this->salesRule); + + // Assert, that rule customer model has been created for specific customer + $this->assertEquals( + $ruleCustomer->getCustomerId(), + $this->customer->getId() + ); + + // Assert, that customer has increased coupon usage of specific rule + $this->assertEquals( + 1, + $ruleCustomer->getTimesUsed() + ); + } + + /** + * @magentoDataFixture Magento/Sales/_files/order.php + */ + public function testOrderCancelingDecreasesCouponUsages() + { + $this->processOrder($this->order); + + // Should not throw exception as bux is fixed now + $this->order->cancel(); + $ruleCustomer = $this->getSalesruleCustomerUsage($this->customer, $this->salesRule); + + // Assert, that rule customer model has been created for specific customer + $this->assertEquals( + $ruleCustomer->getCustomerId(), + $this->customer->getId() + ); + + // Assert, that customer has increased coupon usage of specific rule + $this->assertEquals( + 0, + $ruleCustomer->getTimesUsed() + ); + } + + /** + * @param Order $order + * @return \Magento\Sales\Api\Data\OrderInterface + */ + private function processOrder(Order $order) + { + $order->setState(Order::STATE_PROCESSING); + $order->setStatus(Order::STATE_PROCESSING); + return $this->orderRepository->save($order); + } + + /** + * @param Customer $customer + * @param Rule $rule + * @return Rule\Customer + */ + private function getSalesruleCustomerUsage(Customer $customer, Rule $rule) : \Magento\SalesRule\Model\Rule\Customer + { + $ruleCustomer = $this->ruleCustomerFactory->create(); + return $ruleCustomer->loadByCustomerRule($customer->getId(), $rule->getRuleId()); + } + + /** + * @return Rule + */ + private function prepareSalesRule() : Rule + { + /** @var Rule $salesRule */ + $salesRule = $this->objectManager->create(Rule::class); + $salesRule->setData( + [ + 'name' => '15$ fixed discount on whole cart', + 'is_active' => 1, + 'customer_group_ids' => [GroupManagement::NOT_LOGGED_IN_ID], + 'coupon_type' => Rule::COUPON_TYPE_SPECIFIC, + 'conditions' => [ + [ + 'type' => \Magento\SalesRule\Model\Rule\Condition\Address::class, + 'attribute' => 'base_subtotal', + 'operator' => '>', + 'value' => 45, + ], + ], + 'simple_action' => Rule::CART_FIXED_ACTION, + 'discount_amount' => 15, + 'discount_step' => 0, + 'stop_rules_processing' => 1, + 'website_ids' => [ + $this->objectManager->get(StoreManagerInterface::class)->getWebsite()->getId(), + ], + ] + ); + Bootstrap::getObjectManager()->get( + \Magento\SalesRule\Model\ResourceModel\Rule::class + )->save($salesRule); + + return $salesRule; + } + + /** + * @param Rule $salesRule + * @return Coupon + */ + private function attachSalesruleCoupon(Rule $salesRule) : Coupon + { + $coupon = $this->objectManager->create(Coupon::class); + $coupon->setRuleId($salesRule->getId()) + ->setCode('CART_FIXED_DISCOUNT_15') + ->setType(0); + + Bootstrap::getObjectManager()->get(CouponRepositoryInterface::class)->save($coupon); + + return $coupon; + } + + /** + * @param Coupon $coupon + * @return Order + */ + private function makeOrderWithCouponAsGuest(Coupon $coupon) : Order + { + $order = Bootstrap::getObjectManager()->create(\Magento\Sales\Model\Order::class); + $order->loadByIncrementId('100000001') + ->setCustomerIsGuest(true) + ->setCouponCode($coupon->getCode()) + ->setCreatedAt('2014-10-25 10:10:10') + ->setAppliedRuleIds($coupon->getRuleId()) + ->save(); + + return $order; + } + + /** + * @param Order $order + */ + private function delegateOrderToBeAssigned(Order $order) + { + $this->delegateCustomerService->delegateNew($order->getId()); + } + + /** + * @return Customer + * @throws \Magento\Framework\Exception\InputException + * @throws \Magento\Framework\Exception\LocalizedException + * @throws \Magento\Framework\Exception\State\InputMismatchException + */ + private function registerNewCustomer() : Customer + { + $customer = Bootstrap::getObjectManager()->create( + \Magento\Customer\Api\Data\CustomerInterface::class + ); + + /** @var Magento\Customer\Api\Data\CustomerInterface $customer */ + $customer->setWebsiteId(1) + ->setEmail('customer@example.com') + ->setGroupId(1) + ->setStoreId(1) + ->setPrefix('Mr.') + ->setFirstname('John') + ->setMiddlename('A') + ->setLastname('Smith') + ->setSuffix('Esq.') + ->setDefaultBilling(1) + ->setDefaultShipping(1) + ->setTaxvat('12') + ->setGender(0); + + $customer = $this->customerRepository->save($customer, 'password'); + + return $customer; + } +} diff --git a/dev/tests/integration/testsuite/Magento/SalesRule/Model/Rule/Action/Discount/CartFixedTest.php b/dev/tests/integration/testsuite/Magento/SalesRule/Model/Rule/Action/Discount/CartFixedTest.php index 694310f2cbc89..df26c1cc48f7b 100644 --- a/dev/tests/integration/testsuite/Magento/SalesRule/Model/Rule/Action/Discount/CartFixedTest.php +++ b/dev/tests/integration/testsuite/Magento/SalesRule/Model/Rule/Action/Discount/CartFixedTest.php @@ -10,15 +10,24 @@ use Magento\Catalog\Api\Data\ProductInterface; use Magento\Catalog\Model\Product; use Magento\Catalog\Model\ProductRepository; +use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\Quote\Api\CartRepositoryInterface; use Magento\Quote\Api\Data\CartItemInterface; use Magento\Quote\Api\GuestCartItemRepositoryInterface; use Magento\Quote\Api\GuestCartManagementInterface; use Magento\Quote\Api\GuestCartTotalRepositoryInterface; use Magento\Quote\Api\GuestCouponManagementInterface; +use Magento\Quote\Model\Quote; +use Magento\Quote\Model\QuoteIdMask; +use Magento\Sales\Api\Data\OrderInterface; +use Magento\Sales\Api\OrderRepositoryInterface; use Magento\TestFramework\Helper\Bootstrap; /** * Tests for Magento\SalesRule\Model\Rule\Action\Discount\CartFixed. + * + * @magentoAppArea frontend + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class CartFixedTest extends \PHPUnit\Framework\TestCase { @@ -37,14 +46,33 @@ class CartFixedTest extends \PHPUnit\Framework\TestCase */ private $couponManagement; + /** + * @var \Magento\Framework\ObjectManagerInterface + */ + private $objectManager; + + /** + * @var SearchCriteriaBuilder + */ + private $criteriaBuilder; + + /** + * @var CartRepositoryInterface + */ + private $quoteRepository; + /** * @inheritdoc */ protected function setUp() { - $this->cartManagement = Bootstrap::getObjectManager()->create(GuestCartManagementInterface::class); - $this->couponManagement = Bootstrap::getObjectManager()->create(GuestCouponManagementInterface::class); - $this->cartItemRepository = Bootstrap::getObjectManager()->create(GuestCartItemRepositoryInterface::class); + $objectManager = Bootstrap::getObjectManager(); + $this->cartManagement = $objectManager->create(GuestCartManagementInterface::class); + $this->couponManagement = $objectManager->create(GuestCouponManagementInterface::class); + $this->cartItemRepository = $objectManager->create(GuestCartItemRepositoryInterface::class); + $this->criteriaBuilder = $objectManager->get(SearchCriteriaBuilder::class); + $this->quoteRepository = $objectManager->get(CartRepositoryInterface::class); + $this->objectManager = $objectManager; } /** @@ -53,6 +81,7 @@ protected function setUp() * @param array $productPrices * @return void * @magentoDbIsolation enabled + * @magentoAppIsolation enabled * @magentoDataFixture Magento/SalesRule/_files/coupon_cart_fixed_discount.php * @dataProvider applyFixedDiscountDataProvider */ @@ -82,6 +111,56 @@ public function testApplyFixedDiscount(array $productPrices): void $this->assertEquals($expectedDiscount, $total->getBaseDiscountAmount()); } + /** + * Applies fixed discount amount on whole cart and created order with it + * + * @magentoDbIsolation enabled + * @magentoAppIsolation enabled + * @magentoConfigFixture default_store carriers/freeshipping/active 1 + * @magentoDataFixture Magento/Sales/_files/quote.php + * @magentoDataFixture Magento/SalesRule/_files/coupon_cart_fixed_subtotal_with_discount.php + */ + public function testOrderWithFixedDiscount(): void + { + $expectedGrandTotal = 5; + + $quote = $this->getQuote(); + $quote->getShippingAddress() + ->setShippingMethod('freeshipping_freeshipping') + ->setCollectShippingRates(true); + $quote->setCouponCode('CART_FIXED_DISCOUNT_15'); + $quote->collectTotals(); + $this->quoteRepository->save($quote); + + $this->assertEquals($expectedGrandTotal, $quote->getGrandTotal()); + + /** @var \Magento\Quote\Model\QuoteIdMask $quoteIdMask */ + $quoteIdMask = $this->objectManager->create(QuoteIdMask::class); + $quoteIdMask->load($quote->getId(), 'quote_id'); + Bootstrap::getInstance()->reinitialize(); + $cartManagement = Bootstrap::getObjectManager()->create(GuestCartManagementInterface::class); + $cartManagement->placeOrder($quoteIdMask->getMaskedId()); + $order = $this->getOrder('test01'); + $this->assertEquals($expectedGrandTotal, $order->getGrandTotal()); + } + + /** + * Load cart from fixture. + * + * @return Quote + */ + private function getQuote(): Quote + { + $searchCriteria = $this->criteriaBuilder->addFilter('reserved_order_id', 'test01')->create(); + $carts = $this->quoteRepository->getList($searchCriteria) + ->getItems(); + if (!$carts) { + throw new \RuntimeException('Cart from fixture not found'); + } + + return array_shift($carts); + } + /** * @return array */ @@ -150,4 +229,25 @@ private function createProduct(float $price): ProductInterface return $productRepository->save($product); } + + /** + * Gets order entity by increment id. + * + * @param string $incrementId + * @return OrderInterface + */ + private function getOrder(string $incrementId): OrderInterface + { + /** @var SearchCriteriaBuilder $searchCriteriaBuilder */ + $searchCriteriaBuilder = $this->objectManager->get(SearchCriteriaBuilder::class); + $searchCriteria = $searchCriteriaBuilder->addFilter('increment_id', $incrementId) + ->create(); + + /** @var OrderRepositoryInterface $repository */ + $repository = $this->objectManager->get(OrderRepositoryInterface::class); + $items = $repository->getList($searchCriteria) + ->getItems(); + + return array_pop($items); + } } diff --git a/dev/tests/integration/testsuite/Magento/SalesRule/_files/cart_rule_with_coupon_list.php b/dev/tests/integration/testsuite/Magento/SalesRule/_files/cart_rule_with_coupon_list.php new file mode 100644 index 0000000000000..1fe2bb042de8b --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/SalesRule/_files/cart_rule_with_coupon_list.php @@ -0,0 +1,45 @@ +create(Rule::class); +$salesRule->setData( + [ + 'name' => 'Rule with coupon list', + 'is_active' => 1, + 'customer_group_ids' => [GroupManagement::NOT_LOGGED_IN_ID], + 'coupon_type' => Rule::COUPON_TYPE_SPECIFIC, + 'simple_action' => Rule::CART_FIXED_ACTION, + 'discount_amount' => 10, + 'discount_step' => 0, + 'stop_rules_processing' => 1, + 'use_auto_generation' => 1, + 'website_ids' => [ + $objectManager->get(StoreManagerInterface::class)->getWebsite()->getId(), + ], + ] +); +$objectManager->get(\Magento\SalesRule\Model\ResourceModel\Rule::class)->save($salesRule); + +/* @var CouponRepositoryInterface $couponRepository */ +$couponRepository = $objectManager->get(CouponRepositoryInterface::class); +for ($index = 1; $index <= 30; $index ++) { + $coupon = $objectManager->create(Coupon::class); + $coupon->setRuleId($salesRule->getId()) + ->setCode('coupon_code_' . $index) + ->setType(1); + $couponRepository->save($coupon); +} diff --git a/dev/tests/integration/testsuite/Magento/SalesRule/_files/cart_rule_with_coupon_list_rollback.php b/dev/tests/integration/testsuite/Magento/SalesRule/_files/cart_rule_with_coupon_list_rollback.php new file mode 100644 index 0000000000000..44a91e5810141 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/SalesRule/_files/cart_rule_with_coupon_list_rollback.php @@ -0,0 +1,25 @@ +get(\Magento\Framework\Registry::class); +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); + +/** @var RuleCollection $collection */ +$collection = Bootstrap::getObjectManager()->create(RuleCollection::class); +$collection->addFieldToFilter('name', 'Rule with coupon list'); +$rule = $collection->getFirstItem(); +if ($rule->getId()) { + $rule->delete(); +} + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); diff --git a/dev/tests/integration/testsuite/Magento/SalesRule/_files/coupon_cart_fixed_discount_rollback.php b/dev/tests/integration/testsuite/Magento/SalesRule/_files/coupon_cart_fixed_discount_rollback.php index 33a8b4285d8d7..f65cc3dc1cd57 100644 --- a/dev/tests/integration/testsuite/Magento/SalesRule/_files/coupon_cart_fixed_discount_rollback.php +++ b/dev/tests/integration/testsuite/Magento/SalesRule/_files/coupon_cart_fixed_discount_rollback.php @@ -5,13 +5,32 @@ */ declare(strict_types=1); +use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\Framework\Registry; +use Magento\SalesRule\Api\RuleRepositoryInterface; +use Magento\SalesRule\Model\Rule; use Magento\TestFramework\Helper\Bootstrap; -/** @var Magento\Framework\Registry $registry */ -$registry = Bootstrap::getObjectManager()->get(\Magento\Framework\Registry::class); +$objectManager = Bootstrap::getObjectManager(); -/** @var Magento\SalesRule\Model\Rule $rule */ -$rule = $registry->registry('cart_rule_fixed_discount_coupon'); -if ($rule) { - $rule->delete(); +/** @var Registry $registry */ +$registry = $objectManager->get(Registry::class); +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); + +/** @var SearchCriteriaBuilder $searchCriteriaBuilder */ +$searchCriteriaBuilder = $objectManager->get(SearchCriteriaBuilder::class); +$searchCriteria = $searchCriteriaBuilder->addFilter('name', '15$ fixed discount on whole cart') + ->create(); +/** @var RuleRepositoryInterface $ruleRepository */ +$ruleRepository = $objectManager->get(RuleRepositoryInterface::class); +$items = $ruleRepository->getList($searchCriteria) + ->getItems(); +/** @var Rule $salesRule */ +$salesRule = array_pop($items); +if ($salesRule !== null) { + $ruleRepository->deleteById($salesRule->getRuleId()); } + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); diff --git a/dev/tests/integration/testsuite/Magento/SalesRule/_files/coupon_cart_fixed_subtotal_with_discount.php b/dev/tests/integration/testsuite/Magento/SalesRule/_files/coupon_cart_fixed_subtotal_with_discount.php new file mode 100644 index 0000000000000..b0f21a47a079e --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/SalesRule/_files/coupon_cart_fixed_subtotal_with_discount.php @@ -0,0 +1,35 @@ +getConditions()->loadArray( + [ + 'type' => Combine::class, + 'attribute' => null, + 'operator' => null, + 'value' => '1', + 'is_value_processed' => null, + 'aggregator' => 'any', + 'conditions' => + [ + [ + 'type' => Address::class, + 'attribute' => 'base_subtotal_with_discount', + 'operator' => '>=', + 'value' => 9, + 'is_value_processed' => false + ], + ], + ] +); +$salesRule->setDiscountAmount(5); +$objectManager->get(ResourceModel::class)->save($salesRule); diff --git a/dev/tests/integration/testsuite/Magento/SalesRule/_files/coupon_cart_fixed_subtotal_with_discount_rollback.php b/dev/tests/integration/testsuite/Magento/SalesRule/_files/coupon_cart_fixed_subtotal_with_discount_rollback.php new file mode 100644 index 0000000000000..dc88b210cb706 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/SalesRule/_files/coupon_cart_fixed_subtotal_with_discount_rollback.php @@ -0,0 +1,8 @@ +locator = $this->getMockForAbstractClass(\Zend\ServiceManager\ServiceLocatorInterface::class); + $this->locator = $this->getMockForAbstractClass(ServiceLocatorInterface::class); $this->object = new ObjectManagerProvider($this->locator, new Bootstrap()); + $this->locator->expects($this->any()) + ->method('get') + ->willReturnMap( + [ + [InitParamListener::BOOTSTRAP_PARAM, []], + [Application::class, $this->getMockForAbstractClass(Application::class)], + ] + ); } + /** + * Tests the same instance of ObjectManagerInterface should be provided by the ObjectManagerProvider + */ public function testGet() { - $this->locator->expects($this->once())->method('get')->with(InitParamListener::BOOTSTRAP_PARAM)->willReturn([]); $objectManager = $this->object->get(); - $this->assertInstanceOf(\Magento\Framework\ObjectManagerInterface::class, $objectManager); + $this->assertInstanceOf(ObjectManagerInterface::class, $objectManager); $this->assertSame($objectManager, $this->object->get()); } } diff --git a/dev/tests/integration/testsuite/Magento/Swatches/Model/SwatchAttributeOptionAddTest.php b/dev/tests/integration/testsuite/Magento/Swatches/Model/SwatchAttributeOptionAddTest.php index 84ba587f5e784..b68c9b421851e 100644 --- a/dev/tests/integration/testsuite/Magento/Swatches/Model/SwatchAttributeOptionAddTest.php +++ b/dev/tests/integration/testsuite/Magento/Swatches/Model/SwatchAttributeOptionAddTest.php @@ -36,7 +36,7 @@ public function testSwatchOptionAdd() $attribute = $this->objectManager ->create(\Magento\Catalog\Model\ResourceModel\Eav\Attribute::class) ->load('color_swatch', 'attribute_code'); - $optionsPerAttribute = 3; + $optionsPerAttribute = 4; $data['options']['option'] = array_reduce( range(10, $optionsPerAttribute), diff --git a/dev/tests/integration/testsuite/Magento/UrlRewrite/Model/StoreSwitcher/RewriteUrlTest.php b/dev/tests/integration/testsuite/Magento/UrlRewrite/Model/StoreSwitcher/RewriteUrlTest.php index a8ff9e411785e..8ea9fdcd744f1 100644 --- a/dev/tests/integration/testsuite/Magento/UrlRewrite/Model/StoreSwitcher/RewriteUrlTest.php +++ b/dev/tests/integration/testsuite/Magento/UrlRewrite/Model/StoreSwitcher/RewriteUrlTest.php @@ -11,11 +11,17 @@ use Magento\Framework\App\Config\ReinitableConfigInterface; use Magento\Framework\App\Config\Value; use Magento\Store\Api\Data\StoreInterface; +use Magento\Store\Api\StoreRepositoryInterface; use Magento\Store\Model\ScopeInterface; +use Magento\Store\Model\StoreManagerInterface; use Magento\Store\Model\StoreSwitcher; use Magento\Framework\ObjectManagerInterface as ObjectManager; use Magento\TestFramework\Helper\Bootstrap; +/** + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * Test store switching + */ class RewriteUrlTest extends \PHPUnit\Framework\TestCase { /** @@ -33,6 +39,11 @@ class RewriteUrlTest extends \PHPUnit\Framework\TestCase */ private $productRepository; + /** + * @var StoreManagerInterface + */ + private $storeManager; + /** * Class dependencies initialization * @@ -43,9 +54,12 @@ protected function setUp() $this->objectManager = Bootstrap::getObjectManager(); $this->storeSwitcher = $this->objectManager->get(StoreSwitcher::class); $this->productRepository = $this->objectManager->create(ProductRepositoryInterface::class); + $this->storeManager = $this->objectManager->create(StoreManagerInterface::class); } /** + * Test switching stores with non-existent cms pages and then redirecting to the homepage + * * @magentoDataFixture Magento/UrlRewrite/_files/url_rewrite.php * @magentoDataFixture Magento/Catalog/_files/category_product.php * @return void @@ -54,15 +68,8 @@ protected function setUp() */ public function testSwitchToNonExistingPage(): void { - $fromStoreCode = 'default'; - /** @var \Magento\Store\Api\StoreRepositoryInterface $storeRepository */ - $storeRepository = $this->objectManager->create(\Magento\Store\Api\StoreRepositoryInterface::class); - $fromStore = $storeRepository->get($fromStoreCode); - - $toStoreCode = 'fixture_second_store'; - /** @var \Magento\Store\Api\StoreRepositoryInterface $storeRepository */ - $storeRepository = $this->objectManager->create(\Magento\Store\Api\StoreRepositoryInterface::class); - $toStore = $storeRepository->get($toStoreCode); + $fromStore = $this->getStoreByCode('default'); + $toStore = $this->getStoreByCode('fixture_second_store'); $this->setBaseUrl($toStore); @@ -75,6 +82,8 @@ public function testSwitchToNonExistingPage(): void } /** + * Testing store switching with existing cms pages + * * @magentoDataFixture Magento/UrlRewrite/_files/url_rewrite.php * @return void * @throws StoreSwitcher\CannotSwitchStoreException @@ -82,15 +91,8 @@ public function testSwitchToNonExistingPage(): void */ public function testSwitchToExistingPage(): void { - $fromStoreCode = 'default'; - /** @var \Magento\Store\Api\StoreRepositoryInterface $storeRepository */ - $storeRepository = $this->objectManager->create(\Magento\Store\Api\StoreRepositoryInterface::class); - $fromStore = $storeRepository->get($fromStoreCode); - - $toStoreCode = 'fixture_second_store'; - /** @var \Magento\Store\Api\StoreRepositoryInterface $storeRepository */ - $storeRepository = $this->objectManager->create(\Magento\Store\Api\StoreRepositoryInterface::class); - $toStore = $storeRepository->get($toStoreCode); + $fromStore = $this->getStoreByCode('default'); + $toStore = $this->getStoreByCode('fixture_second_store'); $redirectUrl = "http://localhost/index.php/page-c/"; $expectedUrl = "http://localhost/index.php/page-c-on-2nd-store"; @@ -98,6 +100,22 @@ public function testSwitchToExistingPage(): void $this->assertEquals($expectedUrl, $this->storeSwitcher->switch($fromStore, $toStore, $redirectUrl)); } + /** + * Testing store switching using cms pages with the same url_key but with different page_id + * + * @magentoDataFixture Magento/Cms/_files/two_cms_page_with_same_url_for_different_stores.php + * @magentoDbIsolation disabled + * @return void + */ + public function testSwitchCmsPageToAnotherStore(): void + { + $fromStore = $this->getStoreByCode('default'); + $toStore = $this->getStoreByCode('fixture_second_store'); + $redirectUrl = "http://localhost/index.php/page100/"; + $expectedUrl = "http://localhost/index.php/page100/"; + $this->assertEquals($expectedUrl, $this->storeSwitcher->switch($fromStore, $toStore, $redirectUrl)); + } + /** * Set base url to store. * @@ -120,4 +138,16 @@ private function setBaseUrl(StoreInterface $targetStore): void $reinitibleConfig = $this->objectManager->create(ReinitableConfigInterface::class); $reinitibleConfig->reinit(); } + + /** + * Get store object by storeCode + * + * @param string $storeCode + * @return StoreInterface + */ + private function getStoreByCode(string $storeCode): StoreInterface + { + /** @var StoreRepositoryInterface */ + return $this->storeManager->getStore($storeCode); + } } diff --git a/dev/tests/js/jasmine/tests/app/code/Magento/Ui/base/js/lib/validation/rules.test.js b/dev/tests/js/jasmine/tests/app/code/Magento/Ui/base/js/lib/validation/rules.test.js index 692c843d18e92..d60d9ad9a5b0f 100644 --- a/dev/tests/js/jasmine/tests/app/code/Magento/Ui/base/js/lib/validation/rules.test.js +++ b/dev/tests/js/jasmine/tests/app/code/Magento/Ui/base/js/lib/validation/rules.test.js @@ -58,12 +58,36 @@ define([ expect(rules['validate-number'].handler(value)).toBe(true); }); + it('Check on float without leading zero', function () { + var value = '.50'; + + expect(rules['validate-number'].handler(value)).toBe(true); + }); + it('Check on formatted float', function () { var value = '1,000,000.50'; expect(rules['validate-number'].handler(value)).toBe(true); }); + it('Check on space', function () { + var value = '10 000'; + + expect(rules['validate-number'].handler(value)).toBe(true); + }); + + it('Check on formatted float (For International price)', function () { + var value = '10.000,00'; + + expect(rules['validate-number'].handler(value)).toBe(true); + }); + + it('Check on formatted float (For International price)', function () { + var value = '10\'000.00'; + + expect(rules['validate-number'].handler(value)).toBe(true); + }); + it('Check on not a number', function () { var value = 'string'; diff --git a/dev/tests/static/framework/Magento/CodeMessDetector/Rule/Design/AllPurposeAction.php b/dev/tests/static/framework/Magento/CodeMessDetector/Rule/Design/AllPurposeAction.php index ca257c1f6eb39..00a6e49d4e31d 100644 --- a/dev/tests/static/framework/Magento/CodeMessDetector/Rule/Design/AllPurposeAction.php +++ b/dev/tests/static/framework/Magento/CodeMessDetector/Rule/Design/AllPurposeAction.php @@ -38,7 +38,7 @@ public function apply(AbstractNode $node) return; } - if (in_array(ActionInterface::class, $impl, true)) { + if (is_array($impl) && in_array(ActionInterface::class, $impl, true)) { $methodsDefined = false; foreach ($impl as $i) { if (preg_match('/\\\Http[a-z]+ActionInterface$/i', $i)) { diff --git a/dev/tests/static/framework/Magento/CodeMessDetector/Rule/Design/CookieAndSessionMisuse.php b/dev/tests/static/framework/Magento/CodeMessDetector/Rule/Design/CookieAndSessionMisuse.php index 707c3442d4056..1d49d9343a282 100644 --- a/dev/tests/static/framework/Magento/CodeMessDetector/Rule/Design/CookieAndSessionMisuse.php +++ b/dev/tests/static/framework/Magento/CodeMessDetector/Rule/Design/CookieAndSessionMisuse.php @@ -16,6 +16,8 @@ /** * Session and Cookies must be used only in HTML Presentation layer. + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class CookieAndSessionMisuse extends AbstractRule implements ClassAware { @@ -67,6 +69,19 @@ private function isLayoutProcessor(\ReflectionClass $class): bool ); } + /** + * Is given class a View Model? + * + * @param \ReflectionClass $class + * @return bool + */ + private function isViewModel(\ReflectionClass $class): bool + { + return $class->isSubclassOf( + \Magento\Framework\View\Element\Block\ArgumentInterface::class + ); + } + /** * Is given class an HTML UI Document? * @@ -191,6 +206,7 @@ public function apply(AbstractNode $node) && !$this->isControllerPlugin($class) && !$this->isBlockPlugin($class) && !$this->isLayoutProcessor($class) + && !$this->isViewModel($class) ) { $this->addViolation($node, [$node->getFullQualifiedName()]); } diff --git a/dev/tests/static/framework/Magento/TestFramework/Dependency/PhpRule.php b/dev/tests/static/framework/Magento/TestFramework/Dependency/PhpRule.php index 4958795412681..913cc9448b978 100644 --- a/dev/tests/static/framework/Magento/TestFramework/Dependency/PhpRule.php +++ b/dev/tests/static/framework/Magento/TestFramework/Dependency/PhpRule.php @@ -285,11 +285,12 @@ private function isPluginDependency($dependent, $dependency) * @return array * @throws LocalizedException * @throws \Exception + * @SuppressWarnings(PMD.CyclomaticComplexity) */ protected function _caseGetUrl(string $currentModule, string &$contents): array { - $pattern = '#(\->|:)(?getUrl\(([\'"])(?[a-z0-9\-_]{3,})' - .'(/(?[a-z0-9\-_]+))?(/(?[a-z0-9\-_]+))?\3)#i'; + $pattern = '#(\->|:)(?getUrl\(([\'"])(?[a-z0-9\-_]{3,}|\*)' + .'(/(?[a-z0-9\-_]+|\*))?(/(?[a-z0-9\-_]+|\*))?\3)#i'; $dependencies = []; if (!preg_match_all($pattern, $contents, $matches, PREG_SET_ORDER)) { @@ -298,10 +299,22 @@ protected function _caseGetUrl(string $currentModule, string &$contents): array try { foreach ($matches as $item) { + $routeId = $item['route_id']; + $controllerName = $item['controller_name'] ?? UrlInterface::DEFAULT_CONTROLLER_NAME; + $actionName = $item['action_name'] ?? UrlInterface::DEFAULT_ACTION_NAME; + + // skip rest + if ($routeId === "rest") { //MC-19890 + continue; + } + // skip wildcards + if ($routeId === "*" || $controllerName === "*" || $actionName === "*") { //MC-19890 + continue; + } $modules = $this->routeMapper->getDependencyByRoutePath( - $item['route_id'], - $item['controller_name'] ?? UrlInterface::DEFAULT_CONTROLLER_NAME, - $item['action_name'] ?? UrlInterface::DEFAULT_ACTION_NAME + $routeId, + $controllerName, + $actionName ); if (!in_array($currentModule, $modules)) { if (count($modules) === 1) { diff --git a/dev/tests/static/framework/Magento/TestFramework/Dependency/Route/RouteMapper.php b/dev/tests/static/framework/Magento/TestFramework/Dependency/Route/RouteMapper.php index 87cc0985a053b..315bb2ae26b02 100644 --- a/dev/tests/static/framework/Magento/TestFramework/Dependency/Route/RouteMapper.php +++ b/dev/tests/static/framework/Magento/TestFramework/Dependency/Route/RouteMapper.php @@ -174,6 +174,7 @@ public function getDependencyByRoutePath( $dependencies = []; foreach ($this->getRouterTypes() as $routerId) { if (isset($this->getActionsMap()[$routerId][$routeId][$controllerName][$actionName])) { + //phpcs:ignore Magento2.Performance.ForeachArrayMerge $dependencies = array_merge( $dependencies, $this->getActionsMap()[$routerId][$routeId][$controllerName][$actionName] diff --git a/dev/tests/static/testsuite/Magento/Test/Integrity/DependencyTest.php b/dev/tests/static/testsuite/Magento/Test/Integrity/DependencyTest.php index 16ba295588b58..fa0d365061858 100644 --- a/dev/tests/static/testsuite/Magento/Test/Integrity/DependencyTest.php +++ b/dev/tests/static/testsuite/Magento/Test/Integrity/DependencyTest.php @@ -234,8 +234,8 @@ protected static function _initRules() . '/_files/dependency_test/tables_*.php'; $dbRuleTables = []; foreach (glob($replaceFilePattern) as $fileName) { - //phpcs:ignore Generic.PHP.NoSilencedErrors - $dbRuleTables = array_merge($dbRuleTables, @include $fileName); + //phpcs:ignore Magento2.Performance.ForeachArrayMerge + $dbRuleTables = array_merge($dbRuleTables, include $fileName); } self::$_rulesInstances = [ new PhpRule( @@ -267,11 +267,11 @@ private static function getRoutesWhitelist(): array $routesWhitelistFilePattern = realpath(__DIR__) . '/_files/dependency_test/whitelist/routes_*.php'; $routesWhitelist = []; foreach (glob($routesWhitelistFilePattern) as $fileName) { + //phpcs:ignore Magento2.Performance.ForeachArrayMerge $routesWhitelist = array_merge($routesWhitelist, include $fileName); } self::$routesWhitelist = $routesWhitelist; } - return self::$routesWhitelist; } @@ -284,24 +284,26 @@ private static function getRoutesWhitelist(): array */ protected function _getCleanedFileContents($fileType, $file) { - $contents = (string)file_get_contents($file); + $contents = null; switch ($fileType) { case 'php': - //Removing php comments - $contents = preg_replace('~/\*.*?\*/~m', '', $contents); - $contents = preg_replace('~^\s*/\*.*?\*/~sm', '', $contents); - $contents = preg_replace('~^\s*//.*$~m', '', $contents); + $contents = php_strip_whitespace($file); break; case 'layout': case 'config': //Removing xml comments - $contents = preg_replace('~\~s', '', $contents); + $contents = preg_replace( + '~\~s', + '', + file_get_contents($file) + ); break; case 'template': + $contents = php_strip_whitespace($file); //Removing html $contentsWithoutHtml = ''; preg_replace_callback( - '~(<\?php\s+.*\?>)~sU', + '~(<\?(php|=)\s+.*\?>)~sU', function ($matches) use ($contents, &$contentsWithoutHtml) { $contentsWithoutHtml .= $matches[1]; return $contents; @@ -309,10 +311,9 @@ function ($matches) use ($contents, &$contentsWithoutHtml) { $contents ); $contents = $contentsWithoutHtml; - //Removing php comments - $contents = preg_replace('~/\*.*?\*/~s', '', $contents); - $contents = preg_replace('~^\s*//.*$~s', '', $contents); break; + default: + $contents = file_get_contents($file); } return $contents; } @@ -393,9 +394,9 @@ protected function getDependenciesFromFiles($module, $fileType, $file, $contents foreach (self::$_rulesInstances as $rule) { /** @var \Magento\TestFramework\Dependency\RuleInterface $rule */ $newDependencies = $rule->getDependencyInfo($module, $fileType, $file, $contents); + //phpcs:ignore Magento2.Performance.ForeachArrayMerge $dependencies = array_merge($dependencies, $newDependencies); } - foreach ($dependencies as $key => $dependency) { foreach (self::$whiteList as $namespace) { if (strpos($dependency['source'], $namespace) !== false) { @@ -509,12 +510,12 @@ public function collectRedundant() foreach (array_keys(self::$mapDependencies) as $module) { $declared = $this->_getDependencies($module, self::TYPE_HARD, self::MAP_TYPE_DECLARED); + //phpcs:ignore Magento2.Performance.ForeachArrayMerge $found = array_merge( $this->_getDependencies($module, self::TYPE_HARD, self::MAP_TYPE_FOUND), $this->_getDependencies($module, self::TYPE_SOFT, self::MAP_TYPE_FOUND), $schemaDependencyProvider->getDeclaredExistingModuleDependencies($module) ); - $found['Magento\Framework'] = 'Magento\Framework'; $this->_setDependencies($module, self::TYPE_HARD, self::MAP_TYPE_REDUNDANT, array_diff($declared, $found)); } @@ -578,37 +579,16 @@ protected function _prepareFiles($fileType, $files, $skip = null) */ public function getAllFiles() { - $files = []; - - // Get all php files - $files = array_merge( - $files, + return array_merge( $this->_prepareFiles( 'php', Files::init()->getPhpFiles(Files::INCLUDE_APP_CODE | Files::AS_DATA_SET | Files::INCLUDE_NON_CLASSES), true - ) - ); - - // Get all configuration files - $files = array_merge( - $files, - $this->_prepareFiles('config', Files::init()->getConfigFiles()) - ); - - //Get all layout updates files - $files = array_merge( - $files, - $this->_prepareFiles('layout', Files::init()->getLayoutFiles()) - ); - - // Get all template files - $files = array_merge( - $files, + ), + $this->_prepareFiles('config', Files::init()->getConfigFiles()), + $this->_prepareFiles('layout', Files::init()->getLayoutFiles()), $this->_prepareFiles('template', Files::init()->getPhtmlFiles()) ); - - return $files; } /** diff --git a/lib/internal/Magento/Framework/App/Config/Initial/Reader.php b/lib/internal/Magento/Framework/App/Config/Initial/Reader.php index 076d6f1194351..99ec3d3f0280f 100644 --- a/lib/internal/Magento/Framework/App/Config/Initial/Reader.php +++ b/lib/internal/Magento/Framework/App/Config/Initial/Reader.php @@ -108,7 +108,6 @@ public function read() } else { $domDocument->merge($file); } - // phpcs:ignore Magento2.Exceptions.ThrowCatch.ThrowCatch } catch (\Magento\Framework\Config\Dom\ValidationException $e) { throw new \Magento\Framework\Exception\LocalizedException( new \Magento\Framework\Phrase( diff --git a/lib/internal/Magento/Framework/App/ExceptionHandler.php b/lib/internal/Magento/Framework/App/ExceptionHandler.php new file mode 100644 index 0000000000000..2bec055808aca --- /dev/null +++ b/lib/internal/Magento/Framework/App/ExceptionHandler.php @@ -0,0 +1,284 @@ +encryptor = $encryptor; + $this->filesystem = $filesystem; + $this->logger = $logger; + } + + /** + * Handles exception of HTTP web application + * + * @param Bootstrap $bootstrap + * @param \Exception $exception + * @param ResponseHttp $response + * @param RequestHttp $request + * @return bool + */ + public function handle( + Bootstrap $bootstrap, + \Exception $exception, + ResponseHttp $response, + RequestHttp $request + ): bool { + $result = $this->handleDeveloperMode($bootstrap, $exception, $response) + || $this->handleBootstrapErrors($bootstrap, $exception, $response) + || $this->handleSessionException($exception, $response, $request) + || $this->handleInitException($exception) + || $this->handleGenericReport($bootstrap, $exception); + return $result; + } + + /** + * Error handler for developer mode + * + * @param Bootstrap $bootstrap + * @param \Exception $exception + * @param ResponseHttp $response + * @return bool + */ + private function handleDeveloperMode( + Bootstrap $bootstrap, + \Exception $exception, + ResponseHttp $response + ): bool { + if ($bootstrap->isDeveloperMode()) { + if (Bootstrap::ERR_IS_INSTALLED == $bootstrap->getErrorCode()) { + try { + $this->redirectToSetup($bootstrap, $exception, $response); + return true; + } catch (\Exception $e) { + $exception = $e; + } + } + $response->setHttpResponseCode(500); + $response->setHeader('Content-Type', 'text/plain'); + $response->setBody($this->buildContentFromException($exception)); + $response->sendResponse(); + return true; + } + return false; + } + + /** + * Build content based on an exception + * + * @param \Exception $exception + * @return string + */ + private function buildContentFromException(\Exception $exception): string + { + /** @var \Exception[] $exceptions */ + $exceptions = []; + + do { + $exceptions[] = $exception; + } while ($exception = $exception->getPrevious()); + + $buffer = sprintf("%d exception(s):\n", count($exceptions)); + + foreach ($exceptions as $index => $exception) { + $buffer .= sprintf( + "Exception #%d (%s): %s\n", + $index, + get_class($exception), + $exception->getMessage() + ); + } + + foreach ($exceptions as $index => $exception) { + $buffer .= sprintf( + "\nException #%d (%s): %s\n%s\n", + $index, + get_class($exception), + $exception->getMessage(), + Debug::trace( + $exception->getTrace(), + true, + true, + (bool)getenv('MAGE_DEBUG_SHOW_ARGS') + ) + ); + } + + return $buffer; + } + + /** + * Handler for bootstrap errors + * + * @param Bootstrap $bootstrap + * @param \Exception $exception + * @param ResponseHttp $response + * @return bool + */ + private function handleBootstrapErrors( + Bootstrap $bootstrap, + \Exception &$exception, + ResponseHttp $response + ): bool { + $bootstrapCode = $bootstrap->getErrorCode(); + if (Bootstrap::ERR_MAINTENANCE == $bootstrapCode) { + // phpcs:ignore Magento2.Security.IncludeFile + require $this->filesystem + ->getDirectoryRead(DirectoryList::PUB) + ->getAbsolutePath('errors/503.php'); + return true; + } + if (Bootstrap::ERR_IS_INSTALLED == $bootstrapCode) { + try { + $this->redirectToSetup($bootstrap, $exception, $response); + return true; + } catch (\Exception $e) { + $exception = $e; + } + } + return false; + } + + /** + * Handler for session errors + * + * @param \Exception $exception + * @param ResponseHttp $response + * @param RequestHttp $request + * @return bool + */ + private function handleSessionException( + \Exception $exception, + ResponseHttp $response, + RequestHttp $request + ): bool { + if ($exception instanceof SessionException) { + $response->setRedirect($request->getDistroBaseUrl()); + $response->sendHeaders(); + return true; + } + return false; + } + + /** + * Handler for application initialization errors + * + * @param \Exception $exception + * @return bool + */ + private function handleInitException(\Exception $exception): bool + { + if ($exception instanceof InitException) { + $this->logger->critical($exception); + // phpcs:ignore Magento2.Security.IncludeFile + require $this->filesystem + ->getDirectoryRead(DirectoryList::PUB) + ->getAbsolutePath('errors/404.php'); + return true; + } + return false; + } + + /** + * Handle for any other errors + * + * @param Bootstrap $bootstrap + * @param \Exception $exception + * @return bool + */ + private function handleGenericReport(Bootstrap $bootstrap, \Exception $exception): bool + { + $reportData = [ + $exception->getMessage(), + Debug::trace( + $exception->getTrace(), + true, + false, + (bool)getenv('MAGE_DEBUG_SHOW_ARGS') + ) + ]; + $params = $bootstrap->getParams(); + if (isset($params['REQUEST_URI'])) { + $reportData['url'] = $params['REQUEST_URI']; + } + if (isset($params['SCRIPT_NAME'])) { + $reportData['script_name'] = $params['SCRIPT_NAME']; + } + $reportData['report_id'] = $this->encryptor->getHash(implode('', $reportData)); + $this->logger->critical($exception, ['report_id' => $reportData['report_id']]); + // phpcs:ignore Magento2.Security.IncludeFile + require $this->filesystem + ->getDirectoryRead(DirectoryList::PUB) + ->getAbsolutePath('errors/report.php'); + return true; + } + + /** + * If not installed, try to redirect to installation wizard + * + * @param Bootstrap $bootstrap + * @param \Exception $exception + * @param ResponseHttp $response + * @return void + * @throws \Exception + */ + private function redirectToSetup(Bootstrap $bootstrap, \Exception $exception, ResponseHttp $response) + { + $setupInfo = new SetupInfo($bootstrap->getParams()); + $projectRoot = $this->filesystem->getDirectoryRead(DirectoryList::ROOT)->getAbsolutePath(); + if ($setupInfo->isAvailable()) { + $response->setRedirect($setupInfo->getUrl()); + $response->sendHeaders(); + } else { + $newMessage = $exception->getMessage() . "\nNOTE: You cannot install Magento using the Setup Wizard " + . "because the Magento setup directory cannot be accessed. \n" + . 'You can install Magento using either the command line or you must restore access ' + . 'to the following directory: ' . $setupInfo->getDir($projectRoot) . "\n"; + // phpcs:ignore Magento2.Exceptions.DirectThrow + throw new \Exception($newMessage, 0, $exception); + } + } +} diff --git a/lib/internal/Magento/Framework/App/ExceptionHandlerInterface.php b/lib/internal/Magento/Framework/App/ExceptionHandlerInterface.php new file mode 100644 index 0000000000000..b4bb5d555017d --- /dev/null +++ b/lib/internal/Magento/Framework/App/ExceptionHandlerInterface.php @@ -0,0 +1,31 @@ +_objectManager = $objectManager; $this->_eventManager = $eventManager; @@ -102,30 +96,15 @@ public function __construct( $this->_response = $response; $this->_configLoader = $configLoader; $this->_state = $state; - $this->_filesystem = $filesystem; $this->registry = $registry; - } - - /** - * Add new dependency - * - * @return \Psr\Log\LoggerInterface - * - * @deprecated 100.1.0 - */ - private function getLogger() - { - if (!$this->logger instanceof \Psr\Log\LoggerInterface) { - $this->logger = \Magento\Framework\App\ObjectManager::getInstance()->get(\Psr\Log\LoggerInterface::class); - } - return $this->logger; + $this->exceptionHandler = $exceptionHandler ?: $this->_objectManager->get(ExceptionHandlerInterface::class); } /** * Run application * - * @throws \InvalidArgumentException * @return ResponseInterface + * @throws LocalizedException|\InvalidArgumentException */ public function launch() { @@ -172,193 +151,8 @@ private function handleHeadRequest() /** * @inheritdoc */ - public function catchException(Bootstrap $bootstrap, \Exception $exception) - { - $result = $this->handleDeveloperMode($bootstrap, $exception) - || $this->handleBootstrapErrors($bootstrap, $exception) - || $this->handleSessionException($exception) - || $this->handleInitException($exception) - || $this->handleGenericReport($bootstrap, $exception); - return $result; - } - - /** - * Error handler for developer mode - * - * @param Bootstrap $bootstrap - * @param \Exception $exception - * @return bool - */ - private function handleDeveloperMode(Bootstrap $bootstrap, \Exception $exception) + public function catchException(Bootstrap $bootstrap, \Exception $exception): bool { - if ($bootstrap->isDeveloperMode()) { - if (Bootstrap::ERR_IS_INSTALLED == $bootstrap->getErrorCode()) { - try { - $this->redirectToSetup($bootstrap, $exception); - return true; - } catch (\Exception $e) { - $exception = $e; - } - } - $this->_response->setHttpResponseCode(500); - $this->_response->setHeader('Content-Type', 'text/plain'); - $this->_response->setBody($this->buildContentFromException($exception)); - $this->_response->sendResponse(); - return true; - } - return false; - } - - /** - * Build content based on an exception - * - * @param \Exception $exception - * @return string - */ - private function buildContentFromException(\Exception $exception) - { - /** @var \Exception[] $exceptions */ - $exceptions = []; - - do { - $exceptions[] = $exception; - } while ($exception = $exception->getPrevious()); - - $buffer = sprintf("%d exception(s):\n", count($exceptions)); - - foreach ($exceptions as $index => $exception) { - $buffer .= sprintf("Exception #%d (%s): %s\n", $index, get_class($exception), $exception->getMessage()); - } - - foreach ($exceptions as $index => $exception) { - $buffer .= sprintf( - "\nException #%d (%s): %s\n%s\n", - $index, - get_class($exception), - $exception->getMessage(), - Debug::trace( - $exception->getTrace(), - true, - true, - (bool)getenv('MAGE_DEBUG_SHOW_ARGS') - ) - ); - } - - return $buffer; - } - - /** - * If not installed, try to redirect to installation wizard - * - * @param Bootstrap $bootstrap - * @param \Exception $exception - * @return void - * @throws \Exception - */ - private function redirectToSetup(Bootstrap $bootstrap, \Exception $exception) - { - $setupInfo = new SetupInfo($bootstrap->getParams()); - $projectRoot = $this->_filesystem->getDirectoryRead(DirectoryList::ROOT)->getAbsolutePath(); - if ($setupInfo->isAvailable()) { - $this->_response->setRedirect($setupInfo->getUrl()); - $this->_response->sendHeaders(); - } else { - $newMessage = $exception->getMessage() . "\nNOTE: You cannot install Magento using the Setup Wizard " - . "because the Magento setup directory cannot be accessed. \n" - . 'You can install Magento using either the command line or you must restore access ' - . 'to the following directory: ' . $setupInfo->getDir($projectRoot) . "\n"; - // phpcs:ignore Magento2.Exceptions.DirectThrow - throw new \Exception($newMessage, 0, $exception); - } - } - - /** - * Handler for bootstrap errors - * - * @param Bootstrap $bootstrap - * @param \Exception $exception - * @return bool - */ - private function handleBootstrapErrors(Bootstrap $bootstrap, \Exception &$exception) - { - $bootstrapCode = $bootstrap->getErrorCode(); - if (Bootstrap::ERR_MAINTENANCE == $bootstrapCode) { - // phpcs:ignore Magento2.Security.IncludeFile - require $this->_filesystem->getDirectoryRead(DirectoryList::PUB)->getAbsolutePath('errors/503.php'); - return true; - } - if (Bootstrap::ERR_IS_INSTALLED == $bootstrapCode) { - try { - $this->redirectToSetup($bootstrap, $exception); - return true; - } catch (\Exception $e) { - $exception = $e; - } - } - return false; - } - - /** - * Handler for session errors - * - * @param \Exception $exception - * @return bool - */ - private function handleSessionException(\Exception $exception) - { - if ($exception instanceof \Magento\Framework\Exception\SessionException) { - $this->_response->setRedirect($this->_request->getDistroBaseUrl()); - $this->_response->sendHeaders(); - return true; - } - return false; - } - - /** - * Handler for application initialization errors - * - * @param \Exception $exception - * @return bool - */ - private function handleInitException(\Exception $exception) - { - if ($exception instanceof \Magento\Framework\Exception\State\InitException) { - $this->getLogger()->critical($exception); - // phpcs:ignore Magento2.Security.IncludeFile - require $this->_filesystem->getDirectoryRead(DirectoryList::PUB)->getAbsolutePath('errors/404.php'); - return true; - } - return false; - } - - /** - * Handle for any other errors - * - * @param Bootstrap $bootstrap - * @param \Exception $exception - * @return bool - */ - private function handleGenericReport(Bootstrap $bootstrap, \Exception $exception) - { - $reportData = [ - $exception->getMessage(), - Debug::trace( - $exception->getTrace(), - true, - true, - (bool)getenv('MAGE_DEBUG_SHOW_ARGS') - ) - ]; - $params = $bootstrap->getParams(); - if (isset($params['REQUEST_URI'])) { - $reportData['url'] = $params['REQUEST_URI']; - } - if (isset($params['SCRIPT_NAME'])) { - $reportData['script_name'] = $params['SCRIPT_NAME']; - } - // phpcs:ignore Magento2.Security.IncludeFile - require $this->_filesystem->getDirectoryRead(DirectoryList::PUB)->getAbsolutePath('errors/report.php'); - return true; + return $this->exceptionHandler->handle($bootstrap, $exception, $this->_response, $this->_request); } } diff --git a/lib/internal/Magento/Framework/App/Test/Unit/ExceptionHandlerTest.php b/lib/internal/Magento/Framework/App/Test/Unit/ExceptionHandlerTest.php new file mode 100644 index 0000000000000..bce2fd8113149 --- /dev/null +++ b/lib/internal/Magento/Framework/App/Test/Unit/ExceptionHandlerTest.php @@ -0,0 +1,277 @@ +encryptorInterfaceMock = $this->createMock(EncryptorInterface::class); + $this->filesystemMock = $this->createMock(Filesystem::class); + $this->loggerMock = $this->createMock(LoggerInterface::class); + $this->responseMock = $this->createMock(ResponseHttp::class); + $this->requestMock = $this->getMockBuilder(RequestHttp::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->exceptionHandler = new ExceptionHandler( + $this->encryptorInterfaceMock, + $this->filesystemMock, + $this->loggerMock + ); + } + + public function testHandleDeveloperModeNotInstalled() + { + $dir = $this->getMockForAbstractClass(ReadInterface::class); + $dir->expects($this->once()) + ->method('getAbsolutePath') + ->willReturn(__DIR__); + $this->filesystemMock->expects($this->once()) + ->method('getDirectoryRead') + ->with(DirectoryList::ROOT) + ->willReturn($dir); + $this->responseMock->expects($this->once()) + ->method('setRedirect') + ->with('/_files/'); + $this->responseMock->expects($this->once()) + ->method('sendHeaders'); + $bootstrap = $this->getBootstrapNotInstalled(); + $bootstrap->expects($this->once()) + ->method('getParams') + ->willReturn( + [ + 'SCRIPT_NAME' => '/index.php', + 'DOCUMENT_ROOT' => __DIR__, + 'SCRIPT_FILENAME' => __DIR__ . '/index.php', + SetupInfo::PARAM_NOT_INSTALLED_URL_PATH => '_files', + ] + ); + $this->assertTrue( + $this->exceptionHandler->handle( + $bootstrap, + new \Exception('Test Message'), + $this->responseMock, + $this->requestMock + ) + ); + } + + public function testHandleDeveloperMode() + { + $this->filesystemMock->expects($this->once()) + ->method('getDirectoryRead') + ->will($this->throwException(new \Exception('strange error'))); + $this->responseMock->expects($this->once()) + ->method('setHttpResponseCode') + ->with(500); + $this->responseMock->expects($this->once()) + ->method('setHeader') + ->with('Content-Type', 'text/plain'); + $constraint = new StringStartsWith('1 exception(s):'); + $this->responseMock->expects($this->once()) + ->method('setBody') + ->with($constraint); + $this->responseMock->expects($this->once()) + ->method('sendResponse'); + $bootstrap = $this->getBootstrapNotInstalled(); + $bootstrap->expects($this->once()) + ->method('getParams') + ->willReturn( + ['DOCUMENT_ROOT' => 'something', 'SCRIPT_FILENAME' => 'something/else'] + ); + $this->assertTrue( + $this->exceptionHandler->handle( + $bootstrap, + new \Exception('Test'), + $this->responseMock, + $this->requestMock + ) + ); + } + + public function testCatchExceptionSessionException() + { + $this->responseMock->expects($this->once()) + ->method('setRedirect'); + $this->responseMock->expects($this->once()) + ->method('sendHeaders'); + /** @var Bootstrap|MockObject $bootstrap */ + $bootstrap = $this->createMock(Bootstrap::class); + $bootstrap->expects($this->once()) + ->method('isDeveloperMode') + ->willReturn(false); + $this->assertTrue( + $this->exceptionHandler->handle( + $bootstrap, + new SessionException(new Phrase('Test')), + $this->responseMock, + $this->requestMock + ) + ); + } + + public function testHandleInitException() + { + $bootstrap = $this->getBootstrapInstalled(); + $exception = new InitException(new Phrase('Test')); + $dir = $this->getMockForAbstractClass(ReadInterface::class); + $dir->expects($this->once()) + ->method('getAbsolutePath') + ->with('errors/404.php') + ->willReturn(__DIR__ . '/_files/pub/errors/404.php'); + $this->loggerMock->expects($this->once()) + ->method('critical') + ->with($exception); + $this->filesystemMock->expects($this->once()) + ->method('getDirectoryRead') + ->with(DirectoryList::PUB) + ->willReturn($dir); + + $this->assertTrue( + $this->exceptionHandler->handle( + $bootstrap, + $exception, + $this->responseMock, + $this->requestMock + ) + ); + } + + public function testHandleGenericReport() + { + $bootstrap = $this->getBootstrapInstalled(); + $exception = new \Exception('Test'); + $dir = $this->getMockForAbstractClass(ReadInterface::class); + $dir->expects($this->once()) + ->method('getAbsolutePath') + ->with('errors/report.php') + ->willReturn(__DIR__ . '/_files/pub/errors/report.php'); + $bootstrap->expects($this->once()) + ->method('getParams') + ->willReturn(['REQUEST_URI' => 'some-request-uri', 'SCRIPT_NAME' => 'some-script-name']); + $reportData = [ + $exception->getMessage(), + Debug::trace( + $exception->getTrace(), + true, + false, + (bool)getenv('MAGE_DEBUG_SHOW_ARGS') + ), + 'url' => 'some-request-uri', + 'script_name' => 'some-script-name' + ]; + $this->encryptorInterfaceMock->expects($this->once()) + ->method('getHash') + ->with(implode('', $reportData)) + ->willReturn('some-sha256-hash'); + $this->loggerMock->expects($this->once()) + ->method('critical') + ->with($exception, ['report_id' => 'some-sha256-hash']); + $this->filesystemMock->expects($this->once()) + ->method('getDirectoryRead') + ->with(DirectoryList::PUB) + ->willReturn($dir); + + $this->assertTrue( + $this->exceptionHandler->handle( + $bootstrap, + $exception, + $this->responseMock, + $this->requestMock + ) + ); + } + + /** + * Prepares a mock of bootstrap in "not installed" state + * + * @return Bootstrap|MockObject + */ + private function getBootstrapNotInstalled(): Bootstrap + { + $bootstrap = $this->createMock(Bootstrap::class); + $bootstrap->expects($this->once()) + ->method('isDeveloperMode') + ->willReturn(true); + $bootstrap->expects($this->once()) + ->method('getErrorCode') + ->willReturn(Bootstrap::ERR_IS_INSTALLED); + return $bootstrap; + } + + /** + * Prepares a mock of bootstrap in "installed" state + * + * @return Bootstrap|MockObject + */ + private function getBootstrapInstalled(): Bootstrap + { + /** @var Bootstrap|MockObject $bootstrap */ + $bootstrap = $this->createMock(Bootstrap::class); + $bootstrap->expects($this->once()) + ->method('isDeveloperMode') + ->willReturn(false); + $bootstrap->expects($this->once()) + ->method('getErrorCode') + ->willReturn(0); + return $bootstrap; + } +} diff --git a/lib/internal/Magento/Framework/App/Test/Unit/HttpTest.php b/lib/internal/Magento/Framework/App/Test/Unit/HttpTest.php index dbb315e88a526..5d8e5960e5798 100644 --- a/lib/internal/Magento/Framework/App/Test/Unit/HttpTest.php +++ b/lib/internal/Magento/Framework/App/Test/Unit/HttpTest.php @@ -3,12 +3,23 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - namespace Magento\Framework\App\Test\Unit; -use Magento\Framework\App\Filesystem\DirectoryList; -use Magento\Framework\App\SetupInfo; -use Magento\Framework\App\Bootstrap; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as HelperObjectManager; +use PHPUnit_Framework_MockObject_MockObject as MockObject; +use Magento\Framework\App\Request\Http as RequestHttp; +use Magento\Framework\App\Response\Http as ResponseHttp; +use Magento\Framework\App\Http as AppHttp; +use Magento\Framework\App\FrontControllerInterface; +use Magento\Framework\Event\Manager; +use Magento\Framework\ObjectManagerInterface; +use Magento\Framework\App\AreaList; +use Magento\Framework\App\ObjectManager\ConfigLoader; +use Magento\Framework\App\ExceptionHandlerInterface; +use Magento\Framework\Stdlib\Cookie\CookieReaderInterface; +use Magento\Framework\App\Route\ConfigInterface\Proxy; +use Magento\Framework\App\Request\PathInfoProcessorInterface; +use Magento\Framework\Stdlib\StringUtils; /** * @SuppressWarnings(PHPMD.CouplingBetweenObjects) @@ -16,85 +27,90 @@ class HttpTest extends \PHPUnit\Framework\TestCase { /** - * @var \Magento\Framework\TestFramework\Unit\Helper\ObjectManager + * @var HelperObjectManager */ - protected $objectManager; + private $objectManager; /** - * @var \PHPUnit_Framework_MockObject_MockObject + * @var ResponseHttp|MockObject */ - protected $responseMock; + private $responseMock; /** - * @var \Magento\Framework\App\Http + * @var AppHttp */ - protected $http; + private $http; /** - * @var \PHPUnit_Framework_MockObject_MockObject + * @var FrontControllerInterface|MockObject */ - protected $frontControllerMock; + private $frontControllerMock; /** - * @var \PHPUnit_Framework_MockObject_MockObject + * @var Manager|MockObject */ - protected $eventManagerMock; + private $eventManagerMock; /** - * @var \PHPUnit_Framework_MockObject_MockObject + * @var RequestHttp|MockObject */ - protected $requestMock; + private $requestMock; /** - * @var \PHPUnit_Framework_MockObject_MockObject + * @var ObjectManagerInterface|MockObject */ - protected $objectManagerMock; + private $objectManagerMock; /** - * @var \PHPUnit_Framework_MockObject_MockObject + * @var AreaList|MockObject */ - protected $areaListMock; + private $areaListMock; /** - * @var \PHPUnit_Framework_MockObject_MockObject + * @var ConfigLoader|MockObject */ - protected $configLoaderMock; + private $configLoaderMock; /** - * @var \PHPUnit_Framework_MockObject_MockObject + * @var ExceptionHandlerInterface|MockObject */ - protected $filesystemMock; + private $exceptionHandlerMock; + /** + * @inheritdoc + */ protected function setUp() { - $this->objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); - $cookieReaderMock = $this->getMockBuilder(\Magento\Framework\Stdlib\Cookie\CookieReaderInterface::class) + $this->objectManager = new HelperObjectManager($this); + $cookieReaderMock = $this->getMockBuilder(CookieReaderInterface::class) ->disableOriginalConstructor() ->getMock(); - $routeConfigMock = $this->getMockBuilder(\Magento\Framework\App\Route\ConfigInterface\Proxy::class) + $routeConfigMock = $this->getMockBuilder(Proxy::class) ->disableOriginalConstructor() ->getMock(); - $pathInfoProcessorMock = $this->getMockBuilder(\Magento\Framework\App\Request\PathInfoProcessorInterface::class) + $pathInfoProcessorMock = $this->getMockBuilder(PathInfoProcessorInterface::class) ->disableOriginalConstructor() ->getMock(); - $converterMock = $this->getMockBuilder(\Magento\Framework\Stdlib\StringUtils::class) + $converterMock = $this->getMockBuilder(StringUtils::class) ->disableOriginalConstructor() ->setMethods(['cleanString']) ->getMock(); - $objectManagerMock = $this->getMockBuilder(\Magento\Framework\ObjectManagerInterface::class) + $objectManagerMock = $this->getMockBuilder(ObjectManagerInterface::class) ->disableOriginalConstructor() ->getMock(); - $this->requestMock = $this->getMockBuilder(\Magento\Framework\App\Request\Http::class) - ->setConstructorArgs([ - 'cookieReader' => $cookieReaderMock, - 'converter' => $converterMock, - 'routeConfig' => $routeConfigMock, - 'pathInfoProcessor' => $pathInfoProcessorMock, - 'objectManager' => $objectManagerMock - ]) + $this->requestMock = $this->getMockBuilder(RequestHttp::class) + ->setConstructorArgs( + [ + 'cookieReader' => $cookieReaderMock, + 'converter' => $converterMock, + 'routeConfig' => $routeConfigMock, + 'pathInfoProcessor' => $pathInfoProcessorMock, + 'objectManager' => $objectManagerMock + ] + ) ->setMethods(['getFrontName', 'isHead']) ->getMock(); - $this->areaListMock = $this->getMockBuilder(\Magento\Framework\App\AreaList::class) + $this->areaListMock = $this->getMockBuilder(AreaList::class) ->disableOriginalConstructor() ->setMethods(['getCodeByFrontName']) ->getMock(); @@ -102,20 +118,20 @@ protected function setUp() ->disableOriginalConstructor() ->setMethods(['load']) ->getMock(); - $this->objectManagerMock = $this->createMock(\Magento\Framework\ObjectManagerInterface::class); - $this->responseMock = $this->createMock(\Magento\Framework\App\Response\Http::class); + $this->objectManagerMock = $this->createMock(ObjectManagerInterface::class); + $this->responseMock = $this->createMock(ResponseHttp::class); $this->frontControllerMock = $this->getMockBuilder(\Magento\Framework\App\FrontControllerInterface::class) ->disableOriginalConstructor() ->setMethods(['dispatch']) ->getMock(); - $this->eventManagerMock = $this->getMockBuilder(\Magento\Framework\Event\Manager::class) + $this->eventManagerMock = $this->getMockBuilder(Manager::class) ->disableOriginalConstructor() ->setMethods(['dispatch']) ->getMock(); - $this->filesystemMock = $this->createMock(\Magento\Framework\Filesystem::class); + $this->exceptionHandlerMock = $this->createMock(ExceptionHandlerInterface::class); $this->http = $this->objectManager->getObject( - \Magento\Framework\App\Http::class, + AppHttp::class, [ 'objectManager' => $this->objectManagerMock, 'eventManager' => $this->eventManagerMock, @@ -123,7 +139,7 @@ protected function setUp() 'request' => $this->requestMock, 'response' => $this->responseMock, 'configLoader' => $this->configLoaderMock, - 'filesystem' => $this->filesystemMock, + 'exceptionHandler' => $this->exceptionHandlerMock, ] ); } @@ -247,92 +263,4 @@ public function dataProviderForTestLaunchHeadRequest(): array ] ]; } - - public function testHandleDeveloperModeNotInstalled() - { - $dir = $this->getMockForAbstractClass(\Magento\Framework\Filesystem\Directory\ReadInterface::class); - $dir->expects($this->once()) - ->method('getAbsolutePath') - ->willReturn(__DIR__); - $this->filesystemMock->expects($this->once()) - ->method('getDirectoryRead') - ->with(DirectoryList::ROOT) - ->willReturn($dir); - $this->responseMock->expects($this->once()) - ->method('setRedirect') - ->with('/_files/'); - $this->responseMock->expects($this->once()) - ->method('sendHeaders'); - $bootstrap = $this->getBootstrapNotInstalled(); - $bootstrap->expects($this->once()) - ->method('getParams') - ->willReturn( - [ - 'SCRIPT_NAME' => '/index.php', - 'DOCUMENT_ROOT' => __DIR__, - 'SCRIPT_FILENAME' => __DIR__ . '/index.php', - SetupInfo::PARAM_NOT_INSTALLED_URL_PATH => '_files', - ] - ); - $this->assertTrue($this->http->catchException($bootstrap, new \Exception('Test Message'))); - } - - public function testHandleDeveloperMode() - { - $this->filesystemMock->expects($this->once()) - ->method('getDirectoryRead') - ->will($this->throwException(new \Exception('strange error'))); - $this->responseMock->expects($this->once()) - ->method('setHttpResponseCode') - ->with(500); - $this->responseMock->expects($this->once()) - ->method('setHeader') - ->with('Content-Type', 'text/plain'); - $constraint = new \PHPUnit\Framework\Constraint\StringStartsWith('1 exception(s):'); - $this->responseMock->expects($this->once()) - ->method('setBody') - ->with($constraint); - $this->responseMock->expects($this->once()) - ->method('sendResponse'); - $bootstrap = $this->getBootstrapNotInstalled(); - $bootstrap->expects($this->once()) - ->method('getParams') - ->willReturn( - ['DOCUMENT_ROOT' => 'something', 'SCRIPT_FILENAME' => 'something/else'] - ); - $this->assertTrue($this->http->catchException($bootstrap, new \Exception('Test'))); - } - - public function testCatchExceptionSessionException() - { - $this->responseMock->expects($this->once()) - ->method('setRedirect'); - $this->responseMock->expects($this->once()) - ->method('sendHeaders'); - $bootstrap = $this->createMock(\Magento\Framework\App\Bootstrap::class); - $bootstrap->expects($this->once()) - ->method('isDeveloperMode') - ->willReturn(false); - $this->assertTrue($this->http->catchException( - $bootstrap, - new \Magento\Framework\Exception\SessionException(new \Magento\Framework\Phrase('Test')) - )); - } - - /** - * Prepares a mock of bootstrap in "not installed" state - * - * @return \PHPUnit_Framework_MockObject_MockObject - */ - private function getBootstrapNotInstalled() - { - $bootstrap = $this->createMock(\Magento\Framework\App\Bootstrap::class); - $bootstrap->expects($this->once()) - ->method('isDeveloperMode') - ->willReturn(true); - $bootstrap->expects($this->once()) - ->method('getErrorCode') - ->willReturn(Bootstrap::ERR_IS_INSTALLED); - return $bootstrap; - } } diff --git a/lib/internal/Magento/Framework/App/Test/Unit/_files/pub/errors/404.php b/lib/internal/Magento/Framework/App/Test/Unit/_files/pub/errors/404.php new file mode 100644 index 0000000000000..37121092d0ba0 --- /dev/null +++ b/lib/internal/Magento/Framework/App/Test/Unit/_files/pub/errors/404.php @@ -0,0 +1,6 @@ +_getMethodParameterInfo($parameter); } - $returnTypeValue = $this->getReturnTypeValue($method->getReturnType()); + $returnTypeValue = $this->getReturnTypeValue($method); $methodInfo = [ 'name' => $method->getName(), 'parameters' => $parameters, @@ -208,6 +207,7 @@ protected function _getMethodBody( } else { $methodCall = sprintf('%s(%s)', $name, implode(', ', $parameters)); } + //Waiting for deferred result and using it's methods. return "\$this->wait();\n" .($withoutReturn ? '' : 'return ')."\$this->instance->$methodCall;"; @@ -231,24 +231,27 @@ protected function _validateData() $result = false; } } + return $result; } /** * Returns return type * - * @param mixed $returnType + * @param \ReflectionMethod $method * @return null|string */ - private function getReturnTypeValue($returnType): ?string + private function getReturnTypeValue(\ReflectionMethod $method): ?string { $returnTypeValue = null; + $returnType = $method->getReturnType(); if ($returnType) { $returnTypeValue = ($returnType->allowsNull() ? '?' : ''); $returnTypeValue .= ($returnType->getName() === 'self') - ? $this->getSourceClassName() + ? $this->_getFullyQualifiedClassName($method->getDeclaringClass()->getName()) : $returnType->getName(); } + return $returnTypeValue; } } diff --git a/lib/internal/Magento/Framework/Config/Scope.php b/lib/internal/Magento/Framework/Config/Scope.php index c43a4550cd79d..e4b25c7eb5dcc 100644 --- a/lib/internal/Magento/Framework/Config/Scope.php +++ b/lib/internal/Magento/Framework/Config/Scope.php @@ -5,15 +5,13 @@ */ namespace Magento\Framework\Config; -class Scope implements \Magento\Framework\Config\ScopeInterface, \Magento\Framework\Config\ScopeListInterface -{ - /** - * Default application scope - * - * @var string - */ - protected $_defaultScope; +use Magento\Framework\App\AreaList; +/** + * Scope config + */ +class Scope implements ScopeInterface, ScopeListInterface +{ /** * Current config scope * @@ -24,19 +22,19 @@ class Scope implements \Magento\Framework\Config\ScopeInterface, \Magento\Framew /** * List of all available areas * - * @var \Magento\Framework\App\AreaList + * @var AreaList */ protected $_areaList; /** * Constructor * - * @param \Magento\Framework\App\AreaList $areaList + * @param AreaList $areaList * @param string $defaultScope */ - public function __construct(\Magento\Framework\App\AreaList $areaList, $defaultScope = 'primary') + public function __construct(AreaList $areaList, $defaultScope = 'primary') { - $this->_defaultScope = $this->_currentScope = $defaultScope; + $this->_currentScope = $defaultScope; $this->_areaList = $areaList; } @@ -69,7 +67,9 @@ public function setCurrentScope($scope) public function getAllScopes() { $codes = $this->_areaList->getCodes(); - array_unshift($codes, $this->_defaultScope); + array_unshift($codes, 'global'); + array_unshift($codes, 'primary'); + return $codes; } } diff --git a/lib/internal/Magento/Framework/Config/Test/Unit/ScopeTest.php b/lib/internal/Magento/Framework/Config/Test/Unit/ScopeTest.php index 0ed59e73a25a2..fa2f2c79ef952 100644 --- a/lib/internal/Magento/Framework/Config/Test/Unit/ScopeTest.php +++ b/lib/internal/Magento/Framework/Config/Test/Unit/ScopeTest.php @@ -6,23 +6,25 @@ namespace Magento\Framework\Config\Test\Unit; -use \Magento\Framework\Config\Scope; +use Magento\Framework\App\AreaList; +use Magento\Framework\Config\Scope; +use PHPUnit\Framework\MockObject\MockObject; class ScopeTest extends \PHPUnit\Framework\TestCase { /** - * @var \Magento\Framework\Config\Scope + * @var Scope */ - protected $model; + private $model; /** - * @var \PHPUnit_Framework_MockObject_MockObject|\Magento\Framework\App\AreaList + * @var MockObject|AreaList */ - protected $areaListMock; + private $areaListMock; protected function setUp() { - $this->areaListMock = $this->createPartialMock(\Magento\Framework\App\AreaList::class, ['getCodes']); + $this->areaListMock = $this->createPartialMock(AreaList::class, ['getCodes']); $this->model = new Scope($this->areaListMock); } @@ -35,10 +37,10 @@ public function testScopeSetGet() public function testGetAllScopes() { - $expectedBalances = ['primary', 'test_scope']; + $expectedBalances = ['primary', 'global', 'test_scope']; $this->areaListMock->expects($this->once()) ->method('getCodes') - ->will($this->returnValue(['test_scope'])); + ->willReturn(['test_scope']); $this->assertEquals($expectedBalances, $this->model->getAllScopes()); } } diff --git a/lib/internal/Magento/Framework/Console/Cli.php b/lib/internal/Magento/Framework/Console/Cli.php index 2ef41f361027e..34fd6316ce454 100644 --- a/lib/internal/Magento/Framework/Console/Cli.php +++ b/lib/internal/Magento/Framework/Console/Cli.php @@ -93,6 +93,7 @@ public function __construct($name = 'UNKNOWN', $version = 'UNKNOWN') } parent::__construct($name, $version); + $this->serviceManager->setService(\Symfony\Component\Console\Application::class, $this); } /** diff --git a/lib/internal/Magento/Framework/Crontab/CrontabManager.php b/lib/internal/Magento/Framework/Crontab/CrontabManager.php index da81540faf477..fb3537ca0648f 100644 --- a/lib/internal/Magento/Framework/Crontab/CrontabManager.php +++ b/lib/internal/Magento/Framework/Crontab/CrontabManager.php @@ -207,7 +207,6 @@ private function save($content) try { $this->shell->execute('echo "' . $content . '" | crontab -'); - // phpcs:disable Magento2.Exceptions.ThrowCatch } catch (LocalizedException $e) { throw new LocalizedException( new Phrase('Error during saving of crontab: %1', [$e->getPrevious()->getMessage()]), diff --git a/lib/internal/Magento/Framework/Css/PreProcessor/Adapter/Less/Processor.php b/lib/internal/Magento/Framework/Css/PreProcessor/Adapter/Less/Processor.php index 517c2f5c55b90..8f40ab044c020 100644 --- a/lib/internal/Magento/Framework/Css/PreProcessor/Adapter/Less/Processor.php +++ b/lib/internal/Magento/Framework/Css/PreProcessor/Adapter/Less/Processor.php @@ -61,7 +61,6 @@ public function __construct( /** * @inheritdoc - * @throws ContentProcessorException */ public function processContent(File $asset) { @@ -77,7 +76,9 @@ public function processContent(File $asset) $content = $this->assetSource->getContent($asset); if (trim($content) === '') { - return ''; + throw new ContentProcessorException( + new Phrase('Compilation from source: LESS file is empty: ' . $path) + ); } $tmpFilePath = $this->temporaryFile->createFile($path, $content); @@ -88,8 +89,9 @@ public function processContent(File $asset) gc_enable(); if (trim($content) === '') { - $this->logger->warning('Parsed less file is empty: ' . $path); - return ''; + throw new ContentProcessorException( + new Phrase('Compilation from source: LESS file is empty: ' . $path) + ); } else { return $content; } diff --git a/lib/internal/Magento/Framework/Css/Test/Unit/PreProcessor/Adapter/Less/ProcessorTest.php b/lib/internal/Magento/Framework/Css/Test/Unit/PreProcessor/Adapter/Less/ProcessorTest.php index e1588db4ba97c..7c3fc799c3996 100644 --- a/lib/internal/Magento/Framework/Css/Test/Unit/PreProcessor/Adapter/Less/ProcessorTest.php +++ b/lib/internal/Magento/Framework/Css/Test/Unit/PreProcessor/Adapter/Less/ProcessorTest.php @@ -111,6 +111,9 @@ public function testProcessContentException() /** * Test for processContent method (empty content) + * + * @expectedException \Magento\Framework\View\Asset\ContentProcessorException + * @expectedExceptionMessageRegExp (Compilation from source: LESS file is empty: test-path) */ public function testProcessContentEmpty() { diff --git a/lib/internal/Magento/Framework/Data/Collection.php b/lib/internal/Magento/Framework/Data/Collection.php index 128d3d8e9fd3d..c44916fb5af6f 100644 --- a/lib/internal/Magento/Framework/Data/Collection.php +++ b/lib/internal/Magento/Framework/Data/Collection.php @@ -259,7 +259,7 @@ public function getLastPageNumber() if (0 === $collectionSize) { return 1; } elseif ($this->_pageSize) { - return ceil($collectionSize / $this->_pageSize); + return (int)ceil($collectionSize / $this->_pageSize); } else { return 1; } diff --git a/lib/internal/Magento/Framework/Escaper.php b/lib/internal/Magento/Framework/Escaper.php index 364c8bf766117..68a18964d3428 100644 --- a/lib/internal/Magento/Framework/Escaper.php +++ b/lib/internal/Magento/Framework/Escaper.php @@ -89,7 +89,6 @@ function ($errorNumber, $errorString) { $domDocument->loadHTML( '' . $string . '' ); - // phpcs:disable Magento2.Exceptions.ThrowCatch } catch (\Exception $e) { restore_error_handler(); $this->getLogger()->critical($e); diff --git a/lib/internal/Magento/Framework/GraphQl/Config/Element/Argument.php b/lib/internal/Magento/Framework/GraphQl/Config/Element/Argument.php index 39d767fa74ad8..6a8ab2e61ecba 100644 --- a/lib/internal/Magento/Framework/GraphQl/Config/Element/Argument.php +++ b/lib/internal/Magento/Framework/GraphQl/Config/Element/Argument.php @@ -54,6 +54,11 @@ class Argument implements FieldInterface */ private $defaultValue; + /** + * @var array + */ + private $deprecated; + /** * @param string $name * @param string $type @@ -64,6 +69,8 @@ class Argument implements FieldInterface * @param string $itemType * @param bool $itemsRequired * @param string $defaultValue + * @param array $deprecated + * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( string $name, @@ -74,7 +81,8 @@ public function __construct( bool $isList, string $itemType = '', bool $itemsRequired = false, - string $defaultValue = null + string $defaultValue = null, + array $deprecated = [] ) { $this->name = $name; $this->type = $isList ? $itemType : $type; @@ -84,6 +92,7 @@ public function __construct( $this->isList = $isList; $this->itemsRequired = $itemsRequired; $this->defaultValue = $defaultValue; + $this->deprecated = $deprecated; } /** @@ -175,4 +184,14 @@ public function hasDefaultValue() : bool { return $this->defaultValue ? true: false; } + + /** + * Return the deprecated + * + * @return array + */ + public function getDeprecated() : array + { + return $this->deprecated; + } } diff --git a/lib/internal/Magento/Framework/GraphQl/Config/Element/ArgumentFactory.php b/lib/internal/Magento/Framework/GraphQl/Config/Element/ArgumentFactory.php index 86eee7afd13bd..79114ae1d0e45 100644 --- a/lib/internal/Magento/Framework/GraphQl/Config/Element/ArgumentFactory.php +++ b/lib/internal/Magento/Framework/GraphQl/Config/Element/ArgumentFactory.php @@ -10,7 +10,7 @@ use Magento\Framework\ObjectManagerInterface; /** - * {@inheritdoc} + * @inheritdoc */ class ArgumentFactory { @@ -51,7 +51,8 @@ public function createFromConfigData( 'isList' => isset($argumentData['itemType']), 'itemType' => isset($argumentData['itemType']) ? $argumentData['itemType'] : '', 'itemsRequired' => isset($argumentData['itemsRequired']) ? $argumentData['itemsRequired'] : false, - 'defaultValue' => isset($argumentData['defaultValue']) ? $argumentData['defaultValue'] : null + 'defaultValue' => isset($argumentData['defaultValue']) ? $argumentData['defaultValue'] : null, + 'deprecated' => isset($argumentData['deprecated']) ? $argumentData['deprecated'] : [], ] ); } diff --git a/lib/internal/Magento/Framework/GraphQl/Config/Element/EnumFactory.php b/lib/internal/Magento/Framework/GraphQl/Config/Element/EnumFactory.php index 3e0974aedd473..f8b42f5611f30 100644 --- a/lib/internal/Magento/Framework/GraphQl/Config/Element/EnumFactory.php +++ b/lib/internal/Magento/Framework/GraphQl/Config/Element/EnumFactory.php @@ -12,7 +12,7 @@ use Magento\Framework\ObjectManagerInterface; /** - * {@inheritdoc} + * @inheritdoc */ class EnumFactory implements ConfigElementFactoryInterface { @@ -71,7 +71,8 @@ public function createFromConfigData(array $data): ConfigElementInterface $values[$item['_value']] = $this->enumValueFactory->create( $item['name'], $item['_value'], - isset($item['description']) ? $item['description'] : '' + isset($item['description']) ? $item['description'] : '', + isset($item['deprecationReason']) ? $item['deprecationReason'] : '' ); } return $this->create( diff --git a/lib/internal/Magento/Framework/GraphQl/Config/Element/EnumValue.php b/lib/internal/Magento/Framework/GraphQl/Config/Element/EnumValue.php index 151c07caf16a2..22bb1db8d2787 100644 --- a/lib/internal/Magento/Framework/GraphQl/Config/Element/EnumValue.php +++ b/lib/internal/Magento/Framework/GraphQl/Config/Element/EnumValue.php @@ -29,16 +29,23 @@ class EnumValue implements ConfigElementInterface */ private $description; + /** + * @var string + */ + private $deprecationReason; + /** * @param string $name * @param string $value * @param string $description + * @param string $deprecationReason */ - public function __construct(string $name, string $value, string $description = '') + public function __construct(string $name, string $value, string $description = '', string $deprecationReason = '') { $this->name = $name; $this->value = $value; $this->description = $description; + $this->deprecationReason = $deprecationReason; } /** @@ -70,4 +77,14 @@ public function getDescription() : string { return $this->description; } + + /** + * Get the enum value's deprecatedReason. + * + * @return string + */ + public function getDeprecatedReason() : string + { + return $this->deprecationReason; + } } diff --git a/lib/internal/Magento/Framework/GraphQl/Config/Element/EnumValueFactory.php b/lib/internal/Magento/Framework/GraphQl/Config/Element/EnumValueFactory.php index c5fb75fb3566c..45402d25f3d8a 100644 --- a/lib/internal/Magento/Framework/GraphQl/Config/Element/EnumValueFactory.php +++ b/lib/internal/Magento/Framework/GraphQl/Config/Element/EnumValueFactory.php @@ -34,16 +34,22 @@ public function __construct( * @param string $name * @param string $value * @param string $description + * @param string $deprecationReason * @return EnumValue */ - public function create(string $name, string $value, string $description = ''): EnumValue - { + public function create( + string $name, + string $value, + string $description = '', + string $deprecationReason = '' + ): EnumValue { return $this->objectManager->create( EnumValue::class, [ 'name' => $name, 'value' => $value, - 'description' => $description + 'description' => $description, + 'deprecationReason' => $deprecationReason ] ); } diff --git a/lib/internal/Magento/Framework/GraphQl/Config/Element/Field.php b/lib/internal/Magento/Framework/GraphQl/Config/Element/Field.php index 0fc51e4ecd069..0b1b8ae3da31b 100644 --- a/lib/internal/Magento/Framework/GraphQl/Config/Element/Field.php +++ b/lib/internal/Magento/Framework/GraphQl/Config/Element/Field.php @@ -53,6 +53,11 @@ class Field implements OutputFieldInterface */ private $cache; + /** + * @var array + */ + private $deprecated; + /** * @param string $name * @param string $type @@ -63,6 +68,8 @@ class Field implements OutputFieldInterface * @param string $description * @param array $arguments * @param array $cache + * @param array $deprecated + * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( string $name, @@ -73,7 +80,8 @@ public function __construct( string $resolver = '', string $description = '', array $arguments = [], - array $cache = [] + array $cache = [], + array $deprecated = [] ) { $this->name = $name; $this->type = $isList ? $itemType : $type; @@ -83,6 +91,7 @@ public function __construct( $this->description = $description; $this->arguments = $arguments; $this->cache = $cache; + $this->deprecated = $deprecated; } /** @@ -164,4 +173,14 @@ public function getCache() : array { return $this->cache; } + + /** + * Return the deprecated annotation for the field + * + * @return array + */ + public function getDeprecated() : array + { + return $this->deprecated; + } } diff --git a/lib/internal/Magento/Framework/GraphQl/Config/Element/FieldFactory.php b/lib/internal/Magento/Framework/GraphQl/Config/Element/FieldFactory.php index 60191b69be47f..e4144b3038d33 100644 --- a/lib/internal/Magento/Framework/GraphQl/Config/Element/FieldFactory.php +++ b/lib/internal/Magento/Framework/GraphQl/Config/Element/FieldFactory.php @@ -37,6 +37,7 @@ public function __construct( * @param array $fieldData * @param array $arguments * @return Field + * @SuppressWarnings(PHPMD.CyclomaticComplexity) */ public function createFromConfigData( array $fieldData, @@ -46,7 +47,7 @@ public function createFromConfigData( $isList = false; //check if type ends with [] - if ($fieldType{strlen($fieldType) - 2} == '[' && $fieldType{strlen($fieldType) - 1} == ']') { + if ($fieldType[strlen($fieldType) - 2] == '[' && $fieldType[strlen($fieldType) - 1] == ']') { $isList = true; $fieldData['type'] = str_replace('[]', '', $fieldData['type']); $fieldData['itemType'] = str_replace('[]', '', $fieldData['type']); @@ -62,8 +63,9 @@ public function createFromConfigData( 'itemType' => isset($fieldData['itemType']) ? $fieldData['itemType'] : '', 'resolver' => isset($fieldData['resolver']) ? $fieldData['resolver'] : '', 'description' => isset($fieldData['description']) ? $fieldData['description'] : '', - 'cache' => isset($fieldData['cache']) ? $fieldData['cache'] : [], 'arguments' => $arguments, + 'cache' => isset($fieldData['cache']) ? $fieldData['cache'] : [], + 'deprecated' => isset($fieldData['deprecated']) ? $fieldData['deprecated'] : [], ] ); } diff --git a/lib/internal/Magento/Framework/GraphQl/Config/Element/Input.php b/lib/internal/Magento/Framework/GraphQl/Config/Element/Input.php index 8e86f701672c6..3ebad28f7b308 100644 --- a/lib/internal/Magento/Framework/GraphQl/Config/Element/Input.php +++ b/lib/internal/Magento/Framework/GraphQl/Config/Element/Input.php @@ -27,19 +27,27 @@ class Input implements TypeInterface */ private $description; + /** + * @var array + */ + private $deprecated; + /** * @param string $name * @param Field[] $fields * @param string $description + * @param array $deprecated */ public function __construct( string $name, array $fields, - string $description + string $description, + array $deprecated = [] ) { $this->name = $name; $this->fields = $fields; $this->description = $description; + $this->deprecated = $deprecated; } /** @@ -71,4 +79,14 @@ public function getDescription(): string { return $this->description; } + + /** + * Return the deprecated annotation for the input + * + * @return array + */ + public function getDeprecated() : array + { + return $this->deprecated; + } } diff --git a/lib/internal/Magento/Framework/GraphQl/Config/Element/InputFactory.php b/lib/internal/Magento/Framework/GraphQl/Config/Element/InputFactory.php index 0e7ccb831a5a4..a23e83684d43c 100644 --- a/lib/internal/Magento/Framework/GraphQl/Config/Element/InputFactory.php +++ b/lib/internal/Magento/Framework/GraphQl/Config/Element/InputFactory.php @@ -72,7 +72,8 @@ private function create( [ 'name' => $typeData['name'], 'fields' => $fields, - 'description' => isset($typeData['description']) ? $typeData['description'] : '' + 'description' => isset($typeData['description']) ? $typeData['description'] : '', + 'deprecated' => isset($typeData['deprecated']) ? $typeData['deprecated'] : [] ] ); } diff --git a/lib/internal/Magento/Framework/GraphQl/Query/ErrorHandler.php b/lib/internal/Magento/Framework/GraphQl/Query/ErrorHandler.php new file mode 100644 index 0000000000000..2661034116f9d --- /dev/null +++ b/lib/internal/Magento/Framework/GraphQl/Query/ErrorHandler.php @@ -0,0 +1,48 @@ +logger = $logger; + } + + /** + * @inheritDoc + */ + public function handle(array $errors, callable $formatter): array + { + return array_map( + function (ClientAware $error) use ($formatter) { + $this->logger->error($error); + + return $formatter($error); + }, + $errors + ); + } +} diff --git a/lib/internal/Magento/Framework/GraphQl/Query/ErrorHandlerInterface.php b/lib/internal/Magento/Framework/GraphQl/Query/ErrorHandlerInterface.php new file mode 100644 index 0000000000000..09fd44c19315d --- /dev/null +++ b/lib/internal/Magento/Framework/GraphQl/Query/ErrorHandlerInterface.php @@ -0,0 +1,30 @@ +exceptionFormatter = $exceptionFormatter; $this->queryComplexityLimiter = $queryComplexityLimiter; + $this->errorHandler = $errorHandler; } /** @@ -67,6 +77,8 @@ public function process( $contextValue, $variableValues, $operationName + )->setErrorsHandler( + [$this->errorHandler, 'handle'] )->toArray( $this->exceptionFormatter->shouldShowDetail() ? \GraphQL\Error\Debug::INCLUDE_DEBUG_MESSAGE : false diff --git a/lib/internal/Magento/Framework/GraphQl/Query/Resolver/Argument/AstConverter.php b/lib/internal/Magento/Framework/GraphQl/Query/Resolver/Argument/AstConverter.php index 0b03fc509c786..baf165b0298c3 100644 --- a/lib/internal/Magento/Framework/GraphQl/Query/Resolver/Argument/AstConverter.php +++ b/lib/internal/Magento/Framework/GraphQl/Query/Resolver/Argument/AstConverter.php @@ -54,13 +54,15 @@ public function __construct( * @param string $fieldName * @param array $arguments * @return array + * @throws \LogicException */ public function getClausesFromAst(string $fieldName, array $arguments) : array { $attributes = $this->fieldEntityAttributesPool->getEntityAttributesForEntityFromField($fieldName); $conditions = []; foreach ($arguments as $argumentName => $argument) { - if (in_array($argumentName, $attributes)) { + if (key_exists($argumentName, $attributes)) { + $argumentName = $attributes[$argumentName]['fieldName'] ?? $argumentName; foreach ($argument as $clauseType => $clause) { if (is_array($clause)) { $value = []; @@ -76,12 +78,14 @@ public function getClausesFromAst(string $fieldName, array $arguments) : array $value ); } - } else { + } elseif (is_array($argument)) { $conditions[] = $this->connectiveFactory->create( $this->getClausesFromAst($fieldName, $argument), $argumentName ); + } else { + throw new \LogicException('Attribute not found in the visible attributes list'); } } return $conditions; diff --git a/lib/internal/Magento/Framework/GraphQl/Schema/Type/Enum/Enum.php b/lib/internal/Magento/Framework/GraphQl/Schema/Type/Enum/Enum.php index 6080fd0dd73e2..beb4b5a311c94 100644 --- a/lib/internal/Magento/Framework/GraphQl/Schema/Type/Enum/Enum.php +++ b/lib/internal/Magento/Framework/GraphQl/Schema/Type/Enum/Enum.php @@ -22,12 +22,13 @@ public function __construct(EnumElement $configElement) { $config = [ 'name' => $configElement->getName(), - 'description' => $configElement->getDescription(), + 'description' => $configElement->getDescription() ]; foreach ($configElement->getValues() as $value) { $config['values'][$value->getValue()] = [ 'value' => $value->getValue(), - 'description' => $value->getDescription() + 'description' => $value->getDescription(), + 'deprecationReason'=> $value->getDeprecatedReason() ]; } parent::__construct($config); diff --git a/lib/internal/Magento/Framework/GraphQl/Schema/Type/Output/ElementMapper/Formatter/Fields.php b/lib/internal/Magento/Framework/GraphQl/Schema/Type/Output/ElementMapper/Formatter/Fields.php index 034a5702090d9..e3f0945cb8dfd 100644 --- a/lib/internal/Magento/Framework/GraphQl/Schema/Type/Output/ElementMapper/Formatter/Fields.php +++ b/lib/internal/Magento/Framework/GraphQl/Schema/Type/Output/ElementMapper/Formatter/Fields.php @@ -142,6 +142,12 @@ private function getFieldConfig( $fieldConfig['description'] = $field->getDescription(); } + if (!empty($field->getDeprecated())) { + if (isset($field->getDeprecated()['reason'])) { + $fieldConfig['deprecationReason'] = $field->getDeprecated()['reason']; + } + } + if ($field->getResolver() != null) { /** @var ResolverInterface $resolver */ $resolver = $this->objectManager->get($field->getResolver()); diff --git a/lib/internal/Magento/Framework/GraphQl/Schema/Type/ResolveInfoFactory.php b/lib/internal/Magento/Framework/GraphQl/Schema/Type/ResolveInfoFactory.php index 1dde923bb2a98..335b991f693c8 100644 --- a/lib/internal/Magento/Framework/GraphQl/Schema/Type/ResolveInfoFactory.php +++ b/lib/internal/Magento/Framework/GraphQl/Schema/Type/ResolveInfoFactory.php @@ -7,11 +7,27 @@ namespace Magento\Framework\GraphQl\Schema\Type; +use Magento\Framework\ObjectManagerInterface; + /** * Factory for wrapper of GraphQl ResolveInfo */ class ResolveInfoFactory { + /** + * @var ObjectManagerInterface + */ + private $objectManager; + + /** + * @param ObjectManagerInterface $objectManager + */ + public function __construct( + ObjectManagerInterface $objectManager + ) { + $this->objectManager = $objectManager; + } + /** * Create a wrapper resolver info from the instance of the library object * @@ -25,6 +41,6 @@ public function create(\GraphQL\Type\Definition\ResolveInfo $info) : ResolveInfo $values[$key] = $value; } - return new ResolveInfo($values); + return $this->objectManager->create(ResolveInfo::class, $values); } } diff --git a/lib/internal/Magento/Framework/GraphQl/Schema/Type/ScalarTypes.php b/lib/internal/Magento/Framework/GraphQl/Schema/Type/ScalarTypes.php index 508cada1a6958..dfb8b748469b8 100644 --- a/lib/internal/Magento/Framework/GraphQl/Schema/Type/ScalarTypes.php +++ b/lib/internal/Magento/Framework/GraphQl/Schema/Type/ScalarTypes.php @@ -13,25 +13,29 @@ class ScalarTypes { /** + * Check if type is scalar + * * @param string $typeName * @return bool */ public function isScalarType(string $typeName) : bool { - $internalTypes = \GraphQL\Type\Definition\Type::getInternalTypes(); - return isset($internalTypes[$typeName]) ? true : false; + $standardTypes = \GraphQL\Type\Definition\Type::getStandardTypes(); + return isset($standardTypes[$typeName]) ? true : false; } /** + * Get instance of scalar type + * * @param string $typeName * @return \GraphQL\Type\Definition\ScalarType|\GraphQL\Type\Definition\Type * @throws \LogicException */ public function getScalarTypeInstance(string $typeName) : \GraphQL\Type\Definition\Type { - $internalTypes = \GraphQL\Type\Definition\Type::getInternalTypes(); + $standardTypes = \GraphQL\Type\Definition\Type::getStandardTypes(); if ($this->isScalarType($typeName)) { - return $internalTypes[$typeName]; + return $standardTypes[$typeName]; } else { throw new \LogicException(sprintf('Scalar type %s doesn\'t exist', $typeName)); } diff --git a/lib/internal/Magento/Framework/GraphQlSchemaStitching/GraphQlReader/MetaReader/DeprecatedAnnotationReader.php b/lib/internal/Magento/Framework/GraphQlSchemaStitching/GraphQlReader/MetaReader/DeprecatedAnnotationReader.php new file mode 100644 index 0000000000000..922ae87ecf449 --- /dev/null +++ b/lib/internal/Magento/Framework/GraphQlSchemaStitching/GraphQlReader/MetaReader/DeprecatedAnnotationReader.php @@ -0,0 +1,35 @@ +name->value == 'deprecated') { + foreach ($directive->arguments as $directiveArgument) { + if ($directiveArgument->name->value == 'reason') { + $argumentsMap = ["reason" => $directiveArgument->value->value]; + } + } + } + } + return $argumentsMap; + } +} diff --git a/lib/internal/Magento/Framework/GraphQlSchemaStitching/GraphQlReader/MetaReader/FieldMetaReader.php b/lib/internal/Magento/Framework/GraphQlSchemaStitching/GraphQlReader/MetaReader/FieldMetaReader.php index 7438a4e3da932..217a233eae20c 100644 --- a/lib/internal/Magento/Framework/GraphQlSchemaStitching/GraphQlReader/MetaReader/FieldMetaReader.php +++ b/lib/internal/Magento/Framework/GraphQlSchemaStitching/GraphQlReader/MetaReader/FieldMetaReader.php @@ -27,20 +27,29 @@ class FieldMetaReader */ private $cacheAnnotationReader; + /** + * @var DeprecatedAnnotationReader + */ + private $deprecatedAnnotationReader; + /** * @param TypeMetaWrapperReader $typeMetaReader * @param DocReader $docReader * @param CacheAnnotationReader|null $cacheAnnotationReader + * @param DeprecatedAnnotationReader|null $deprecatedAnnotationReader */ public function __construct( TypeMetaWrapperReader $typeMetaReader, DocReader $docReader, - CacheAnnotationReader $cacheAnnotationReader = null + CacheAnnotationReader $cacheAnnotationReader = null, + DeprecatedAnnotationReader $deprecatedAnnotationReader = null ) { $this->typeMetaReader = $typeMetaReader; $this->docReader = $docReader; - $this->cacheAnnotationReader = $cacheAnnotationReader ?? \Magento\Framework\App\ObjectManager::getInstance() - ->get(CacheAnnotationReader::class); + $this->cacheAnnotationReader = $cacheAnnotationReader + ?? \Magento\Framework\App\ObjectManager::getInstance()->get(CacheAnnotationReader::class); + $this->deprecatedAnnotationReader = $deprecatedAnnotationReader + ?? \Magento\Framework\App\ObjectManager::getInstance()->get(DeprecatedAnnotationReader::class); } /** @@ -72,10 +81,14 @@ public function read(\GraphQL\Type\Definition\FieldDefinition $fieldMeta) : arra $result['description'] = $this->docReader->read($fieldMeta->astNode->directives); } - if ($this->docReader->read($fieldMeta->astNode->directives)) { + if ($this->cacheAnnotationReader->read($fieldMeta->astNode->directives)) { $result['cache'] = $this->cacheAnnotationReader->read($fieldMeta->astNode->directives); } + if ($this->deprecatedAnnotationReader->read($fieldMeta->astNode->directives)) { + $result['deprecated'] = $this->deprecatedAnnotationReader->read($fieldMeta->astNode->directives); + } + $arguments = $fieldMeta->args; foreach ($arguments as $argumentMeta) { $argumentName = $argumentMeta->name; @@ -86,19 +99,43 @@ public function read(\GraphQL\Type\Definition\FieldDefinition $fieldMeta) : arra $result['arguments'][$argumentName]['defaultValue'] = $argumentMeta->defaultValue; } $typeMeta = $argumentMeta->getType(); - $result['arguments'][$argumentName] = array_merge( - $result['arguments'][$argumentName], - $this->typeMetaReader->read($typeMeta, TypeMetaWrapperReader::ARGUMENT_PARAMETER) - ); + $result['arguments'][$argumentName] = $this->argumentMetaType($typeMeta, $argumentMeta, $result); if ($this->docReader->read($argumentMeta->astNode->directives)) { $result['arguments'][$argumentName]['description'] = $this->docReader->read($argumentMeta->astNode->directives); } + + if ($this->deprecatedAnnotationReader->read($argumentMeta->astNode->directives)) { + $result['arguments'][$argumentName]['deprecated'] = + $this->deprecatedAnnotationReader->read($argumentMeta->astNode->directives); + } } return $result; } + /** + * Get the argumentMetaType result array + * + * @param \GraphQL\Type\Definition\InputType $typeMeta + * @param \GraphQL\Type\Definition\FieldArgument $argumentMeta + * @param array $result + * @return array + */ + private function argumentMetaType( + \GraphQL\Type\Definition\InputType $typeMeta, + \GraphQL\Type\Definition\FieldArgument $argumentMeta, + $result + ) : array { + $argumentName = $argumentMeta->name; + $result['arguments'][$argumentName] = array_merge( + $result['arguments'][$argumentName], + $this->typeMetaReader->read($typeMeta, TypeMetaWrapperReader::ARGUMENT_PARAMETER) + ); + + return $result['arguments'][$argumentName]; + } + /** * Read resolver if an annotation with the class of the resolver is defined in the meta * diff --git a/lib/internal/Magento/Framework/GraphQlSchemaStitching/GraphQlReader/Reader/EnumType.php b/lib/internal/Magento/Framework/GraphQlSchemaStitching/GraphQlReader/Reader/EnumType.php index 3e9e819078db8..e4dec7afdab0a 100644 --- a/lib/internal/Magento/Framework/GraphQlSchemaStitching/GraphQlReader/Reader/EnumType.php +++ b/lib/internal/Magento/Framework/GraphQlSchemaStitching/GraphQlReader/Reader/EnumType.php @@ -23,13 +23,14 @@ class EnumType implements TypeMetaReaderInterface /** * @param DocReader $docReader */ - public function __construct(DocReader $docReader) - { + public function __construct( + DocReader $docReader + ) { $this->docReader = $docReader; } /** - * {@inheritdoc} + * @inheritdoc */ public function read(\GraphQL\Type\Definition\Type $typeMeta) : array { @@ -42,7 +43,9 @@ public function read(\GraphQL\Type\Definition\Type $typeMeta) : array foreach ($typeMeta->getValues() as $enumValueMeta) { $result['items'][$enumValueMeta->value] = [ 'name' => strtolower($enumValueMeta->name), - '_value' => $enumValueMeta->value + '_value' => $enumValueMeta->value, + 'description' => $enumValueMeta->description, + 'deprecationReason' =>$enumValueMeta->deprecationReason ]; if ($this->docReader->read($enumValueMeta->astNode->directives)) { @@ -56,6 +59,7 @@ public function read(\GraphQL\Type\Definition\Type $typeMeta) : array } return $result; + } else { return []; } diff --git a/lib/internal/Magento/Framework/GraphQlSchemaStitching/GraphQlReader/Reader/ObjectType.php b/lib/internal/Magento/Framework/GraphQlSchemaStitching/GraphQlReader/Reader/ObjectType.php index 7614c4954091d..ba8e46dd60557 100644 --- a/lib/internal/Magento/Framework/GraphQlSchemaStitching/GraphQlReader/Reader/ObjectType.php +++ b/lib/internal/Magento/Framework/GraphQlSchemaStitching/GraphQlReader/Reader/ObjectType.php @@ -12,6 +12,7 @@ use Magento\Framework\GraphQlSchemaStitching\GraphQlReader\MetaReader\DocReader; use Magento\Framework\GraphQlSchemaStitching\GraphQlReader\MetaReader\ImplementsReader; use Magento\Framework\GraphQlSchemaStitching\GraphQlReader\MetaReader\CacheAnnotationReader; +use Magento\Framework\GraphQlSchemaStitching\GraphQlReader\MetaReader\DeprecatedAnnotationReader; /** * Composite configuration reader to handle the object type meta @@ -38,24 +39,33 @@ class ObjectType implements TypeMetaReaderInterface */ private $cacheAnnotationReader; + /** + * @var DeprecatedAnnotationReader + */ + private $deprecatedAnnotationReader; + /** * ObjectType constructor. * @param FieldMetaReader $fieldMetaReader * @param DocReader $docReader * @param ImplementsReader $implementsAnnotation * @param CacheAnnotationReader|null $cacheAnnotationReader + * @param DeprecatedAnnotationReader|null $deprecatedAnnotationReader */ public function __construct( FieldMetaReader $fieldMetaReader, DocReader $docReader, ImplementsReader $implementsAnnotation, - CacheAnnotationReader $cacheAnnotationReader = null + CacheAnnotationReader $cacheAnnotationReader = null, + DeprecatedAnnotationReader $deprecatedAnnotationReader = null ) { $this->fieldMetaReader = $fieldMetaReader; $this->docReader = $docReader; $this->implementsAnnotation = $implementsAnnotation; $this->cacheAnnotationReader = $cacheAnnotationReader ?? \Magento\Framework\App\ObjectManager::getInstance() ->get(CacheAnnotationReader::class); + $this->deprecatedAnnotationReader = $deprecatedAnnotationReader + ?? \Magento\Framework\App\ObjectManager::getInstance()->get(DeprecatedAnnotationReader::class); } /** @@ -85,13 +95,17 @@ public function read(\GraphQL\Type\Definition\Type $typeMeta) : array } if ($this->docReader->read($typeMeta->astNode->directives)) { - $result['description'] = $this->docReader->read($typeMeta->astNode->directives); + $result['description'] = $this->docReader->read($typeMeta->astNode->directives); } - if ($this->docReader->read($typeMeta->astNode->directives)) { + if ($this->cacheAnnotationReader->read($typeMeta->astNode->directives)) { $result['cache'] = $this->cacheAnnotationReader->read($typeMeta->astNode->directives); } + if ($this->deprecatedAnnotationReader->read($typeMeta->astNode->directives)) { + $result['deprecated'] = $this->deprecatedAnnotationReader->read($typeMeta->astNode->directives); + } + return $result; } else { return []; diff --git a/lib/internal/Magento/Framework/Indexer/etc/indexer.xsd b/lib/internal/Magento/Framework/Indexer/etc/indexer.xsd index 6a06a4d55ee35..fc38a5206e855 100644 --- a/lib/internal/Magento/Framework/Indexer/etc/indexer.xsd +++ b/lib/internal/Magento/Framework/Indexer/etc/indexer.xsd @@ -67,11 +67,11 @@ - Class name can contain only [a-zA-Z\]. + Class name can contain only [a-zA-Z|\\]+[a-zA-Z0-9\\]+. - + diff --git a/lib/internal/Magento/Framework/Interception/Code/Generator/Interceptor.php b/lib/internal/Magento/Framework/Interception/Code/Generator/Interceptor.php index 9297ca25928d3..69a0d3029e18d 100644 --- a/lib/internal/Magento/Framework/Interception/Code/Generator/Interceptor.php +++ b/lib/internal/Magento/Framework/Interception/Code/Generator/Interceptor.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); namespace Magento\Framework\Interception\Code\Generator; @@ -58,6 +59,7 @@ protected function _getDefaultConstructorDefinition() ? "parent::__construct({$this->_getParameterList($parameters)});" : "parent::__construct();"; } + return [ 'name' => '__construct', 'parameters' => $parameters, @@ -81,6 +83,7 @@ protected function _getClassMethods() $methods[] = $this->_getMethodInfo($method); } } + return $methods; } @@ -109,7 +112,7 @@ protected function _getMethodInfo(\ReflectionMethod $method) $parameters[] = $this->_getMethodParameterInfo($parameter); } - $returnTypeValue = $this->getReturnTypeValue($method->getReturnType()); + $returnTypeValue = $this->getReturnTypeValue($method); $methodInfo = [ 'name' => ($method->returnsReference() ? '& ' : '') . $method->getName(), 'parameters' => $parameters, @@ -133,8 +136,8 @@ protected function _getMethodInfo(\ReflectionMethod $method) } METHOD_BODY ), - 'returnType' => $returnTypeValue, - 'docblock' => ['shortDescription' => '{@inheritdoc}'], + 'returnType' => $returnTypeValue, + 'docblock' => ['shortDescription' => '{@inheritdoc}'], ]; return $methodInfo; @@ -184,6 +187,7 @@ protected function _generateCode() $this->_classGenerator->addTrait('\\' . \Magento\Framework\Interception\Interceptor::class); $interfaces[] = '\\' . \Magento\Framework\Interception\InterceptorInterface::class; $this->_classGenerator->setImplementedInterfaces($interfaces); + return parent::_generateCode(); } @@ -211,24 +215,27 @@ protected function _validateData() $result = false; } } + return $result; } /** * Returns return type * - * @param mixed $returnType + * @param \ReflectionMethod $method * @return null|string */ - private function getReturnTypeValue($returnType): ?string + private function getReturnTypeValue(\ReflectionMethod $method): ?string { $returnTypeValue = null; + $returnType = $method->getReturnType(); if ($returnType) { $returnTypeValue = ($returnType->allowsNull() ? '?' : ''); $returnTypeValue .= ($returnType->getName() === 'self') - ? $this->getSourceClassName() + ? $this->_getFullyQualifiedClassName($method->getDeclaringClass()->getName()) : $returnType->getName(); } + return $returnTypeValue; } } diff --git a/lib/internal/Magento/Framework/Locale/Test/Unit/TranslatedListsTest.php b/lib/internal/Magento/Framework/Locale/Test/Unit/TranslatedListsTest.php index bac6025c5f1d5..9e247a8e21ac6 100644 --- a/lib/internal/Magento/Framework/Locale/Test/Unit/TranslatedListsTest.php +++ b/lib/internal/Magento/Framework/Locale/Test/Unit/TranslatedListsTest.php @@ -6,36 +6,42 @@ namespace Magento\Framework\Locale\Test\Unit; -class TranslatedListsTest extends \PHPUnit\Framework\TestCase +use Magento\Framework\Locale\ConfigInterface; +use Magento\Framework\Locale\ResolverInterface; +use Magento\Framework\Locale\TranslatedLists; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +class TranslatedListsTest extends TestCase { /** - * @var \Magento\Framework\Locale\TranslatedLists + * @var TranslatedLists */ protected $listsModel; /** - * @var \PHPUnit_Framework_MockObject_MockObject | \Magento\Framework\Locale\ConfigInterface + * @var MockObject | ConfigInterface */ protected $mockConfig; /** - * @var \PHPUnit_Framework_MockObject_MockObject | \Magento\Framework\Locale\ResolverInterface + * @var MockObject | ResolverInterface */ protected $mockLocaleResolver; protected function setUp() { - $this->mockConfig = $this->getMockBuilder(\Magento\Framework\Locale\ConfigInterface::class) + $this->mockConfig = $this->getMockBuilder(ConfigInterface::class) ->disableOriginalConstructor() ->getMock(); - $this->mockLocaleResolver = $this->getMockBuilder(\Magento\Framework\Locale\ResolverInterface::class) + $this->mockLocaleResolver = $this->getMockBuilder(ResolverInterface::class) ->disableOriginalConstructor() ->getMock(); $this->mockLocaleResolver->expects($this->once()) ->method('getLocale') - ->will($this->returnValue('en_US')); + ->willReturn('en_US'); - $this->listsModel = new \Magento\Framework\Locale\TranslatedLists( + $this->listsModel = new TranslatedLists( $this->mockConfig, $this->mockLocaleResolver ); @@ -67,13 +73,13 @@ public function testGetOptionCurrencies() $this->mockConfig->expects($this->once()) ->method('getAllowedCurrencies') - ->will($this->returnValue($allowedCurrencies)); + ->willReturn($allowedCurrencies); $expectedResults = ['USD', 'EUR', 'GBP', 'UAH']; $currencyList = $this->listsModel->getOptionCurrencies(); $currencyCodes = array_map( - function ($data) { + static function ($data) { return $data['value']; }, $currencyList @@ -166,6 +172,6 @@ protected function setupForOptionLocales() $allowedLocales = ['en_US', 'uk_UA', 'de_DE']; $this->mockConfig->expects($this->once()) ->method('getAllowedLocales') - ->will($this->returnValue($allowedLocales)); + ->willReturn($allowedLocales); } } diff --git a/lib/internal/Magento/Framework/Locale/TranslatedLists.php b/lib/internal/Magento/Framework/Locale/TranslatedLists.php index f404e897790f2..2087564dcec20 100644 --- a/lib/internal/Magento/Framework/Locale/TranslatedLists.php +++ b/lib/internal/Magento/Framework/Locale/TranslatedLists.php @@ -11,6 +11,9 @@ use Magento\Framework\Locale\Bundle\LanguageBundle; use Magento\Framework\Locale\Bundle\RegionBundle; +/** + * Translated lists. + */ class TranslatedLists implements ListsInterface { /** @@ -176,6 +179,8 @@ public function getOptionAllCurrencies() } /** + * Sort option array. + * * @param array $option * @return array */ @@ -199,9 +204,11 @@ protected function _sortOptionArray($option) public function getCountryTranslation($value, $locale = null) { if ($locale == null) { - return (new RegionBundle())->get($this->localeResolver->getLocale())['Countries'][$value]; - } else { - return (new RegionBundle())->get($locale)['Countries'][$value]; + $locale = $this->localeResolver->getLocale(); } + + $translation = (new RegionBundle())->get($locale)['Countries'][$value]; + + return $translation ? (string)__($translation) : $translation; } } diff --git a/lib/internal/Magento/Framework/MessageQueue/CallbackInvoker.php b/lib/internal/Magento/Framework/MessageQueue/CallbackInvoker.php index fe0a84af3ca93..559959b55fc61 100644 --- a/lib/internal/Magento/Framework/MessageQueue/CallbackInvoker.php +++ b/lib/internal/Magento/Framework/MessageQueue/CallbackInvoker.php @@ -8,6 +8,7 @@ use Magento\Framework\MessageQueue\PoisonPill\PoisonPillCompareInterface; use Magento\Framework\MessageQueue\PoisonPill\PoisonPillReadInterface; +use Magento\Framework\App\DeploymentConfig; /** * Class CallbackInvoker to invoke callbacks for consumer classes @@ -29,16 +30,24 @@ class CallbackInvoker implements CallbackInvokerInterface */ private $poisonPillCompare; + /** + * @var DeploymentConfig + */ + private $deploymentConfig; + /** * @param PoisonPillReadInterface $poisonPillRead * @param PoisonPillCompareInterface $poisonPillCompare + * @param DeploymentConfig $deploymentConfig */ public function __construct( PoisonPillReadInterface $poisonPillRead, - PoisonPillCompareInterface $poisonPillCompare + PoisonPillCompareInterface $poisonPillCompare, + DeploymentConfig $deploymentConfig ) { $this->poisonPillRead = $poisonPillRead; $this->poisonPillCompare = $poisonPillCompare; + $this->deploymentConfig = $deploymentConfig; } /** @@ -56,13 +65,29 @@ public function invoke(QueueInterface $queue, $maxNumberOfMessages, $callback) do { $message = $queue->dequeue(); // phpcs:ignore Magento2.Functions.DiscouragedFunction - } while ($message === null && (sleep(1) === 0)); + } while ($message === null && $this->isWaitingNextMessage() && (sleep(1) === 0)); + + if ($message === null) { + break; + } + if (false === $this->poisonPillCompare->isLatestVersion($this->poisonPillVersion)) { $queue->reject($message); // phpcs:ignore Magento2.Security.LanguageConstruct.ExitUsage exit(0); } + $callback($message); } } + + /** + * Checks if consumers should wait for message from the queue + * + * @return bool + */ + private function isWaitingNextMessage(): bool + { + return $this->deploymentConfig->get('queue/consumers_wait_for_messages', 1) === 1; + } } diff --git a/lib/internal/Magento/Framework/MessageQueue/Test/Unit/ConsumerTest.php b/lib/internal/Magento/Framework/MessageQueue/Test/Unit/ConsumerTest.php index 7a3eb3b16baca..e7ee0e19a1d43 100644 --- a/lib/internal/Magento/Framework/MessageQueue/Test/Unit/ConsumerTest.php +++ b/lib/internal/Magento/Framework/MessageQueue/Test/Unit/ConsumerTest.php @@ -77,6 +77,11 @@ class ConsumerTest extends \PHPUnit\Framework\TestCase */ private $poisonPillCompare; + /** + * @var \Magento\Framework\App\DeploymentConfig|\PHPUnit_Framework_MockObject_MockObject + */ + private $deploymentConfig; + /** * Set up. * @@ -95,6 +100,7 @@ protected function setUp() ->disableOriginalConstructor()->getMock(); $this->logger = $this->getMockBuilder(\Psr\Log\LoggerInterface::class) ->disableOriginalConstructor()->getMock(); + $this->deploymentConfig = $this->createMock(\Magento\Framework\App\DeploymentConfig::class); $objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); $this->poisonPillCompare = $this->getMockBuilder(PoisonPillCompareInterface::class) @@ -104,7 +110,8 @@ protected function setUp() //Hard dependency used because CallbackInvoker invokes closure logic defined inside of Customer class. $this->callbackInvoker = new \Magento\Framework\MessageQueue\CallbackInvoker( $this->poisonPillRead, - $this->poisonPillCompare + $this->poisonPillCompare, + $this->deploymentConfig ); $this->consumer = $objectManager->getObject( \Magento\Framework\MessageQueue\Consumer::class, diff --git a/lib/internal/Magento/Framework/MessageQueue/etc/queue.xsd b/lib/internal/Magento/Framework/MessageQueue/etc/queue.xsd index 7c018403d176d..d27c2cd8b7025 100644 --- a/lib/internal/Magento/Framework/MessageQueue/etc/queue.xsd +++ b/lib/internal/Magento/Framework/MessageQueue/etc/queue.xsd @@ -11,6 +11,7 @@ @deprecated + Deprecated for RabbitMQ connection. diff --git a/lib/internal/Magento/Framework/Model/EntitySnapshot.php b/lib/internal/Magento/Framework/Model/EntitySnapshot.php index 99f0c7f4ed42d..6ae7cb346a7df 100644 --- a/lib/internal/Magento/Framework/Model/EntitySnapshot.php +++ b/lib/internal/Magento/Framework/Model/EntitySnapshot.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); namespace Magento\Framework\Model; @@ -44,6 +45,8 @@ public function __construct( } /** + * Register snapshot of entity data. + * * @param string $entityType * @param object $entity * @return void @@ -55,7 +58,7 @@ public function registerSnapshot($entityType, $entity) $entityData = $hydrator->extract($entity); $attributes = $this->attributeProvider->getAttributes($entityType); $this->snapshotData[$entityType][$entityData[$metadata->getIdentifierField()]] - = array_intersect_key($entityData, $attributes); + = array_intersect(\array_keys($entityData), $attributes); } /** diff --git a/lib/internal/Magento/Framework/Model/EntitySnapshot/AttributeProvider.php b/lib/internal/Magento/Framework/Model/EntitySnapshot/AttributeProvider.php index beb0b2784f5fd..6b7fcd131ba8b 100644 --- a/lib/internal/Magento/Framework/Model/EntitySnapshot/AttributeProvider.php +++ b/lib/internal/Magento/Framework/Model/EntitySnapshot/AttributeProvider.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); namespace Magento\Framework\Model\EntitySnapshot; @@ -53,31 +54,29 @@ public function __construct( * Returns array of fields * * @param string $entityType - * @return array + * @return string[] * @throws \Exception */ public function getAttributes($entityType) { if (!isset($this->registry[$entityType])) { $metadata = $this->metadataPool->getMetadata($entityType); - $this->registry[$entityType] = $metadata->getEntityConnection()->describeTable($metadata->getEntityTable()); - if ($metadata->getLinkField() != $metadata->getIdentifierField()) { - unset($this->registry[$entityType][$metadata->getLinkField()]); - } - $providers = []; - if (isset($this->providers[$entityType])) { - $providers = $this->providers[$entityType]; - } elseif (isset($this->providers['default'])) { - $providers = $this->providers['default']; + $entityDescription = $metadata->getEntityConnection()->describeTable($metadata->getEntityTable()); + if ($metadata->getLinkField() !== $metadata->getIdentifierField()) { + unset($entityDescription[$metadata->getLinkField()]); } + $attributes = []; + $attributes[] = \array_keys($entityDescription); + + $providers = $this->providers[$entityType] ?? $this->providers['default'] ?? []; foreach ($providers as $providerClass) { $provider = $this->objectManager->get($providerClass); - $this->registry[$entityType] = array_merge( - $this->registry[$entityType], - $provider->getAttributes($entityType) - ); + $attributes[] = $provider->getAttributes($entityType); } + + $this->registry[$entityType] = \array_merge(...$attributes); } + return $this->registry[$entityType]; } } diff --git a/lib/internal/Magento/Framework/Model/EntitySnapshot/AttributeProviderInterface.php b/lib/internal/Magento/Framework/Model/EntitySnapshot/AttributeProviderInterface.php index f71f9e591630f..ad09e2ef5a479 100644 --- a/lib/internal/Magento/Framework/Model/EntitySnapshot/AttributeProviderInterface.php +++ b/lib/internal/Magento/Framework/Model/EntitySnapshot/AttributeProviderInterface.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); namespace Magento\Framework\Model\EntitySnapshot; @@ -12,8 +13,10 @@ interface AttributeProviderInterface { /** + * Returns array of fields + * * @param string $entityType - * @return array + * @return string[] */ public function getAttributes($entityType); } diff --git a/lib/internal/Magento/Framework/Model/ExecuteCommitCallbacks.php b/lib/internal/Magento/Framework/Model/ExecuteCommitCallbacks.php new file mode 100644 index 0000000000000..799f8ffda253c --- /dev/null +++ b/lib/internal/Magento/Framework/Model/ExecuteCommitCallbacks.php @@ -0,0 +1,67 @@ +logger = $logger; + } + + /** + * Execute callbacks after commit. + * + * @param AdapterInterface $subject + * @param AdapterInterface $result + * @return AdapterInterface + */ + public function afterCommit(AdapterInterface $subject, AdapterInterface $result): AdapterInterface + { + if ($result->getTransactionLevel() === 0) { + $callbacks = CallbackPool::get(spl_object_hash($subject)); + foreach ($callbacks as $callback) { + try { + call_user_func($callback); + } catch (\Throwable $e) { + $this->logger->critical($e); + } + } + } + + return $result; + } + + /** + * Drop callbacks after rollBack. + * + * @param AdapterInterface $subject + * @param AdapterInterface $result + * @return AdapterInterface + */ + public function afterRollBack(AdapterInterface $subject, AdapterInterface $result): AdapterInterface + { + CallbackPool::clear(spl_object_hash($subject)); + + return $result; + } +} diff --git a/lib/internal/Magento/Framework/Mview/View/Subscription.php b/lib/internal/Magento/Framework/Mview/View/Subscription.php index 67dff1a2cc5db..ddfa39f0a089f 100644 --- a/lib/internal/Magento/Framework/Mview/View/Subscription.php +++ b/lib/internal/Magento/Framework/Mview/View/Subscription.php @@ -214,7 +214,7 @@ protected function buildStatement($event, $changelog) $columns = []; foreach ($columnNames as $columnName) { $columns[] = sprintf( - 'NEW.%1$s <=> OLD.%1$s', + 'NOT(NEW.%1$s <=> OLD.%1$s)', $this->connection->quoteIdentifier($columnName) ); } diff --git a/lib/internal/Magento/Framework/Mview/etc/mview.xsd b/lib/internal/Magento/Framework/Mview/etc/mview.xsd index e2f185041962a..dfff4964f6587 100644 --- a/lib/internal/Magento/Framework/Mview/etc/mview.xsd +++ b/lib/internal/Magento/Framework/Mview/etc/mview.xsd @@ -51,11 +51,11 @@ - Class name can contain only [a-zA-Z\]. + Class name can contain only [a-zA-Z|\\]+[a-zA-Z0-9\\]+. - + diff --git a/lib/internal/Magento/Framework/ObjectManager/Code/Generator/Proxy.php b/lib/internal/Magento/Framework/ObjectManager/Code/Generator/Proxy.php index efa1b4be60ced..a45795e5df16b 100644 --- a/lib/internal/Magento/Framework/ObjectManager/Code/Generator/Proxy.php +++ b/lib/internal/Magento/Framework/ObjectManager/Code/Generator/Proxy.php @@ -5,6 +5,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); namespace Magento\Framework\ObjectManager\Code\Generator; @@ -73,6 +74,7 @@ protected function _getClassProperties() 'tags' => [['name' => 'var', 'description' => 'bool']], ], ]; + return $properties; } @@ -154,6 +156,7 @@ protected function _generateCode() $this->_classGenerator->setExtendedClass($typeName); $this->_classGenerator->setImplementedInterfaces(['\\' . self::NON_INTERCEPTABLE_INTERFACE]); } + return parent::_generateCode(); } @@ -173,7 +176,7 @@ protected function _getMethodInfo(\ReflectionMethod $method) $parameters[] = $this->_getMethodParameterInfo($parameter); } - $returnTypeValue = $this->getReturnTypeValue($method->getReturnType()); + $returnTypeValue = $this->getReturnTypeValue($method); $methodInfo = [ 'name' => $method->getName(), 'parameters' => $parameters, @@ -269,24 +272,27 @@ protected function _validateData() $result = false; } } + return $result; } /** * Returns return type * - * @param mixed $returnType + * @param \ReflectionMethod $method * @return null|string */ - private function getReturnTypeValue($returnType): ?string + private function getReturnTypeValue(\ReflectionMethod $method): ?string { $returnTypeValue = null; + $returnType = $method->getReturnType(); if ($returnType) { $returnTypeValue = ($returnType->allowsNull() ? '?' : ''); $returnTypeValue .= ($returnType->getName() === 'self') - ? $this->getSourceClassName() + ? $this->_getFullyQualifiedClassName($method->getDeclaringClass()->getName()) : $returnType->getName(); } + return $returnTypeValue; } } diff --git a/lib/internal/Magento/Framework/Phrase/Renderer/MessageFormatter.php b/lib/internal/Magento/Framework/Phrase/Renderer/MessageFormatter.php new file mode 100644 index 0000000000000..13d8c85efe069 --- /dev/null +++ b/lib/internal/Magento/Framework/Phrase/Renderer/MessageFormatter.php @@ -0,0 +1,46 @@ +translate = $translate; + } + + /** + * @inheritdoc + */ + public function render(array $source, array $arguments) + { + $text = end($source); + + if (strpos($text, '{') === false) { + // About 5x faster for non-MessageFormatted strings + // Only slightly slower for MessageFormatted strings (~0.3x) + return $text; + } + + $result = \MessageFormatter::formatMessage($this->translate->getLocale(), $text, $arguments); + return $result !== false ? $result : $text; + } +} diff --git a/lib/internal/Magento/Framework/Phrase/Test/Unit/Renderer/MessageFormatterTest.php b/lib/internal/Magento/Framework/Phrase/Test/Unit/Renderer/MessageFormatterTest.php new file mode 100644 index 0000000000000..aa71d801f35b6 --- /dev/null +++ b/lib/internal/Magento/Framework/Phrase/Test/Unit/Renderer/MessageFormatterTest.php @@ -0,0 +1,115 @@ +objectManager = new ObjectManager($this); + } + + /** + * Retrieve test cases + * + * @return array [Raw Phrase, Locale, Arguments, Expected Result] + * @throws \Exception + */ + public function renderMessageFormatterDataProvider(): array + { + $twentynineteenJuneTwentyseven = new \DateTime('2019-06-27'); + + return [ + [ + 'A table has {legs, plural, =0 {no legs} =1 {one leg} other {# legs}}.', + 'en_US', + ['legs' => 4], + 'A table has 4 legs.' + ], + [ + 'A table has {legs, plural, =0 {no legs} =1 {one leg} other {# legs}}.', + 'en_US', + ['legs' => 0], + 'A table has no legs.' + ], + [ + 'A table has {legs, plural, =0 {no legs} =1 {one leg} other {# legs}}.', + 'en_US', + ['legs' => 1], + 'A table has one leg.' + ], + ['The table costs {price, number, currency}.', 'en_US', ['price' => 23.4], 'The table costs $23.40.'], + [ + 'Today is {date, date, long}.', + 'en_US', + ['date' => $twentynineteenJuneTwentyseven], + 'Today is June 27, 2019.' + ], + [ + 'Today is {date, date, long}.', + 'ja_JP', + ['date' => $twentynineteenJuneTwentyseven], + 'Today is 2019年6月27日.' + ], + ]; + } + + /** + * Test MessageFormatter + * + * @param string $text The text with MessageFormat markers + * @param string $locale + * @param array $arguments The arguments supplying values for the variables + * @param string $result The expected result of Phrase rendering + * + * @dataProvider renderMessageFormatterDataProvider + */ + public function testRenderMessageFormatter(string $text, string $locale, array $arguments, string $result): void + { + $renderer = $this->getMessageFormatter($locale); + + $this->assertEquals($result, $renderer->render([$text], $arguments)); + } + + /** + * Create a MessageFormatter object provided a locale + * + * Automatically sets up the Translate dependency to return the provided locale and returns a MessageFormatter + * that has been provided that dependency + * + * @param string $locale + * @return MessageFormatter + */ + private function getMessageFormatter(string $locale): MessageFormatter + { + $translateMock = $this->getMockBuilder(Translate::class) + ->disableOriginalConstructor() + ->setMethods(['getLocale']) + ->getMock(); + $translateMock->method('getLocale') + ->willReturn($locale); + + return $this->objectManager->getObject(MessageFormatter::class, ['translate' => $translateMock]); + } +} diff --git a/lib/internal/Magento/Framework/Phrase/__.php b/lib/internal/Magento/Framework/Phrase/__.php index 6f3186231e3bb..0f828acd828b5 100644 --- a/lib/internal/Magento/Framework/Phrase/__.php +++ b/lib/internal/Magento/Framework/Phrase/__.php @@ -7,13 +7,14 @@ /** * Create value-object \Magento\Framework\Phrase + * * @SuppressWarnings(PHPMD.ShortMethodName) + * phpcs:disable Squiz.Functions.GlobalFunction + * @param array $argc * @return \Magento\Framework\Phrase */ -function __() +function __(...$argc) { - $argc = func_get_args(); - $text = array_shift($argc); if (!empty($argc) && is_array($argc[0])) { $argc = $argc[0]; diff --git a/lib/internal/Magento/Framework/Search/Adapter/Mysql/TemporaryStorage.php b/lib/internal/Magento/Framework/Search/Adapter/Mysql/TemporaryStorage.php index 60ee2d5706067..7f8ef8c422b92 100644 --- a/lib/internal/Magento/Framework/Search/Adapter/Mysql/TemporaryStorage.php +++ b/lib/internal/Magento/Framework/Search/Adapter/Mysql/TemporaryStorage.php @@ -152,7 +152,7 @@ private function createTemporaryTable() self::FIELD_SCORE, Table::TYPE_DECIMAL, [32, 16], - ['unsigned' => true, 'nullable' => false], + ['unsigned' => true, 'nullable' => true], 'Score' ); $table->setOption('type', 'memory'); diff --git a/lib/internal/Magento/Framework/Search/Request/Builder.php b/lib/internal/Magento/Framework/Search/Request/Builder.php index 74bc65010a934..0cf959b657c76 100644 --- a/lib/internal/Magento/Framework/Search/Request/Builder.php +++ b/lib/internal/Magento/Framework/Search/Request/Builder.php @@ -6,6 +6,7 @@ namespace Magento\Framework\Search\Request; +use Magento\Framework\Api\SortOrder; use Magento\Framework\ObjectManagerInterface; use Magento\Framework\Phrase; use Magento\Framework\Search\RequestInterface; @@ -173,7 +174,13 @@ public function create() private function prepareSorts(array $sorts) { $sortData = []; - foreach ($sorts as $sortField => $direction) { + foreach ($sorts as $sortField => $sort) { + if ($sort instanceof SortOrder) { + $sortField = $sort->getField(); + $direction = $sort->getDirection(); + } else { + $direction = $sort; + } $sortData[] = [ 'field' => $sortField, 'direction' => $direction, diff --git a/lib/internal/Magento/Framework/Search/Test/Unit/Adapter/Mysql/TemporaryStorageTest.php b/lib/internal/Magento/Framework/Search/Test/Unit/Adapter/Mysql/TemporaryStorageTest.php index d71bd447cc578..cd7c0debc76c7 100644 --- a/lib/internal/Magento/Framework/Search/Test/Unit/Adapter/Mysql/TemporaryStorageTest.php +++ b/lib/internal/Magento/Framework/Search/Test/Unit/Adapter/Mysql/TemporaryStorageTest.php @@ -176,7 +176,7 @@ private function createTemporaryTable($persistentConnection = true) if ($persistentConnection) { $this->adapter->expects($this->once()) ->method('dropTemporaryTable'); - $tableInteractionCount += 1; + $tableInteractionCount++; } $table->expects($this->at($tableInteractionCount)) ->method('addColumn') @@ -187,14 +187,14 @@ private function createTemporaryTable($persistentConnection = true) ['unsigned' => true, 'nullable' => false, 'primary' => true], 'Entity ID' ); - $tableInteractionCount += 1; + $tableInteractionCount++; $table->expects($this->at($tableInteractionCount)) ->method('addColumn') ->with( 'score', Table::TYPE_DECIMAL, [32, 16], - ['unsigned' => true, 'nullable' => false], + ['unsigned' => true, 'nullable' => true], 'Score' ); $table->expects($this->once()) diff --git a/lib/internal/Magento/Framework/Session/SessionManager.php b/lib/internal/Magento/Framework/Session/SessionManager.php index a6f8d7b86bf90..b96925facf528 100644 --- a/lib/internal/Magento/Framework/Session/SessionManager.php +++ b/lib/internal/Magento/Framework/Session/SessionManager.php @@ -183,8 +183,6 @@ public function start() try { $this->appState->getAreaCode(); - // @todo MC-18221 need to fix check false positive - // phpcs:ignore Magento2.Exceptions.ThrowCatch } catch (\Magento\Framework\Exception\LocalizedException $e) { throw new \Magento\Framework\Exception\SessionException( new \Magento\Framework\Phrase( diff --git a/lib/internal/Magento/Framework/Setup/Declaration/Schema/Dto/Factories/Integer.php b/lib/internal/Magento/Framework/Setup/Declaration/Schema/Dto/Factories/Integer.php index 5cd7bcac39736..b35e2c9864e83 100644 --- a/lib/internal/Magento/Framework/Setup/Declaration/Schema/Dto/Factories/Integer.php +++ b/lib/internal/Magento/Framework/Setup/Declaration/Schema/Dto/Factories/Integer.php @@ -50,7 +50,7 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc */ public function create(array $data) { @@ -63,7 +63,7 @@ public function create(array $data) } if (isset($data['default'])) { - $data['default'] = (int) $data['default']; + $data['default'] = $data['default'] !== 'null' ? (int) $data['default'] : null; } return $this->objectManager->create($this->className, $data); diff --git a/lib/internal/Magento/Framework/Setup/Declaration/Schema/etc/types/integers/integer.xsd b/lib/internal/Magento/Framework/Setup/Declaration/Schema/etc/types/integers/integer.xsd index e264060cba63d..ff68beb27191d 100644 --- a/lib/internal/Magento/Framework/Setup/Declaration/Schema/etc/types/integers/integer.xsd +++ b/lib/internal/Magento/Framework/Setup/Declaration/Schema/etc/types/integers/integer.xsd @@ -17,7 +17,13 @@ Size is 4 bytes. - + + + + + + + diff --git a/lib/internal/Magento/Framework/Simplexml/Element.php b/lib/internal/Magento/Framework/Simplexml/Element.php index eec48bc555010..27e59b5442763 100644 --- a/lib/internal/Magento/Framework/Simplexml/Element.php +++ b/lib/internal/Magento/Framework/Simplexml/Element.php @@ -30,11 +30,12 @@ class Element extends \SimpleXMLElement * @param \Magento\Framework\Simplexml\Element $element * @return void * @SuppressWarnings(PHPMD.UnusedFormalParameter) + * phpcs:disable Magento2.CodeAnalysis.EmptyBlock */ public function setParent($element) { - //$this->_parent = $element; } + // phpcs:enable /** * Returns parent node for the element @@ -179,7 +180,8 @@ public function asArray() } /** - * asArray() analog, but without attributes + * The asArray() analog, but without attributes + * * @return array|string */ public function asCanonicalArray() @@ -245,7 +247,7 @@ public function asNiceXml($filename = '', $level = 0) $attributes = $this->attributes(); if ($attributes) { foreach ($attributes as $key => $value) { - $out .= ' ' . $key . '="' . str_replace('"', '\"', (string)$value) . '"'; + $out .= ' ' . $key . '="' . str_replace('"', '\"', $this->xmlentities($value)) . '"'; } } @@ -471,6 +473,7 @@ public function setNode($path, $value, $overwrite = true) * Unset self from the XML-node tree * * Note: trying to refer this object as a variable after "unsetting" like this will result in E_WARNING + * * @return void */ public function unsetSelf() diff --git a/lib/internal/Magento/Framework/Stdlib/DateTime/Timezone.php b/lib/internal/Magento/Framework/Stdlib/DateTime/Timezone.php index 45c31d367b34a..118a3e053bd79 100644 --- a/lib/internal/Magento/Framework/Stdlib/DateTime/Timezone.php +++ b/lib/internal/Magento/Framework/Stdlib/DateTime/Timezone.php @@ -195,12 +195,43 @@ public function date($date = null, $locale = null, $useTimezone = true, $include */ public function scopeDate($scope = null, $date = null, $includeTime = false) { - $timezone = $this->_scopeConfig->getValue($this->getDefaultTimezonePath(), $this->_scopeType, $scope); - $date = new \DateTime(is_numeric($date) ? '@' . $date : $date); - $date->setTimezone(new \DateTimeZone($timezone)); + $timezone = new \DateTimeZone( + $this->_scopeConfig->getValue($this->getDefaultTimezonePath(), $this->_scopeType, $scope) + ); + switch (true) { + case (empty($date)): + $date = new \DateTime('now', $timezone); + break; + case ($date instanceof \DateTime): + case ($date instanceof \DateTimeImmutable): + $date = $date->setTimezone($timezone); + break; + case (!is_numeric($date)): + $timeType = $includeTime ? \IntlDateFormatter::SHORT : \IntlDateFormatter::NONE; + $formatter = new \IntlDateFormatter( + $this->_localeResolver->getLocale(), + \IntlDateFormatter::SHORT, + $timeType, + $timezone + ); + $timestamp = $formatter->parse($date); + $date = $timestamp + ? (new \DateTime('@' . $timestamp))->setTimezone($timezone) + : new \DateTime($date, $timezone); + break; + case (is_numeric($date)): + $date = new \DateTime('@' . $date); + $date = $date->setTimezone($timezone); + break; + default: + $date = new \DateTime($date, $timezone); + break; + } + if (!$includeTime) { $date->setTime(0, 0, 0); } + return $date; } diff --git a/lib/internal/Magento/Framework/Stdlib/Test/Unit/DateTime/TimezoneTest.php b/lib/internal/Magento/Framework/Stdlib/Test/Unit/DateTime/TimezoneTest.php index 3d7d14a394629..53980e574c267 100644 --- a/lib/internal/Magento/Framework/Stdlib/Test/Unit/DateTime/TimezoneTest.php +++ b/lib/internal/Magento/Framework/Stdlib/Test/Unit/DateTime/TimezoneTest.php @@ -22,6 +22,16 @@ class TimezoneTest extends \PHPUnit\Framework\TestCase */ private $defaultTimeZone; + /** + * @var string + */ + private $scopeType; + + /** + * @var string + */ + private $defaultTimezonePath; + /** * @var ObjectManager */ @@ -49,6 +59,8 @@ protected function setUp() { $this->defaultTimeZone = date_default_timezone_get(); date_default_timezone_set('UTC'); + $this->scopeType = 'store'; + $this->defaultTimezonePath = 'default/timezone/path'; $this->objectManager = new ObjectManager($this); $this->scopeResolver = $this->getMockBuilder(ScopeResolverInterface::class)->getMock(); @@ -86,9 +98,10 @@ public function testDateIncludeTime($date, $locale, $includeTime, $expectedTimes /** * DataProvider for testDateIncludeTime + * * @return array */ - public function dateIncludeTimeDataProvider() + public function dateIncludeTimeDataProvider(): array { return [ 'Parse d/m/y date without time' => [ @@ -133,9 +146,10 @@ public function testConvertConfigTimeToUtc($date, $configuredTimezone, $expected /** * Data provider for testConvertConfigTimeToUtc + * * @return array */ - public function getConvertConfigTimeToUtcFixtures() + public function getConvertConfigTimeToUtcFixtures(): array { return [ 'string' => [ @@ -181,9 +195,10 @@ public function testDate() /** * DataProvider for testDate + * * @return array */ - private function getDateFixtures() + private function getDateFixtures(): array { return [ 'now_datetime_utc' => [ @@ -239,29 +254,71 @@ private function getTimezone() return new Timezone( $this->scopeResolver, $this->localeResolver, - $this->getMockBuilder(DateTime::class)->getMock(), + $this->createMock(DateTime::class), $this->scopeConfig, - '', - '' + $this->scopeType, + $this->defaultTimezonePath ); } /** * @param string $configuredTimezone + * @param string|null $scope */ - private function scopeConfigWillReturnConfiguredTimezone($configuredTimezone) + private function scopeConfigWillReturnConfiguredTimezone(string $configuredTimezone, string $scope = null) { - $this->scopeConfig->method('getValue')->with('', '', null)->willReturn($configuredTimezone); + $this->scopeConfig->expects($this->atLeastOnce()) + ->method('getValue') + ->with($this->defaultTimezonePath, $this->scopeType, $scope) + ->willReturn($configuredTimezone); } - public function testCheckIfScopeDateSetsTimeZone() + /** + * @dataProvider scopeDateDataProvider + * @param \DateTimeInterface|string|int $date + * @param string $timezone + * @param string $locale + * @param string $expectedDate + */ + public function testScopeDate($date, string $timezone, string $locale, string $expectedDate) { - $scopeDate = new \DateTime('now', new \DateTimeZone('America/Vancouver')); - $this->scopeConfig->method('getValue')->willReturn('America/Vancouver'); + $scopeCode = 'test'; - $this->assertEquals( - $scopeDate->getTimezone(), - $this->getTimezone()->scopeDate(0, $scopeDate->getTimestamp())->getTimezone() - ); + $this->scopeConfigWillReturnConfiguredTimezone($timezone, $scopeCode); + $this->localeResolver->method('getLocale') + ->willReturn($locale); + + $scopeDate = $this->getTimezone()->scopeDate($scopeCode, $date, true); + $this->assertEquals($expectedDate, $scopeDate->format('Y-m-d H:i:s')); + $this->assertEquals($timezone, $scopeDate->getTimezone()->getName()); + } + + /** + * @return array + */ + public function scopeDateDataProvider(): array + { + $utcTz = new \DateTimeZone('UTC'); + + return [ + ['2018-10-20 00:00:00', 'UTC', 'en_US', '2018-10-20 00:00:00'], + ['2018-10-20 00:00:00', 'America/Los_Angeles', 'en_US', '2018-10-20 00:00:00'], + ['2018-10-20 00:00:00', 'Asia/Qatar', 'en_US', '2018-10-20 00:00:00'], + ['10/20/18 00:00', 'UTC', 'en_US', '2018-10-20 00:00:00'], + ['10/20/18 00:00', 'America/Los_Angeles', 'en_US', '2018-10-20 00:00:00'], + ['10/20/18 00:00', 'Asia/Qatar', 'en_US', '2018-10-20 00:00:00'], + ['20/10/18 00:00', 'UTC', 'fr_FR', '2018-10-20 00:00:00'], + ['20/10/18 00:00', 'America/Los_Angeles', 'fr_FR', '2018-10-20 00:00:00'], + ['20/10/18 00:00', 'Asia/Qatar', 'fr_FR', '2018-10-20 00:00:00'], + [1539993600, 'UTC', 'en_US', '2018-10-20 00:00:00'], + [1539993600, 'America/Los_Angeles', 'en_US', '2018-10-19 17:00:00'], + [1539993600, 'Asia/Qatar', 'en_US', '2018-10-20 03:00:00'], + [new \DateTime('2018-10-20', $utcTz), 'UTC', 'en_US', '2018-10-20 00:00:00'], + [new \DateTime('2018-10-20', $utcTz), 'America/Los_Angeles', 'en_US', '2018-10-19 17:00:00'], + [new \DateTime('2018-10-20', $utcTz), 'Asia/Qatar', 'en_US', '2018-10-20 03:00:00'], + [new \DateTimeImmutable('2018-10-20', $utcTz), 'UTC', 'en_US', '2018-10-20 00:00:00'], + [new \DateTimeImmutable('2018-10-20', $utcTz), 'America/Los_Angeles', 'en_US', '2018-10-19 17:00:00'], + [new \DateTimeImmutable('2018-10-20', $utcTz), 'Asia/Qatar', 'en_US', '2018-10-20 03:00:00'], + ]; } } diff --git a/lib/internal/Magento/Framework/View/Asset/LockerProcess.php b/lib/internal/Magento/Framework/View/Asset/LockerProcess.php index d320f951548b4..d74ee0490cdfb 100644 --- a/lib/internal/Magento/Framework/View/Asset/LockerProcess.php +++ b/lib/internal/Magento/Framework/View/Asset/LockerProcess.php @@ -62,7 +62,7 @@ public function __construct(Filesystem $filesystem) */ public function lockProcess($lockName) { - if ($this->getState()->getMode() == State::MODE_PRODUCTION) { + if ($this->getState()->getMode() === State::MODE_PRODUCTION || PHP_SAPI === 'cli') { return; } @@ -78,11 +78,12 @@ public function lockProcess($lockName) /** * @inheritdoc + * * @throws FileSystemException */ public function unlockProcess() { - if ($this->getState()->getMode() == State::MODE_PRODUCTION) { + if ($this->getState()->getMode() === State::MODE_PRODUCTION || PHP_SAPI === 'cli') { return; } @@ -115,9 +116,10 @@ private function isProcessLocked() } /** - * Get name of lock file + * Get path to lock file * * @param string $name + * * @return string */ private function getFilePath($name) @@ -126,7 +128,10 @@ private function getFilePath($name) } /** + * Get State object + * * @return State + * * @deprecated 100.1.1 */ private function getState() diff --git a/lib/internal/Magento/Framework/View/Design/FileResolution/Fallback/Resolver/Simple.php b/lib/internal/Magento/Framework/View/Design/FileResolution/Fallback/Resolver/Simple.php index fb2d8f4290881..d625ad1523554 100644 --- a/lib/internal/Magento/Framework/View/Design/FileResolution/Fallback/Resolver/Simple.php +++ b/lib/internal/Magento/Framework/View/Design/FileResolution/Fallback/Resolver/Simple.php @@ -54,7 +54,6 @@ public function __construct(ReadFactory $readFactory, RulePool $rulePool) */ public function resolve($type, $file, $area = null, ThemeInterface $theme = null, $locale = null, $module = null) { - $params = ['area' => $area, 'theme' => $theme, 'locale' => $locale]; foreach ($params as $key => $param) { if ($param === null) { diff --git a/lib/internal/Magento/Framework/View/Model/Layout/Merge.php b/lib/internal/Magento/Framework/View/Model/Layout/Merge.php index 3ccc144ebecd5..d307935375f41 100644 --- a/lib/internal/Magento/Framework/View/Model/Layout/Merge.php +++ b/lib/internal/Magento/Framework/View/Model/Layout/Merge.php @@ -18,6 +18,7 @@ * Layout merge model * @SuppressWarnings(PHPMD.TooManyFields) * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) */ class Merge implements \Magento\Framework\View\Layout\ProcessorInterface { @@ -462,6 +463,8 @@ public function load($handles = []) return $this; } + $this->extractHandlers(); + foreach ($this->getHandles() as $handle) { $this->_merge($handle); } @@ -951,4 +954,25 @@ public function getCacheId() // phpcs:ignore Magento2.Security.InsecureFunction return $this->generateCacheId(md5(implode('|', array_merge($this->getHandles(), $layoutCacheKeys)))); } + + /** + * Walk all updates and extract handles before the merge step. + */ + private function extractHandlers(): void + { + foreach ($this->updates as $update) { + $updateXml = null; + + try { + $updateXml = $this->_loadXmlString($update); + // phpcs:ignore Magento2.CodeAnalysis.EmptyBlock + } catch (\Exception $exception) { + // ignore invalid + } + + if ($updateXml && strtolower($updateXml->getName()) == 'update' && isset($updateXml['handle'])) { + $this->addHandle((string)$updateXml['handle']); + } + } + } } diff --git a/lib/internal/Magento/Framework/View/Page/Config/Metadata/MsApplicationTileImage.php b/lib/internal/Magento/Framework/View/Page/Config/Metadata/MsApplicationTileImage.php new file mode 100644 index 0000000000000..ae6401334a251 --- /dev/null +++ b/lib/internal/Magento/Framework/View/Page/Config/Metadata/MsApplicationTileImage.php @@ -0,0 +1,52 @@ +assetRepo = $assetRepo; + } + + /** + * Get asset URL from given metadata content + * + * @param string $content + * + * @return string + */ + public function getUrl(string $content): string + { + if (!parse_url($content, PHP_URL_SCHEME)) { + return $this->assetRepo->getUrl($content); + } + + return $content; + } +} diff --git a/lib/internal/Magento/Framework/View/Page/Config/Renderer.php b/lib/internal/Magento/Framework/View/Page/Config/Renderer.php index b7a1e0b707013..eae6126fa39c0 100644 --- a/lib/internal/Magento/Framework/View/Page/Config/Renderer.php +++ b/lib/internal/Magento/Framework/View/Page/Config/Renderer.php @@ -9,6 +9,7 @@ use Magento\Framework\Exception\LocalizedException; use Magento\Framework\View\Asset\GroupedCollection; use Magento\Framework\View\Page\Config; +use Magento\Framework\View\Page\Config\Metadata\MsApplicationTileImage; /** * Page config Renderer model @@ -74,6 +75,11 @@ class Renderer implements RendererInterface */ protected $urlBuilder; + /** + * @var MsApplicationTileImage + */ + private $msApplicationTileImage; + /** * @param Config $pageConfig * @param \Magento\Framework\View\Asset\MergeService $assetMergeService @@ -81,6 +87,7 @@ class Renderer implements RendererInterface * @param \Magento\Framework\Escaper $escaper * @param \Magento\Framework\Stdlib\StringUtils $string * @param \Psr\Log\LoggerInterface $logger + * @param MsApplicationTileImage|null $msApplicationTileImage */ public function __construct( Config $pageConfig, @@ -88,7 +95,8 @@ public function __construct( \Magento\Framework\UrlInterface $urlBuilder, \Magento\Framework\Escaper $escaper, \Magento\Framework\Stdlib\StringUtils $string, - \Psr\Log\LoggerInterface $logger + \Psr\Log\LoggerInterface $logger, + MsApplicationTileImage $msApplicationTileImage = null ) { $this->pageConfig = $pageConfig; $this->assetMergeService = $assetMergeService; @@ -96,6 +104,8 @@ public function __construct( $this->escaper = $escaper; $this->string = $string; $this->logger = $logger; + $this->msApplicationTileImage = $msApplicationTileImage ?: + \Magento\Framework\App\ObjectManager::getInstance()->get(MsApplicationTileImage::class); } /** @@ -179,6 +189,10 @@ protected function processMetadataContent($name, $content) if (method_exists($this->pageConfig, $method)) { $content = $this->pageConfig->$method(); } + if ($content && $name === $this->msApplicationTileImage::META_NAME) { + $content = $this->msApplicationTileImage->getUrl($content); + } + return $content; } diff --git a/lib/internal/Magento/Framework/View/Test/Unit/Asset/LockerProcessTest.php b/lib/internal/Magento/Framework/View/Test/Unit/Asset/LockerProcessTest.php index aba3ff1099cc0..ca065bbbc4f6e 100644 --- a/lib/internal/Magento/Framework/View/Test/Unit/Asset/LockerProcessTest.php +++ b/lib/internal/Magento/Framework/View/Test/Unit/Asset/LockerProcessTest.php @@ -64,24 +64,6 @@ protected function setUp() ); } - /** - * Test for lockProcess method - * - * @param string $method - * - * @dataProvider dataProviderTestLockProcess - */ - public function testLockProcess($method) - { - $this->stateMock->expects(self::once())->method('getMode')->willReturn(State::MODE_DEVELOPER); - $this->filesystemMock->expects(self::once()) - ->method('getDirectoryWrite') - ->with(DirectoryList::VAR_DIR) - ->willReturn($this->$method()); - - $this->lockerProcess->lockProcess(self::LOCK_NAME); - } - public function testNotLockProcessInProductionMode() { $this->stateMock->expects(self::once())->method('getMode')->willReturn(State::MODE_PRODUCTION); @@ -90,21 +72,6 @@ public function testNotLockProcessInProductionMode() $this->lockerProcess->lockProcess(self::LOCK_NAME); } - /** - * Test for unlockProcess method - */ - public function testUnlockProcess() - { - $this->stateMock->expects(self::exactly(2))->method('getMode')->willReturn(State::MODE_DEVELOPER); - $this->filesystemMock->expects(self::once()) - ->method('getDirectoryWrite') - ->with(DirectoryList::VAR_DIR) - ->willReturn($this->getTmpDirectoryMockFalse(1)); - - $this->lockerProcess->lockProcess(self::LOCK_NAME); - $this->lockerProcess->unlockProcess(); - } - public function testNotUnlockProcessInProductionMode() { $this->stateMock->expects(self::exactly(2))->method('getMode')->willReturn(State::MODE_PRODUCTION); @@ -114,17 +81,6 @@ public function testNotUnlockProcessInProductionMode() $this->lockerProcess->unlockProcess(); } - /** - * @return array - */ - public function dataProviderTestLockProcess() - { - return [ - ['method' => 'getTmpDirectoryMockTrue'], - ['method' => 'getTmpDirectoryMockFalse'] - ]; - } - /** * @return WriteInterface|\PHPUnit_Framework_MockObject_MockObject */ diff --git a/lib/internal/Magento/Framework/View/Test/Unit/Page/Config/RendererTest.php b/lib/internal/Magento/Framework/View/Test/Unit/Page/Config/RendererTest.php index 1f110a9ec19b5..a702ea5458a87 100644 --- a/lib/internal/Magento/Framework/View/Test/Unit/Page/Config/RendererTest.php +++ b/lib/internal/Magento/Framework/View/Test/Unit/Page/Config/RendererTest.php @@ -8,6 +8,7 @@ use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; use Magento\Framework\View\Asset\GroupedCollection; +use Magento\Framework\View\Page\Config\Metadata\MsApplicationTileImage; use Magento\Framework\View\Page\Config\Renderer; use Magento\Framework\View\Page\Config\Generator; @@ -58,6 +59,11 @@ class RendererTest extends \PHPUnit\Framework\TestCase */ protected $loggerMock; + /** + * @var MsApplicationTileImage|\PHPUnit_Framework_MockObject_MockObject + */ + protected $msApplicationTileImageMock; + /** * @var \Magento\Framework\View\Asset\GroupedCollection|\PHPUnit_Framework_MockObject_MockObject */ @@ -99,6 +105,10 @@ protected function setUp() $this->loggerMock = $this->getMockBuilder(\Psr\Log\LoggerInterface::class) ->getMock(); + $this->msApplicationTileImageMock = $this->getMockBuilder(MsApplicationTileImage::class) + ->disableOriginalConstructor() + ->getMock(); + $this->assetsCollection = $this->getMockBuilder(\Magento\Framework\View\Asset\GroupedCollection::class) ->setMethods(['getGroups']) ->disableOriginalConstructor() @@ -120,7 +130,8 @@ protected function setUp() 'urlBuilder' => $this->urlBuilderMock, 'escaper' => $this->escaperMock, 'string' => $this->stringMock, - 'logger' => $this->loggerMock + 'logger' => $this->loggerMock, + 'msApplicationTileImage' => $this->msApplicationTileImageMock ] ); } @@ -147,7 +158,8 @@ public function testRenderMetadata() 'content_type' => 'content_type_value', 'x_ua_compatible' => 'x_ua_compatible_value', 'media_type' => 'media_type_value', - 'og:video:secure_url' => 'secure_url' + 'og:video:secure_url' => 'secure_url', + 'msapplication-TileImage' => 'https://site.domain/ms-tile.jpg' ]; $metadataValueCharset = 'newCharsetValue'; @@ -155,7 +167,8 @@ public function testRenderMetadata() . '' . "\n" . '' . "\n" . '' . "\n" - . '' . "\n"; + . '' . "\n" + . '' . "\n"; $this->stringMock->expects($this->at(0)) ->method('upperCaseWords') @@ -171,6 +184,37 @@ public function testRenderMetadata() ->method('getMetadata') ->will($this->returnValue($metadata)); + $this->msApplicationTileImageMock + ->expects($this->once()) + ->method('getUrl') + ->with('https://site.domain/ms-tile.jpg') + ->will($this->returnValue('https://site.domain/ms-tile.jpg')); + + $this->assertEquals($expected, $this->renderer->renderMetadata()); + } + + /** + * Test renderMetadata when it has 'msapplication-TileImage' meta passed + */ + public function testRenderMetadataWithMsApplicationTileImageAsset() + { + $metadata = [ + 'msapplication-TileImage' => 'images/ms-tile.jpg' + ]; + $expectedMetaUrl = 'https://site.domain/images/ms-tile.jpg'; + $expected = '' . "\n"; + + $this->pageConfigMock + ->expects($this->once()) + ->method('getMetadata') + ->will($this->returnValue($metadata)); + + $this->msApplicationTileImageMock + ->expects($this->once()) + ->method('getUrl') + ->with('images/ms-tile.jpg') + ->will($this->returnValue($expectedMetaUrl)); + $this->assertEquals($expected, $this->renderer->renderMetadata()); } @@ -277,12 +321,14 @@ public function testRenderAssets($groupOne, $groupTwo, $expectedResult) ->willReturn($groupAssetsOne); $groupMockOne->expects($this->any()) ->method('getProperty') - ->willReturnMap([ - [GroupedCollection::PROPERTY_CAN_MERGE, true], - [GroupedCollection::PROPERTY_CONTENT_TYPE, $groupOne['type']], - ['attributes', $groupOne['attributes']], - ['ie_condition', $groupOne['condition']], - ]); + ->willReturnMap( + [ + [GroupedCollection::PROPERTY_CAN_MERGE, true], + [GroupedCollection::PROPERTY_CONTENT_TYPE, $groupOne['type']], + ['attributes', $groupOne['attributes']], + ['ie_condition', $groupOne['condition']], + ] + ); $assetMockTwo = $this->createMock(\Magento\Framework\View\Asset\AssetInterface::class); $assetMockTwo->expects($this->once()) @@ -300,12 +346,14 @@ public function testRenderAssets($groupOne, $groupTwo, $expectedResult) ->willReturn($groupAssetsTwo); $groupMockTwo->expects($this->any()) ->method('getProperty') - ->willReturnMap([ - [GroupedCollection::PROPERTY_CAN_MERGE, true], - [GroupedCollection::PROPERTY_CONTENT_TYPE, $groupTwo['type']], - ['attributes', $groupTwo['attributes']], - ['ie_condition', $groupTwo['condition']], - ]); + ->willReturnMap( + [ + [GroupedCollection::PROPERTY_CAN_MERGE, true], + [GroupedCollection::PROPERTY_CONTENT_TYPE, $groupTwo['type']], + ['attributes', $groupTwo['attributes']], + ['ie_condition', $groupTwo['condition']], + ] + ); $this->pageConfigMock->expects($this->once()) ->method('getAssetCollection') diff --git a/lib/internal/Magento/Framework/Webapi/ServiceInputProcessor.php b/lib/internal/Magento/Framework/Webapi/ServiceInputProcessor.php index c253a400bed93..f93d7efda5c8a 100644 --- a/lib/internal/Magento/Framework/Webapi/ServiceInputProcessor.php +++ b/lib/internal/Magento/Framework/Webapi/ServiceInputProcessor.php @@ -275,7 +275,6 @@ protected function _createFromArray($className, $data) } else { $setterValue = $this->convertValue($value, $returnType); } - // phpcs:ignore Magento2.Exceptions.ThrowCatch } catch (SerializationException $e) { throw new SerializationException( new Phrase( @@ -324,7 +323,6 @@ protected function convertCustomAttributeValue($customAttributesValueArray, $dat ) { try { $attributeValue = $this->convertValue($customAttributeValue, $type); - // phpcs:ignore Magento2.Exceptions.ThrowCatch } catch (SerializationException $e) { throw new SerializationException( new Phrase( diff --git a/lib/internal/Magento/Framework/composer.json b/lib/internal/Magento/Framework/composer.json index c42323a2ecc06..af2eb913fe3fe 100644 --- a/lib/internal/Magento/Framework/composer.json +++ b/lib/internal/Magento/Framework/composer.json @@ -16,6 +16,7 @@ "ext-gd": "*", "ext-hash": "*", "ext-iconv": "*", + "ext-intl": "*", "ext-openssl": "*", "ext-simplexml": "*", "ext-spl": "*", diff --git a/lib/web/css/source/components/_modals.less b/lib/web/css/source/components/_modals.less index 396930cce6d86..58c9c0674b6ad 100644 --- a/lib/web/css/source/components/_modals.less +++ b/lib/web/css/source/components/_modals.less @@ -150,7 +150,6 @@ // Modals overlay .modals-overlay { - &:extend(.abs-modal-overlay all); .lib-css(z-index, @overlay__z-index); } diff --git a/lib/web/css/source/lib/_buttons.less b/lib/web/css/source/lib/_buttons.less index a92093b742902..a575442b77e55 100644 --- a/lib/web/css/source/lib/_buttons.less +++ b/lib/web/css/source/lib/_buttons.less @@ -200,15 +200,14 @@ .lib-css(line-height, @_line-height); .lib-css(margin, @_margin); .lib-css(padding, @_padding); - .lib-link(); + .lib-link( + @_link-color: @_link-color, + @_link-color-hover: @_link-color-hover + ); background: none; border: 0; display: inline; - &:hover { - .lib-css(color, @_link-color-hover); - } - &:hover, &:active, &:focus { diff --git a/lib/web/css/source/lib/_icons.less b/lib/web/css/source/lib/_icons.less index abb8b43368f13..28d148da7c016 100644 --- a/lib/web/css/source/lib/_icons.less +++ b/lib/web/css/source/lib/_icons.less @@ -190,7 +190,7 @@ display: inline-block; & when not (@_icon-image = false) { - ._lib-icon-text-hide(@_icon-font-text-hide); + ._lib-icon-text-hide(@_icon-image-text-hide); } &:after { diff --git a/lib/web/jquery/jquery.storageapi.min.js b/lib/web/jquery/jquery.storageapi.min.js index e196b0678934b..886c3d847ed3b 100644 --- a/lib/web/jquery/jquery.storageapi.min.js +++ b/lib/web/jquery/jquery.storageapi.min.js @@ -1,2 +1,2 @@ /* jQuery Storage API Plugin 1.7.3 https://github.com/julien-maurel/jQuery-Storage-API */ -!function(e){"function"==typeof define&&define.amd?define(["jquery"],e):e("object"==typeof exports?require("jquery"):jQuery)}(function(e){function t(t){var r,i,n,o=arguments.length,s=window[t],a=arguments,u=a[1];if(2>o)throw Error("Minimum 2 arguments must be given");if(e.isArray(u)){i={};for(var f in u){r=u[f];try{i[r]=JSON.parse(s.getItem(r))}catch(c){i[r]=s.getItem(r)}}return i}if(2!=o){try{i=JSON.parse(s.getItem(u))}catch(c){throw new ReferenceError(u+" is not defined in this storage")}for(var f=2;o-1>f;f++)if(i=i[a[f]],void 0===i)throw new ReferenceError([].slice.call(a,1,f+1).join(".")+" is not defined in this storage");if(e.isArray(a[f])){n=i,i={};for(var m in a[f])i[a[f][m]]=n[a[f][m]];return i}return i[a[f]]}try{return JSON.parse(s.getItem(u))}catch(c){return s.getItem(u)}}function r(t){var r,i,n=arguments.length,o=window[t],s=arguments,a=s[1],u=s[2],f={};if(2>n||!e.isPlainObject(a)&&3>n)throw Error("Minimum 3 arguments must be given or second parameter must be an object");if(e.isPlainObject(a)){for(var c in a)r=a[c],e.isPlainObject(r)?o.setItem(c,JSON.stringify(r)):o.setItem(c,r);return a}if(3==n)return"object"==typeof u?o.setItem(a,JSON.stringify(u)):o.setItem(a,u),u;try{i=o.getItem(a),null!=i&&(f=JSON.parse(i))}catch(m){}i=f;for(var c=2;n-2>c;c++)r=s[c],i[r]&&e.isPlainObject(i[r])||(i[r]={}),i=i[r];return i[s[c]]=s[c+1],o.setItem(a,JSON.stringify(f)),f}function i(t){var r,i,n=arguments.length,o=window[t],s=arguments,a=s[1];if(2>n)throw Error("Minimum 2 arguments must be given");if(e.isArray(a)){for(var u in a)o.removeItem(a[u]);return!0}if(2==n)return o.removeItem(a),!0;try{r=i=JSON.parse(o.getItem(a))}catch(f){throw new ReferenceError(a+" is not defined in this storage")}for(var u=2;n-1>u;u++)if(i=i[s[u]],void 0===i)throw new ReferenceError([].slice.call(s,1,u).join(".")+" is not defined in this storage");if(e.isArray(s[u]))for(var c in s[u])delete i[s[u][c]];else delete i[s[u]];return o.setItem(a,JSON.stringify(r)),!0}function n(t,r){var n=a(t);for(var o in n)i(t,n[o]);if(r)for(var o in e.namespaceStorages)u(o)}function o(r){var i=arguments.length,n=arguments,s=(window[r],n[1]);if(1==i)return 0==a(r).length;if(e.isArray(s)){for(var u=0;ui)throw Error("Minimum 2 arguments must be given");if(e.isArray(o)){for(var a=0;a1?t.apply(this,o):n,a._cookie)for(var u in e.cookie())""!=u&&s.push(u.replace(a._prefix,""));else for(var f in a)s.push(f);return s}function u(t){if(!t||"string"!=typeof t)throw Error("First parameter must be a string");g?(window.localStorage.getItem(t)||window.localStorage.setItem(t,"{}"),window.sessionStorage.getItem(t)||window.sessionStorage.setItem(t,"{}")):(window.localCookieStorage.getItem(t)||window.localCookieStorage.setItem(t,"{}"),window.sessionCookieStorage.getItem(t)||window.sessionCookieStorage.setItem(t,"{}"));var r={localStorage:e.extend({},e.localStorage,{_ns:t}),sessionStorage:e.extend({},e.sessionStorage,{_ns:t})};return e.cookie&&(window.cookieStorage.getItem(t)||window.cookieStorage.setItem(t,"{}"),r.cookieStorage=e.extend({},e.cookieStorage,{_ns:t})),e.namespaceStorages[t]=r,r}function f(e){if(!window[e])return!1;var t="jsapi";try{return window[e].setItem(t,t),window[e].removeItem(t),!0}catch(r){return!1}}var c="ls_",m="ss_",g=f("localStorage"),h={_type:"",_ns:"",_callMethod:function(e,t){var r=[this._type],t=Array.prototype.slice.call(t),i=t[0];return this._ns&&r.push(this._ns),"string"==typeof i&&-1!==i.indexOf(".")&&(t.shift(),[].unshift.apply(t,i.split("."))),[].push.apply(r,t),e.apply(this,r)},get:function(){return this._callMethod(t,arguments)},set:function(){var t=arguments.length,i=arguments,n=i[0];if(1>t||!e.isPlainObject(n)&&2>t)throw Error("Minimum 2 arguments must be given or first parameter must be an object");if(e.isPlainObject(n)&&this._ns){for(var o in n)r(this._type,this._ns,o,n[o]);return n}var s=this._callMethod(r,i);return this._ns?s[n.split(".")[0]]:s},remove:function(){if(arguments.length<1)throw Error("Minimum 1 argument must be given");return this._callMethod(i,arguments)},removeAll:function(e){return this._ns?(r(this._type,this._ns,{}),!0):n(this._type,e)},isEmpty:function(){return this._callMethod(o,arguments)},isSet:function(){if(arguments.length<1)throw Error("Minimum 1 argument must be given");return this._callMethod(s,arguments)},keys:function(){return this._callMethod(a,arguments)}};if(e.cookie){window.name||(window.name=Math.floor(1e8*Math.random()));var l={_cookie:!0,_prefix:"",_expires:null,_path:null,_domain:null,setItem:function(t,r){e.cookie(this._prefix+t,r,{expires:this._expires,path:this._path,domain:this._domain})},getItem:function(t){return e.cookie(this._prefix+t)},removeItem:function(t){return e.removeCookie(this._prefix+t)},clear:function(){for(var t in e.cookie())""!=t&&(!this._prefix&&-1===t.indexOf(c)&&-1===t.indexOf(m)||this._prefix&&0===t.indexOf(this._prefix))&&e.removeCookie(t)},setExpires:function(e){return this._expires=e,this},setPath:function(e){return this._path=e,this},setDomain:function(e){return this._domain=e,this},setConf:function(e){return e.path&&(this._path=e.path),e.domain&&(this._domain=e.domain),e.expires&&(this._expires=e.expires),this},setDefaultConf:function(){this._path=this._domain=this._expires=null}};g||(window.localCookieStorage=e.extend({},l,{_prefix:c,_expires:3650}),window.sessionCookieStorage=e.extend({},l,{_prefix:m+window.name+"_"})),window.cookieStorage=e.extend({},l),e.cookieStorage=e.extend({},h,{_type:"cookieStorage",setExpires:function(e){return window.cookieStorage.setExpires(e),this},setPath:function(e){return window.cookieStorage.setPath(e),this},setDomain:function(e){return window.cookieStorage.setDomain(e),this},setConf:function(e){return window.cookieStorage.setConf(e),this},setDefaultConf:function(){return window.cookieStorage.setDefaultConf(),this}})}e.initNamespaceStorage=function(e){return u(e)},g?(e.localStorage=e.extend({},h,{_type:"localStorage"}),e.sessionStorage=e.extend({},h,{_type:"sessionStorage"})):(e.localStorage=e.extend({},h,{_type:"localCookieStorage"}),e.sessionStorage=e.extend({},h,{_type:"sessionCookieStorage"})),e.namespaceStorages={},e.removeAllStorages=function(t){e.localStorage.removeAll(t),e.sessionStorage.removeAll(t),e.cookieStorage&&e.cookieStorage.removeAll(t),t||(e.namespaceStorages={})}}); \ No newline at end of file +!function(e){"function"==typeof define&&define.amd?define(["jquery", "jquery/jquery.cookie"],e):e("object"==typeof exports?require("jquery"):jQuery)}(function(e){function t(t){var r,i,n,o=arguments.length,s=window[t],a=arguments,u=a[1];if(2>o)throw Error("Minimum 2 arguments must be given");if(e.isArray(u)){i={};for(var f in u){r=u[f];try{i[r]=JSON.parse(s.getItem(r))}catch(c){i[r]=s.getItem(r)}}return i}if(2!=o){try{i=JSON.parse(s.getItem(u))}catch(c){throw new ReferenceError(u+" is not defined in this storage")}for(var f=2;o-1>f;f++)if(i=i[a[f]],void 0===i)throw new ReferenceError([].slice.call(a,1,f+1).join(".")+" is not defined in this storage");if(e.isArray(a[f])){n=i,i={};for(var m in a[f])i[a[f][m]]=n[a[f][m]];return i}return i[a[f]]}try{return JSON.parse(s.getItem(u))}catch(c){return s.getItem(u)}}function r(t){var r,i,n=arguments.length,o=window[t],s=arguments,a=s[1],u=s[2],f={};if(2>n||!e.isPlainObject(a)&&3>n)throw Error("Minimum 3 arguments must be given or second parameter must be an object");if(e.isPlainObject(a)){for(var c in a)r=a[c],e.isPlainObject(r)?o.setItem(c,JSON.stringify(r)):o.setItem(c,r);return a}if(3==n)return"object"==typeof u?o.setItem(a,JSON.stringify(u)):o.setItem(a,u),u;try{i=o.getItem(a),null!=i&&(f=JSON.parse(i))}catch(m){}i=f;for(var c=2;n-2>c;c++)r=s[c],i[r]&&e.isPlainObject(i[r])||(i[r]={}),i=i[r];return i[s[c]]=s[c+1],o.setItem(a,JSON.stringify(f)),f}function i(t){var r,i,n=arguments.length,o=window[t],s=arguments,a=s[1];if(2>n)throw Error("Minimum 2 arguments must be given");if(e.isArray(a)){for(var u in a)o.removeItem(a[u]);return!0}if(2==n)return o.removeItem(a),!0;try{r=i=JSON.parse(o.getItem(a))}catch(f){throw new ReferenceError(a+" is not defined in this storage")}for(var u=2;n-1>u;u++)if(i=i[s[u]],void 0===i)throw new ReferenceError([].slice.call(s,1,u).join(".")+" is not defined in this storage");if(e.isArray(s[u]))for(var c in s[u])delete i[s[u][c]];else delete i[s[u]];return o.setItem(a,JSON.stringify(r)),!0}function n(t,r){var n=a(t);for(var o in n)i(t,n[o]);if(r)for(var o in e.namespaceStorages)u(o)}function o(r){var i=arguments.length,n=arguments,s=(window[r],n[1]);if(1==i)return 0==a(r).length;if(e.isArray(s)){for(var u=0;ui)throw Error("Minimum 2 arguments must be given");if(e.isArray(o)){for(var a=0;a1?t.apply(this,o):n,a._cookie)for(var u in e.cookie())""!=u&&s.push(u.replace(a._prefix,""));else for(var f in a)s.push(f);return s}function u(t){if(!t||"string"!=typeof t)throw Error("First parameter must be a string");g?(window.localStorage.getItem(t)||window.localStorage.setItem(t,"{}"),window.sessionStorage.getItem(t)||window.sessionStorage.setItem(t,"{}")):(window.localCookieStorage.getItem(t)||window.localCookieStorage.setItem(t,"{}"),window.sessionCookieStorage.getItem(t)||window.sessionCookieStorage.setItem(t,"{}"));var r={localStorage:e.extend({},e.localStorage,{_ns:t}),sessionStorage:e.extend({},e.sessionStorage,{_ns:t})};return e.cookie&&(window.cookieStorage.getItem(t)||window.cookieStorage.setItem(t,"{}"),r.cookieStorage=e.extend({},e.cookieStorage,{_ns:t})),e.namespaceStorages[t]=r,r}function f(e){if(!window[e])return!1;var t="jsapi";try{return window[e].setItem(t,t),window[e].removeItem(t),!0}catch(r){return!1}}var c="ls_",m="ss_",g=f("localStorage"),h={_type:"",_ns:"",_callMethod:function(e,t){var r=[this._type],t=Array.prototype.slice.call(t),i=t[0];return this._ns&&r.push(this._ns),"string"==typeof i&&-1!==i.indexOf(".")&&(t.shift(),[].unshift.apply(t,i.split("."))),[].push.apply(r,t),e.apply(this,r)},get:function(){return this._callMethod(t,arguments)},set:function(){var t=arguments.length,i=arguments,n=i[0];if(1>t||!e.isPlainObject(n)&&2>t)throw Error("Minimum 2 arguments must be given or first parameter must be an object");if(e.isPlainObject(n)&&this._ns){for(var o in n)r(this._type,this._ns,o,n[o]);return n}var s=this._callMethod(r,i);return this._ns?s[n.split(".")[0]]:s},remove:function(){if(arguments.length<1)throw Error("Minimum 1 argument must be given");return this._callMethod(i,arguments)},removeAll:function(e){return this._ns?(r(this._type,this._ns,{}),!0):n(this._type,e)},isEmpty:function(){return this._callMethod(o,arguments)},isSet:function(){if(arguments.length<1)throw Error("Minimum 1 argument must be given");return this._callMethod(s,arguments)},keys:function(){return this._callMethod(a,arguments)}};if(e.cookie){window.name||(window.name=Math.floor(1e8*Math.random()));var l={_cookie:!0,_prefix:"",_expires:null,_path:null,_domain:null,setItem:function(t,r){e.cookie(this._prefix+t,r,{expires:this._expires,path:this._path,domain:this._domain})},getItem:function(t){return e.cookie(this._prefix+t)},removeItem:function(t){return e.removeCookie(this._prefix+t)},clear:function(){for(var t in e.cookie())""!=t&&(!this._prefix&&-1===t.indexOf(c)&&-1===t.indexOf(m)||this._prefix&&0===t.indexOf(this._prefix))&&e.removeCookie(t)},setExpires:function(e){return this._expires=e,this},setPath:function(e){return this._path=e,this},setDomain:function(e){return this._domain=e,this},setConf:function(e){return e.path&&(this._path=e.path),e.domain&&(this._domain=e.domain),e.expires&&(this._expires=e.expires),this},setDefaultConf:function(){this._path=this._domain=this._expires=null}};g||(window.localCookieStorage=e.extend({},l,{_prefix:c,_expires:3650}),window.sessionCookieStorage=e.extend({},l,{_prefix:m+window.name+"_"})),window.cookieStorage=e.extend({},l),e.cookieStorage=e.extend({},h,{_type:"cookieStorage",setExpires:function(e){return window.cookieStorage.setExpires(e),this},setPath:function(e){return window.cookieStorage.setPath(e),this},setDomain:function(e){return window.cookieStorage.setDomain(e),this},setConf:function(e){return window.cookieStorage.setConf(e),this},setDefaultConf:function(){return window.cookieStorage.setDefaultConf(),this}})}e.initNamespaceStorage=function(e){return u(e)},g?(e.localStorage=e.extend({},h,{_type:"localStorage"}),e.sessionStorage=e.extend({},h,{_type:"sessionStorage"})):(e.localStorage=e.extend({},h,{_type:"localCookieStorage"}),e.sessionStorage=e.extend({},h,{_type:"sessionCookieStorage"})),e.namespaceStorages={},e.removeAllStorages=function(t){e.localStorage.removeAll(t),e.sessionStorage.removeAll(t),e.cookieStorage&&e.cookieStorage.removeAll(t),t||(e.namespaceStorages={})}}); \ No newline at end of file diff --git a/lib/web/jquery/patches/jquery-ui.js b/lib/web/jquery/patches/jquery-ui.js index ae2d8da7ece66..cb67fab8030b4 100644 --- a/lib/web/jquery/patches/jquery-ui.js +++ b/lib/web/jquery/patches/jquery-ui.js @@ -4,7 +4,9 @@ */ define([ - 'jquery' + 'jquery', + 'jquery-ui-modules/widget' + // 'jquery-ui-modules/dialog' - do not enable this dependency because this is already a mixin for the dialog ui component ], function ($) { 'use strict'; diff --git a/lib/web/mage/adminhtml/browser.js b/lib/web/mage/adminhtml/browser.js index 09ceafc011d6f..137ad5541a87f 100644 --- a/lib/web/mage/adminhtml/browser.js +++ b/lib/web/mage/adminhtml/browser.js @@ -18,6 +18,7 @@ define([ ], function ($, wysiwyg, prompt, confirm, alert) { window.MediabrowserUtility = { windowId: 'modal_dialog_message', + modalLoaded: false, /** * @return {Number} @@ -51,6 +52,19 @@ define([ content = '', self = this; + if (this.modalLoaded === true && + options && + self.targetElementId && + self.targetElementId === options.targetElementId + ) { + if (typeof options.closed !== 'undefined') { + this.modal.modal('option', 'closed', options.closed); + } + this.modal.modal('openModal'); + + return; + } + if (this.modal) { this.modal.html($(content).html()); @@ -74,6 +88,8 @@ define([ }).done(function (data) { self.modal.html(data).trigger('contentUpdated'); + self.modalLoaded = true; + self.targetElementId = options.targetElementId; }); }, diff --git a/lib/web/mage/adminhtml/wysiwyg/tiny_mce/plugins/magentovariable/editor_plugin.js b/lib/web/mage/adminhtml/wysiwyg/tiny_mce/plugins/magentovariable/editor_plugin.js index 96091e4099676..92c13fca63920 100644 --- a/lib/web/mage/adminhtml/wysiwyg/tiny_mce/plugins/magentovariable/editor_plugin.js +++ b/lib/web/mage/adminhtml/wysiwyg/tiny_mce/plugins/magentovariable/editor_plugin.js @@ -171,7 +171,8 @@ define([ * @returns {String} */ decodeVariables: function (content) { - var doc = (new DOMParser()).parseFromString(content.replace(/"/g, '"'), 'text/html'); + var doc = (new DOMParser()).parseFromString(content.replace(/"/g, '"'), 'text/html'), + returnval = ''; [].forEach.call(doc.querySelectorAll('span.magento-variable'), function (el) { var $el = jQuery(el); @@ -195,7 +196,12 @@ define([ } }); - return doc.body ? doc.body.innerHTML.replace(/"/g, '"') : content; + returnval += doc.head.innerHTML ? + doc.head.innerHTML.replace(/"/g, '"') : ''; + returnval += doc.body.innerHTML ? + doc.body.innerHTML.replace(/"/g, '"') : ''; + + return returnval ? returnval : content; }, /** diff --git a/lib/web/mage/adminhtml/wysiwyg/tiny_mce/plugins/magentowidget/editor_plugin.js b/lib/web/mage/adminhtml/wysiwyg/tiny_mce/plugins/magentowidget/editor_plugin.js index cfcdef0b701c9..e6669d77a3889 100644 --- a/lib/web/mage/adminhtml/wysiwyg/tiny_mce/plugins/magentowidget/editor_plugin.js +++ b/lib/web/mage/adminhtml/wysiwyg/tiny_mce/plugins/magentowidget/editor_plugin.js @@ -189,7 +189,7 @@ define([ * @return {String} */ removeDuplicateAncestorWidgetSpanElement: function (content) { - var parser, doc; + var parser, doc, returnval = ''; if (!window.DOMParser) { return content; @@ -212,7 +212,12 @@ define([ widgetEl.parentNode.removeChild(widgetEl); }); - return doc.body ? doc.body.innerHTML.replace(/"/g, '"') : content; + returnval += doc.head.innerHTML ? + doc.head.innerHTML.replace(/"/g, '"') : ''; + returnval += doc.body.innerHTML ? + doc.body.innerHTML.replace(/"/g, '"') : ''; + + return returnval ? returnval : content; }, /** diff --git a/lib/web/mage/adminhtml/wysiwyg/tiny_mce/tinymce4Adapter.js b/lib/web/mage/adminhtml/wysiwyg/tiny_mce/tinymce4Adapter.js index 93badda89df6e..4dafc845309cb 100644 --- a/lib/web/mage/adminhtml/wysiwyg/tiny_mce/tinymce4Adapter.js +++ b/lib/web/mage/adminhtml/wysiwyg/tiny_mce/tinymce4Adapter.js @@ -117,6 +117,7 @@ define([ jQuery.when.apply(jQuery, deferreds).done(function () { tinyMCE4.init(settings); this.getPluginButtons().hide(); + varienGlobalEvents.clearEventHandlers('open_browser_callback'); this.eventBus.attachEventHandler('open_browser_callback', tinyMceEditors.get(self.id).openFileBrowser); }.bind(this)); }, @@ -198,6 +199,7 @@ define([ 'convert_urls': false, 'content_css': this.config.tinymce4['content_css'], 'relative_urls': true, + 'valid_children': '+body[style]', menubar: false, plugins: this.config.tinymce4.plugins, toolbar: this.config.tinymce4.toolbar, @@ -375,6 +377,7 @@ define([ var typeTitle = this.translate('Select Images'), storeId = this.config['store_id'] !== null ? this.config['store_id'] : 0, frameDialog = jQuery('div.mce-container[role="dialog"]'), + self = this, wUrl = this.config['files_browser_window_url'] + 'target_element_id/' + this.getId() + '/' + 'store/' + storeId + '/'; @@ -391,14 +394,17 @@ define([ require(['mage/adminhtml/browser'], function () { MediabrowserUtility.openDialog(wUrl, false, false, typeTitle, { - /** - * Closed. - */ - closed: function () { - frameDialog.show(); - jQuery('#mce-modal-block').show(); + /** + * Closed. + */ + closed: function () { + frameDialog.show(); + jQuery('#mce-modal-block').show(); + }, + + targetElementId: self.activeEditor() ? self.activeEditor().id : null } - }); + ); }); }, diff --git a/lib/web/mage/adminhtml/wysiwyg/widget.js b/lib/web/mage/adminhtml/wysiwyg/widget.js index 5c1a77b6382a2..f39fc22034104 100644 --- a/lib/web/mage/adminhtml/wysiwyg/widget.js +++ b/lib/web/mage/adminhtml/wysiwyg/widget.js @@ -223,7 +223,9 @@ define([ * @param {*} containerId */ enableOptionsContainer: function (containerId) { - $$('#' + containerId + ' .widget-option').each(function (e) { + var container = $(containerId); + + container.select('.widget-option').each(function (e) { e.removeClassName('skip-submit'); if (e.hasClassName('obligatory')) { @@ -231,18 +233,19 @@ define([ e.addClassName('required-entry'); } }); - $(containerId).removeClassName('no-display'); + container.removeClassName('no-display'); }, /** * @param {*} containerId */ disableOptionsContainer: function (containerId) { + var container = $(containerId); - if ($(containerId).hasClassName('no-display')) { + if (container.hasClassName('no-display')) { return; } - $$('#' + containerId + ' .widget-option').each(function (e) { + container.select('.widget-option').each(function (e) { // Avoid submitting fields of unactive container if (!e.hasClassName('skip-submit')) { e.addClassName('skip-submit'); @@ -253,7 +256,7 @@ define([ e.addClassName('obligatory'); } }); - $(containerId).addClassName('no-display'); + container.addClassName('no-display'); }, /** @@ -439,7 +442,7 @@ define([ i = 0; Form.getElements($(this.formEl)).each(function (e) { - if (!e.hasClassName('skip-submit')) { + if (jQuery(e).closest('.skip-submit, .no-display').length === 0) { formElements[i] = e; i++; } diff --git a/pub/errors/local.xml.sample b/pub/errors/local.xml.sample index b89dbb3fee8f5..c5c35559bd6d0 100644 --- a/pub/errors/local.xml.sample +++ b/pub/errors/local.xml.sample @@ -27,5 +27,22 @@ value "delete" is for cleaning --> leave + + 0 diff --git a/pub/errors/processor.php b/pub/errors/processor.php index ab21f791bc021..7cab4add51a92 100644 --- a/pub/errors/processor.php +++ b/pub/errors/processor.php @@ -8,11 +8,15 @@ namespace Magento\Framework\Error; use Magento\Framework\Serialize\Serializer\Json; +use Magento\Framework\Escaper; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\App\Response\Http; /** * Error processor * * @SuppressWarnings(PHPMD.TooManyFields) + * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) * phpcs:ignoreFile */ class Processor @@ -21,6 +25,7 @@ class Processor const MAGE_ERRORS_DESIGN_XML = 'design.xml'; const DEFAULT_SKIN = 'default'; const ERROR_DIR = 'pub/errors'; + const NUMBER_SYMBOLS_IN_SUBDIR_NAME = 2; /** * Page title @@ -67,7 +72,7 @@ class Processor /** * Report ID * - * @var int + * @var string */ public $reportId; @@ -128,7 +133,7 @@ class Processor /** * Http response * - * @var \Magento\Framework\App\Response\Http + * @var Http */ protected $_response; @@ -140,15 +145,25 @@ class Processor private $serializer; /** - * @param \Magento\Framework\App\Response\Http $response + * @var Escaper + */ + private $escaper; + + /** + * @param Http $response * @param Json $serializer + * @param Escaper $escaper */ - public function __construct(\Magento\Framework\App\Response\Http $response, Json $serializer = null) - { + public function __construct( + Http $response, + Json $serializer = null, + Escaper $escaper = null + ) { $this->_response = $response; $this->_errorDir = __DIR__ . '/'; $this->_reportDir = dirname(dirname($this->_errorDir)) . '/var/report/'; - $this->serializer = $serializer ?: \Magento\Framework\App\ObjectManager::getInstance()->get(Json::class); + $this->serializer = $serializer ?: ObjectManager::getInstance()->get(Json::class); + $this->escaper = $escaper ?: ObjectManager::getInstance()->get(Escaper::class); if (!empty($_SERVER['SCRIPT_NAME'])) { if (in_array(basename($_SERVER['SCRIPT_NAME'], '.php'), ['404', '503', 'report'])) { @@ -158,11 +173,6 @@ public function __construct(\Magento\Framework\App\Response\Http $response, Json } } - $reportId = (isset($_GET['id'])) ? (int)$_GET['id'] : null; - if ($reportId) { - $this->loadReport($reportId); - } - $this->_indexDir = $this->_getIndexDir(); $this->_root = is_dir($this->_indexDir . 'app'); @@ -170,6 +180,9 @@ public function __construct(\Magento\Framework\App\Response\Http $response, Json if (isset($_GET['skin'])) { $this->_setSkin($_GET['skin']); } + if (isset($_GET['id'])) { + $this->loadReport($_GET['id']); + } } /** @@ -371,6 +384,9 @@ protected function _prepareConfig() if ((string)$local->report->trash) { $config->trash = $local->report->trash; } + if ($local->report->dir_nesting_level) { + $config->dir_nesting_level = (int)$local->report->dir_nesting_level; + } if ((string)$local->skin) { $this->_setSkin((string)$local->skin, $config); } @@ -467,7 +483,7 @@ protected function _setReportData($reportData) $this->reportData['url'] = $this->getHostUrl() . $reportData['url']; } - if ($this->reportData['script_name']) { + if (isset($this->reportData['script_name'])) { $this->_scriptName = $this->reportData['script_name']; } } @@ -478,18 +494,20 @@ protected function _setReportData($reportData) * @param array $reportData * @return string */ - public function saveReport($reportData) + public function saveReport(array $reportData): string { - $this->reportData = $reportData; - $this->reportId = abs((int)(microtime(true) * random_int(100, 1000))); - $this->_reportFile = $this->_reportDir . '/' . $this->reportId; - $this->_setReportData($reportData); - - if (!file_exists($this->_reportDir)) { - @mkdir($this->_reportDir, 0777, true); + $this->reportId = $reportData['report_id']; + $this->_reportFile = $this->getReportPath( + $this->getReportDirNestingLevel($this->reportId), + $this->reportId + ); + $reportDirName = dirname($this->_reportFile); + if (!file_exists($reportDirName)) { + @mkdir($reportDirName, 0777, true); } + $this->_setReportData($reportData); - @file_put_contents($this->_reportFile, $this->serializer->serialize($reportData)); + @file_put_contents($this->_reportFile, $this->serializer->serialize($reportData). PHP_EOL); if (isset($reportData['skin']) && self::DEFAULT_SKIN != $reportData['skin']) { $this->_setSkin($reportData['skin']); @@ -502,19 +520,117 @@ public function saveReport($reportData) /** * Get report * - * @param int $reportId + * @param string $reportId * @return void */ public function loadReport($reportId) { - $this->reportId = $reportId; - $this->_reportFile = $this->_reportDir . '/' . $reportId; + try { + if (!$this->isReportIdValid($reportId)) { + throw new \RuntimeException("Report Id is invalid"); + } + $reportFile = $this->findReportFile($reportId); + if (!is_readable($reportFile)) { + throw new \RuntimeException("Report file cannot be read"); + } + $this->reportId = $reportId; + $this->_reportFile = $reportFile; + $this->_setReportData($this->serializer->unserialize(file_get_contents($this->_reportFile))); + } catch (\RuntimeException $e) { + $this->redirectToBaseUrl(); + } + } + + /** + * Searches for the report file and returns the path to it + * + * @param string $reportId + * @return string + * @throws \RuntimeException + */ + private function findReportFile(string $reportId): string + { + $reportFile = $this->getReportPath( + $this->getReportDirNestingLevel($reportId), + $reportId + ); + if (file_exists($reportFile)) { + return $reportFile; + } + $maxReportDirNestingLevel = $this->getMaxReportDirNestingLevel($reportId); + for ($i = 0; $i <= $maxReportDirNestingLevel; $i++) { + $reportFile = $this->getReportPath($i, $reportId); + if (file_exists($reportFile)) { + return $reportFile; + } + } + throw new \RuntimeException("Report file not found"); + } + + /** + * Redirect to a base url + * @return void + */ + private function redirectToBaseUrl() + { + header("Location: " . $this->getBaseUrl()); + die(); + } + + /** + * Checks report id + * + * @param string $reportId + * @return bool + */ + private function isReportIdValid(string $reportId): bool + { + return (bool)preg_match('/[a-fA-F0-9]{64}/', $reportId); + } + + /** + * Get path to reports + * + * @param integer $reportDirNestingLevel + * @param string $reportId + * @return string + */ + private function getReportPath(int $reportDirNestingLevel, string $reportId): string + { + $reportDirPath = $this->_reportDir; + for ($i = 0, $j = 0; $j < $reportDirNestingLevel; $i += 2, $j++) { + $reportDirPath .= $reportId[$i] . $reportId[$i + 1] . '/'; + } + return $reportDirPath . $reportId; + } - if (!file_exists($this->_reportFile) || !is_readable($this->_reportFile)) { - header("Location: " . $this->getBaseUrl()); - die(); + /** + * Returns nesting Level for the report files + * + * @var $reportId + * @return int + */ + private function getReportDirNestingLevel(string $reportId): int + { + $envName = 'MAGE_ERROR_REPORT_DIR_NESTING_LEVEL'; + $value = $_ENV[$envName] ?? getenv($envName); + if(false === $value && property_exists($this->_config, 'dir_nesting_level')) { + $value = $this->_config->dir_nesting_level; } - $this->_setReportData($this->serializer->unserialize(file_get_contents($this->_reportFile))); + $value = (int)$value; + $maxValue= $this->getMaxReportDirNestingLevel($reportId); + return 0 < $value && $maxValue >= $value ? $value : 0; + } + + /** + * Returns maximum nesting level directories of report files + * + * @param string $reportId + * @return integer + */ + private function getMaxReportDirNestingLevel(string $reportId): int + { + return (int)floor(strlen($reportId) / self::NUMBER_SYMBOLS_IN_SUBDIR_NAME); } /** @@ -528,11 +644,16 @@ public function sendReport() { $this->pageTitle = 'Error Submission Form'; - $this->postData['firstName'] = (isset($_POST['firstname'])) ? trim(htmlspecialchars($_POST['firstname'])) : ''; - $this->postData['lastName'] = (isset($_POST['lastname'])) ? trim(htmlspecialchars($_POST['lastname'])) : ''; - $this->postData['email'] = (isset($_POST['email'])) ? trim(htmlspecialchars($_POST['email'])) : ''; - $this->postData['telephone'] = (isset($_POST['telephone'])) ? trim(htmlspecialchars($_POST['telephone'])) : ''; - $this->postData['comment'] = (isset($_POST['comment'])) ? trim(htmlspecialchars($_POST['comment'])) : ''; + $this->postData['firstName'] = (isset($_POST['firstname'])) + ? trim($this->escaper->escapeHtml($_POST['firstname'])) : ''; + $this->postData['lastName'] = (isset($_POST['lastname'])) + ? trim($this->escaper->escapeHtml($_POST['lastname'])) : ''; + $this->postData['email'] = (isset($_POST['email'])) + ? trim($this->escaper->escapeHtml($_POST['email'])) : ''; + $this->postData['telephone'] = (isset($_POST['telephone'])) + ? trim($this->escaper->escapeHtml($_POST['telephone'])) : ''; + $this->postData['comment'] = (isset($_POST['comment'])) + ? trim($this->escaper->escapeHtml($_POST['comment'])) : ''; if (isset($_POST['submit'])) { if ($this->_validate()) { diff --git a/setup/performance-toolkit/benchmark.jmx b/setup/performance-toolkit/benchmark.jmx index d53d59c4f7de8..40983a097c58d 100644 --- a/setup/performance-toolkit/benchmark.jmx +++ b/setup/performance-toolkit/benchmark.jmx @@ -38390,7 +38390,7 @@ vars.put("configurable_sku", "Configurable Product - ${__time(YMD)}-${__threadNu false - {"query":"{\n products(\n filter: {\n price: {gt: \"10\"}\n or: {\n sku:{like:\"%Product%\"}\n name:{like:\"%Configurable Product%\"}\n }\n }\n pageSize: 20\n currentPage: 1\n sort: {\n price: ASC\n name:DESC\n }\n ) {\n total_count\n items {\n attribute_set_id\n country_of_manufacture\n created_at\n description {\n html\n }\n gift_message_available\n image\n {\n url\n label\n }\n meta_description\n meta_keyword\n meta_title\n name\n new_from_date\n new_to_date\n options_container\n ... on CustomizableProductInterface {\n options\n {\n title\n required\n sort_order\n }\n }\n \n price {\n minimalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n maximalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n regularPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n }\n short_description {\n html\n }\n sku\n small_image {\n url\n label\n }\n special_from_date\n special_price\n special_to_date\n swatch_image\n thumbnail\n {\n url\n label\n }\n tier_price\n type_id\n updated_at\n url_key\n url_path\n websites { id name code sort_order default_group_id is_default }\n \t... on PhysicalProductInterface {\n \tweight\n \t}\n }\n page_info {\n page_size\n current_page\n }\n }\n}\n","variables":null,"operationName":null} + {"query":"{\n products(\n filter: {\n price: {from: \"5\"}\n name:{match:\"Product\"}\n }\n pageSize: 20\n currentPage: 1\n sort: {\n price: ASC\n name:DESC\n }\n ) {\n total_count\n items {\n attribute_set_id\n country_of_manufacture\n created_at\n description {\n html\n }\n gift_message_available\n image\n {\n url\n label\n }\n meta_description\n meta_keyword\n meta_title\n name\n new_from_date\n new_to_date\n options_container\n ... on CustomizableProductInterface {\n options\n {\n title\n required\n sort_order\n }\n }\n \n price {\n minimalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n maximalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n regularPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n }\n short_description {\n html\n }\n sku\n small_image {\n url\n label\n }\n special_from_date\n special_price\n special_to_date\n swatch_image\n thumbnail\n {\n url\n label\n }\n tier_price\n type_id\n updated_at\n url_key\n url_path\n websites { id name code sort_order default_group_id is_default }\n \t... on PhysicalProductInterface {\n \tweight\n \t}\n }\n page_info {\n page_size\n current_page\n }\n }\n}\n","variables":null,"operationName":null} = @@ -38447,7 +38447,7 @@ vars.put("configurable_sku", "Configurable Product - ${__time(YMD)}-${__threadNu false - {"query":"{\n products(filter: {sku: { eq: \"${simple_product_sku}\" } })\n {\n total_count\n items {\n attribute_set_id\n categories\n {\n id\n position\n }\n country_of_manufacture\n created_at\n description {\n html\n }\n gift_message_available\n id\n image\n {\n url\n label\n }\n meta_description\n meta_keyword\n meta_title\n media_gallery_entries\n {\n disabled\n file\n id\n label\n media_type\n position\n types\n content\n {\n base64_encoded_data\n type\n name\n }\n video_content\n {\n media_type\n video_description\n video_metadata\n video_provider\n video_title\n video_url\n }\n }\n name\n new_from_date\n new_to_date\n options_container\n ... on CustomizableProductInterface {\n options\n {\n title\n required\n sort_order\n }\n }\n \n price {\n minimalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n maximalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n regularPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n }\n product_links\n {\n link_type\n linked_product_sku\n linked_product_type\n position\n sku\n }\n short_description {\n html\n }\n sku\n small_image\n {\n url\n label\n }\n special_from_date\n special_price\n special_to_date\n swatch_image\n thumbnail\n {\n url\n label\n }\n tier_price\n tier_prices\n {\n customer_group_id\n percentage_value\n qty\n value\n website_id\n }\n type_id\n updated_at\n url_key\n url_path\n websites { id name code sort_order default_group_id is_default }\n ... on PhysicalProductInterface {\n \t\t\tweight\n \t\t }\n }\n }\n}\n","variables":null,"operationName":null} + {"query":"{\n products(filter: {sku: { eq: \"${simple_product_sku}\" } },sort: {name: ASC})\n {\n total_count\n items {\n attribute_set_id\n categories\n {\n id\n position\n }\n country_of_manufacture\n created_at\n description {\n html\n }\n gift_message_available\n id\n image\n {\n url\n label\n }\n meta_description\n meta_keyword\n meta_title\n media_gallery_entries\n {\n disabled\n file\n id\n label\n media_type\n position\n types\n content\n {\n base64_encoded_data\n type\n name\n }\n video_content\n {\n media_type\n video_description\n video_metadata\n video_provider\n video_title\n video_url\n }\n }\n name\n new_from_date\n new_to_date\n options_container\n ... on CustomizableProductInterface {\n options\n {\n title\n required\n sort_order\n }\n }\n \n price {\n minimalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n maximalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n regularPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n }\n product_links\n {\n link_type\n linked_product_sku\n linked_product_type\n position\n sku\n }\n short_description {\n html\n }\n sku\n small_image\n {\n url\n label\n }\n special_from_date\n special_price\n special_to_date\n swatch_image\n thumbnail\n {\n url\n label\n }\n tier_price\n tier_prices\n {\n customer_group_id\n percentage_value\n qty\n value\n website_id\n }\n type_id\n updated_at\n url_key\n url_path\n websites { id name code sort_order default_group_id is_default }\n ... on PhysicalProductInterface {\n \t\t\tweight\n \t\t }\n }\n }\n}\n","variables":null,"operationName":null} = @@ -38523,7 +38523,7 @@ if (totalCount == null) { false - {"query":"{\n products(filter: {sku: {eq:\"${configurable_product_sku}\"} }) {\n total_count\n items {\n ... on PhysicalProductInterface {\n weight\n }\n attribute_set_id\n categories\n {\n id\n position\n }\n country_of_manufacture\n created_at\n description {\n html\n }\n gift_message_available\n id\n image\n {\n url\n label\n }\n meta_description\n meta_keyword\n meta_title\n media_gallery_entries\n {\n disabled\n file\n id\n label\n media_type\n position\n types\n content\n {\n base64_encoded_data\n type\n name\n }\n video_content\n {\n media_type\n video_description\n video_metadata\n video_provider\n video_title\n video_url\n }\n }\n name\n new_from_date\n new_to_date\n options_container\n ... on CustomizableProductInterface {\n options\n {\n title\n required\n sort_order\n }\n }\n \n price {\n minimalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n maximalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n regularPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n }\n product_links\n {\n link_type\n linked_product_sku\n linked_product_type\n position\n sku\n }\n short_description {\n html\n }\n sku\n small_image\n {\n url\n label\n }\n special_from_date\n special_price\n special_to_date\n swatch_image\n thumbnail\n {\n url\n label\n }\n tier_price\n tier_prices\n {\n customer_group_id\n percentage_value\n qty\n value\n website_id\n }\n type_id\n updated_at\n url_key\n url_path\n websites { id name code sort_order default_group_id is_default }\n ... on ConfigurableProduct {\n configurable_options {\n id\n attribute_id\n label\n position\n use_default\n attribute_code\n values {\n value_index\n label\n store_label\n default_label\n use_default_value\n }\n product_id\n }\n variants {\n product {\n ... on PhysicalProductInterface {\n weight\n }\n sku\n color\n attribute_set_id\n categories\n {\n id\n position\n }\n country_of_manufacture\n created_at\n description {\n html\n }\n gift_message_available\n id\n image\n {\n url\n label\n }\n meta_description\n meta_keyword\n meta_title\n media_gallery_entries\n {\n disabled\n file\n id\n label\n media_type\n position\n types\n content\n {\n base64_encoded_data\n type\n name\n }\n video_content\n {\n media_type\n video_description\n video_metadata\n video_provider\n video_title\n video_url\n }\n }\n name\n new_from_date\n new_to_date\n options_container\n ... on CustomizableProductInterface {\n options\n {\n title\n required\n sort_order\n }\n }\n \n price {\n minimalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n maximalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n regularPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n }\n product_links\n {\n link_type\n linked_product_sku\n linked_product_type\n position\n sku\n }\n short_description {\n html\n }\n sku\n small_image\n {\n url\n label\n }\n special_from_date\n special_price\n special_to_date\n swatch_image\n thumbnail\n {\n url\n label\n }\n tier_price\n tier_prices\n {\n customer_group_id\n percentage_value\n qty\n value\n website_id\n }\n type_id\n updated_at\n url_key\n url_path\n websites { id name code sort_order default_group_id is_default }\n\n\n }\n attributes {\n label\n code\n value_index\n }\n }\n }\n }\n }\n}\n","variables":null,"operationName":null} + {"query":"{\n products(filter: {sku: {eq:\"${configurable_product_sku}\"} }, sort: {name: ASC}) {\n total_count\n items {\n ... on PhysicalProductInterface {\n weight\n }\n attribute_set_id\n categories\n {\n id\n position\n }\n country_of_manufacture\n created_at\n description {\n html\n }\n gift_message_available\n id\n image\n {\n url\n label\n }\n meta_description\n meta_keyword\n meta_title\n media_gallery_entries\n {\n disabled\n file\n id\n label\n media_type\n position\n types\n content\n {\n base64_encoded_data\n type\n name\n }\n video_content\n {\n media_type\n video_description\n video_metadata\n video_provider\n video_title\n video_url\n }\n }\n name\n new_from_date\n new_to_date\n options_container\n ... on CustomizableProductInterface {\n options\n {\n title\n required\n sort_order\n }\n }\n \n price {\n minimalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n maximalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n regularPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n }\n product_links\n {\n link_type\n linked_product_sku\n linked_product_type\n position\n sku\n }\n short_description {\n html\n }\n sku\n small_image\n {\n url\n label\n }\n special_from_date\n special_price\n special_to_date\n swatch_image\n thumbnail\n {\n url\n label\n }\n tier_price\n tier_prices\n {\n customer_group_id\n percentage_value\n qty\n value\n website_id\n }\n type_id\n updated_at\n url_key\n url_path\n websites { id name code sort_order default_group_id is_default }\n ... on ConfigurableProduct {\n configurable_options {\n id\n attribute_id\n label\n position\n use_default\n attribute_code\n values {\n value_index\n label\n store_label\n default_label\n use_default_value\n }\n product_id\n }\n variants {\n product {\n ... on PhysicalProductInterface {\n weight\n }\n sku\n color\n attribute_set_id\n categories\n {\n id\n position\n }\n country_of_manufacture\n created_at\n description {\n html\n }\n gift_message_available\n id\n image\n {\n url\n label\n }\n meta_description\n meta_keyword\n meta_title\n media_gallery_entries\n {\n disabled\n file\n id\n label\n media_type\n position\n types\n content\n {\n base64_encoded_data\n type\n name\n }\n video_content\n {\n media_type\n video_description\n video_metadata\n video_provider\n video_title\n video_url\n }\n }\n name\n new_from_date\n new_to_date\n options_container\n ... on CustomizableProductInterface {\n options\n {\n title\n required\n sort_order\n }\n }\n \n price {\n minimalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n maximalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n regularPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n }\n product_links\n {\n link_type\n linked_product_sku\n linked_product_type\n position\n sku\n }\n short_description {\n html\n }\n sku\n small_image\n {\n url\n label\n }\n special_from_date\n special_price\n special_to_date\n swatch_image\n thumbnail\n {\n url\n label\n }\n tier_price\n tier_prices\n {\n customer_group_id\n percentage_value\n qty\n value\n website_id\n }\n type_id\n updated_at\n url_key\n url_path\n websites { id name code sort_order default_group_id is_default }\n\n\n }\n attributes {\n label\n code\n value_index\n }\n }\n }\n }\n }\n}\n","variables":null,"operationName":null} = @@ -38592,7 +38592,7 @@ if (totalCount == null) { false - {"query":"{\n products(\n pageSize:20\n currentPage:1\n search: \"configurable\"\n filter: {name: {like: \"Configurable Product%\"} }\n ) {\n total_count\n page_info {\n current_page\n page_size\n total_pages\n }\n items {\n attribute_set_id\n categories\n {\n id\n position\n }\n country_of_manufacture\n created_at\n description {\n html\n }\n gift_message_available\n id\n image\n {\n url\n label\n }\n meta_description\n meta_keyword\n meta_title\n media_gallery_entries\n {\n disabled\n file\n id\n label\n media_type\n position\n types\n content\n {\n base64_encoded_data\n type\n name\n }\n video_content\n {\n media_type\n video_description\n video_metadata\n video_provider\n video_title\n video_url\n }\n }\n name\n new_from_date\n new_to_date\n options_container\n ... on CustomizableProductInterface {\n options\n {\n title\n required\n sort_order\n }\n }\n \n price {\n minimalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n maximalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n regularPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n }\n product_links\n {\n link_type\n linked_product_sku\n linked_product_type\n position\n sku\n }\n short_description {\n html\n }\n sku\n small_image\n {\n url\n label\n }\n special_from_date\n special_price\n special_to_date\n swatch_image\n thumbnail\n {\n url\n label\n }\n tier_price\n tier_prices\n {\n customer_group_id\n percentage_value\n qty\n value\n website_id\n }\n type_id\n updated_at\n url_key\n url_path\n websites { id name code sort_order default_group_id is_default }\n ... on PhysicalProductInterface {\n \t\t\tweight\n \t\t\t}\n ... on ConfigurableProduct {\n configurable_options {\n id\n attribute_id\n label\n position\n use_default\n attribute_code\n values {\n value_index\n label\n store_label\n default_label\n use_default_value\n }\n product_id\n }\n variants {\n product {\n ... on PhysicalProductInterface {\n weight\n }\n sku\n color\n attribute_set_id\n categories\n {\n id\n position\n }\n country_of_manufacture\n created_at\n description {\n html\n }\n gift_message_available\n id\n image\n {\n url\n label\n }\n meta_description\n meta_keyword\n meta_title\n media_gallery_entries\n {\n disabled\n file\n id\n label\n media_type\n position\n types\n content\n {\n base64_encoded_data\n type\n name\n }\n video_content\n {\n media_type\n video_description\n video_metadata\n video_provider\n video_title\n video_url\n }\n }\n name\n new_from_date\n new_to_date\n options_container\n ... on CustomizableProductInterface {\n options\n {\n title\n required\n sort_order\n }\n }\n \n price {\n minimalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n maximalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n regularPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n }\n product_links\n {\n link_type\n linked_product_sku\n linked_product_type\n position\n sku\n }\n short_description {\n html\n }\n sku\n small_image\n {\n url\n label\n }\n special_from_date\n special_price\n special_to_date\n swatch_image\n thumbnail\n {\n url\n label\n }\n tier_price\n tier_prices\n {\n customer_group_id\n percentage_value\n qty\n value\n website_id\n }\n type_id\n updated_at\n url_key\n url_path\n websites { id name code sort_order default_group_id is_default }\n\n\n }\n attributes {\n label\n code\n value_index\n }\n }\n }\n }\n }\n}\n","variables":null,"operationName":null} + {"query":"{\n products(\n pageSize:20\n currentPage:1\n search: \"configurable\"\n filter: {name: {match: \"Configurable Product\"} }\n sort: {name: ASC}\n ) {\n total_count\n page_info {\n current_page\n page_size\n total_pages\n }\n items {\n attribute_set_id\n categories\n {\n id\n position\n }\n country_of_manufacture\n created_at\n description {\n html\n }\n gift_message_available\n id\n image\n {\n url\n label\n }\n meta_description\n meta_keyword\n meta_title\n media_gallery_entries\n {\n disabled\n file\n id\n label\n media_type\n position\n types\n content\n {\n base64_encoded_data\n type\n name\n }\n video_content\n {\n media_type\n video_description\n video_metadata\n video_provider\n video_title\n video_url\n }\n }\n name\n new_from_date\n new_to_date\n options_container\n ... on CustomizableProductInterface {\n options\n {\n title\n required\n sort_order\n }\n }\n \n price {\n minimalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n maximalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n regularPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n }\n product_links\n {\n link_type\n linked_product_sku\n linked_product_type\n position\n sku\n }\n short_description {\n html\n }\n sku\n small_image\n {\n url\n label\n }\n special_from_date\n special_price\n special_to_date\n swatch_image\n thumbnail\n {\n url\n label\n }\n tier_price\n tier_prices\n {\n customer_group_id\n percentage_value\n qty\n value\n website_id\n }\n type_id\n updated_at\n url_key\n url_path\n websites { id name code sort_order default_group_id is_default }\n ... on PhysicalProductInterface {\n \t\t\tweight\n \t\t\t}\n ... on ConfigurableProduct {\n configurable_options {\n id\n attribute_id\n label\n position\n use_default\n attribute_code\n values {\n value_index\n label\n store_label\n default_label\n use_default_value\n }\n product_id\n }\n variants {\n product {\n ... on PhysicalProductInterface {\n weight\n }\n sku\n color\n attribute_set_id\n categories\n {\n id\n position\n }\n country_of_manufacture\n created_at\n description {\n html\n }\n gift_message_available\n id\n image\n {\n url\n label\n }\n meta_description\n meta_keyword\n meta_title\n media_gallery_entries\n {\n disabled\n file\n id\n label\n media_type\n position\n types\n content\n {\n base64_encoded_data\n type\n name\n }\n video_content\n {\n media_type\n video_description\n video_metadata\n video_provider\n video_title\n video_url\n }\n }\n name\n new_from_date\n new_to_date\n options_container\n ... on CustomizableProductInterface {\n options\n {\n title\n required\n sort_order\n }\n }\n \n price {\n minimalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n maximalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n regularPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n }\n product_links\n {\n link_type\n linked_product_sku\n linked_product_type\n position\n sku\n }\n short_description {\n html\n }\n sku\n small_image\n {\n url\n label\n }\n special_from_date\n special_price\n special_to_date\n swatch_image\n thumbnail\n {\n url\n label\n }\n tier_price\n tier_prices\n {\n customer_group_id\n percentage_value\n qty\n value\n website_id\n }\n type_id\n updated_at\n url_key\n url_path\n websites { id name code sort_order default_group_id is_default }\n\n\n }\n attributes {\n label\n code\n value_index\n }\n }\n }\n }\n }\n}\n","variables":null,"operationName":null} = @@ -38631,7 +38631,7 @@ if (totalCount == null) { - String totalCount=vars.get("graphql_search_products_query_total_count_fulltext_filter"); + String totalCount=vars.get("graphql_search_products_query_total_count_fulltext_filter"); if (totalCount == null) { Failure = true; @@ -38660,7 +38660,7 @@ if (totalCount == null) { false - {"query":"{\n products(\n pageSize:20\n currentPage:${graphql_search_products_query_total_pages_fulltext_filter}\n search: \"configurable\"\n filter: {name: {like: \"Configurable Product%\"} }\n ) {\n total_count\n page_info {\n current_page\n page_size\n total_pages\n }\n items {\n attribute_set_id\n categories\n {\n id\n position\n }\n country_of_manufacture\n created_at\n description {\n html\n }\n gift_message_available\n id\n image\n {\n url\n label\n }\n meta_description\n meta_keyword\n meta_title\n media_gallery_entries\n {\n disabled\n file\n id\n label\n media_type\n position\n types\n content\n {\n base64_encoded_data\n type\n name\n }\n video_content\n {\n media_type\n video_description\n video_metadata\n video_provider\n video_title\n video_url\n }\n }\n name\n new_from_date\n new_to_date\n options_container\n ... on CustomizableProductInterface {\n options\n {\n title\n required\n sort_order\n }\n }\n \n price {\n minimalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n maximalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n regularPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n }\n product_links\n {\n link_type\n linked_product_sku\n linked_product_type\n position\n sku\n }\n short_description {\n html\n }\n sku\n small_image\n {\n url\n label\n }\n special_from_date\n special_price\n special_to_date\n swatch_image\n thumbnail\n {\n url\n label\n }\n tier_price\n tier_prices\n {\n customer_group_id\n percentage_value\n qty\n value\n website_id\n }\n type_id\n updated_at\n url_key\n url_path\n websites { id name code sort_order default_group_id is_default }\n ... on PhysicalProductInterface {\n \t\t\tweight\n \t\t\t}\n ... on ConfigurableProduct {\n configurable_options {\n id\n attribute_id\n label\n position\n use_default\n attribute_code\n values {\n value_index\n label\n store_label\n default_label\n use_default_value\n }\n product_id\n }\n variants {\n product {\n ... on PhysicalProductInterface {\n weight\n }\n sku\n color\n attribute_set_id\n categories\n {\n id\n position\n }\n country_of_manufacture\n created_at\n description {\n html\n }\n gift_message_available\n id\n image\n {\n url\n label\n }\n meta_description\n meta_keyword\n meta_title\n media_gallery_entries\n {\n disabled\n file\n id\n label\n media_type\n position\n types\n content\n {\n base64_encoded_data\n type\n name\n }\n video_content\n {\n media_type\n video_description\n video_metadata\n video_provider\n video_title\n video_url\n }\n }\n name\n new_from_date\n new_to_date\n options_container\n ... on CustomizableProductInterface {\n options\n {\n title\n required\n sort_order\n }\n }\n \n price {\n minimalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n maximalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n regularPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n }\n product_links\n {\n link_type\n linked_product_sku\n linked_product_type\n position\n sku\n }\n short_description {\n html\n }\n sku\n small_image\n {\n url\n label\n }\n special_from_date\n special_price\n special_to_date\n swatch_image\n thumbnail\n {\n url\n label\n }\n tier_price\n tier_prices\n {\n customer_group_id\n percentage_value\n qty\n value\n website_id\n }\n type_id\n updated_at\n url_key\n url_path\n websites { id name code sort_order default_group_id is_default }\n\n\n }\n attributes {\n label\n code\n value_index\n }\n }\n }\n }\n }\n}\n","variables":null,"operationName":null} + {"query":"{\n products(\n pageSize:20\n currentPage:${graphql_search_products_query_total_pages_fulltext_filter}\n search: \"configurable\"\n filter: {name: {match: \"Configurable Product\"} }\n sort: {name: ASC}\n ) {\n total_count\n page_info {\n current_page\n page_size\n total_pages\n }\n items {\n attribute_set_id\n categories\n {\n id\n position\n }\n country_of_manufacture\n created_at\n description {\n html\n }\n gift_message_available\n id\n image\n {\n url\n label\n }\n meta_description\n meta_keyword\n meta_title\n media_gallery_entries\n {\n disabled\n file\n id\n label\n media_type\n position\n types\n content\n {\n base64_encoded_data\n type\n name\n }\n video_content\n {\n media_type\n video_description\n video_metadata\n video_provider\n video_title\n video_url\n }\n }\n name\n new_from_date\n new_to_date\n options_container\n ... on CustomizableProductInterface {\n options\n {\n title\n required\n sort_order\n }\n }\n \n price {\n minimalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n maximalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n regularPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n }\n product_links\n {\n link_type\n linked_product_sku\n linked_product_type\n position\n sku\n }\n short_description {\n html\n }\n sku\n small_image\n {\n url\n label\n }\n special_from_date\n special_price\n special_to_date\n swatch_image\n thumbnail\n {\n url\n label\n }\n tier_price\n tier_prices\n {\n customer_group_id\n percentage_value\n qty\n value\n website_id\n }\n type_id\n updated_at\n url_key\n url_path\n websites { id name code sort_order default_group_id is_default }\n ... on PhysicalProductInterface {\n \t\t\tweight\n \t\t\t}\n ... on ConfigurableProduct {\n configurable_options {\n id\n attribute_id\n label\n position\n use_default\n attribute_code\n values {\n value_index\n label\n store_label\n default_label\n use_default_value\n }\n product_id\n }\n variants {\n product {\n ... on PhysicalProductInterface {\n weight\n }\n sku\n color\n attribute_set_id\n categories\n {\n id\n position\n }\n country_of_manufacture\n created_at\n description {\n html\n }\n gift_message_available\n id\n image\n {\n url\n label\n }\n meta_description\n meta_keyword\n meta_title\n media_gallery_entries\n {\n disabled\n file\n id\n label\n media_type\n position\n types\n content\n {\n base64_encoded_data\n type\n name\n }\n video_content\n {\n media_type\n video_description\n video_metadata\n video_provider\n video_title\n video_url\n }\n }\n name\n new_from_date\n new_to_date\n options_container\n ... on CustomizableProductInterface {\n options\n {\n title\n required\n sort_order\n }\n }\n \n price {\n minimalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n maximalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n regularPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n }\n product_links\n {\n link_type\n linked_product_sku\n linked_product_type\n position\n sku\n }\n short_description {\n html\n }\n sku\n small_image\n {\n url\n label\n }\n special_from_date\n special_price\n special_to_date\n swatch_image\n thumbnail\n {\n url\n label\n }\n tier_price\n tier_prices\n {\n customer_group_id\n percentage_value\n qty\n value\n website_id\n }\n type_id\n updated_at\n url_key\n url_path\n websites { id name code sort_order default_group_id is_default }\n\n\n }\n attributes {\n label\n code\n value_index\n }\n }\n }\n }\n }\n}\n","variables":null,"operationName":null} = @@ -38720,7 +38720,7 @@ if (totalCount == null) { false - {"query":"{\n products(\n pageSize:20\n currentPage:1\n search: \"configurable\") {\n total_count\n items {\n attribute_set_id\n categories\n {\n id\n position\n }\n country_of_manufacture\n created_at\n description {\n html\n }\n gift_message_available\n id\n image\n {\n url\n label\n }\n meta_description\n meta_keyword\n meta_title\n media_gallery_entries\n {\n disabled\n file\n id\n label\n media_type\n position\n types\n content\n {\n base64_encoded_data\n type\n name\n }\n video_content\n {\n media_type\n video_description\n video_metadata\n video_provider\n video_title\n video_url\n }\n }\n name\n new_from_date\n new_to_date\n options_container\n ... on CustomizableProductInterface {\n options\n {\n title\n required\n sort_order\n }\n }\n \n price {\n minimalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n maximalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n regularPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n }\n product_links\n {\n link_type\n linked_product_sku\n linked_product_type\n position\n sku\n }\n short_description {\n html\n }\n sku\n small_image\n {\n url\n label\n }\n special_from_date\n special_price\n special_to_date\n swatch_image\n thumbnail\n {\n url\n label\n }\n tier_price\n tier_prices\n {\n customer_group_id\n percentage_value\n qty\n value\n website_id\n }\n type_id\n updated_at\n url_key\n url_path\n websites { id name code sort_order default_group_id is_default }\n ... on PhysicalProductInterface {\n \t\t\tweight\n \t\t\t}\n ... on ConfigurableProduct {\n configurable_options {\n id\n attribute_id\n label\n position\n use_default\n attribute_code\n values {\n value_index\n label\n store_label\n default_label\n use_default_value\n }\n product_id\n }\n variants {\n product {\n ... on PhysicalProductInterface {\n weight\n }\n sku\n color\n attribute_set_id\n categories\n {\n id\n position\n }\n country_of_manufacture\n created_at\n description {\n html\n }\n gift_message_available\n id\n image\n {\n url\n label\n }\n meta_description\n meta_keyword\n meta_title\n media_gallery_entries\n {\n disabled\n file\n id\n label\n media_type\n position\n types\n content\n {\n base64_encoded_data\n type\n name\n }\n video_content\n {\n media_type\n video_description\n video_metadata\n video_provider\n video_title\n video_url\n }\n }\n name\n new_from_date\n new_to_date\n options_container\n ... on CustomizableProductInterface {\n options\n {\n title\n required\n sort_order\n }\n }\n \n price {\n minimalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n maximalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n regularPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n }\n product_links\n {\n link_type\n linked_product_sku\n linked_product_type\n position\n sku\n }\n short_description {\n html\n }\n sku\n small_image {\n url\n label\n }\n special_from_date\n special_price\n special_to_date\n swatch_image\n thumbnail\n {\n url\n label\n }\n tier_price\n tier_prices\n {\n customer_group_id\n percentage_value\n qty\n value\n website_id\n }\n type_id\n updated_at\n url_key\n url_path\n websites { id name code sort_order default_group_id is_default }\n\n\n }\n attributes {\n label\n code\n value_index\n }\n }\n }\n }\n }\n}\n","variables":null,"operationName":null} + {"query":"{\n products(\n pageSize:20\n currentPage:1\n search: \"configurable\"\n sort: {name: ASC}) {\n total_count\n items {\n attribute_set_id\n categories\n {\n id\n position\n }\n country_of_manufacture\n created_at\n description {\n html\n }\n gift_message_available\n id\n image\n {\n url\n label\n }\n meta_description\n meta_keyword\n meta_title\n media_gallery_entries\n {\n disabled\n file\n id\n label\n media_type\n position\n types\n content\n {\n base64_encoded_data\n type\n name\n }\n video_content\n {\n media_type\n video_description\n video_metadata\n video_provider\n video_title\n video_url\n }\n }\n name\n new_from_date\n new_to_date\n options_container\n ... on CustomizableProductInterface {\n options\n {\n title\n required\n sort_order\n }\n }\n \n price {\n minimalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n maximalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n regularPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n }\n product_links\n {\n link_type\n linked_product_sku\n linked_product_type\n position\n sku\n }\n short_description {\n html\n }\n sku\n small_image\n {\n url\n label\n }\n special_from_date\n special_price\n special_to_date\n swatch_image\n thumbnail\n {\n url\n label\n }\n tier_price\n tier_prices\n {\n customer_group_id\n percentage_value\n qty\n value\n website_id\n }\n type_id\n updated_at\n url_key\n url_path\n websites { id name code sort_order default_group_id is_default }\n ... on PhysicalProductInterface {\n \t\t\tweight\n \t\t\t}\n ... on ConfigurableProduct {\n configurable_options {\n id\n attribute_id\n label\n position\n use_default\n attribute_code\n values {\n value_index\n label\n store_label\n default_label\n use_default_value\n }\n product_id\n }\n variants {\n product {\n ... on PhysicalProductInterface {\n weight\n }\n sku\n color\n attribute_set_id\n categories\n {\n id\n position\n }\n country_of_manufacture\n created_at\n description {\n html\n }\n gift_message_available\n id\n image\n {\n url\n label\n }\n meta_description\n meta_keyword\n meta_title\n media_gallery_entries\n {\n disabled\n file\n id\n label\n media_type\n position\n types\n content\n {\n base64_encoded_data\n type\n name\n }\n video_content\n {\n media_type\n video_description\n video_metadata\n video_provider\n video_title\n video_url\n }\n }\n name\n new_from_date\n new_to_date\n options_container\n ... on CustomizableProductInterface {\n options\n {\n title\n required\n sort_order\n }\n }\n \n price {\n minimalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n maximalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n regularPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n }\n product_links\n {\n link_type\n linked_product_sku\n linked_product_type\n position\n sku\n }\n short_description {\n html\n }\n sku\n small_image {\n url\n label\n }\n special_from_date\n special_price\n special_to_date\n swatch_image\n thumbnail\n {\n url\n label\n }\n tier_price\n tier_prices\n {\n customer_group_id\n percentage_value\n qty\n value\n website_id\n }\n type_id\n updated_at\n url_key\n url_path\n websites { id name code sort_order default_group_id is_default }\n\n\n }\n attributes {\n label\n code\n value_index\n }\n }\n }\n }\n }\n}\n","variables":null,"operationName":null} = @@ -38780,7 +38780,7 @@ if (totalCount == null) { false - {"query":"{\n products(\n pageSize:20\n currentPage:1\n search: \"Option 1\") {\n filters {\n name\n filter_items_count\n request_var\n filter_items {\n label\n value_string\n items_count\n ... on SwatchLayerFilterItemInterface {\n swatch_data {\n type\n value\n }\n }\n }\n }\n total_count\n items {\n attribute_set_id\n categories\n {\n id\n position\n }\n country_of_manufacture\n created_at\n description {\n html\n }\n gift_message_available\n id\n image\n {\n url\n label\n }\n meta_description\n meta_keyword\n meta_title\n media_gallery_entries\n {\n disabled\n file\n id\n label\n media_type\n position\n types\n content\n {\n base64_encoded_data\n type\n name\n }\n video_content\n {\n media_type\n video_description\n video_metadata\n video_provider\n video_title\n video_url\n }\n }\n name\n new_from_date\n new_to_date\n options_container\n ... on CustomizableProductInterface {\n options\n {\n title\n required\n sort_order\n }\n }\n \n price {\n minimalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n maximalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n regularPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n }\n product_links\n {\n link_type\n linked_product_sku\n linked_product_type\n position\n sku\n }\n short_description {\n html\n }\n sku\n small_image\n {\n url\n label\n }\n special_from_date\n special_price\n special_to_date\n swatch_image\n thumbnail\n {\n url\n label\n }\n tier_price\n tier_prices\n {\n customer_group_id\n percentage_value\n qty\n value\n website_id\n }\n type_id\n updated_at\n url_key\n url_path\n websites { id name code sort_order default_group_id is_default }\n ... on PhysicalProductInterface {\n weight\n }\n ... on ConfigurableProduct {\n configurable_options {\n id\n attribute_id\n label\n position\n use_default\n attribute_code\n values {\n value_index\n label\n store_label\n default_label\n use_default_value\n }\n product_id\n }\n variants {\n product {\n ... on PhysicalProductInterface {\n weight\n }\n sku\n color\n attribute_set_id\n categories\n {\n id\n position\n }\n country_of_manufacture\n created_at\n description {\n html\n }\n gift_message_available\n id\n image\n {\n url\n label\n }\n meta_description\n meta_keyword\n meta_title\n media_gallery_entries\n {\n disabled\n file\n id\n label\n media_type\n position\n types\n content\n {\n base64_encoded_data\n type\n name\n }\n video_content\n {\n media_type\n video_description\n video_metadata\n video_provider\n video_title\n video_url\n }\n }\n name\n new_from_date\n new_to_date\n options_container\n ... on CustomizableProductInterface {\n options\n {\n title\n required\n sort_order\n }\n }\n \n price {\n minimalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n maximalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n regularPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n }\n product_links\n {\n link_type\n linked_product_sku\n linked_product_type\n position\n sku\n }\n short_description {\n html\n }\n sku\n small_image\n {\n url\n label\n }\n special_from_date\n special_price\n special_to_date\n swatch_image\n thumbnail\n {\n url\n label\n }\n tier_price\n tier_prices\n {\n customer_group_id\n percentage_value\n qty\n value\n website_id\n }\n type_id\n updated_at\n url_key\n url_path\n websites { id name code sort_order default_group_id is_default }\n\n\n }\n attributes {\n label\n code\n value_index\n }\n }\n }\n }\n }\n}","variables":null,"operationName":null} + {"query":"{\n products(\n pageSize:20\n currentPage:1\n search: \"Option 1\"\n sort: {name: ASC}) {\n filters {\n name\n filter_items_count\n request_var\n filter_items {\n label\n value_string\n items_count\n ... on SwatchLayerFilterItemInterface {\n swatch_data {\n type\n value\n }\n }\n }\n }\n total_count\n items {\n attribute_set_id\n categories\n {\n id\n position\n }\n country_of_manufacture\n created_at\n description {\n html\n }\n gift_message_available\n id\n image\n {\n url\n label\n }\n meta_description\n meta_keyword\n meta_title\n media_gallery_entries\n {\n disabled\n file\n id\n label\n media_type\n position\n types\n content\n {\n base64_encoded_data\n type\n name\n }\n video_content\n {\n media_type\n video_description\n video_metadata\n video_provider\n video_title\n video_url\n }\n }\n name\n new_from_date\n new_to_date\n options_container\n ... on CustomizableProductInterface {\n options\n {\n title\n required\n sort_order\n }\n }\n \n price {\n minimalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n maximalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n regularPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n }\n product_links\n {\n link_type\n linked_product_sku\n linked_product_type\n position\n sku\n }\n short_description {\n html\n }\n sku\n small_image\n {\n url\n label\n }\n special_from_date\n special_price\n special_to_date\n swatch_image\n thumbnail\n {\n url\n label\n }\n tier_price\n tier_prices\n {\n customer_group_id\n percentage_value\n qty\n value\n website_id\n }\n type_id\n updated_at\n url_key\n url_path\n websites { id name code sort_order default_group_id is_default }\n ... on PhysicalProductInterface {\n weight\n }\n ... on ConfigurableProduct {\n configurable_options {\n id\n attribute_id\n label\n position\n use_default\n attribute_code\n values {\n value_index\n label\n store_label\n default_label\n use_default_value\n }\n product_id\n }\n variants {\n product {\n ... on PhysicalProductInterface {\n weight\n }\n sku\n color\n attribute_set_id\n categories\n {\n id\n position\n }\n country_of_manufacture\n created_at\n description {\n html\n }\n gift_message_available\n id\n image\n {\n url\n label\n }\n meta_description\n meta_keyword\n meta_title\n media_gallery_entries\n {\n disabled\n file\n id\n label\n media_type\n position\n types\n content\n {\n base64_encoded_data\n type\n name\n }\n video_content\n {\n media_type\n video_description\n video_metadata\n video_provider\n video_title\n video_url\n }\n }\n name\n new_from_date\n new_to_date\n options_container\n ... on CustomizableProductInterface {\n options\n {\n title\n required\n sort_order\n }\n }\n \n price {\n minimalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n maximalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n regularPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n }\n product_links\n {\n link_type\n linked_product_sku\n linked_product_type\n position\n sku\n }\n short_description {\n html\n }\n sku\n small_image\n {\n url\n label\n }\n special_from_date\n special_price\n special_to_date\n swatch_image\n thumbnail\n {\n url\n label\n }\n tier_price\n tier_prices\n {\n customer_group_id\n percentage_value\n qty\n value\n website_id\n }\n type_id\n updated_at\n url_key\n url_path\n websites { id name code sort_order default_group_id is_default }\n\n\n }\n attributes {\n label\n code\n value_index\n }\n }\n }\n }\n }\n}","variables":null,"operationName":null} = @@ -38834,13 +38834,70 @@ if (totalCount == null) { + + true + + + + false + {"query":"{\n products(\n pageSize:20\n currentPage:1\n search: \"Option 1\"\n sort: {name: ASC}) {\n aggregations{\n attribute_code\n count\n label\n options{\n count\n label\n value\n }\n }\n total_count\n items {\n attribute_set_id\n categories\n {\n id\n position\n }\n country_of_manufacture\n created_at\n description {\n html\n }\n gift_message_available\n id\n image\n {\n url\n label\n }\n meta_description\n meta_keyword\n meta_title\n media_gallery_entries\n {\n disabled\n file\n id\n label\n media_type\n position\n types\n content\n {\n base64_encoded_data\n type\n name\n }\n video_content\n {\n media_type\n video_description\n video_metadata\n video_provider\n video_title\n video_url\n }\n }\n name\n new_from_date\n new_to_date\n options_container\n ... on CustomizableProductInterface {\n options\n {\n title\n required\n sort_order\n }\n }\n \n price {\n minimalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n maximalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n regularPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n }\n product_links\n {\n link_type\n linked_product_sku\n linked_product_type\n position\n sku\n }\n short_description {\n html\n }\n sku\n small_image\n {\n url\n label\n }\n special_from_date\n special_price\n special_to_date\n swatch_image\n thumbnail\n {\n url\n label\n }\n tier_price\n tier_prices\n {\n customer_group_id\n percentage_value\n qty\n value\n website_id\n }\n type_id\n updated_at\n url_key\n url_path\n websites { id name code sort_order default_group_id is_default }\n ... on PhysicalProductInterface {\n weight\n }\n ... on ConfigurableProduct {\n configurable_options {\n id\n attribute_id\n label\n position\n use_default\n attribute_code\n values {\n value_index\n label\n store_label\n default_label\n use_default_value\n }\n product_id\n }\n variants {\n product {\n ... on PhysicalProductInterface {\n weight\n }\n sku\n color\n attribute_set_id\n categories\n {\n id\n position\n }\n country_of_manufacture\n created_at\n description {\n html\n }\n gift_message_available\n id\n image\n {\n url\n label\n }\n meta_description\n meta_keyword\n meta_title\n media_gallery_entries\n {\n disabled\n file\n id\n label\n media_type\n position\n types\n content\n {\n base64_encoded_data\n type\n name\n }\n video_content\n {\n media_type\n video_description\n video_metadata\n video_provider\n video_title\n video_url\n }\n }\n name\n new_from_date\n new_to_date\n options_container\n ... on CustomizableProductInterface {\n options\n {\n title\n required\n sort_order\n }\n }\n \n price {\n minimalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n maximalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n regularPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n }\n product_links\n {\n link_type\n linked_product_sku\n linked_product_type\n position\n sku\n }\n short_description {\n html\n }\n sku\n small_image\n {\n url\n label\n }\n special_from_date\n special_price\n special_to_date\n swatch_image\n thumbnail\n {\n url\n label\n }\n tier_price\n tier_prices\n {\n customer_group_id\n percentage_value\n qty\n value\n website_id\n }\n type_id\n updated_at\n url_key\n url_path\n websites { id name code sort_order default_group_id is_default }\n\n\n }\n attributes {\n label\n code\n value_index\n }\n }\n }\n }\n }\n}","variables":null,"operationName":null} + = + + + + + + + + ${request_protocol} + + ${base_path}graphql + POST + true + false + true + false + false + + mpaf/tool/fragments/ce/graphql/query_multiple_products_with_extensible_data_objects_using_full_text_and_aggregations.jmx + + + + graphql_search_products_query_total_count + $.data.products.total_count + + + BODY + + + + String totalCount=vars.get("graphql_search_products_query_total_count"); + if (totalCount == null) { + Failure = true; + FailureMessage = "Not Expected \"totalCount\" to be null"; + } else { + if (Integer.parseInt(totalCount) < 1) { + Failure = true; + FailureMessage = "Expected \"totalCount\" to be greater than zero, Actual: " + totalCount; + } else { + Failure = false; + } + } + + + + false + + + + true false - {"query":"{\nproducts(filter: {sku: {eq:\"${bundle_product_sku}\"} }) {\n total_count\n items {\n ... on PhysicalProductInterface {\n weight\n }\n attribute_set_id\n categories\n {\n id\n position\n }\n country_of_manufacture\n created_at\n description {\n html\n }\n gift_message_available\n id\n image\n {\n url\n label\n }\n meta_description\n meta_keyword\n meta_title\n media_gallery_entries\n {\n disabled\n file\n id\n label\n media_type\n position\n types\n content\n {\n base64_encoded_data\n type\n name\n }\n video_content\n {\n media_type\n video_description\n video_metadata\n video_provider\n video_title\n video_url\n }\n }\n name\n new_from_date\n new_to_date\n options_container\n ... on CustomizableProductInterface {\n options\n {\n title\n required\n sort_order\n }\n }\n \n price {\n minimalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n maximalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n regularPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n }\n product_links\n {\n link_type\n linked_product_sku\n linked_product_type\n position\n sku\n }\n short_description {\n html\n }\n sku\n small_image {\n url\n label\n }\n special_from_date\n special_price\n special_to_date\n swatch_image\n thumbnail\n {\n url\n label\n }\n tier_price\n tier_prices\n {\n customer_group_id\n percentage_value\n qty\n value\n website_id\n }\n type_id\n updated_at\n url_key\n url_path\n websites { id name code sort_order default_group_id is_default }\n ... on BundleProduct {\n weight\n price_view\n dynamic_price\n dynamic_sku\n ship_bundle_items\n dynamic_weight\n items {\n option_id\n title\n required\n type\n position\n sku\n options {\n id\n qty\n position\n is_default\n price\n price_type\n can_change_quantity\n product {\n attribute_set_id\n categories\n {\n id\n position\n }\n country_of_manufacture\n created_at\n description {\n html\n }\n gift_message_available\n id\n image\n {\n url\n label\n }\n meta_description\n meta_keyword\n meta_title\n media_gallery_entries\n {\n disabled\n file\n id\n label\n media_type\n position\n types\n content\n {\n base64_encoded_data\n type\n name\n }\n video_content\n {\n media_type\n video_description\n video_metadata\n video_provider\n video_title\n video_url\n }\n }\n name\n new_from_date\n new_to_date\n options_container\n ... on CustomizableProductInterface {\n options\n {\n title\n required\n sort_order\n }\n }\n \n price {\n minimalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n maximalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n regularPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n }\n product_links\n {\n link_type\n linked_product_sku\n linked_product_type\n position\n sku\n }\n short_description {\n html\n }\n sku\n small_image {\n url\n label\n }\n special_from_date\n special_price\n special_to_date\n swatch_image\n thumbnail\n {\n url\n label\n }\n tier_price\n tier_prices\n {\n customer_group_id\n percentage_value\n qty\n value\n website_id\n }\n type_id\n updated_at\n url_key\n url_path\n websites { id name code sort_order default_group_id is_default }\n }\n }\n }\n }\n }\n }\n}\n","variables":null,"operationName":null} + {"query":"{\nproducts(filter: {sku: {eq:\"${bundle_product_sku}\"} }, sort: {name: ASC}) {\n total_count\n items {\n ... on PhysicalProductInterface {\n weight\n }\n attribute_set_id\n categories\n {\n id\n position\n }\n country_of_manufacture\n created_at\n description {\n html\n }\n gift_message_available\n id\n image\n {\n url\n label\n }\n meta_description\n meta_keyword\n meta_title\n media_gallery_entries\n {\n disabled\n file\n id\n label\n media_type\n position\n types\n content\n {\n base64_encoded_data\n type\n name\n }\n video_content\n {\n media_type\n video_description\n video_metadata\n video_provider\n video_title\n video_url\n }\n }\n name\n new_from_date\n new_to_date\n options_container\n ... on CustomizableProductInterface {\n options\n {\n title\n required\n sort_order\n }\n }\n \n price {\n minimalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n maximalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n regularPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n }\n product_links\n {\n link_type\n linked_product_sku\n linked_product_type\n position\n sku\n }\n short_description {\n html\n }\n sku\n small_image {\n url\n label\n }\n special_from_date\n special_price\n special_to_date\n swatch_image\n thumbnail\n {\n url\n label\n }\n tier_price\n tier_prices\n {\n customer_group_id\n percentage_value\n qty\n value\n website_id\n }\n type_id\n updated_at\n url_key\n url_path\n websites { id name code sort_order default_group_id is_default }\n ... on BundleProduct {\n weight\n price_view\n dynamic_price\n dynamic_sku\n ship_bundle_items\n dynamic_weight\n items {\n option_id\n title\n required\n type\n position\n sku\n options {\n id\n qty\n position\n is_default\n price\n price_type\n can_change_quantity\n product {\n attribute_set_id\n categories\n {\n id\n position\n }\n country_of_manufacture\n created_at\n description {\n html\n }\n gift_message_available\n id\n image\n {\n url\n label\n }\n meta_description\n meta_keyword\n meta_title\n media_gallery_entries\n {\n disabled\n file\n id\n label\n media_type\n position\n types\n content\n {\n base64_encoded_data\n type\n name\n }\n video_content\n {\n media_type\n video_description\n video_metadata\n video_provider\n video_title\n video_url\n }\n }\n name\n new_from_date\n new_to_date\n options_container\n ... on CustomizableProductInterface {\n options\n {\n title\n required\n sort_order\n }\n }\n \n price {\n minimalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n maximalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n regularPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n }\n product_links\n {\n link_type\n linked_product_sku\n linked_product_type\n position\n sku\n }\n short_description {\n html\n }\n sku\n small_image {\n url\n label\n }\n special_from_date\n special_price\n special_to_date\n swatch_image\n thumbnail\n {\n url\n label\n }\n tier_price\n tier_prices\n {\n customer_group_id\n percentage_value\n qty\n value\n website_id\n }\n type_id\n updated_at\n url_key\n url_path\n websites { id name code sort_order default_group_id is_default }\n }\n }\n }\n }\n }\n }\n}\n","variables":null,"operationName":null} = @@ -38909,7 +38966,7 @@ if (totalCount == null) { false - {"query":"{\n products(filter: {sku: { eq: \"${downloadable_product_sku}\" } })\n {\n total_count\n items {\n attribute_set_id\n categories\n {\n id\n position\n }\n country_of_manufacture\n created_at\n description {\n html\n }\n gift_message_available\n id\n image\n {\n url\n label\n }\n meta_description\n meta_keyword\n meta_title\n media_gallery_entries\n {\n disabled\n file\n id\n label\n media_type\n position\n types\n content\n {\n base64_encoded_data\n type\n name\n }\n video_content\n {\n media_type\n video_description\n video_metadata\n video_provider\n video_title\n video_url\n }\n }\n name\n new_from_date\n new_to_date\n options_container\n ... on CustomizableProductInterface {\n options\n {\n title\n required\n sort_order\n }\n }\n \n price {\n minimalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n maximalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n regularPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n }\n product_links\n {\n link_type\n linked_product_sku\n linked_product_type\n position\n sku\n }\n short_description {\n html\n }\n sku\n small_image\n {\n url\n label\n }\n special_from_date\n special_price\n special_to_date\n swatch_image\n thumbnail\n {\n url\n label\n }\n tier_price\n tier_prices\n {\n customer_group_id\n percentage_value\n qty\n value\n website_id\n }\n type_id\n updated_at\n url_key\n url_path\n websites { id name code sort_order default_group_id is_default }\n ... on PhysicalProductInterface {\n \t\t\tweight\n \t\t }\n ... on DownloadableProduct {\n links_purchased_separately\n links_title\n downloadable_product_samples {\n id\n title\n sort_order\n sample_type\n sample_file\n sample_url\n }\n downloadable_product_links {\n id\n title\n sort_order\n is_shareable\n price\n number_of_downloads\n link_type\n sample_type\n sample_file\n sample_url\n }\n }\n }\n }\n}\n","variables":null,"operationName":null} + {"query":"{\n products(filter: {sku: { eq: \"${downloadable_product_sku}\" } }, sort: {name: ASC})\n {\n total_count\n items {\n attribute_set_id\n categories\n {\n id\n position\n }\n country_of_manufacture\n created_at\n description {\n html\n }\n gift_message_available\n id\n image\n {\n url\n label\n }\n meta_description\n meta_keyword\n meta_title\n media_gallery_entries\n {\n disabled\n file\n id\n label\n media_type\n position\n types\n content\n {\n base64_encoded_data\n type\n name\n }\n video_content\n {\n media_type\n video_description\n video_metadata\n video_provider\n video_title\n video_url\n }\n }\n name\n new_from_date\n new_to_date\n options_container\n ... on CustomizableProductInterface {\n options\n {\n title\n required\n sort_order\n }\n }\n \n price {\n minimalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n maximalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n regularPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n }\n product_links\n {\n link_type\n linked_product_sku\n linked_product_type\n position\n sku\n }\n short_description {\n html\n }\n sku\n small_image\n {\n url\n label\n }\n special_from_date\n special_price\n special_to_date\n swatch_image\n thumbnail\n {\n url\n label\n }\n tier_price\n tier_prices\n {\n customer_group_id\n percentage_value\n qty\n value\n website_id\n }\n type_id\n updated_at\n url_key\n url_path\n websites { id name code sort_order default_group_id is_default }\n ... on PhysicalProductInterface {\n \t\t\tweight\n \t\t }\n ... on DownloadableProduct {\n links_purchased_separately\n links_title\n downloadable_product_samples {\n id\n title\n sort_order\n sample_type\n sample_file\n sample_url\n }\n downloadable_product_links {\n id\n title\n sort_order\n is_shareable\n price\n number_of_downloads\n link_type\n sample_type\n sample_file\n sample_url\n }\n }\n }\n }\n}\n","variables":null,"operationName":null} = @@ -38996,7 +39053,7 @@ if (totalCount == null) { false - {"query":"{\n products(filter: {sku: { eq: \"${virtual_product_sku}\" } })\n {\n total_count\n items {\n attribute_set_id\n categories\n {\n id\n position\n }\n country_of_manufacture\n created_at\n description {\n html\n }\n gift_message_available\n id\n image\n {\n url\n label\n }\n meta_description\n meta_keyword\n meta_title\n media_gallery_entries\n {\n disabled\n file\n id\n label\n media_type\n position\n types\n content\n {\n base64_encoded_data\n type\n name\n }\n video_content\n {\n media_type\n video_description\n video_metadata\n video_provider\n video_title\n video_url\n }\n }\n name\n new_from_date\n new_to_date\n options_container\n ... on CustomizableProductInterface {\n options\n {\n title\n required\n sort_order\n }\n }\n \n price {\n minimalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n maximalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n regularPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n }\n product_links\n {\n link_type\n linked_product_sku\n linked_product_type\n position\n sku\n }\n short_description {\n html\n }\n sku\n small_image\n {\n url\n label\n }\n special_from_date\n special_price\n special_to_date\n swatch_image\n thumbnail\n {\n url\n label\n }\n tier_price\n tier_prices\n {\n customer_group_id\n percentage_value\n qty\n value\n website_id\n }\n type_id\n updated_at\n url_key\n url_path\n websites { id name code sort_order default_group_id is_default }\n ... on PhysicalProductInterface {\n \t\t\tweight\n \t\t }\n }\n }\n}\n","variables":null,"operationName":null} + {"query":"{\n products(filter: {sku: { eq: \"${virtual_product_sku}\" } },sort: {name: ASC})\n {\n total_count\n items {\n attribute_set_id\n categories\n {\n id\n position\n }\n country_of_manufacture\n created_at\n description {\n html\n }\n gift_message_available\n id\n image\n {\n url\n label\n }\n meta_description\n meta_keyword\n meta_title\n media_gallery_entries\n {\n disabled\n file\n id\n label\n media_type\n position\n types\n content\n {\n base64_encoded_data\n type\n name\n }\n video_content\n {\n media_type\n video_description\n video_metadata\n video_provider\n video_title\n video_url\n }\n }\n name\n new_from_date\n new_to_date\n options_container\n ... on CustomizableProductInterface {\n options\n {\n title\n required\n sort_order\n }\n }\n \n price {\n minimalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n maximalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n regularPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n }\n product_links\n {\n link_type\n linked_product_sku\n linked_product_type\n position\n sku\n }\n short_description {\n html\n }\n sku\n small_image\n {\n url\n label\n }\n special_from_date\n special_price\n special_to_date\n swatch_image\n thumbnail\n {\n url\n label\n }\n tier_price\n tier_prices\n {\n customer_group_id\n percentage_value\n qty\n value\n website_id\n }\n type_id\n updated_at\n url_key\n url_path\n websites { id name code sort_order default_group_id is_default }\n ... on PhysicalProductInterface {\n \t\t\tweight\n \t\t }\n }\n }\n}\n","variables":null,"operationName":null} = @@ -39063,7 +39120,7 @@ if (totalCount == null) { false - {"query":"{\nproducts(filter: {sku: {eq:\"${grouped_product_sku}\"} }) {\n total_count\n items {\n attribute_set_id\n categories\n {\n id\n position\n }\n country_of_manufacture\n created_at\n description {\n html\n }\n gift_message_available\n id\n image\n {\n url\n label\n }\n meta_description\n meta_keyword\n meta_title\n media_gallery_entries\n {\n disabled\n file\n id\n label\n media_type\n position\n types\n content\n {\n base64_encoded_data\n type\n name\n }\n video_content\n {\n media_type\n video_description\n video_metadata\n video_provider\n video_title\n video_url\n }\n }\n name\n new_from_date\n new_to_date\n options_container\n ... on CustomizableProductInterface {\n options\n {\n title\n required\n sort_order\n }\n }\n \n price {\n minimalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n maximalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n regularPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n }\n product_links\n {\n link_type\n linked_product_sku\n linked_product_type\n position\n sku\n }\n short_description {\n html\n }\n sku\n small_image\n {\n url\n label\n }\n special_from_date\n special_price\n special_to_date\n swatch_image\n thumbnail\n {\n url\n label\n }\n tier_price\n tier_prices\n {\n customer_group_id\n percentage_value\n qty\n value\n website_id\n }\n type_id\n updated_at\n url_key\n url_path\n websites { id name code sort_order default_group_id is_default }\n ... on GroupedProduct {\n weight\n items {\n qty\n position\n product {\n attribute_set_id\n categories\n {\n id\n position\n }\n country_of_manufacture\n created_at\n description {\n html\n }\n gift_message_available\n id\n image\n {\n url\n label\n }\n meta_description\n meta_keyword\n meta_title\n media_gallery_entries\n {\n disabled\n file\n id\n label\n media_type\n position\n types\n content\n {\n base64_encoded_data\n type\n name\n }\n video_content\n {\n media_type\n video_description\n video_metadata\n video_provider\n video_title\n video_url\n }\n }\n name\n new_from_date\n new_to_date\n options_container\n ... on CustomizableProductInterface {\n options\n {\n title\n required\n sort_order\n }\n }\n \n price {\n minimalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n maximalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n regularPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n }\n product_links\n {\n link_type\n linked_product_sku\n linked_product_type\n position\n sku\n }\n short_description {\n html\n }\n sku\n small_image\n {\n url\n label\n }\n special_from_date\n special_price\n special_to_date\n swatch_image\n thumbnail\n {\n url\n label\n }\n tier_price\n tier_prices\n {\n customer_group_id\n percentage_value\n qty\n value\n website_id\n }\n type_id\n updated_at\n url_key\n url_path\n websites { id name code sort_order default_group_id is_default }\n }\n }\n }\n }\n }\n}\n","variables":null,"operationName":null} + {"query":"{\nproducts(filter: {sku: {eq:\"${grouped_product_sku}\"} }, sort: {name: ASC}) {\n total_count\n items {\n attribute_set_id\n categories\n {\n id\n position\n }\n country_of_manufacture\n created_at\n description {\n html\n }\n gift_message_available\n id\n image\n {\n url\n label\n }\n meta_description\n meta_keyword\n meta_title\n media_gallery_entries\n {\n disabled\n file\n id\n label\n media_type\n position\n types\n content\n {\n base64_encoded_data\n type\n name\n }\n video_content\n {\n media_type\n video_description\n video_metadata\n video_provider\n video_title\n video_url\n }\n }\n name\n new_from_date\n new_to_date\n options_container\n ... on CustomizableProductInterface {\n options\n {\n title\n required\n sort_order\n }\n }\n \n price {\n minimalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n maximalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n regularPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n }\n product_links\n {\n link_type\n linked_product_sku\n linked_product_type\n position\n sku\n }\n short_description {\n html\n }\n sku\n small_image\n {\n url\n label\n }\n special_from_date\n special_price\n special_to_date\n swatch_image\n thumbnail\n {\n url\n label\n }\n tier_price\n tier_prices\n {\n customer_group_id\n percentage_value\n qty\n value\n website_id\n }\n type_id\n updated_at\n url_key\n url_path\n websites { id name code sort_order default_group_id is_default }\n ... on GroupedProduct {\n weight\n items {\n qty\n position\n product {\n attribute_set_id\n categories\n {\n id\n position\n }\n country_of_manufacture\n created_at\n description {\n html\n }\n gift_message_available\n id\n image\n {\n url\n label\n }\n meta_description\n meta_keyword\n meta_title\n media_gallery_entries\n {\n disabled\n file\n id\n label\n media_type\n position\n types\n content\n {\n base64_encoded_data\n type\n name\n }\n video_content\n {\n media_type\n video_description\n video_metadata\n video_provider\n video_title\n video_url\n }\n }\n name\n new_from_date\n new_to_date\n options_container\n ... on CustomizableProductInterface {\n options\n {\n title\n required\n sort_order\n }\n }\n \n price {\n minimalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n maximalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n regularPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n }\n product_links\n {\n link_type\n linked_product_sku\n linked_product_type\n position\n sku\n }\n short_description {\n html\n }\n sku\n small_image\n {\n url\n label\n }\n special_from_date\n special_price\n special_to_date\n swatch_image\n thumbnail\n {\n url\n label\n }\n tier_price\n tier_prices\n {\n customer_group_id\n percentage_value\n qty\n value\n website_id\n }\n type_id\n updated_at\n url_key\n url_path\n websites { id name code sort_order default_group_id is_default }\n }\n }\n }\n }\n }\n}\n","variables":null,"operationName":null} = @@ -39479,7 +39536,7 @@ vars.putObject("category", categories[number]); false - {"query":"query category($id: Int!, $currentPage: Int, $pageSize: Int) {\n category(id: $id) {\n product_count\n description\n url_key\n name\n id\n breadcrumbs {\n category_name\n category_url_key\n __typename\n }\n products(pageSize: $pageSize, currentPage: $currentPage) {\n total_count\n items {\n id\n name\n # small_image\n # short_description\n url_key\n special_price\n special_from_date\n special_to_date\n price {\n regularPrice {\n amount {\n value\n currency\n __typename\n }\n __typename\n }\n __typename\n }\n __typename\n }\n __typename\n }\n __typename\n }\n}\n","variables":{"id":${category_id},"currentPage":1,"pageSize":12},"operationName":"category"} + {"query":"query category($id: Int!, $currentPage: Int, $pageSize: Int) {\n category(id: $id) {\n product_count\n description\n url_key\n name\n id\n breadcrumbs {\n category_name\n category_url_key\n __typename\n }\n products(pageSize: $pageSize, currentPage: $currentPage, sort: {name: ASC}) {\n total_count\n items {\n id\n name\n # small_image\n # short_description\n url_key\n special_price\n special_from_date\n special_to_date\n price {\n regularPrice {\n amount {\n value\n currency\n __typename\n }\n __typename\n }\n __typename\n }\n __typename\n }\n __typename\n }\n __typename\n }\n}\n","variables":{"id":${category_id},"currentPage":1,"pageSize":12},"operationName":"category"} = @@ -39603,7 +39660,7 @@ vars.put("product_sku", product.get("sku")); false - {"query":"query productDetail($urlKey: String, $onServer: Boolean!) {\n productDetail: products(filter: { url_key: { eq: $urlKey } }) {\n items {\n sku\n name\n price {\n regularPrice {\n amount {\n currency\n value\n }\n }\n }\n description {html}\n media_gallery_entries {\n label\n position\n disabled\n file\n }\n ... on ConfigurableProduct {\n configurable_options {\n attribute_code\n attribute_id\n id\n label\n values {\n default_label\n label\n store_label\n use_default_value\n value_index\n }\n }\n variants {\n product {\n id\n media_gallery_entries {\n disabled\n file\n label\n position\n }\n sku\n stock_status\n }\n }\n }\n meta_title @include(if: $onServer)\n # Yes, Products have `meta_keyword` and\n # everything else has `meta_keywords`.\n meta_keyword @include(if: $onServer)\n meta_description @include(if: $onServer)\n }\n }\n}","variables":{"urlKey":"${product_url_key}","onServer":false},"operationName":"productDetail"} + {"query":"query productDetail($urlKey: String, $onServer: Boolean!) {\n productDetail: products(filter: { url_key: { eq: $urlKey } }, sort: {name: ASC}) {\n items {\n sku\n name\n price {\n regularPrice {\n amount {\n currency\n value\n }\n }\n }\n description {html}\n media_gallery_entries {\n label\n position\n disabled\n file\n }\n ... on ConfigurableProduct {\n configurable_options {\n attribute_code\n attribute_id\n id\n label\n values {\n default_label\n label\n store_label\n use_default_value\n value_index\n }\n }\n variants {\n product {\n id\n media_gallery_entries {\n disabled\n file\n label\n position\n }\n sku\n stock_status\n }\n }\n }\n meta_title @include(if: $onServer)\n # Yes, Products have `meta_keyword` and\n # everything else has `meta_keywords`.\n meta_keyword @include(if: $onServer)\n meta_description @include(if: $onServer)\n }\n }\n}","variables":{"urlKey":"${product_url_key}","onServer":false},"operationName":"productDetail"} = @@ -39727,7 +39784,7 @@ vars.put("product_sku", product.get("sku")); false - {"query":"query productDetail($name: String, $onServer: Boolean!) {\n productDetail: products(filter: { name: { eq: $name } }) {\n items {\n sku\n name\n price {\n regularPrice {\n amount {\n currency\n value\n }\n }\n }\n description {html}\n media_gallery_entries {\n label\n position\n disabled\n file\n }\n ... on ConfigurableProduct {\n configurable_options {\n attribute_code\n attribute_id\n id\n label\n values {\n default_label\n label\n store_label\n use_default_value\n value_index\n }\n }\n variants {\n product {\n id\n media_gallery_entries {\n disabled\n file\n label\n position\n }\n sku\n stock_status\n }\n }\n }\n meta_title @include(if: $onServer)\n # Yes, Products have `meta_keyword` and\n # everything else has `meta_keywords`.\n meta_keyword @include(if: $onServer)\n meta_description @include(if: $onServer)\n }\n }\n}","variables":{"name":"${product_name}","onServer":false},"operationName":"productDetail"} + {"query":"query productDetail($product_sku: String, $onServer: Boolean!) {\n productDetail: products(filter: { sku: { eq: $product_sku } }, sort: {name: ASC}) {\n items {\n sku\n name\n price {\n regularPrice {\n amount {\n currency\n value\n }\n }\n }\n description {html}\n media_gallery_entries {\n label\n position\n disabled\n file\n }\n ... on ConfigurableProduct {\n configurable_options {\n attribute_code\n attribute_id\n id\n label\n values {\n default_label\n label\n store_label\n use_default_value\n value_index\n }\n }\n variants {\n product {\n id\n media_gallery_entries {\n disabled\n file\n label\n position\n }\n sku\n stock_status\n }\n }\n }\n meta_title @include(if: $onServer)\n # Yes, Products have `meta_keyword` and\n # everything else has `meta_keywords`.\n meta_keyword @include(if: $onServer)\n meta_description @include(if: $onServer)\n }\n }\n}","variables":{"product_sku":"${product_sku}","onServer":false},"operationName":"productDetail"} = @@ -39851,7 +39908,7 @@ vars.put("product_sku", product.get("sku")); false - {"query":"query productDetail($urlKey: String, $onServer: Boolean!) {\n productDetail: products(filter: { url_key: { eq: $urlKey } }) {\n items {\n sku\n name\n price {\n regularPrice {\n amount {\n currency\n value\n }\n }\n }\n description {html}\n media_gallery_entries {\n label\n position\n disabled\n file\n }\n ... on ConfigurableProduct {\n configurable_options {\n attribute_code\n attribute_id\n id\n label\n values {\n default_label\n label\n store_label\n use_default_value\n value_index\n }\n }\n variants {\n product {\n id\n media_gallery_entries {\n disabled\n file\n label\n position\n }\n sku\n stock_status\n }\n }\n }\n meta_title @include(if: $onServer)\n # Yes, Products have `meta_keyword` and\n # everything else has `meta_keywords`.\n meta_keyword @include(if: $onServer)\n meta_description @include(if: $onServer)\n }\n }\n}","variables":{"urlKey":"${product_url_key}","onServer":false},"operationName":"productDetail"} + {"query":"query productDetail($urlKey: String, $onServer: Boolean!) {\n productDetail: products(filter: { url_key: { eq: $urlKey } }, sort: {name: ASC}) {\n items {\n sku\n name\n price {\n regularPrice {\n amount {\n currency\n value\n }\n }\n }\n description {html}\n media_gallery_entries {\n label\n position\n disabled\n file\n }\n ... on ConfigurableProduct {\n configurable_options {\n attribute_code\n attribute_id\n id\n label\n values {\n default_label\n label\n store_label\n use_default_value\n value_index\n }\n }\n variants {\n product {\n id\n media_gallery_entries {\n disabled\n file\n label\n position\n }\n sku\n stock_status\n }\n }\n }\n meta_title @include(if: $onServer)\n # Yes, Products have `meta_keyword` and\n # everything else has `meta_keywords`.\n meta_keyword @include(if: $onServer)\n meta_description @include(if: $onServer)\n }\n }\n}","variables":{"urlKey":"${product_url_key}","onServer":false},"operationName":"productDetail"} = @@ -39975,7 +40032,7 @@ vars.put("product_sku", product.get("sku")); false - {"query":"query productDetailByName($name: String, $onServer: Boolean!) {\n products(filter: { name: { eq: $name } }) {\n items {\n id\n sku\n name\n ... on ConfigurableProduct {\n configurable_options {\n attribute_code\n attribute_id\n id\n label\n values {\n default_label\n label\n store_label\n use_default_value\n value_index\n }\n }\n variants {\n product {\n #fashion_color\n #fashion_size\n id\n media_gallery_entries {\n disabled\n file\n label\n position\n }\n sku\n stock_status\n }\n }\n }\n meta_title @include(if: $onServer)\n meta_keyword @include(if: $onServer)\n meta_description @include(if: $onServer)\n }\n }\n}","variables":{"name":"${product_name}","onServer":false},"operationName":"productDetailByName"} + {"query":"query productDetailByName($product_sku: String, $onServer: Boolean!) {\n products(filter: { sku: { eq: $product_sku } }, sort: {name: ASC}) {\n items {\n id\n sku\n name\n ... on ConfigurableProduct {\n configurable_options {\n attribute_code\n attribute_id\n id\n label\n values {\n default_label\n label\n store_label\n use_default_value\n value_index\n }\n }\n variants {\n product {\n #fashion_color\n #fashion_size\n id\n media_gallery_entries {\n disabled\n file\n label\n position\n }\n sku\n stock_status\n }\n }\n }\n meta_title @include(if: $onServer)\n meta_keyword @include(if: $onServer)\n meta_description @include(if: $onServer)\n }\n }\n}","variables":{"product_sku":"${product_sku}","onServer":false},"operationName":"productDetailByName"} = @@ -40097,7 +40154,7 @@ vars.putObject("category", categories[number]); false - {"query":"query productSearch($inputText: String!, $categoryId: String) {\n products(\n pageSize:12\n search: $inputText, filter: { category_id: { eq: $categoryId } }) {\n items {\n id\n name\n small_image {\n label\n url\n }\n url_key\n price {\n regularPrice {\n amount {\n value\n currency\n }\n }\n }\n }\n total_count\n filters {\n name\n filter_items_count\n request_var\n filter_items {\n label\n value_string\n }\n }\n }\n}","variables":{"inputText":"Product","categoryId":"${category_id}"},"operationName":"productSearch"} + {"query":"query productSearch($inputText: String!, $categoryId: String) {\n products(\n pageSize:12\n search: $inputText, filter: { category_id: { eq: $categoryId } }, sort: {name: ASC}) {\n items {\n id\n name\n small_image {\n label\n url\n }\n url_key\n price {\n regularPrice {\n amount {\n value\n currency\n }\n }\n }\n }\n total_count\n filters {\n name\n filter_items_count\n request_var\n filter_items {\n label\n value_string\n }\n }\n }\n}","variables":{"inputText":"Product","categoryId":"${category_id}"},"operationName":"productSearch"} = @@ -40241,7 +40298,7 @@ vars.putObject("category", categories[number]); false - {"query":"query categoryList($id: Int!) {\n category(id: $id) {\n id\n children {\n id\n name\n url_key\n url_path\n children_count\n path\n image\n productImagePreview: products(pageSize: 1) {\n items {\n small_image {\n label\n url\n }\n }\n }\n }\n }\n}","variables":{"id":${category_id}},"operationName":"categoryList"} + {"query":"query categoryList($id: Int!) {\n category(id: $id) {\n id\n children {\n id\n name\n url_key\n url_path\n children_count\n path\n image\n productImagePreview: products(pageSize: 1, sort: {name: ASC}) {\n items {\n small_image {\n label\n url\n }\n }\n }\n }\n }\n}","variables":{"id":${category_id}},"operationName":"categoryList"} = @@ -40631,7 +40688,7 @@ vars.putObject("category", categories[number]); false - {"query":"query navigationMenu($id: Int!) {\n category(id: $id) {\n id\n name\n product_count\n path\n children {\n id\n name\n position\n level\n url_key\n url_path\n product_count\n children_count\n path\n productImagePreview: products(pageSize: 1) {\n items {\n small_image {\n label\n url\n }\n }\n }\n }\n }\n}","variables":{"id":${category_id}},"operationName":"navigationMenu"} + {"query":"query navigationMenu($id: Int!) {\n category(id: $id) {\n id\n name\n product_count\n path\n children {\n id\n name\n position\n level\n url_key\n url_path\n product_count\n children_count\n path\n productImagePreview: products(pageSize: 1, sort: {name: ASC}) {\n items {\n small_image {\n label\n url\n }\n }\n }\n }\n }\n}","variables":{"id":${category_id}},"operationName":"navigationMenu"} = @@ -41617,7 +41674,7 @@ vars.put("product_sku", product.get("sku")); false - {"query":"query productDetailByName($name: String, $onServer: Boolean!) {\n products(filter: { name: { eq: $name } }) {\n items {\n id\n sku\n name\n ... on ConfigurableProduct {\n configurable_options {\n attribute_code\n attribute_id\n id\n label\n values {\n default_label\n label\n store_label\n use_default_value\n value_index\n }\n }\n variants {\n product {\n #fashion_color\n #fashion_size\n id\n media_gallery_entries {\n disabled\n file\n label\n position\n }\n sku\n stock_status\n }\n }\n }\n meta_title @include(if: $onServer)\n meta_keyword @include(if: $onServer)\n meta_description @include(if: $onServer)\n }\n }\n}","variables":{"name":"${product_name}","onServer":false},"operationName":"productDetailByName"} + {"query":"query productDetailByName($product_sku: String, $onServer: Boolean!) {\n products(filter: { sku: { eq: $product_sku } }, sort: {name: ASC}) {\n items {\n id\n sku\n name\n ... on ConfigurableProduct {\n configurable_options {\n attribute_code\n attribute_id\n id\n label\n values {\n default_label\n label\n store_label\n use_default_value\n value_index\n }\n }\n variants {\n product {\n #fashion_color\n #fashion_size\n id\n media_gallery_entries {\n disabled\n file\n label\n position\n }\n sku\n stock_status\n }\n }\n }\n meta_title @include(if: $onServer)\n meta_keyword @include(if: $onServer)\n meta_description @include(if: $onServer)\n }\n }\n}","variables":{"product_sku":"${product_sku}","onServer":false},"operationName":"productDetailByName"} = @@ -42095,7 +42152,7 @@ vars.put("product_sku", product.get("sku")); false - {"query":"query productDetailByName($name: String, $onServer: Boolean!) {\n products(filter: { name: { eq: $name } }) {\n items {\n id\n sku\n name\n ... on ConfigurableProduct {\n configurable_options {\n attribute_code\n attribute_id\n id\n label\n values {\n default_label\n label\n store_label\n use_default_value\n value_index\n }\n }\n variants {\n product {\n #fashion_color\n #fashion_size\n id\n media_gallery_entries {\n disabled\n file\n label\n position\n }\n sku\n stock_status\n }\n }\n }\n meta_title @include(if: $onServer)\n meta_keyword @include(if: $onServer)\n meta_description @include(if: $onServer)\n }\n }\n}","variables":{"name":"${product_name}","onServer":false},"operationName":"productDetailByName"} + {"query":"query productDetailByName($product_sku: String, $onServer: Boolean!) {\n products(filter: { sku: { eq: $product_sku } }, sort: {name: ASC}) {\n items {\n id\n sku\n name\n ... on ConfigurableProduct {\n configurable_options {\n attribute_code\n attribute_id\n id\n label\n values {\n default_label\n label\n store_label\n use_default_value\n value_index\n }\n }\n variants {\n product {\n #fashion_color\n #fashion_size\n id\n media_gallery_entries {\n disabled\n file\n label\n position\n }\n sku\n stock_status\n }\n }\n }\n meta_title @include(if: $onServer)\n meta_keyword @include(if: $onServer)\n meta_description @include(if: $onServer)\n }\n }\n}","variables":{"product_sku":"${product_sku}","onServer":false},"operationName":"productDetailByName"} = @@ -42659,7 +42716,7 @@ vars.put("product_sku", product.get("sku")); false - {"query":"query productDetailByName($name: String, $onServer: Boolean!) {\n products(filter: { name: { eq: $name } }) {\n items {\n id\n sku\n name\n ... on ConfigurableProduct {\n configurable_options {\n attribute_code\n attribute_id\n id\n label\n values {\n default_label\n label\n store_label\n use_default_value\n value_index\n }\n }\n variants {\n product {\n #fashion_color\n #fashion_size\n id\n media_gallery_entries {\n disabled\n file\n label\n position\n }\n sku\n stock_status\n }\n }\n }\n meta_title @include(if: $onServer)\n meta_keyword @include(if: $onServer)\n meta_description @include(if: $onServer)\n }\n }\n}","variables":{"name":"${product_name}","onServer":false},"operationName":"productDetailByName"} + {"query":"query productDetailByName($product_sku: String, $onServer: Boolean!) {\n products(filter: { sku: { eq: $product_sku } }, sort: {name: ASC}) {\n items {\n id\n sku\n name\n ... on ConfigurableProduct {\n configurable_options {\n attribute_code\n attribute_id\n id\n label\n values {\n default_label\n label\n store_label\n use_default_value\n value_index\n }\n }\n variants {\n product {\n #fashion_color\n #fashion_size\n id\n media_gallery_entries {\n disabled\n file\n label\n position\n }\n sku\n stock_status\n }\n }\n }\n meta_title @include(if: $onServer)\n meta_keyword @include(if: $onServer)\n meta_description @include(if: $onServer)\n }\n }\n}","variables":{"product_sku":"${product_sku}","onServer":false},"operationName":"productDetailByName"} = @@ -43409,7 +43466,7 @@ vars.putObject("category", categories[number]); false - {"query":"query navigationMenu($id: Int!) {\n category(id: $id) {\n id\n name\n product_count\n path\n children {\n id\n name\n position\n level\n url_key\n url_path\n product_count\n children_count\n path\n productImagePreview: products(pageSize: 1) {\n items {\n small_image {\n label\n url\n }\n }\n }\n }\n }\n}","variables":{"id":${category_id}},"operationName":"navigationMenu"} + {"query":"query navigationMenu($id: Int!) {\n category(id: $id) {\n id\n name\n product_count\n path\n children {\n id\n name\n position\n level\n url_key\n url_path\n product_count\n children_count\n path\n productImagePreview: products(pageSize: 1, sort: {name: ASC}) {\n items {\n small_image {\n label\n url\n }\n }\n }\n }\n }\n}","variables":{"id":${category_id}},"operationName":"navigationMenu"} = @@ -43448,7 +43505,7 @@ vars.putObject("category", categories[number]); false - {"query":"query productSearch($inputText: String!, $categoryId: String) {\n products(\n pageSize:12\n search: $inputText, filter: { category_id: { eq: $categoryId } }) {\n items {\n id\n name\n small_image {\n label\n url\n }\n url_key\n price {\n regularPrice {\n amount {\n value\n currency\n }\n }\n }\n }\n total_count\n filters {\n name\n filter_items_count\n request_var\n filter_items {\n label\n value_string\n }\n }\n }\n}","variables":{"inputText":"Product","categoryId":"${category_id}"},"operationName":"productSearch"} + {"query":"query productSearch($inputText: String!, $categoryId: String) {\n products(\n pageSize:12\n search: $inputText, filter: { category_id: { eq: $categoryId } }, sort: {name: ASC}) {\n items {\n id\n name\n small_image {\n label\n url\n }\n url_key\n price {\n regularPrice {\n amount {\n value\n currency\n }\n }\n }\n }\n total_count\n filters {\n name\n filter_items_count\n request_var\n filter_items {\n label\n value_string\n }\n }\n }\n}","variables":{"inputText":"Product","categoryId":"${category_id}"},"operationName":"productSearch"} = @@ -43548,7 +43605,7 @@ if (totalCount == null) { false - {"query":"query category($id: Int!, $currentPage: Int, $pageSize: Int) {\n category(id: $id) {\n product_count\n description\n url_key\n name\n id\n breadcrumbs {\n category_name\n category_url_key\n __typename\n }\n products(pageSize: $pageSize, currentPage: $currentPage) {\n total_count\n items {\n id\n name\n # small_image\n # short_description\n url_key\n special_price\n special_from_date\n special_to_date\n price {\n regularPrice {\n amount {\n value\n currency\n __typename\n }\n __typename\n }\n __typename\n }\n __typename\n }\n __typename\n }\n __typename\n }\n}\n","variables":{"id":${category_id},"currentPage":1,"pageSize":12},"operationName":"category"} + {"query":"query category($id: Int!, $currentPage: Int, $pageSize: Int) {\n category(id: $id) {\n product_count\n description\n url_key\n name\n id\n breadcrumbs {\n category_name\n category_url_key\n __typename\n }\n products(pageSize: $pageSize, currentPage: $currentPage, sort: {name: ASC}) {\n total_count\n items {\n id\n name\n # small_image\n # short_description\n url_key\n special_price\n special_from_date\n special_to_date\n price {\n regularPrice {\n amount {\n value\n currency\n __typename\n }\n __typename\n }\n __typename\n }\n __typename\n }\n __typename\n }\n __typename\n }\n}\n","variables":{"id":${category_id},"currentPage":1,"pageSize":12},"operationName":"category"} = @@ -43607,7 +43664,7 @@ vars.put("product_sku", product.get("sku")); false - {"query":"query productDetailByName($name: String, $onServer: Boolean!) {\n products(filter: { name: { eq: $name } }) {\n items {\n id\n sku\n name\n ... on ConfigurableProduct {\n configurable_options {\n attribute_code\n attribute_id\n id\n label\n values {\n default_label\n label\n store_label\n use_default_value\n value_index\n }\n }\n variants {\n product {\n #fashion_color\n #fashion_size\n id\n media_gallery_entries {\n disabled\n file\n label\n position\n }\n sku\n stock_status\n }\n }\n }\n meta_title @include(if: $onServer)\n meta_keyword @include(if: $onServer)\n meta_description @include(if: $onServer)\n }\n }\n}","variables":{"name":"${product_name}","onServer":false},"operationName":"productDetailByName"} + {"query":"query productDetailByName($product_sku: String, $onServer: Boolean!) {\n products(filter: { sku: { eq: $product_sku } }, sort: {name: ASC}) {\n items {\n id\n sku\n name\n ... on ConfigurableProduct {\n configurable_options {\n attribute_code\n attribute_id\n id\n label\n values {\n default_label\n label\n store_label\n use_default_value\n value_index\n }\n }\n variants {\n product {\n #fashion_color\n #fashion_size\n id\n media_gallery_entries {\n disabled\n file\n label\n position\n }\n sku\n stock_status\n }\n }\n }\n meta_title @include(if: $onServer)\n meta_keyword @include(if: $onServer)\n meta_description @include(if: $onServer)\n }\n }\n}","variables":{"product_sku":"${product_sku}","onServer":false},"operationName":"productDetailByName"} = @@ -43646,7 +43703,7 @@ vars.put("product_sku", product.get("sku")); false - {"query":"query productDetail($urlKey: String, $onServer: Boolean!) {\n productDetail: products(filter: { url_key: { eq: $urlKey } }) {\n items {\n sku\n name\n price {\n regularPrice {\n amount {\n currency\n value\n }\n }\n }\n description {html}\n media_gallery_entries {\n label\n position\n disabled\n file\n }\n ... on ConfigurableProduct {\n configurable_options {\n attribute_code\n attribute_id\n id\n label\n values {\n default_label\n label\n store_label\n use_default_value\n value_index\n }\n }\n variants {\n product {\n id\n media_gallery_entries {\n disabled\n file\n label\n position\n }\n sku\n stock_status\n }\n }\n }\n meta_title @include(if: $onServer)\n # Yes, Products have `meta_keyword` and\n # everything else has `meta_keywords`.\n meta_keyword @include(if: $onServer)\n meta_description @include(if: $onServer)\n }\n }\n}","variables":{"urlKey":"${product_url_key}","onServer":false},"operationName":"productDetail"} + {"query":"query productDetail($urlKey: String, $onServer: Boolean!) {\n productDetail: products(filter: { url_key: { eq: $urlKey } }, sort: {name: ASC}) {\n items {\n sku\n name\n price {\n regularPrice {\n amount {\n currency\n value\n }\n }\n }\n description {html}\n media_gallery_entries {\n label\n position\n disabled\n file\n }\n ... on ConfigurableProduct {\n configurable_options {\n attribute_code\n attribute_id\n id\n label\n values {\n default_label\n label\n store_label\n use_default_value\n value_index\n }\n }\n variants {\n product {\n id\n media_gallery_entries {\n disabled\n file\n label\n position\n }\n sku\n stock_status\n }\n }\n }\n meta_title @include(if: $onServer)\n # Yes, Products have `meta_keyword` and\n # everything else has `meta_keywords`.\n meta_keyword @include(if: $onServer)\n meta_description @include(if: $onServer)\n }\n }\n}","variables":{"urlKey":"${product_url_key}","onServer":false},"operationName":"productDetail"} = @@ -43705,7 +43762,7 @@ vars.put("product_sku", product.get("sku")); false - {"query":"query productDetail($name: String, $onServer: Boolean!) {\n productDetail: products(filter: { name: { eq: $name } }) {\n items {\n sku\n name\n price {\n regularPrice {\n amount {\n currency\n value\n }\n }\n }\n description {html}\n media_gallery_entries {\n label\n position\n disabled\n file\n }\n ... on ConfigurableProduct {\n configurable_options {\n attribute_code\n attribute_id\n id\n label\n values {\n default_label\n label\n store_label\n use_default_value\n value_index\n }\n }\n variants {\n product {\n id\n media_gallery_entries {\n disabled\n file\n label\n position\n }\n sku\n stock_status\n }\n }\n }\n meta_title @include(if: $onServer)\n # Yes, Products have `meta_keyword` and\n # everything else has `meta_keywords`.\n meta_keyword @include(if: $onServer)\n meta_description @include(if: $onServer)\n }\n }\n}","variables":{"name":"${product_name}","onServer":false},"operationName":"productDetail"} + {"query":"query productDetail($product_sku: String, $onServer: Boolean!) {\n productDetail: products(filter: { sku: { eq: $product_sku } }, sort: {name: ASC}) {\n items {\n sku\n name\n price {\n regularPrice {\n amount {\n currency\n value\n }\n }\n }\n description {html}\n media_gallery_entries {\n label\n position\n disabled\n file\n }\n ... on ConfigurableProduct {\n configurable_options {\n attribute_code\n attribute_id\n id\n label\n values {\n default_label\n label\n store_label\n use_default_value\n value_index\n }\n }\n variants {\n product {\n id\n media_gallery_entries {\n disabled\n file\n label\n position\n }\n sku\n stock_status\n }\n }\n }\n meta_title @include(if: $onServer)\n # Yes, Products have `meta_keyword` and\n # everything else has `meta_keywords`.\n meta_keyword @include(if: $onServer)\n meta_description @include(if: $onServer)\n }\n }\n}","variables":{"product_sku":"${product_sku}","onServer":false},"operationName":"productDetail"} = @@ -43744,7 +43801,7 @@ vars.put("product_sku", product.get("sku")); false - {"query":"query productDetail($urlKey: String, $onServer: Boolean!) {\n productDetail: products(filter: { url_key: { eq: $urlKey } }) {\n items {\n sku\n name\n price {\n regularPrice {\n amount {\n currency\n value\n }\n }\n }\n description {html}\n media_gallery_entries {\n label\n position\n disabled\n file\n }\n ... on ConfigurableProduct {\n configurable_options {\n attribute_code\n attribute_id\n id\n label\n values {\n default_label\n label\n store_label\n use_default_value\n value_index\n }\n }\n variants {\n product {\n id\n media_gallery_entries {\n disabled\n file\n label\n position\n }\n sku\n stock_status\n }\n }\n }\n meta_title @include(if: $onServer)\n # Yes, Products have `meta_keyword` and\n # everything else has `meta_keywords`.\n meta_keyword @include(if: $onServer)\n meta_description @include(if: $onServer)\n }\n }\n}","variables":{"urlKey":"${product_url_key}","onServer":false},"operationName":"productDetail"} + {"query":"query productDetail($urlKey: String, $onServer: Boolean!) {\n productDetail: products(filter: { url_key: { eq: $urlKey } }, sort: {name: ASC}) {\n items {\n sku\n name\n price {\n regularPrice {\n amount {\n currency\n value\n }\n }\n }\n description {html}\n media_gallery_entries {\n label\n position\n disabled\n file\n }\n ... on ConfigurableProduct {\n configurable_options {\n attribute_code\n attribute_id\n id\n label\n values {\n default_label\n label\n store_label\n use_default_value\n value_index\n }\n }\n variants {\n product {\n id\n media_gallery_entries {\n disabled\n file\n label\n position\n }\n sku\n stock_status\n }\n }\n }\n meta_title @include(if: $onServer)\n # Yes, Products have `meta_keyword` and\n # everything else has `meta_keywords`.\n meta_keyword @include(if: $onServer)\n meta_description @include(if: $onServer)\n }\n }\n}","variables":{"urlKey":"${product_url_key}","onServer":false},"operationName":"productDetail"} = @@ -44009,7 +44066,7 @@ vars.put("product_sku", product.get("sku")); false - {"query":"query productDetailByName($name: String, $onServer: Boolean!) {\n products(filter: { name: { eq: $name } }) {\n items {\n id\n sku\n name\n ... on ConfigurableProduct {\n configurable_options {\n attribute_code\n attribute_id\n id\n label\n values {\n default_label\n label\n store_label\n use_default_value\n value_index\n }\n }\n variants {\n product {\n #fashion_color\n #fashion_size\n id\n media_gallery_entries {\n disabled\n file\n label\n position\n }\n sku\n stock_status\n }\n }\n }\n meta_title @include(if: $onServer)\n meta_keyword @include(if: $onServer)\n meta_description @include(if: $onServer)\n }\n }\n}","variables":{"name":"${product_name}","onServer":false},"operationName":"productDetailByName"} + {"query":"query productDetailByName($product_sku: String, $onServer: Boolean!) {\n products(filter: { sku: { eq: $product_sku } }, sort: {name: ASC}) {\n items {\n id\n sku\n name\n ... on ConfigurableProduct {\n configurable_options {\n attribute_code\n attribute_id\n id\n label\n values {\n default_label\n label\n store_label\n use_default_value\n value_index\n }\n }\n variants {\n product {\n #fashion_color\n #fashion_size\n id\n media_gallery_entries {\n disabled\n file\n label\n position\n }\n sku\n stock_status\n }\n }\n }\n meta_title @include(if: $onServer)\n meta_keyword @include(if: $onServer)\n meta_description @include(if: $onServer)\n }\n }\n}","variables":{"product_sku":"${product_sku}","onServer":false},"operationName":"productDetailByName"} = diff --git a/setup/src/Magento/Setup/Model/Installer.php b/setup/src/Magento/Setup/Model/Installer.php index f80a35937d5dc..7a097e49c6289 100644 --- a/setup/src/Magento/Setup/Model/Installer.php +++ b/setup/src/Magento/Setup/Model/Installer.php @@ -16,6 +16,7 @@ use Magento\Framework\Config\ConfigOptionsListConstants; use Magento\Framework\Config\Data\ConfigData; use Magento\Framework\Config\File\ConfigFilePool; +use Magento\Framework\DB\Adapter\AdapterInterface; use Magento\Framework\DB\Adapter\Pdo\Mysql; use Magento\Framework\Exception\FileSystemException; use Magento\Framework\Filesystem; @@ -603,7 +604,7 @@ private function setupModuleRegistry(SchemaSetupInterface $setup) */ private function setupCoreTables(SchemaSetupInterface $setup) { - /* @var $connection \Magento\Framework\DB\Adapter\AdapterInterface */ + /* @var $connection AdapterInterface */ $connection = $setup->getConnection(); $setup->startSetup(); @@ -619,12 +620,12 @@ private function setupCoreTables(SchemaSetupInterface $setup) * Create table 'session' * * @param SchemaSetupInterface $setup - * @param \Magento\Framework\DB\Adapter\AdapterInterface $connection + * @param AdapterInterface $connection * @return void */ private function setupSessionTable( SchemaSetupInterface $setup, - \Magento\Framework\DB\Adapter\AdapterInterface $connection + AdapterInterface $connection ) { if (!$connection->isTableExists($setup->getTable('session'))) { $table = $connection->newTable( @@ -658,12 +659,12 @@ private function setupSessionTable( * Create table 'cache' * * @param SchemaSetupInterface $setup - * @param \Magento\Framework\DB\Adapter\AdapterInterface $connection + * @param AdapterInterface $connection * @return void */ private function setupCacheTable( SchemaSetupInterface $setup, - \Magento\Framework\DB\Adapter\AdapterInterface $connection + AdapterInterface $connection ) { if (!$connection->isTableExists($setup->getTable('cache'))) { $table = $connection->newTable( @@ -712,12 +713,12 @@ private function setupCacheTable( * Create table 'cache_tag' * * @param SchemaSetupInterface $setup - * @param \Magento\Framework\DB\Adapter\AdapterInterface $connection + * @param AdapterInterface $connection * @return void */ private function setupCacheTagTable( SchemaSetupInterface $setup, - \Magento\Framework\DB\Adapter\AdapterInterface $connection + AdapterInterface $connection ) { if (!$connection->isTableExists($setup->getTable('cache_tag'))) { $table = $connection->newTable( @@ -748,16 +749,17 @@ private function setupCacheTagTable( * Create table 'flag' * * @param SchemaSetupInterface $setup - * @param \Magento\Framework\DB\Adapter\AdapterInterface $connection + * @param AdapterInterface $connection * @return void */ private function setupFlagTable( SchemaSetupInterface $setup, - \Magento\Framework\DB\Adapter\AdapterInterface $connection + AdapterInterface $connection ) { - if (!$connection->isTableExists($setup->getTable('flag'))) { + $tableName = $setup->getTable('flag'); + if (!$connection->isTableExists($tableName)) { $table = $connection->newTable( - $setup->getTable('flag') + $tableName )->addColumn( 'flag_id', \Magento\Framework\DB\Ddl\Table::TYPE_INTEGER, @@ -779,7 +781,7 @@ private function setupFlagTable( )->addColumn( 'flag_data', \Magento\Framework\DB\Ddl\Table::TYPE_TEXT, - '64k', + '16m', [], 'Flag Data' )->addColumn( @@ -795,6 +797,8 @@ private function setupFlagTable( 'Flag' ); $connection->createTable($table); + } else { + $this->updateColumnType($connection, $tableName, 'flag_data', 'mediumtext'); } } @@ -1548,4 +1552,28 @@ function ($value, $key) { return !empty($adminData); } + + /** + * Update flag_data column data type to maintain consistency. + * + * @param AdapterInterface $connection + * @param string $tableName + * @param string $columnName + * @param string $typeName + */ + private function updateColumnType( + AdapterInterface $connection, + string $tableName, + string $columnName, + string $typeName + ): void { + $tableDescription = $connection->describeTable($tableName); + if ($tableDescription[$columnName]['DATA_TYPE'] !== $typeName) { + $connection->modifyColumn( + $tableName, + $columnName, + $typeName + ); + } + } } diff --git a/setup/src/Magento/Setup/Model/ObjectManagerProvider.php b/setup/src/Magento/Setup/Model/ObjectManagerProvider.php index e25b976e9207f..79216c8ec89b5 100644 --- a/setup/src/Magento/Setup/Model/ObjectManagerProvider.php +++ b/setup/src/Magento/Setup/Model/ObjectManagerProvider.php @@ -76,10 +76,9 @@ private function createCliCommands() { /** @var CommandListInterface $commandList */ $commandList = $this->objectManager->create(CommandListInterface::class); + $application = $this->serviceLocator->get(Application::class); foreach ($commandList->getCommands() as $command) { - $command->setApplication( - $this->serviceLocator->get(Application::class) - ); + $application->add($command); } } diff --git a/setup/src/Magento/Setup/Module/Di/App/Task/Operation/Area.php b/setup/src/Magento/Setup/Module/Di/App/Task/Operation/Area.php index edc2a485278a6..61eae4b2ffff6 100644 --- a/setup/src/Magento/Setup/Module/Di/App/Task/Operation/Area.php +++ b/setup/src/Magento/Setup/Module/Di/App/Task/Operation/Area.php @@ -88,6 +88,8 @@ public function doOperation() } } + $this->sortDefinitions($definitionsCollection); + $areaCodes = array_merge([App\Area::AREA_GLOBAL], $this->areaList->getCodes()); foreach ($areaCodes as $areaCode) { $config = $this->configReader->generateCachePerScope($definitionsCollection, $areaCode); @@ -124,4 +126,18 @@ public function getName() { return 'Area configuration aggregation'; } + + /** + * Sort definitions to make reproducible result + * + * @param DefinitionsCollection $definitionsCollection + */ + private function sortDefinitions(DefinitionsCollection $definitionsCollection): void + { + $definitions = $definitionsCollection->getCollection(); + + ksort($definitions); + + $definitionsCollection->initialize($definitions); + } } diff --git a/setup/src/Magento/Setup/Test/Unit/Model/ObjectManagerProviderTest.php b/setup/src/Magento/Setup/Test/Unit/Model/ObjectManagerProviderTest.php index 9d40b053e394e..552453c4a185c 100644 --- a/setup/src/Magento/Setup/Test/Unit/Model/ObjectManagerProviderTest.php +++ b/setup/src/Magento/Setup/Test/Unit/Model/ObjectManagerProviderTest.php @@ -47,6 +47,14 @@ public function setUp() public function testGet() { $initParams = ['param' => 'value']; + $commands = [ + new Command('setup:install'), + new Command('setup:upgrade'), + ]; + + $application = $this->getMockBuilder(Application::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); $this->serviceLocatorMock ->expects($this->atLeastOnce()) @@ -56,16 +64,21 @@ public function testGet() [InitParamListener::BOOTSTRAP_PARAM, $initParams], [ Application::class, - $this->getMockBuilder(Application::class)->disableOriginalConstructor()->getMock(), + $application, ], ] ); + $commandListMock = $this->createMock(CommandListInterface::class); + $commandListMock->expects($this->once()) + ->method('getCommands') + ->willReturn($commands); + $objectManagerMock = $this->createMock(ObjectManagerInterface::class); $objectManagerMock->expects($this->once()) ->method('create') ->with(CommandListInterface::class) - ->willReturn($this->getCommandListMock()); + ->willReturn($commandListMock); $objectManagerFactoryMock = $this->getMockBuilder(ObjectManagerFactory::class) ->disableOriginalConstructor() @@ -81,21 +94,9 @@ public function testGet() ->willReturn($objectManagerFactoryMock); $this->assertInstanceOf(ObjectManagerInterface::class, $this->model->get()); - } - - /** - * @return \PHPUnit_Framework_MockObject_MockObject - */ - private function getCommandListMock() - { - $commandMock = $this->getMockBuilder(Command::class)->disableOriginalConstructor()->getMock(); - $commandMock->expects($this->once())->method('setApplication'); - - $commandListMock = $this->createMock(CommandListInterface::class); - $commandListMock->expects($this->once()) - ->method('getCommands') - ->willReturn([$commandMock]); - return $commandListMock; + foreach ($commands as $command) { + $this->assertSame($application, $command->getApplication()); + } } } diff --git a/setup/src/Magento/Setup/Test/Unit/Module/Di/_files/app/code/Magento/SomeModule/etc/adminhtml/system.xml b/setup/src/Magento/Setup/Test/Unit/Module/Di/_files/app/code/Magento/SomeModule/etc/adminhtml/system.xml index 6d6c5954757c9..1733ba06c15d2 100644 --- a/setup/src/Magento/Setup/Test/Unit/Module/Di/_files/app/code/Magento/SomeModule/etc/adminhtml/system.xml +++ b/setup/src/Magento/Setup/Test/Unit/Module/Di/_files/app/code/Magento/SomeModule/etc/adminhtml/system.xml @@ -10,7 +10,7 @@ Advanced advanced - Magento_Backend::advanced + Magento_Config::advanced Disable Modules Output Magento\Config\Block\System\Config\Form\Fieldset\Modules\DisableOutput\Proxy
= $block->escapeHtml($block->getMethod()->getTitle()) ?>
= $block->escapeHtml($block->getTitle()) ?>
' . __('Notified Date: %1', $this->localeDate->formatDate($result['created_at'])) + $type = __(ucwords($type))->render(); + $title = __('Details for %1 #%2', $type, $result['increment_id'])->render(); + $description = '
' + . __('Notified Date: %1', $this->localeDate->formatDate($result['created_at']))->render() . '' - . __('Comment: %1', $result['comment']) . '
' . __('Current Status: %1', $this->order->getStatusLabel()) . - __('Total: %1', $this->order->formatPrice($this->order->getGrandTotal())) . '
' . __('Current Status: %1', $this->order->getStatusLabel())->render() . + __('Total: %1', $this->order->formatPrice($this->order->getGrandTotal()))->render() . '