diff --git a/.gitignore b/.gitignore index 9269cefc51fa0..a5b85031db205 100644 --- a/.gitignore +++ b/.gitignore @@ -23,6 +23,8 @@ atlassian* /lib/internal/flex/varien/.settings /node_modules /.grunt +/Gruntfile.js +/package.json /pub/media/*.* !/pub/media/.htaccess diff --git a/app/bootstrap.php b/app/bootstrap.php index ec60a1708dacc..c8e676cb69cff 100644 --- a/app/bootstrap.php +++ b/app/bootstrap.php @@ -11,14 +11,14 @@ #ini_set('display_errors', 1); /* PHP version validation */ -if (!defined('PHP_VERSION_ID') || !(PHP_VERSION_ID >= 50600 && PHP_VERSION_ID < 50700 || PHP_VERSION_ID === 70002 || PHP_VERSION_ID === 70004 || PHP_VERSION_ID >= 70006)) { +if (!defined('PHP_VERSION_ID') || !(PHP_VERSION_ID >= 50605 && PHP_VERSION_ID < 50700 || PHP_VERSION_ID === 70002 || PHP_VERSION_ID === 70004 || PHP_VERSION_ID >= 70006)) { if (PHP_SAPI == 'cli') { - echo 'Magento supports PHP 5.6, 7.0.2, 7.0.4, and 7.0.6 or later. ' . + echo 'Magento supports PHP 5.6.5, 7.0.2, 7.0.4, and 7.0.6 or later. ' . 'Please read http://devdocs.magento.com/guides/v1.0/install-gde/system-requirements.html'; } else { echo << -

Magento supports PHP 5.6, 7.0.2, 7.0.4, and 7.0.6 or later. Please read +

Magento supports PHP 5.6.5, 7.0.2, 7.0.4, and 7.0.6 or later. Please read Magento System Requirements. @@ -35,6 +35,17 @@ $mask = file_exists($umaskFile) ? octdec(file_get_contents($umaskFile)) : 002; umask($mask); +if (empty($_SERVER['ENABLE_IIS_REWRITES']) || ($_SERVER['ENABLE_IIS_REWRITES'] != 1)) { + /* + * Unset headers used by IIS URL rewrites. + */ + unset($_SERVER['HTTP_X_REWRITE_URL']); + unset($_SERVER['HTTP_X_ORIGINAL_URL']); + unset($_SERVER['IIS_WasUrlRewritten']); + unset($_SERVER['UNENCODED_URL']); + unset($_SERVER['ORIG_PATH_INFO']); +} + if (!empty($_SERVER['MAGE_PROFILER']) && isset($_SERVER['HTTP_ACCEPT']) && strpos($_SERVER['HTTP_ACCEPT'], 'text/html') !== false diff --git a/app/code/Magento/AdminNotification/composer.json b/app/code/Magento/AdminNotification/composer.json index 527268df36b41..09d8ce41bbb03 100644 --- a/app/code/Magento/AdminNotification/composer.json +++ b/app/code/Magento/AdminNotification/composer.json @@ -2,7 +2,7 @@ "name": "magento/module-admin-notification", "description": "N/A", "require": { - "php": "~5.6.0|7.0.2|7.0.4|~7.0.6", + "php": "~5.6.5|7.0.2|7.0.4|~7.0.6", "magento/module-store": "100.2.*", "magento/module-backend": "100.2.*", "magento/module-media-storage": "100.2.*", diff --git a/app/code/Magento/AdvancedPricingImportExport/composer.json b/app/code/Magento/AdvancedPricingImportExport/composer.json index 65ea7524dffff..2fc465fa3c3df 100644 --- a/app/code/Magento/AdvancedPricingImportExport/composer.json +++ b/app/code/Magento/AdvancedPricingImportExport/composer.json @@ -2,7 +2,7 @@ "name": "magento/module-advanced-pricing-import-export", "description": "N/A", "require": { - "php": "~5.6.0|7.0.2|7.0.4|~7.0.6", + "php": "~5.6.5|7.0.2|7.0.4|~7.0.6", "magento/module-catalog": "101.1.*", "magento/module-catalog-inventory": "100.2.*", "magento/module-eav": "100.2.*", diff --git a/app/code/Magento/Authorization/composer.json b/app/code/Magento/Authorization/composer.json index 0ca367d4854df..af88e8376dc75 100644 --- a/app/code/Magento/Authorization/composer.json +++ b/app/code/Magento/Authorization/composer.json @@ -2,7 +2,7 @@ "name": "magento/module-authorization", "description": "Authorization module provides access to Magento ACL functionality.", "require": { - "php": "~5.6.0|7.0.2|7.0.4|~7.0.6", + "php": "~5.6.5|7.0.2|7.0.4|~7.0.6", "magento/module-backend": "100.2.*", "magento/framework": "100.2.*" }, diff --git a/app/code/Magento/Authorizenet/composer.json b/app/code/Magento/Authorizenet/composer.json index 0c9e9641b6076..b93cb6688f56d 100644 --- a/app/code/Magento/Authorizenet/composer.json +++ b/app/code/Magento/Authorizenet/composer.json @@ -2,7 +2,7 @@ "name": "magento/module-authorizenet", "description": "N/A", "require": { - "php": "~5.6.0|7.0.2|7.0.4|~7.0.6", + "php": "~5.6.5|7.0.2|7.0.4|~7.0.6", "magento/module-sales": "100.2.*", "magento/module-store": "100.2.*", "magento/module-quote": "100.2.*", diff --git a/app/code/Magento/Authorizenet/i18n/en_US.csv b/app/code/Magento/Authorizenet/i18n/en_US.csv index 7183c706dc0a2..45025304c4e44 100644 --- a/app/code/Magento/Authorizenet/i18n/en_US.csv +++ b/app/code/Magento/Authorizenet/i18n/en_US.csv @@ -64,3 +64,4 @@ Debug,Debug "Minimum Order Total","Minimum Order Total" "Maximum Order Total","Maximum Order Total" "Sort Order","Sort Order" +"Sorry, but something went wrong. Please contact the seller.","Sorry, but something went wrong. Please contact the seller." diff --git a/app/code/Magento/Authorizenet/view/frontend/web/js/view/payment/method-renderer/authorizenet-directpost.js b/app/code/Magento/Authorizenet/view/frontend/web/js/view/payment/method-renderer/authorizenet-directpost.js index cd05960c17633..86ec8e0c39221 100644 --- a/app/code/Magento/Authorizenet/view/frontend/web/js/view/payment/method-renderer/authorizenet-directpost.js +++ b/app/code/Magento/Authorizenet/view/frontend/web/js/view/payment/method-renderer/authorizenet-directpost.js @@ -5,15 +5,16 @@ define( [ 'jquery', - 'Magento_Payment/js/view/payment/iframe' + 'Magento_Payment/js/view/payment/iframe', + 'mage/translate' ], - function ($, Component) { + function ($, Component, $t) { 'use strict'; return Component.extend({ defaults: { template: 'Magento_Authorizenet/payment/authorizenet-directpost', - timeoutMessage: 'Sorry, but something went wrong. Please contact the seller.' + timeoutMessage: $t('Sorry, but something went wrong. Please contact the seller.') }, placeOrderHandler: null, validateHandler: null, diff --git a/app/code/Magento/Backend/composer.json b/app/code/Magento/Backend/composer.json index 7d428636a1f45..5cfe6955b46bb 100644 --- a/app/code/Magento/Backend/composer.json +++ b/app/code/Magento/Backend/composer.json @@ -2,7 +2,7 @@ "name": "magento/module-backend", "description": "N/A", "require": { - "php": "~5.6.0|7.0.2|7.0.4|~7.0.6", + "php": "~5.6.5|7.0.2|7.0.4|~7.0.6", "magento/module-store": "100.2.*", "magento/module-directory": "100.2.*", "magento/module-developer": "100.2.*", diff --git a/app/code/Magento/Backup/composer.json b/app/code/Magento/Backup/composer.json index 21ed6f1780a41..ee05d6726db4c 100644 --- a/app/code/Magento/Backup/composer.json +++ b/app/code/Magento/Backup/composer.json @@ -2,7 +2,7 @@ "name": "magento/module-backup", "description": "N/A", "require": { - "php": "~5.6.0|7.0.2|7.0.4|~7.0.6", + "php": "~5.6.5|7.0.2|7.0.4|~7.0.6", "magento/module-store": "100.2.*", "magento/module-backend": "100.2.*", "magento/module-cron": "100.2.*", diff --git a/app/code/Magento/Braintree/Model/Ui/Adminhtml/PayPal/TokenUiComponentProvider.php b/app/code/Magento/Braintree/Model/Ui/Adminhtml/PayPal/TokenUiComponentProvider.php new file mode 100644 index 0000000000000..fd94e18e2cd94 --- /dev/null +++ b/app/code/Magento/Braintree/Model/Ui/Adminhtml/PayPal/TokenUiComponentProvider.php @@ -0,0 +1,84 @@ +componentFactory = $componentFactory; + $this->urlBuilder = $urlBuilder; + $this->config = $config; + } + + /** + * @inheritdoc + */ + public function getComponentForToken(PaymentTokenInterface $paymentToken) + { + $data = json_decode($paymentToken->getTokenDetails() ?: '{}', true); + $data['icon'] = $this->config->getPayPalIcon(); + $component = $this->componentFactory->create( + [ + 'config' => [ + 'code' => PayPalConfigProvider::PAYPAL_VAULT_CODE, + 'nonceUrl' => $this->getNonceRetrieveUrl(), + TokenUiComponentProviderInterface::COMPONENT_DETAILS => $data, + TokenUiComponentProviderInterface::COMPONENT_PUBLIC_HASH => $paymentToken->getPublicHash(), + 'template' => 'Magento_Braintree::form/paypal/vault.phtml' + ], + 'name' => Template::class + ] + ); + + return $component; + } + + /** + * Get url to retrieve payment method nonce + * @return string + */ + private function getNonceRetrieveUrl() + { + return $this->urlBuilder->getUrl(ConfigProvider::CODE . '/payment/getnonce', ['_secure' => true]); + } +} diff --git a/app/code/Magento/Braintree/Model/Ui/Adminhtml/TokenUiComponentProvider.php b/app/code/Magento/Braintree/Model/Ui/Adminhtml/TokenUiComponentProvider.php index 6cfc96ea23d0d..420b8365b3ea4 100644 --- a/app/code/Magento/Braintree/Model/Ui/Adminhtml/TokenUiComponentProvider.php +++ b/app/code/Magento/Braintree/Model/Ui/Adminhtml/TokenUiComponentProvider.php @@ -49,6 +49,7 @@ public function getComponentForToken(PaymentTokenInterface $paymentToken) $component = $this->componentFactory->create( [ 'config' => [ + 'code' => ConfigProvider::CC_VAULT_CODE, 'nonceUrl' => $this->getNonceRetrieveUrl(), TokenUiComponentProviderInterface::COMPONENT_DETAILS => $data, TokenUiComponentProviderInterface::COMPONENT_PUBLIC_HASH => $paymentToken->getPublicHash(), diff --git a/app/code/Magento/Braintree/Test/Unit/Model/Ui/Adminhtml/PayPal/TokenUiComponentProviderTest.php b/app/code/Magento/Braintree/Test/Unit/Model/Ui/Adminhtml/PayPal/TokenUiComponentProviderTest.php new file mode 100644 index 0000000000000..bdc39cbc5b868 --- /dev/null +++ b/app/code/Magento/Braintree/Test/Unit/Model/Ui/Adminhtml/PayPal/TokenUiComponentProviderTest.php @@ -0,0 +1,114 @@ +componentFactory = $this->getMockBuilder(TokenUiComponentInterfaceFactory::class) + ->disableOriginalConstructor() + ->setMethods(['create']) + ->getMock(); + + $this->urlBuilder = $this->getMock(UrlInterface::class); + + $this->config = $this->getMockBuilder(Config::class) + ->disableOriginalConstructor() + ->setMethods(['getPayPalIcon']) + ->getMock(); + + $this->tokenUiComponentProvider = new TokenUiComponentProvider( + $this->componentFactory, + $this->urlBuilder, + $this->config + ); + } + + /** + * @covers \Magento\Braintree\Model\Ui\Adminhtml\PayPal\TokenUiComponentProvider::getComponentForToken + */ + public function testGetComponentForToken() + { + $nonceUrl = 'https://payment/adminhtml/nonce/url'; + $payerEmail = 'john.doe@test.com'; + $icon = [ + 'url' => 'https://payment/adminhtml/icon.png', + 'width' => 48, + 'height' => 32 + ]; + + $expected = [ + 'code' => 'vault', + 'nonceUrl' => $nonceUrl, + 'details' => [ + 'payerEmail' => $payerEmail, + 'icon' => $icon + ], + 'template' => 'vault.phtml' + ]; + + $this->config->expects(static::once()) + ->method('getPayPalIcon') + ->willReturn($icon); + + $paymentToken = $this->getMock(PaymentTokenInterface::class); + $paymentToken->expects(static::once()) + ->method('getTokenDetails') + ->willReturn('{"payerEmail":" ' . $payerEmail . '"}'); + $paymentToken->expects(static::once()) + ->method('getPublicHash') + ->willReturn('cmk32dl21l'); + + $this->urlBuilder->expects(static::once()) + ->method('getUrl') + ->willReturn($nonceUrl); + + $tokenComponent = $this->getMock(TokenUiComponentInterface::class); + $tokenComponent->expects(static::once()) + ->method('getConfig') + ->willReturn($expected); + + $this->componentFactory->expects(static::once()) + ->method('create') + ->willReturn($tokenComponent); + + $component = $this->tokenUiComponentProvider->getComponentForToken($paymentToken); + static::assertEquals($tokenComponent, $component); + static::assertEquals($expected, $component->getConfig()); + } +} diff --git a/app/code/Magento/Braintree/Test/Unit/Model/Ui/Adminhtml/TokenUiComponentProviderTest.php b/app/code/Magento/Braintree/Test/Unit/Model/Ui/Adminhtml/TokenUiComponentProviderTest.php index d1665c71804cf..f159136cf4c46 100644 --- a/app/code/Magento/Braintree/Test/Unit/Model/Ui/Adminhtml/TokenUiComponentProviderTest.php +++ b/app/code/Magento/Braintree/Test/Unit/Model/Ui/Adminhtml/TokenUiComponentProviderTest.php @@ -7,10 +7,10 @@ use Magento\Braintree\Model\Ui\Adminhtml\TokenUiComponentProvider; use Magento\Framework\UrlInterface; -use Magento\Framework\View\Element\Template; use Magento\Vault\Api\Data\PaymentTokenInterface; use Magento\Vault\Model\Ui\TokenUiComponentInterface; use Magento\Vault\Model\Ui\TokenUiComponentInterfaceFactory; +use PHPUnit_Framework_MockObject_MockObject as MockObject; /** * Class TokenUiComponentProviderTest @@ -19,12 +19,12 @@ class TokenUiComponentProviderTest extends \PHPUnit_Framework_TestCase { /** - * @var TokenUiComponentInterfaceFactory|\PHPUnit_Framework_MockObject_MockObject + * @var TokenUiComponentInterfaceFactory|MockObject */ private $componentFactory; /** - * @var UrlInterface|\PHPUnit_Framework_MockObject_MockObject + * @var UrlInterface|MockObject */ private $urlBuilder; @@ -59,6 +59,7 @@ public function testGetComponentForToken() $expirationDate = '12/2015'; $expected = [ + 'code' => 'vault', 'nonceUrl' => $nonceUrl, 'details' => [ 'type' => $type, diff --git a/app/code/Magento/Braintree/composer.json b/app/code/Magento/Braintree/composer.json index 3b77e208837a4..91cacf6add2b8 100644 --- a/app/code/Magento/Braintree/composer.json +++ b/app/code/Magento/Braintree/composer.json @@ -2,7 +2,7 @@ "name": "magento/module-braintree", "description": "N/A", "require": { - "php": "~5.6.0|7.0.2|7.0.4|~7.0.6", + "php": "~5.6.5|7.0.2|7.0.4|~7.0.6", "magento/framework": "100.2.*", "magento/magento-composer-installer": "*", "magento/module-config": "100.2.*", diff --git a/app/code/Magento/Braintree/etc/adminhtml/di.xml b/app/code/Magento/Braintree/etc/adminhtml/di.xml index f252b977f20bd..d154aabbb01b5 100644 --- a/app/code/Magento/Braintree/etc/adminhtml/di.xml +++ b/app/code/Magento/Braintree/etc/adminhtml/di.xml @@ -47,6 +47,7 @@ Magento\Braintree\Model\Ui\Adminhtml\TokenUiComponentProvider + Magento\Braintree\Model\Ui\Adminhtml\PayPal\TokenUiComponentProvider diff --git a/app/code/Magento/Braintree/etc/config.xml b/app/code/Magento/Braintree/etc/config.xml index 095a8419c8529..bf19324ae7a02 100644 --- a/app/code/Magento/Braintree/etc/config.xml +++ b/app/code/Magento/Braintree/etc/config.xml @@ -71,7 +71,8 @@ BraintreePayPalVaultFacade - Vault Token (Braintree PayPal) + Stored Accounts (Braintree PayPal) + 1 diff --git a/app/code/Magento/Braintree/view/adminhtml/layout/sales_order_create_index.xml b/app/code/Magento/Braintree/view/adminhtml/layout/sales_order_create_index.xml index 571c5ededeb99..5e4f36e1c1fb4 100644 --- a/app/code/Magento/Braintree/view/adminhtml/layout/sales_order_create_index.xml +++ b/app/code/Magento/Braintree/view/adminhtml/layout/sales_order_create_index.xml @@ -18,6 +18,10 @@ braintree_cc_vault Magento_Vault::form/vault.phtml + + braintree_paypal_vault + Magento_Vault::form/vault.phtml + braintree_cc_vault Magento_Vault::form/vault.phtml + + braintree_paypal_vault + Magento_Vault::form/vault.phtml + \ No newline at end of file diff --git a/app/code/Magento/Braintree/view/adminhtml/templates/form/paypal/vault.phtml b/app/code/Magento/Braintree/view/adminhtml/templates/form/paypal/vault.phtml new file mode 100644 index 0000000000000..22930bbc65666 --- /dev/null +++ b/app/code/Magento/Braintree/view/adminhtml/templates/form/paypal/vault.phtml @@ -0,0 +1,30 @@ +getData(TokenUiComponentProviderInterface::COMPONENT_DETAILS); +$icon = $details['icon']; +$id = $block->escapeHtml($block->getData('id')); +?> +

", + "nonceUrl": "escapeUrl($block->getData('nonceUrl')); ?>" + } + }' id="payment_" class="admin__field"> +
+ + + escapeHtml($details['payerEmail']); ?> +
+
diff --git a/app/code/Magento/Braintree/view/adminhtml/templates/form/vault.phtml b/app/code/Magento/Braintree/view/adminhtml/templates/form/vault.phtml index 3811461884725..001422d4bf911 100644 --- a/app/code/Magento/Braintree/view/adminhtml/templates/form/vault.phtml +++ b/app/code/Magento/Braintree/view/adminhtml/templates/form/vault.phtml @@ -7,7 +7,7 @@ use Magento\Vault\Model\Ui\TokenUiComponentProviderInterface; // @codingStandardsIgnoreFile /** @var \Magento\Framework\View\Element\Template $block */ -$details = $block->getData('details'); +$details = $block->getData(TokenUiComponentProviderInterface::COMPONENT_DETAILS); $icon = $block->getData('icons')[$details['type']]; $id = $block->escapeHtml($block->getData('id')); ?> @@ -15,6 +15,7 @@ $id = $block->escapeHtml($block->getData('id')); "Magento_Braintree/js/vault": { "container": "payment_", "publicHash": "escapeHtml($block->getData(TokenUiComponentProviderInterface::COMPONENT_PUBLIC_HASH)); ?>", + "code": "escapeHtml($block->getData('code')); ?>", "nonceUrl": "escapeUrl($block->getData('nonceUrl')); ?>" } }' id="payment_" class="admin__field"> diff --git a/app/code/Magento/Braintree/view/adminhtml/web/js/vault.js b/app/code/Magento/Braintree/view/adminhtml/web/js/vault.js index fcff173e7fcd4..ea832acb537e0 100644 --- a/app/code/Magento/Braintree/view/adminhtml/web/js/vault.js +++ b/app/code/Magento/Braintree/view/adminhtml/web/js/vault.js @@ -14,7 +14,8 @@ define([ return Class.extend({ defaults: { $selector: null, - selector: 'edit_form' + selector: 'edit_form', + $container: null }, /** @@ -25,17 +26,18 @@ define([ var self = this; self.$selector = $('#' + self.selector); + self.$container = $('#' + self.container); self.$selector.on( 'setVaultNotActive', function () { - self.$selector.off('submitOrder.braintree_vault'); + self.$selector.off('submitOrder.' + self.getCode()); } ); - this._super(); + self._super(); - this.initEventHandlers(); + self.initEventHandlers(); - return this; + return self; }, /** @@ -43,14 +45,14 @@ define([ * @returns {String} */ getCode: function () { - return 'braintree'; + return this.code; }, /** * Init event handlers */ initEventHandlers: function () { - $('#' + this.container).find('[name="payment[token_switcher]"]') + $(this.$container).find('[name="payment[token_switcher]"]') .on('click', this.selectPaymentMethod.bind(this)); }, @@ -66,7 +68,7 @@ define([ * Enable form event listeners */ enableEventListeners: function () { - this.$selector.on('submitOrder.braintree_vault', this.submitOrder.bind(this)); + this.$selector.on('submitOrder.' + this.getCode(), this.submitOrder.bind(this)); }, /** @@ -129,7 +131,7 @@ define([ this.createPublicHashSelector(); this.$selector.find('[name="payment[public_hash]"]').val(this.publicHash); - this.$selector.find('#braintree_nonce').val(nonce); + this.$container.find('#' + this.getNonceSelectorName()).val(nonce); }, /** @@ -138,16 +140,16 @@ define([ createPublicHashSelector: function () { var $input; - if (this.$selector.find('#braintree_nonce').size() === 0) { + if (this.$container.find('#' + this.getNonceSelectorName()).size() === 0) { $input = $('').attr( { type: 'hidden', - id: 'braintree_nonce', + id: this.getNonceSelectorName(), name: 'payment[payment_method_nonce]' } ); - $input.appendTo(this.$selector); + $input.appendTo(this.$container); $input.prop('disabled', false); } }, @@ -160,6 +162,14 @@ define([ alert({ content: message }); + }, + + /** + * Get selector name for nonce input + * @returns {String} + */ + getNonceSelectorName: function () { + return 'nonce_' + this.getCode(); } }); }); 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 184260c39e954..075a1fdaf1fc1 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 @@ -98,7 +98,6 @@ define([ quote.totals.subscribe(function () { if (self.grandTotalAmount !== quote.totals()['base_grand_total']) { self.grandTotalAmount = quote.totals()['base_grand_total']; - self.reInitPayPal(); } }); diff --git a/app/code/Magento/Bundle/Block/Catalog/Product/View/Type/Bundle.php b/app/code/Magento/Bundle/Block/Catalog/Product/View/Type/Bundle.php index b349d7da577a4..30b0d6f2ac72c 100644 --- a/app/code/Magento/Bundle/Block/Catalog/Product/View/Type/Bundle.php +++ b/app/code/Magento/Bundle/Block/Catalog/Product/View/Type/Bundle.php @@ -78,9 +78,14 @@ public function __construct( } /** + * Returns the bundle product options + * Will return cached options data if the product options are already initialized + * In a case when $stripSelection parameter is true will reload stored bundle selections collection from DB + * + * @param bool $stripSelection * @return array */ - public function getOptions() + public function getOptions($stripSelection = false) { if (!$this->options) { $product = $this->getProduct(); @@ -96,7 +101,7 @@ public function getOptions() $this->options = $optionCollection->appendSelections( $selectionCollection, - false, + $stripSelection, $this->catalogProduct->getSkipSaleableCheck() ); } diff --git a/app/code/Magento/Bundle/Setup/InstallSchema.php b/app/code/Magento/Bundle/Setup/InstallSchema.php index 6f98cd386327a..5c7d5a47da616 100644 --- a/app/code/Magento/Bundle/Setup/InstallSchema.php +++ b/app/code/Magento/Bundle/Setup/InstallSchema.php @@ -24,7 +24,9 @@ public function install(SchemaSetupInterface $setup, ModuleContextInterface $con $installer = $setup; $installer->startSetup(); - + $customerGroupTable = $setup->getConnection()->describeTable($setup->getTable('customer_group')); + $customerGroupIdType = $customerGroupTable['customer_group_id']['DATA_TYPE'] == 'int' + ? \Magento\Framework\DB\Ddl\Table::TYPE_INTEGER : $customerGroupTable['customer_group_id']['DATA_TYPE']; /** * Create table 'catalog_product_bundle_option' */ @@ -340,7 +342,7 @@ public function install(SchemaSetupInterface $setup, ModuleContextInterface $con ) ->addColumn( 'customer_group_id', - \Magento\Framework\DB\Ddl\Table::TYPE_SMALLINT, + $customerGroupIdType, null, ['unsigned' => true, 'nullable' => false, 'primary' => true], 'Customer Group Id' diff --git a/app/code/Magento/Bundle/Setup/UpgradeSchema.php b/app/code/Magento/Bundle/Setup/UpgradeSchema.php old mode 100644 new mode 100755 index e48ad97922dc9..ced66a03a3a86 --- a/app/code/Magento/Bundle/Setup/UpgradeSchema.php +++ b/app/code/Magento/Bundle/Setup/UpgradeSchema.php @@ -44,6 +44,24 @@ public function upgrade(SchemaSetupInterface $setup, ModuleContextInterface $con } } + if (version_compare($context->getVersion(), '2.0.3', '<')) { + $tables = [ + 'catalog_product_index_price_bundle_idx', + 'catalog_product_index_price_bundle_opt_idx', + 'catalog_product_index_price_bundle_opt_tmp', + 'catalog_product_index_price_bundle_sel_idx', + 'catalog_product_index_price_bundle_sel_tmp', + 'catalog_product_index_price_bundle_tmp', + ]; + foreach ($tables as $table) { + $setup->getConnection()->modifyColumn( + $setup->getTable($table), + 'customer_group_id', + ['type' => 'integer', 'nullable' => false] + ); + } + } + $setup->endSetup(); } } diff --git a/app/code/Magento/Bundle/Test/Unit/Block/Catalog/Product/View/Type/BundleTest.php b/app/code/Magento/Bundle/Test/Unit/Block/Catalog/Product/View/Type/BundleTest.php index 362394ccd8bd1..f11fc30f5b28f 100644 --- a/app/code/Magento/Bundle/Test/Unit/Block/Catalog/Product/View/Type/BundleTest.php +++ b/app/code/Magento/Bundle/Test/Unit/Block/Catalog/Product/View/Type/BundleTest.php @@ -19,32 +19,34 @@ class BundleTest extends \PHPUnit_Framework_TestCase /** @var \Magento\Bundle\Model\Product\PriceFactory|\PHPUnit_Framework_MockObject_MockObject */ private $bundleProductPriceFactory; - /** - * @var \Magento\Framework\TestFramework\Unit\Helper\ObjectManager - */ - protected $_objectHelper; + /** @var \Magento\Framework\Json\Encoder|\PHPUnit_Framework_MockObject_MockObject */ + private $jsonEncoder; + + /** @var \Magento\Catalog\Helper\Product|\PHPUnit_Framework_MockObject_MockObject */ + private $catalogProduct; /** - * @var \Magento\Bundle\Block\Catalog\Product\View\Type\Bundle + * @var \Magento\Framework\Event\ManagerInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $_bundleBlock; + private $eventManager; /** @var \Magento\Catalog\Model\Product|\PHPUnit_Framework_MockObject_MockObject */ private $product; + /** + * @var \Magento\Bundle\Block\Catalog\Product\View\Type\Bundle + */ + private $bundleBlock; + protected function setUp() { $objectHelper = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); + $this->bundleProductPriceFactory = $this->getMockBuilder(\Magento\Bundle\Model\Product\PriceFactory::class) ->disableOriginalConstructor() ->setMethods(['create']) ->getMock(); - $this->_bundleBlock = $objectHelper->getObject( - \Magento\Bundle\Block\Catalog\Product\View\Type\Bundle::class, - [ - 'productPrice' => $this->bundleProductPriceFactory - ] - ); + $this->product = $this->getMockBuilder(\Magento\Catalog\Model\Product::class) ->disableOriginalConstructor() ->setMethods( @@ -57,45 +59,78 @@ protected function setUp() 'getPreconfiguredValues' ] )->getMock(); + $registry = $this->getMockBuilder(\Magento\Framework\Registry::class) + ->disableOriginalConstructor() + ->setMethods(['registry']) + ->getMock(); + $registry->expects($this->any()) + ->method('registry') + ->willReturn($this->product); + $this->eventManager = $this->getMockBuilder(\Magento\Framework\Event\ManagerInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $this->jsonEncoder = $this->getMockBuilder(\Magento\Framework\Json\Encoder::class) + ->disableOriginalConstructor() + ->getMock(); + $this->catalogProduct = $this->getMockBuilder(\Magento\Catalog\Helper\Product::class) + ->disableOriginalConstructor() + ->getMock(); + /** @var $bundleBlock BundleBlock */ + $this->bundleBlock = $objectHelper->getObject( + \Magento\Bundle\Block\Catalog\Product\View\Type\Bundle::class, + [ + 'registry' => $registry, + 'eventManager' => $this->eventManager, + 'jsonEncoder' => $this->jsonEncoder, + 'productPrice' => $this->bundleProductPriceFactory, + 'catalogProduct' => $this->catalogProduct + ] + ); } public function testGetOptionHtmlNoRenderer() { - $option = $this->getMock(\Magento\Bundle\Model\Option::class, ['getType', '__wakeup'], [], '', false); - $option->expects($this->exactly(2))->method('getType')->will($this->returnValue('checkbox')); + $option = $this->getMockBuilder(\Magento\Bundle\Model\Option::class) + ->setMethods(['getType']) + ->disableOriginalConstructor() + ->getMock(); + $option->expects($this->any())->method('getType')->willReturn('checkbox'); + + $layout = $this->getMockBuilder(\Magento\Framework\View\Layout::class) + ->setMethods(['getChildName', 'getBlock']) + ->disableOriginalConstructor() + ->getMock(); + $layout->expects($this->any())->method('getChildName')->willReturn(false); + $this->bundleBlock->setLayout($layout); $this->assertEquals( 'There is no defined renderer for "checkbox" option type.', - $this->_bundleBlock->getOptionHtml($option) + $this->bundleBlock->getOptionHtml($option) ); } public function testGetOptionHtml() { - $option = $this->getMock(\Magento\Bundle\Model\Option::class, ['getType', '__wakeup'], [], '', false); - $option->expects($this->exactly(1))->method('getType')->will($this->returnValue('checkbox')); - - $optionBlock = $this->getMock( - \Magento\Bundle\Block\Catalog\Product\View\Type\Bundle\Option\Checkbox::class, - ['setOption', 'toHtml'], - [], - '', - false - ); - $optionBlock->expects($this->any())->method('setOption')->will($this->returnValue($optionBlock)); - $optionBlock->expects($this->any())->method('toHtml')->will($this->returnValue('option html')); - $layout = $this->getMock( - \Magento\Framework\View\Layout::class, - ['getChildName', 'getBlock'], - [], - '', - false - ); - $layout->expects($this->any())->method('getChildName')->will($this->returnValue('name')); - $layout->expects($this->any())->method('getBlock')->will($this->returnValue($optionBlock)); - $this->_bundleBlock->setLayout($layout); + $option = $this->getMockBuilder(\Magento\Bundle\Model\Option::class) + ->setMethods(['getType']) + ->disableOriginalConstructor() + ->getMock(); + $option->expects($this->once())->method('getType')->willReturn('checkbox'); + + $optionBlock = $this->getMockBuilder( + \Magento\Bundle\Block\Catalog\Product\View\Type\Bundle\Option\Checkbox::class + )->setMethods(['setOption', 'toHtml'])->disableOriginalConstructor()->getMock(); + $optionBlock->expects($this->any())->method('setOption')->willReturnSelf(); + $optionBlock->expects($this->any())->method('toHtml')->willReturn('option html'); + $layout = $this->getMockBuilder(\Magento\Framework\View\Layout::class) + ->setMethods(['getChildName', 'getBlock']) + ->disableOriginalConstructor() + ->getMock(); + $layout->expects($this->any())->method('getChildName')->willReturn('name'); + $layout->expects($this->any())->method('getBlock')->willReturn($optionBlock); + $this->bundleBlock->setLayout($layout); - $this->assertEquals('option html', $this->_bundleBlock->getOptionHtml($option)); + $this->assertEquals('option html', $this->bundleBlock->getOptionHtml($option)); } public function testGetJsonConfigFixedPriceBundleNoOption() @@ -127,12 +162,12 @@ public function testGetJsonConfigFixedPriceBundleNoOption() ]; $priceInfo = $this->getPriceInfoMock($prices); - $this->_bundleBlock = $this->setupBundleBlock( + $this->updateBundleBlock( $options, $priceInfo, \Magento\Bundle\Model\Product\Price::PRICE_TYPE_FIXED ); - $jsonConfig = $this->_bundleBlock->getJsonConfig(); + $jsonConfig = $this->bundleBlock->getJsonConfig(); $this->assertEquals(110, $jsonConfig['prices']['oldPrice']['amount']); $this->assertEquals(100, $jsonConfig['prices']['basePrice']['amount']); $this->assertEquals(100, $jsonConfig['prices']['finalPrice']['amount']); @@ -162,14 +197,14 @@ public function testGetJsonConfigFixedPriceBundle() $bundleProductPrice->expects($this->at(0)) ->method('getLowestPrice') ->with($this->product, $baseAmount) - ->will($this->returnValue(999)); + ->willReturn(999); $bundleProductPrice->expects($this->at(1)) ->method('getLowestPrice') ->with($this->product, $basePriceValue) - ->will($this->returnValue(888)); + ->willReturn(888); $this->bundleProductPriceFactory->expects($this->once()) ->method('create') - ->will($this->returnValue($bundleProductPrice)); + ->willReturn($bundleProductPrice); $options = [ $this->createOption(1, 'Title `1', $selections), @@ -207,7 +242,7 @@ public function testGetJsonConfigFixedPriceBundle() $this->product->expects($this->once()) ->method('hasPreconfiguredValues') - ->will($this->returnValue(true)); + ->willReturn(true); $preconfiguredValues = new \Magento\Framework\DataObject( [ 'bundle_option' => [ @@ -217,14 +252,14 @@ public function testGetJsonConfigFixedPriceBundle() ); $this->product->expects($this->once()) ->method('getPreconfiguredValues') - ->will($this->returnValue($preconfiguredValues)); + ->willReturn($preconfiguredValues); - $this->_bundleBlock = $this->setupBundleBlock( + $this->updateBundleBlock( $options, $priceInfo, \Magento\Bundle\Model\Product\Price::PRICE_TYPE_FIXED ); - $jsonConfig = $this->_bundleBlock->getJsonConfig(); + $jsonConfig = $this->bundleBlock->getJsonConfig(); $this->assertEquals(110, $jsonConfig['prices']['oldPrice']['amount']); $this->assertEquals(100, $jsonConfig['prices']['basePrice']['amount']); $this->assertEquals(100, $jsonConfig['prices']['finalPrice']['amount']); @@ -236,86 +271,38 @@ public function testGetJsonConfigFixedPriceBundle() * @param string $priceType * @return BundleBlock */ - private function setupBundleBlock($options, $priceInfo, $priceType) + private function updateBundleBlock($options, $priceInfo, $priceType) { - $objectHelper = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); - - - $eventManager = $this->getMockBuilder(\Magento\Framework\Event\Manager::class) - ->disableOriginalConstructor() - ->getMock(); - $eventManager->expects($this->any())->method('dispatch')->will($this->returnValue(true)); - + $this->eventManager->expects($this->any())->method('dispatch')->willReturn(true); $optionCollection = $this->getMockBuilder(\Magento\Bundle\Model\ResourceModel\Option\Collection::class) ->disableOriginalConstructor() ->getMock(); $optionCollection->expects($this->any()) ->method('appendSelections') - ->will($this->returnValue($options)); + ->willReturn($options); $typeInstance = $this->getMockBuilder(\Magento\Bundle\Model\Product\Type::class) ->disableOriginalConstructor() ->getMock(); $typeInstance->expects($this->any()) ->method('getOptionsCollection') - ->will($this->returnValue($optionCollection)); + ->willReturn($optionCollection); $typeInstance->expects($this->any()) ->method('getStoreFilter') - ->will($this->returnValue(true)); + ->willReturn(true); $this->product->expects($this->any()) ->method('getTypeInstance') - ->will($this->returnValue($typeInstance)); + ->willReturn($typeInstance); $this->product->expects($this->any()) ->method('getPriceInfo') - ->will($this->returnValue($priceInfo)); + ->willReturn($priceInfo); $this->product->expects($this->any()) ->method('getPriceType') - ->will($this->returnValue($priceType)); - - $registry = $this->getMockBuilder(\Magento\Framework\Registry::class) - ->disableOriginalConstructor() - ->setMethods(['registry']) - ->getMock(); - $registry->expects($this->once()) - ->method('registry') - ->will($this->returnValue($this->product)); - - $taxHelperMock = $this->getMockBuilder(\Magento\Tax\Helper\Data::class) - ->disableOriginalConstructor() - ->getMock(); - - $context = $this->getMockBuilder(\Magento\Catalog\Block\Product\Context::class) - ->disableOriginalConstructor() - ->getMock(); - $context->expects($this->any()) - ->method('getRegistry') - ->will($this->returnValue($registry)); - $context->expects($this->any()) - ->method('getTaxData') - ->will($this->returnValue($taxHelperMock)); - $context->expects($this->any()) - ->method('getEventManager') - ->will($this->returnValue($eventManager)); - - $jsonEncoderMock = $this->getMockBuilder(\Magento\Framework\Json\Encoder::class) - ->disableOriginalConstructor() - ->getMock(); - $jsonEncoderMock->expects($this->any()) + ->willReturn($priceType); + $this->jsonEncoder->expects($this->any()) ->method('encode') ->will($this->returnArgument(0)); - - /** @var $bundleBlock BundleBlock */ - $bundleBlock = $objectHelper->getObject( - \Magento\Bundle\Block\Catalog\Product\View\Type\Bundle::class, - [ - 'context' => $context, - 'jsonEncoder' => $jsonEncoderMock, - 'productPrice' => $this->bundleProductPriceFactory - ] - ); - - return $bundleBlock; } private function getPriceInfoMock($price) @@ -331,13 +318,13 @@ private function getPriceInfoMock($price) $priceInfoMock->expects($this->at($counter)) ->method('getPrice') ->with($priceType) - ->will($this->returnValue($priceValue)); + ->willReturn($priceValue); $counter++; } } else { $priceInfoMock->expects($this->any()) ->method('getPrice') - ->will($this->returnValue($price)); + ->willReturn($price); } return $priceInfoMock; } @@ -355,7 +342,7 @@ private function getPriceMock($prices) foreach ($prices as $methodName => $amount) { $priceMock->expects($this->any()) ->method($methodName) - ->will($this->returnValue($amount)); + ->willReturn($amount); } return $priceMock; @@ -373,8 +360,8 @@ private function getAmountPriceMock($value, $baseAmount, array $selectionAmounts ->disableOriginalConstructor() ->setMethods(['getValue', 'getBaseAmount', 'getOptionSelectionAmount']) ->getMockForAbstractClass(); - $amountPrice->expects($this->any())->method('getValue')->will($this->returnValue($value)); - $amountPrice->expects($this->any())->method('getBaseAmount')->will($this->returnValue($baseAmount)); + $amountPrice->expects($this->any())->method('getValue')->willReturn($value); + $amountPrice->expects($this->any())->method('getBaseAmount')->willReturn($baseAmount); foreach ($selectionAmounts as $selectionAmount) { $amountPrice->expects($this->any()) ->method('getOptionSelectionAmount') @@ -414,7 +401,6 @@ private function createOption( ->disableOriginalConstructor() ->setMethods( [ - '__wakeup', 'getId', 'getTitle', 'getSelections', @@ -423,12 +409,12 @@ private function createOption( 'getIsDefault', ] ) - ->getMock(); - $option->expects($this->any())->method('getId')->will($this->returnValue($id)); - $option->expects($this->any())->method('getTitle')->will($this->returnValue($title)); - $option->expects($this->any())->method('getSelections')->will($this->returnValue($selections)); - $option->expects($this->any())->method('getType')->will($this->returnValue($type)); - $option->expects($this->any())->method('getRequired')->will($this->returnValue($isRequired)); + ->getMockForAbstractClass(); + $option->expects($this->any())->method('getId')->willReturn($id); + $option->expects($this->any())->method('getTitle')->willReturn($title); + $option->expects($this->any())->method('getSelections')->willReturn($selections); + $option->expects($this->any())->method('getType')->willReturn($type); + $option->expects($this->any())->method('getRequired')->willReturn($isRequired); return $option; } @@ -453,42 +439,72 @@ private function createOptionSelection( ) { $selection = $this->getMockBuilder(\Magento\Catalog\Model\Product::class) ->disableOriginalConstructor() - ->setMethods( - [ - 'getSelectionId', - 'getSelectionQty', - 'getPriceInfo', - 'getSelectionCanChangeQty', - 'getName', - 'getIsDefault', - 'isSalable', - ] - )->getMock(); + ->getMock(); $tierPrice = $this->getMockBuilder(\Magento\Bundle\Pricing\Price\TierPrice::class) ->disableOriginalConstructor() ->setMethods(['getTierPriceList']) ->getMock(); - $tierPrice->expects($this->any()) - ->method('getTierPriceList') - ->will($this->returnValue($tierPriceList)); + $tierPrice->expects($this->any())->method('getTierPriceList')->willReturn($tierPriceList); $priceInfo = $this->getMockBuilder(\Magento\Framework\Pricing\PriceInfo\Base::class) ->disableOriginalConstructor() ->setMethods(['getPrice']) ->getMock(); - $priceInfo->expects($this->any()) - ->method('getPrice') - ->will($this->returnValue($tierPrice)); - - $selection->expects($this->any())->method('getSelectionId')->will($this->returnValue($id)); - $selection->expects($this->any())->method('getName')->will($this->returnValue($name)); - $selection->expects($this->any())->method('getSelectionQty')->will($this->returnValue($qty)); - $selection->expects($this->any())->method('getPriceInfo')->will($this->returnValue($priceInfo)); - $selection->expects($this->any())->method('getSelectionCanChangeQty')->will( - $this->returnValue($isCanChangeQty) - ); - $selection->expects($this->any())->method('getIsDefault')->will($this->returnValue($isDefault)); - $selection->expects($this->any())->method('isSalable')->will($this->returnValue($isSalable)); + $priceInfo->expects($this->any())->method('getPrice')->willReturn($tierPrice); + $selection->expects($this->any())->method('getSelectionId')->willReturn($id); + $selection->expects($this->any())->method('getName')->willReturn($name); + $selection->expects($this->any())->method('getSelectionQty')->willReturn($qty); + $selection->expects($this->any())->method('getPriceInfo')->willReturn($priceInfo); + $selection->expects($this->any())->method('getSelectionCanChangeQty')->willReturn($isCanChangeQty); + $selection->expects($this->any())->method('getIsDefault')->willReturn($isDefault); + $selection->expects($this->any())->method('isSalable')->willReturn($isSalable); return $selection; } + + /** + * @dataProvider getOptionsDataProvider + * @param bool $stripSelection + */ + public function testGetOptions($stripSelection) + { + $newOptions = ['option_1', 'option_2']; + + $optionCollection = $this->getMockBuilder(\Magento\Bundle\Model\ResourceModel\Option\Collection::class) + ->disableOriginalConstructor() + ->getMock(); + $selectionConnection = $this->getMockBuilder(\Magento\Bundle\Model\ResourceModel\Selection\Collection::class) + ->disableOriginalConstructor() + ->getMock(); + $typeInstance = $this->getMockBuilder(\Magento\Bundle\Model\Product\Type::class) + ->disableOriginalConstructor() + ->getMock(); + + $optionCollection->expects($this->any())->method('appendSelections') + ->with($selectionConnection, $stripSelection, true) + ->willReturn($newOptions); + $typeInstance->expects($this->any())->method('setStoreFilter')->with(0, $this->product) + ->willReturn($optionCollection); + $typeInstance->expects($this->any())->method('getStoreFilter')->willReturn(true); + $typeInstance->expects($this->any())->method('getOptionsCollection')->willReturn($optionCollection); + $typeInstance->expects($this->any())->method('getOptionsIds')->willReturn([1,2]); + $typeInstance->expects($this->once())->method('getSelectionsCollection')->with([1,2], $this->product) + ->willReturn($selectionConnection); + $this->product->expects($this->any()) + ->method('getTypeInstance')->willReturn($typeInstance); + $this->product->expects($this->any())->method('getStoreId') ->willReturn(0); + $this->catalogProduct->expects($this->once())->method('getSkipSaleableCheck')->willReturn(true); + + $this->assertEquals($newOptions, $this->bundleBlock->getOptions($stripSelection)); + } + + /** + * @return array + */ + public function getOptionsDataProvider() + { + return [ + [true], + [false] + ]; + } } diff --git a/app/code/Magento/Bundle/Ui/DataProvider/Product/Form/Modifier/BundlePanel.php b/app/code/Magento/Bundle/Ui/DataProvider/Product/Form/Modifier/BundlePanel.php index 50a3caaf68b3b..538c80d9b1cf2 100644 --- a/app/code/Magento/Bundle/Ui/DataProvider/Product/Form/Modifier/BundlePanel.php +++ b/app/code/Magento/Bundle/Ui/DataProvider/Product/Form/Modifier/BundlePanel.php @@ -268,15 +268,12 @@ protected function getBundleOptions() 'arguments' => [ 'data' => [ 'config' => [ - 'componentType' => 'dynamicRows', + 'componentType' => Container::NAME, + 'component' => 'Magento_Bundle/js/components/bundle-dynamic-rows', 'template' => 'ui/dynamic-rows/templates/collapsible', - 'label' => '', 'additionalClasses' => 'admin__field-wide', - 'collapsibleHeader' => true, - 'columnsHeader' => false, - 'deleteProperty' => false, - 'addButton' => false, 'dataScope' => 'data.bundle_options', + 'bundleSelectionsName' => 'product_bundle_container.bundle_selections' ], ], ], @@ -318,14 +315,11 @@ protected function getBundleOptions() 'arguments' => [ 'data' => [ 'config' => [ - 'componentType' => DynamicRows::NAME, - 'label' => '', + 'componentType' => Container::NAME, + 'component' => 'Magento_Bundle/js/components/bundle-dynamic-rows-grid', 'sortOrder' => 50, 'additionalClasses' => 'admin__field-wide', - 'component' => 'Magento_Ui/js/dynamic-rows/dynamic-rows-grid', 'template' => 'ui/dynamic-rows/templates/default', - 'columnsHeader' => false, - 'columnsHeaderAfterRender' => true, 'provider' => 'product_form.product_form_data_source', 'dataProvider' => '${ $.dataScope }' . '.bundle_button_proxy', 'identificationDRProperty' => 'product_id', @@ -343,8 +337,7 @@ protected function getBundleOptions() 'selection_qty' => '', ], 'links' => ['insertData' => '${ $.provider }:${ $.dataProvider }'], - 'source' => 'product', - 'addButton' => false, + 'source' => 'product' ], ], ], @@ -561,7 +554,7 @@ protected function getBundleSelections() 'componentType' => Container::NAME, 'isTemplate' => true, 'component' => 'Magento_Ui/js/dynamic-rows/record', - 'is_collection' => true, + 'is_collection' => true ], ], ], diff --git a/app/code/Magento/Bundle/composer.json b/app/code/Magento/Bundle/composer.json index ab587044476db..24756bdf950a7 100644 --- a/app/code/Magento/Bundle/composer.json +++ b/app/code/Magento/Bundle/composer.json @@ -2,7 +2,7 @@ "name": "magento/module-bundle", "description": "N/A", "require": { - "php": "~5.6.0|7.0.2|7.0.4|~7.0.6", + "php": "~5.6.5|7.0.2|7.0.4|~7.0.6", "magento/module-store": "100.2.*", "magento/module-catalog": "101.1.*", "magento/module-tax": "100.2.*", diff --git a/app/code/Magento/Bundle/etc/module.xml b/app/code/Magento/Bundle/etc/module.xml index 982a33d00bc6b..34a88d4447cc3 100644 --- a/app/code/Magento/Bundle/etc/module.xml +++ b/app/code/Magento/Bundle/etc/module.xml @@ -6,7 +6,7 @@ */ --> - + diff --git a/app/code/Magento/Bundle/view/adminhtml/templates/product/composite/fieldset/options/bundle.phtml b/app/code/Magento/Bundle/view/adminhtml/templates/product/composite/fieldset/options/bundle.phtml index f0cb656843bca..1272e13e42526 100644 --- a/app/code/Magento/Bundle/view/adminhtml/templates/product/composite/fieldset/options/bundle.phtml +++ b/app/code/Magento/Bundle/view/adminhtml/templates/product/composite/fieldset/options/bundle.phtml @@ -9,7 +9,7 @@ ?> -decorateArray($block->getOptions()); ?> +decorateArray($block->getOptions(true)); ?>
diff --git a/app/code/Magento/Bundle/view/adminhtml/web/js/components/bundle-dynamic-rows-grid.js b/app/code/Magento/Bundle/view/adminhtml/web/js/components/bundle-dynamic-rows-grid.js new file mode 100644 index 0000000000000..e9a924e1cffe6 --- /dev/null +++ b/app/code/Magento/Bundle/view/adminhtml/web/js/components/bundle-dynamic-rows-grid.js @@ -0,0 +1,59 @@ +/** + * Copyright © 2016 Magento. All rights reserved. + * See COPYING.txt for license details. + */ + +define([ + 'underscore', + 'Magento_Ui/js/dynamic-rows/dynamic-rows-grid' +], function (_, dynamicRowsGrid) { + 'use strict'; + + return dynamicRowsGrid.extend({ + defaults: { + label: '', + columnsHeader: false, + columnsHeaderAfterRender: true, + addButton: false + }, + + /** + * Initialize elements from grid + * + * @param {Array} data + * + * @returns {Object} Chainable. + */ + initElements: function (data) { + var newData = this.getNewData(data), + recordIndex; + + this.parsePagesData(data); + + if (newData.length) { + if (this.insertData().length) { + recordIndex = data.length - newData.length - 1; + + _.each(newData, function (newRecord) { + this.processingAddChild(newRecord, ++recordIndex, newRecord[this.identificationProperty]); + }, this); + } + } + + return this; + }, + + /** + * Mapping value from grid + * + * @param {Array} data + */ + mappingValue: function (data) { + if (_.isEmpty(data)) { + return; + } + + this._super(); + } + }); +}); diff --git a/app/code/Magento/Bundle/view/adminhtml/web/js/components/bundle-dynamic-rows.js b/app/code/Magento/Bundle/view/adminhtml/web/js/components/bundle-dynamic-rows.js new file mode 100644 index 0000000000000..b36d8003a399f --- /dev/null +++ b/app/code/Magento/Bundle/view/adminhtml/web/js/components/bundle-dynamic-rows.js @@ -0,0 +1,98 @@ +/** + * Copyright © 2016 Magento. All rights reserved. + * See COPYING.txt for license details. + */ + +define([ + 'underscore', + 'mageUtils', + 'uiRegistry', + 'Magento_Ui/js/dynamic-rows/dynamic-rows' +], function (_, utils, registry, dynamicRows) { + 'use strict'; + + return dynamicRows.extend({ + defaults: { + label: '', + collapsibleHeader: true, + columnsHeader: false, + deleteProperty: false, + addButton: false + }, + + /** + * Set new data to dataSource, + * delete element + * + * @param {Array} data - record data + */ + _updateData: function (data) { + var elems = _.clone(this.elems()), + path, + dataArr, + optionBaseData; + + dataArr = this.recordData.splice(this.startIndex, this.recordData().length - this.startIndex); + dataArr.splice(0, this.pageSize); + elems = _.sortBy(this.elems(), function (elem) { + return ~~elem.index; + }); + + data.concat(dataArr).forEach(function (rec, idx) { + if (elems[idx]) { + elems[idx].recordId = rec[this.identificationProperty]; + } + + if (!rec.position) { + rec.position = this.maxPosition; + this.setMaxPosition(); + } + + path = this.dataScope + '.' + this.index + '.' + (this.startIndex + idx); + optionBaseData = _.pick(rec, function (value) { + return !_.isObject(value); + }); + this.source.set(path, optionBaseData); + this.source.set(path + '.bundle_button_proxy', []); + this.source.set(path + '.bundle_selections', []); + this.removeBundleItemsFromOption(idx); + _.each(rec['bundle_selections'], function (obj, index) { + this.source.set(path + '.bundle_button_proxy' + '.' + index, rec['bundle_button_proxy'][index]); + this.source.set(path + '.bundle_selections' + '.' + index, obj); + }, this); + }, this); + + this.elems(elems); + }, + + /** + * Removes nested dynamic-rows-grid rendered records from option + * + * @param {Number|String} index - element index + */ + removeBundleItemsFromOption: function (index) { + var bundleSelections = registry.get(this.name + '.' + index + '.' + this.bundleSelectionsName), + bundleSelectionsLength = (bundleSelections.elems() || []).length, + i; + + if (bundleSelectionsLength) { + for (i = 0; i < bundleSelectionsLength; i++) { + bundleSelections.elems()[0].destroy(); + } + } + }, + + /** + * {@inheritdoc} + */ + processingAddChild: function (ctx, index, prop) { + var recordIds = _.map(this.recordData(), function (rec) { + return parseInt(rec['record_id'], 10); + }), + maxRecordId = _.max(recordIds); + + prop = maxRecordId > -1 ? maxRecordId + 1 : prop; + this._super(ctx, index, prop); + } + }); +}); diff --git a/app/code/Magento/BundleImportExport/composer.json b/app/code/Magento/BundleImportExport/composer.json index 3d7900d98287c..2f5e3afdbff86 100644 --- a/app/code/Magento/BundleImportExport/composer.json +++ b/app/code/Magento/BundleImportExport/composer.json @@ -2,7 +2,7 @@ "name": "magento/module-bundle-import-export", "description": "N/A", "require": { - "php": "~5.6.0|7.0.2|7.0.4|~7.0.6", + "php": "~5.6.5|7.0.2|7.0.4|~7.0.6", "magento/module-catalog": "101.1.*", "magento/module-import-export": "100.2.*", "magento/module-catalog-import-export": "100.2.*", diff --git a/app/code/Magento/CacheInvalidate/composer.json b/app/code/Magento/CacheInvalidate/composer.json index cafccadb41ad8..c886e069b66e3 100644 --- a/app/code/Magento/CacheInvalidate/composer.json +++ b/app/code/Magento/CacheInvalidate/composer.json @@ -2,7 +2,7 @@ "name": "magento/module-cache-invalidate", "description": "N/A", "require": { - "php": "~5.6.0|7.0.2|7.0.4|~7.0.6", + "php": "~5.6.5|7.0.2|7.0.4|~7.0.6", "magento/module-page-cache": "100.2.*", "magento/framework": "100.2.*" }, diff --git a/app/code/Magento/Captcha/composer.json b/app/code/Magento/Captcha/composer.json index 9d0ce3db92aa4..45ecd32464e9c 100644 --- a/app/code/Magento/Captcha/composer.json +++ b/app/code/Magento/Captcha/composer.json @@ -2,7 +2,7 @@ "name": "magento/module-captcha", "description": "N/A", "require": { - "php": "~5.6.0|7.0.2|7.0.4|~7.0.6", + "php": "~5.6.5|7.0.2|7.0.4|~7.0.6", "magento/module-store": "100.2.*", "magento/module-customer": "100.2.*", "magento/module-checkout": "100.2.*", diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Helper/Form/Wysiwyg/Content.php b/app/code/Magento/Catalog/Block/Adminhtml/Helper/Form/Wysiwyg/Content.php index d5488ecc5d085..ac8cb91222fba 100644 --- a/app/code/Magento/Catalog/Block/Adminhtml/Helper/Form/Wysiwyg/Content.php +++ b/app/code/Magento/Catalog/Block/Adminhtml/Helper/Form/Wysiwyg/Content.php @@ -14,6 +14,12 @@ use Magento\Backend\Block\Widget\Form; use Magento\Backend\Block\Widget\Form\Generic; +/** + * Class Content + * + * @deprecated + * @see \Magento\Catalog\Ui\DataProvider\Product\Form\Modifier\Eav + */ class Content extends Generic { /** diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Category/Image/Upload.php b/app/code/Magento/Catalog/Controller/Adminhtml/Category/Image/Upload.php index 285f4f1e3ffad..25bd24ef70b94 100644 --- a/app/code/Magento/Catalog/Controller/Adminhtml/Category/Image/Upload.php +++ b/app/code/Magento/Catalog/Controller/Adminhtml/Category/Image/Upload.php @@ -50,8 +50,10 @@ protected function _isAllowed() */ public function execute() { + $imageId = $this->_request->getParam('param_name', 'image'); + try { - $result = $this->imageUploader->saveFileToTmpDir('image'); + $result = $this->imageUploader->saveFileToTmpDir($imageId); $result['cookie'] = [ 'name' => $this->_getSession()->getName(), diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Category/Save.php b/app/code/Magento/Catalog/Controller/Adminhtml/Category/Save.php index 55ae96faeb7e6..fa29d46c61593 100644 --- a/app/code/Magento/Catalog/Controller/Adminhtml/Category/Save.php +++ b/app/code/Magento/Catalog/Controller/Adminhtml/Category/Save.php @@ -6,6 +6,7 @@ namespace Magento\Catalog\Controller\Adminhtml\Category; use Magento\Store\Model\StoreManagerInterface; +use Magento\Catalog\Api\Data\CategoryAttributeInterface; /** * Class Save @@ -48,6 +49,11 @@ class Save extends \Magento\Catalog\Controller\Adminhtml\Category */ private $storeManager; + /** + * @var \Magento\Eav\Model\Config + */ + private $eavConfig; + /** * Constructor * @@ -56,31 +62,35 @@ class Save extends \Magento\Catalog\Controller\Adminhtml\Category * @param \Magento\Framework\Controller\Result\JsonFactory $resultJsonFactory * @param \Magento\Framework\View\LayoutFactory $layoutFactory * @param StoreManagerInterface $storeManager + * @param \Magento\Eav\Model\Config $eavConfig */ public function __construct( \Magento\Backend\App\Action\Context $context, \Magento\Framework\Controller\Result\RawFactory $resultRawFactory, \Magento\Framework\Controller\Result\JsonFactory $resultJsonFactory, \Magento\Framework\View\LayoutFactory $layoutFactory, - StoreManagerInterface $storeManager + StoreManagerInterface $storeManager, + \Magento\Eav\Model\Config $eavConfig = null ) { parent::__construct($context); $this->resultRawFactory = $resultRawFactory; $this->resultJsonFactory = $resultJsonFactory; $this->layoutFactory = $layoutFactory; $this->storeManager = $storeManager; + $this->eavConfig = $eavConfig + ?: \Magento\Framework\App\ObjectManager::getInstance()->get(\Magento\Eav\Model\Config::class); } /** * Filter category data * + * @deprecated * @param array $rawData * @return array */ protected function _filterCategoryPostData(array $rawData) { $data = $rawData; - // @todo It is a workaround to prevent saving this data in category model and it has to be refactored in future if (isset($data['image']) && is_array($data['image'])) { if (!empty($data['image']['delete'])) { $data['image'] = null; @@ -126,7 +136,7 @@ public function execute() $this->storeManager->setCurrentStore($store->getCode()); $parentId = isset($categoryPostData['parent']) ? $categoryPostData['parent'] : null; if ($categoryPostData) { - $category->addData($this->_filterCategoryPostData($categoryPostData)); + $category->addData($categoryPostData); if ($isNewCategory) { $parentCategory = $this->getParentCategory($parentId, $storeId); $category->setPath($parentCategory->getPath()); @@ -248,18 +258,30 @@ public function execute() } /** - * Image data preprocessing + * Sets image attribute data to false if image was removed * * @param array $data - * * @return array */ public function imagePreprocessing($data) { - if (empty($data['image'])) { - unset($data['image']); - $data['image']['delete'] = true; + $entityType = $this->eavConfig->getEntityType(CategoryAttributeInterface::ENTITY_TYPE_CODE); + + foreach ($entityType->getAttributeCollection() as $attributeModel) { + $attributeCode = $attributeModel->getAttributeCode(); + $backendModel = $attributeModel->getBackend(); + + if (isset($data[$attributeCode])) { + continue; + } + + if (!$backendModel instanceof \Magento\Catalog\Model\Category\Attribute\Backend\Image) { + continue; + } + + $data[$attributeCode] = false; } + return $data; } diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/AddAttributeToTemplate.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/AddAttributeToTemplate.php index 8a2b76b1bbd56..c338937ae1be1 100644 --- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/AddAttributeToTemplate.php +++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/AddAttributeToTemplate.php @@ -16,7 +16,6 @@ use Magento\Eav\Api\Data\AttributeInterface; use Magento\Eav\Api\Data\AttributeSetInterface; use Magento\Framework\Api\SearchCriteriaBuilder; -use Magento\Framework\Api\SortOrderBuilder; use Magento\Framework\Exception\LocalizedException; use Psr\Log\LoggerInterface; use Magento\Framework\Api\ExtensionAttributesFactory; @@ -53,11 +52,6 @@ class AddAttributeToTemplate extends \Magento\Catalog\Controller\Adminhtml\Produ */ protected $searchCriteriaBuilder; - /** - * @var SortOrderBuilder - */ - protected $sortOrderBuilder; - /** * @var AttributeGroupInterfaceFactory */ @@ -115,7 +109,6 @@ public function execute() $attributeGroupSearchCriteria = $this->getSearchCriteriaBuilder() ->addFilter('attribute_set_id', $attributeSet->getAttributeSetId()) ->addFilter('attribute_group_code', $groupCode) - ->addSortOrder($this->getSortOrderBuilder()->setAscendingDirection()->create()) ->setPageSize(1) ->create(); @@ -252,18 +245,6 @@ private function getSearchCriteriaBuilder() return $this->searchCriteriaBuilder; } - /** - * @return SortOrderBuilder - */ - private function getSortOrderBuilder() - { - if (null === $this->sortOrderBuilder) { - $this->sortOrderBuilder = \Magento\Framework\App\ObjectManager::getInstance() - ->get(\Magento\Framework\Api\SortOrderBuilder::class); - } - return $this->sortOrderBuilder; - } - /** * @return AttributeManagementInterface */ diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Initialization/Helper.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Initialization/Helper.php index d59492c4065e7..4eed888b3cb08 100644 --- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Initialization/Helper.php +++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Initialization/Helper.php @@ -131,12 +131,7 @@ public function initializeFromData(\Magento\Catalog\Model\Product $product, arra $productData[$field] = []; } } - - foreach ($productData['website_ids'] as $websiteId => $checkboxValue) { - if (!$checkboxValue) { - unset($productData['website_ids'][$websiteId]); - } - } + $productData['website_ids'] = $this->filterWebsiteIds($productData['website_ids']); $wasLockedMedia = false; if ($product->isLockedAttribute('media')) { @@ -422,4 +417,23 @@ private function getDateTimeFilter() } return $this->dateTimeFilter; } + + /** + * Remove ids of non selected websites from $websiteIds array and return filtered data + * $websiteIds parameter expects array with website ids as keys and 1 (selected) or 0 (non selected) as values + * Only one id (default website ID) will be set to $websiteIds array when the single store mode is turned on + * + * @param array $websiteIds + * @return array + */ + private function filterWebsiteIds($websiteIds) + { + if (!$this->storeManager->isSingleStoreMode()) { + $websiteIds = array_filter((array)$websiteIds); + } else { + $websiteIds[$this->storeManager->getWebsite(true)->getId()] = 1; + } + + return $websiteIds; + } } diff --git a/app/code/Magento/Catalog/Model/Category.php b/app/code/Magento/Catalog/Model/Category.php index 9e855e05c7ce3..906718c64c6e0 100644 --- a/app/code/Magento/Catalog/Model/Category.php +++ b/app/code/Magento/Catalog/Model/Category.php @@ -652,14 +652,14 @@ public function formatUrlKey($str) } /** - * Retrieve image URL - * - * @return string + * @param string $attributeCode + * @return bool|string + * @throws \Magento\Framework\Exception\LocalizedException */ - public function getImageUrl() + public function getImageUrl($attributeCode = 'image') { $url = false; - $image = $this->getImage(); + $image = $this->getData($attributeCode); if ($image) { if (is_string($image)) { $url = $this->_storeManager->getStore()->getBaseUrl( 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 5eb4461ace5f0..5e8589428fad1 100644 --- a/app/code/Magento/Catalog/Model/Category/Attribute/Backend/Image.php +++ b/app/code/Magento/Catalog/Model/Category/Attribute/Backend/Image.php @@ -21,8 +21,6 @@ class Image extends \Magento\Eav\Model\Entity\Attribute\Backend\AbstractBackend protected $_uploaderFactory; /** - * Filesystem facade - * * @var \Magento\Framework\Filesystem * * @deprecated @@ -30,8 +28,6 @@ class Image extends \Magento\Eav\Model\Entity\Attribute\Backend\AbstractBackend protected $_filesystem; /** - * File Uploader factory - * * @var \Magento\MediaStorage\Model\File\UploaderFactory * * @deprecated @@ -46,15 +42,16 @@ class Image extends \Magento\Eav\Model\Entity\Attribute\Backend\AbstractBackend protected $_logger; /** - * Image uploader - * * @var \Magento\Catalog\Model\ImageUploader */ private $imageUploader; /** - * Image constructor. - * + * @var string + */ + private $additionalData = '_additional_data_'; + + /** * @param \Psr\Log\LoggerInterface $logger * @param \Magento\Framework\Filesystem $filesystem * @param \Magento\MediaStorage\Model\File\UploaderFactory $fileUploaderFactory @@ -70,8 +67,44 @@ public function __construct( } /** - * Get image uploader + * Gets image name from $value array. + * Will return empty string in a case when $value is not an array + * + * @param array $value Attribute value + * @return string + */ + private function getUploadedImageName($value) + { + if (is_array($value) && isset($value[0]['name'])) { + return $value[0]['name']; + } + + return ''; + } + + /** + * Avoiding saving potential upload data to DB + * Will set empty image attribute value if image was not uploaded * + * @param \Magento\Framework\DataObject $object + * @return $this + */ + public function beforeSave($object) + { + $attributeName = $this->getAttribute()->getName(); + $value = $object->getData($attributeName); + + if ($imageName = $this->getUploadedImageName($value)) { + $object->setData($this->additionalData . $attributeName, $value); + $object->setData($attributeName, $imageName); + } else if (!is_string($value)) { + $object->setData($attributeName, ''); + } + + return parent::beforeSave($object); + } + + /** * @return \Magento\Catalog\Model\ImageUploader * * @deprecated @@ -79,10 +112,10 @@ public function __construct( private function getImageUploader() { if ($this->imageUploader === null) { - $this->imageUploader = \Magento\Framework\App\ObjectManager::getInstance()->get( - \Magento\Catalog\CategoryImageUpload::class - ); + $this->imageUploader = \Magento\Framework\App\ObjectManager::getInstance() + ->get(\Magento\Catalog\CategoryImageUpload::class); } + return $this->imageUploader; } @@ -94,15 +127,16 @@ private function getImageUploader() */ public function afterSave($object) { - $image = $object->getData($this->getAttribute()->getName(), null); + $value = $object->getData($this->additionalData . $this->getAttribute()->getName()); - if ($image !== null) { + if ($imageName = $this->getUploadedImageName($value)) { try { - $this->getImageUploader()->moveFileFromTmp($image); + $this->getImageUploader()->moveFileFromTmp($imageName); } catch (\Exception $e) { $this->_logger->critical($e); } } + return $this; } } diff --git a/app/code/Magento/Catalog/Model/Category/DataProvider.php b/app/code/Magento/Catalog/Model/Category/DataProvider.php index 8626d183e2fac..aefd31e21ad62 100644 --- a/app/code/Magento/Catalog/Model/Category/DataProvider.php +++ b/app/code/Magento/Catalog/Model/Category/DataProvider.php @@ -17,6 +17,7 @@ use Magento\Ui\DataProvider\EavValidationRules; use Magento\Catalog\Model\CategoryFactory; use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Catalog\Model\Category\Attribute\Backend\Image as ImageBackendModel; /** * Class DataProvider @@ -206,11 +207,8 @@ public function getData() $categoryData = $this->addUseDefaultSettings($category, $categoryData); $categoryData = $this->addUseConfigSettings($categoryData); $categoryData = $this->filterFields($categoryData); - if (isset($categoryData['image'])) { - unset($categoryData['image']); - $categoryData['image'][0]['name'] = $category->getData('image'); - $categoryData['image'][0]['url'] = $category->getImageUrl(); - } + $categoryData = $this->convertValues($category, $categoryData); + $this->loadedData[$category->getId()] = $categoryData; } return $this->loadedData; @@ -371,6 +369,31 @@ protected function filterFields($categoryData) return array_diff_key($categoryData, array_flip($this->ignoreFields)); } + /** + * Converts category image data to acceptable for rendering format + * + * @param \Magento\Catalog\Model\Category $category + * @param array $categoryData + * @return array + */ + private function convertValues($category, $categoryData) + { + foreach ($category->getAttributes() as $attributeCode => $attribute) { + if (!isset($categoryData[$attributeCode])) { + continue; + } + + if ($attribute->getBackend() instanceof ImageBackendModel) { + unset($categoryData[$attributeCode]); + + $categoryData[$attributeCode][0]['name'] = $category->getData($attributeCode); + $categoryData[$attributeCode][0]['url'] = $category->getImageUrl($attributeCode); + } + } + + return $categoryData; + } + /** * Category's fields default values * diff --git a/app/code/Magento/Catalog/Model/Product.php b/app/code/Magento/Catalog/Model/Product.php index 9e9f18e011371..c913c82de1acd 100644 --- a/app/code/Magento/Catalog/Model/Product.php +++ b/app/code/Magento/Catalog/Model/Product.php @@ -22,7 +22,6 @@ * @method Product setHasError(bool $value) * @method \Magento\Catalog\Model\ResourceModel\Product getResource() * @method null|bool getHasError() - * @method Product setAssociatedProductIds(array $productIds) * @method array getAssociatedProductIds() * @method Product setNewVariationsAttributeSetId(int $value) * @method int getNewVariationsAttributeSetId() @@ -2614,4 +2613,16 @@ private function getMediaGalleryProcessor() } return $this->mediaGalleryProcessor; } + + /** + * Set the associated products + * + * @param array $productIds + * @return $this + */ + public function setAssociatedProductIds(array $productIds) + { + $this->getExtensionAttributes()->setConfigurableProductLinks($productIds); + return $this; + } } diff --git a/app/code/Magento/Catalog/Model/Product/Attribute/Backend/Stock.php b/app/code/Magento/Catalog/Model/Product/Attribute/Backend/Stock.php index 5c3fd4730aaed..0bb468b77ee6e 100644 --- a/app/code/Magento/Catalog/Model/Product/Attribute/Backend/Stock.php +++ b/app/code/Magento/Catalog/Model/Product/Attribute/Backend/Stock.php @@ -59,7 +59,7 @@ public function beforeSave($object) if (isset($stockData['qty']) && $stockData['qty'] === '') { $stockData['qty'] = null; } - if ($object->getStockData() !== null || $stockData !== null) { + if ($object->getStockData() !== null && $stockData !== null) { $object->setStockData(array_replace((array)$object->getStockData(), (array)$stockData)); } $object->unsetData($this->getAttribute()->getAttributeCode()); diff --git a/app/code/Magento/Catalog/Model/Product/Image.php b/app/code/Magento/Catalog/Model/Product/Image.php index 7d8b464db3b34..34e1ad30ad434 100644 --- a/app/code/Magento/Catalog/Model/Product/Image.php +++ b/app/code/Magento/Catalog/Model/Product/Image.php @@ -498,8 +498,7 @@ public function setBaseFile($file) $path = [ $this->_catalogProductMediaConfig->getBaseMediaPath(), 'cache', - $this->_storeManager->getStore()->getId(), - $path[] = $this->getDestinationSubdir(), + $this->getDestinationSubdir(), ]; if (!empty($this->_width) || !empty($this->_height)) { $path[] = "{$this->_width}x{$this->_height}"; diff --git a/app/code/Magento/Catalog/Model/ProductRepository.php b/app/code/Magento/Catalog/Model/ProductRepository.php index 4385c8add37ae..27796e1cda2a1 100644 --- a/app/code/Magento/Catalog/Model/ProductRepository.php +++ b/app/code/Magento/Catalog/Model/ProductRepository.php @@ -640,6 +640,7 @@ public function getList(\Magento\Framework\Api\SearchCriteriaInterface $searchCr $collection->load(); + $collection->addCategoryIds(); $searchResult = $this->searchResultsFactory->create(); $searchResult->setSearchCriteria($searchCriteria); $searchResult->setItems($collection->getItems()); diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/BaseSelectProcessorInterface.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/BaseSelectProcessorInterface.php new file mode 100644 index 0000000000000..e6a995b654703 --- /dev/null +++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/BaseSelectProcessorInterface.php @@ -0,0 +1,26 @@ +getMetadataPool()->getMetadata(ProductInterface::class)->getLinkField(); + $items = $this->getItems(); + + $select->where('entity.' . $linkField . ' IN (?)', array_map(function ($item) { + return $item->getId(); + }, $items)); foreach ($this->getConnection()->fetchAll($select) as $row) { $mediaGalleries[$row[$linkField]][] = $row; } - foreach ($this->getItems() as $item) { + foreach ($items as $item) { $mediaEntries = isset($mediaGalleries[$item->getId()]) ? $mediaGalleries[$item->getId()] : []; $this->getGalleryReadHandler()->addMediaDataToProduct($item, $mediaEntries); } diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/CompositeBaseSelectProcessor.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/CompositeBaseSelectProcessor.php new file mode 100644 index 0000000000000..86f5a23708400 --- /dev/null +++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/CompositeBaseSelectProcessor.php @@ -0,0 +1,49 @@ +baseSelectProcessors = $baseSelectProcessors; + } + + /** + * @param Select $select + * @return Select + */ + public function process(Select $select) + { + foreach ($this->baseSelectProcessors as $baseSelectProcessor) { + $select = $baseSelectProcessor->process($select); + } + return $select; + } +} diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Eav/AbstractEav.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Eav/AbstractEav.php index d9612f18c9111..432bc696ef7ce 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Eav/AbstractEav.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Eav/AbstractEav.php @@ -197,7 +197,7 @@ protected function _prepareRelationIndexSelect($parentIds = null) )->joinLeft( ['e' => $this->getTable('catalog_product_entity')], 'e.' . $linkField .' = l.parent_id', - ['e.entity_id as parent_id'] + [] )->join( ['cs' => $this->getTable('store')], '', @@ -205,9 +205,17 @@ protected function _prepareRelationIndexSelect($parentIds = null) )->join( ['i' => $idxTable], 'l.child_id = i.entity_id AND cs.store_id = i.store_id', - ['attribute_id', 'store_id', 'value'] + [] )->group( - ['parent_id', 'i.attribute_id', 'i.store_id', 'i.value'] + ['parent_id', 'i.attribute_id', 'i.store_id', 'i.value', 'l.child_id'] + )->columns( + [ + 'parent_id' => 'e.entity_id', + 'attribute_id' => 'i.attribute_id', + 'store_id' => 'i.store_id', + 'value' => 'i.value', + 'source_id' => 'l.child_id' + ] ); if ($parentIds !== null) { $select->where('e.entity_id IN(?)', $parentIds); @@ -222,7 +230,7 @@ protected function _prepareRelationIndexSelect($parentIds = null) 'select' => $select, 'entity_field' => new \Zend_Db_Expr('l.parent_id'), 'website_field' => new \Zend_Db_Expr('cs.website_id'), - 'store_field' => new \Zend_Db_Expr('cs.store_id') + 'store_field' => new \Zend_Db_Expr('cs.store_id'), ] ); diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Eav/Decimal.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Eav/Decimal.php index e8d9889e68d59..a45d4f13a1a9a 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Eav/Decimal.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Eav/Decimal.php @@ -85,6 +85,7 @@ protected function _prepareIndex($entityIds = null, $attributeId = null) 'pdd.attribute_id', 'cs.store_id', 'value' => $productValueExpression, + 'source_id' => 'cpe.entity_id', ] ); @@ -116,7 +117,7 @@ protected function _prepareIndex($entityIds = null, $attributeId = null) 'select' => $select, 'entity_field' => new \Zend_Db_Expr('cpe.entity_id'), 'website_field' => new \Zend_Db_Expr('cs.website_id'), - 'store_field' => new \Zend_Db_Expr('cs.store_id') + 'store_field' => new \Zend_Db_Expr('cs.store_id'), ] ); diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Eav/Source.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Eav/Source.php index c4eda1c987192..1d37c57aa8b25 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Eav/Source.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Eav/Source.php @@ -178,6 +178,7 @@ protected function _prepareSelectIndex($entityIds = null, $attributeId = null) 'pid.attribute_id', 'pid.store_id', 'value' => $ifNullSql, + 'pid.entity_id', ] )->where( 'pid.attribute_id IN(?)', @@ -200,7 +201,7 @@ protected function _prepareSelectIndex($entityIds = null, $attributeId = null) 'select' => $select, 'entity_field' => new \Zend_Db_Expr('pid.entity_id'), 'website_field' => new \Zend_Db_Expr('pid.website_id'), - 'store_field' => new \Zend_Db_Expr('pid.store_id') + 'store_field' => new \Zend_Db_Expr('pid.store_id'), ] ); $query = $select->insertFromSelect($idxTable); @@ -221,11 +222,7 @@ protected function _prepareMultiselectIndex($entityIds = null, $attributeId = nu $connection = $this->getConnection(); // prepare multiselect attributes - if ($attributeId === null) { - $attrIds = $this->_getIndexableAttributes(true); - } else { - $attrIds = [$attributeId]; - } + $attrIds = $attributeId === null ? $this->_getIndexableAttributes(true) : [$attributeId]; if (!$attrIds) { return $this; @@ -247,20 +244,20 @@ protected function _prepareMultiselectIndex($entityIds = null, $attributeId = nu $productValueExpression = $connection->getCheckSql('pvs.value_id > 0', 'pvs.value', 'pvd.value'); $select = $connection->select()->from( ['pvd' => $this->getTable('catalog_product_entity_varchar')], - [$productIdField, 'attribute_id'] + [] )->join( ['cs' => $this->getTable('store')], '', - ['store_id'] + [] )->joinLeft( ['pvs' => $this->getTable('catalog_product_entity_varchar')], "pvs.{$productIdField} = pvd.{$productIdField} AND pvs.attribute_id = pvd.attribute_id" . ' AND pvs.store_id=cs.store_id', - ['value' => $productValueExpression] + [] )->joinLeft( ['cpe' => $this->getTable('catalog_product_entity')], "cpe.{$productIdField} = pvd.{$productIdField}", - ['entity_id'] + [] )->where( 'pvd.store_id=?', $connection->getIfNullSql('pvs.store_id', \Magento\Store\Model\Store::DEFAULT_STORE_ID) @@ -272,6 +269,14 @@ protected function _prepareMultiselectIndex($entityIds = null, $attributeId = nu $attrIds )->where( 'cpe.entity_id IS NOT NULL' + )->columns( + [ + 'entity_id' => 'cpe.entity_id', + 'attribute_id' => 'attribute_id', + 'store_id' => 'cs.store_id', + 'value' => $productValueExpression, + 'source_id' => 'cpe.entity_id', + ] ); $statusCond = $connection->quoteInto('=?', ProductStatus::STATUS_ENABLED); @@ -289,30 +294,11 @@ protected function _prepareMultiselectIndex($entityIds = null, $attributeId = nu 'select' => $select, 'entity_field' => new \Zend_Db_Expr('cpe.entity_id'), 'website_field' => new \Zend_Db_Expr('cs.website_id'), - 'store_field' => new \Zend_Db_Expr('cs.store_id') + 'store_field' => new \Zend_Db_Expr('cs.store_id'), ] ); - $i = 0; - $data = []; - $query = $select->query(); - while ($row = $query->fetch()) { - $values = explode(',', $row['value']); - foreach ($values as $valueId) { - if (isset($options[$row['attribute_id']][$valueId])) { - $data[] = [$row['entity_id'], $row['attribute_id'], $row['store_id'], $valueId]; - $i++; - if ($i % 10000 == 0) { - $this->_saveIndexData($data); - $data = []; - } - } - } - } - - $this->_saveIndexData($data); - unset($options); - unset($data); + $this->saveDataFromSelect($select, $options); return $this; } @@ -331,7 +317,7 @@ protected function _saveIndexData(array $data) $connection = $this->getConnection(); $connection->insertArray( $this->getIdxTable(), - ['entity_id', 'attribute_id', 'store_id', 'value'], + ['entity_id', 'attribute_id', 'store_id', 'value', 'source_id'], $data ); return $this; @@ -348,4 +334,31 @@ public function getIdxTable($table = null) { return $this->tableStrategy->getTableName('catalog_product_index_eav'); } + + /** + * @param \Magento\Framework\DB\Select $select + * @param array $options + * @return void + */ + private function saveDataFromSelect(\Magento\Framework\DB\Select $select, array $options) + { + $i = 0; + $data = []; + $query = $select->query(); + while ($row = $query->fetch()) { + $values = explode(',', $row['value']); + foreach ($values as $valueId) { + if (isset($options[$row['attribute_id']][$valueId])) { + $data[] = [$row['entity_id'], $row['attribute_id'], $row['store_id'], $valueId, $row['source_id']]; + $i++; + if ($i % 10000 == 0) { + $this->_saveIndexData($data); + $data = []; + } + } + } + } + + $this->_saveIndexData($data); + } } diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/LinkedProductSelectBuilderByIndexPrice.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/LinkedProductSelectBuilderByIndexPrice.php index ea72691ea0039..3aa6642c82d84 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/LinkedProductSelectBuilderByIndexPrice.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/LinkedProductSelectBuilderByIndexPrice.php @@ -6,7 +6,8 @@ namespace Magento\Catalog\Model\ResourceModel\Product\Indexer; use Magento\Catalog\Api\Data\ProductInterface; -use Magento\Catalog\Model\Product; +use Magento\Catalog\Model\ResourceModel\Product\BaseSelectProcessorInterface; +use Magento\Framework\App\ObjectManager; use Magento\Framework\DB\Select; use Magento\Catalog\Model\ResourceModel\Product\LinkedProductSelectBuilderInterface; @@ -32,22 +33,31 @@ class LinkedProductSelectBuilderByIndexPrice implements LinkedProductSelectBuild */ private $metadataPool; + /** + * @var BaseSelectProcessorInterface + */ + private $baseSelectProcessor; + /** * @param \Magento\Store\Model\StoreManagerInterface $storeManager * @param \Magento\Framework\App\ResourceConnection $resourceConnection * @param \Magento\Customer\Model\Session $customerSession * @param \Magento\Framework\EntityManager\MetadataPool $metadataPool + * @param BaseSelectProcessorInterface $baseSelectProcessor */ public function __construct( \Magento\Store\Model\StoreManagerInterface $storeManager, \Magento\Framework\App\ResourceConnection $resourceConnection, \Magento\Customer\Model\Session $customerSession, - \Magento\Framework\EntityManager\MetadataPool $metadataPool + \Magento\Framework\EntityManager\MetadataPool $metadataPool, + BaseSelectProcessorInterface $baseSelectProcessor = null ) { $this->storeManager = $storeManager; $this->resource = $resourceConnection; $this->customerSession = $customerSession; $this->metadataPool = $metadataPool; + $this->baseSelectProcessor = (null !== $baseSelectProcessor) + ? $baseSelectProcessor : ObjectManager::getInstance()->get(BaseSelectProcessorInterface::class); } /** @@ -58,24 +68,27 @@ public function build($productId) $linkField = $this->metadataPool->getMetadata(ProductInterface::class)->getLinkField(); $productTable = $this->resource->getTableName('catalog_product_entity'); - return [$this->resource->getConnection()->select() + $priceSelect = $this->resource->getConnection()->select() ->from(['parent' => $productTable], '') ->joinInner( ['link' => $this->resource->getTableName('catalog_product_relation')], "link.parent_id = parent.$linkField", [] )->joinInner( - ['child' => $productTable], - "child.entity_id = link.child_id", + [BaseSelectProcessorInterface::PRODUCT_TABLE_ALIAS => $productTable], + sprintf('%s.entity_id = link.child_id', BaseSelectProcessorInterface::PRODUCT_TABLE_ALIAS), ['entity_id'] )->joinInner( ['t' => $this->resource->getTableName('catalog_product_index_price')], - 't.entity_id = child.entity_id', + sprintf('t.entity_id = %s.entity_id', BaseSelectProcessorInterface::PRODUCT_TABLE_ALIAS), [] - )->where('parent.entity_id = ? ', $productId) + )->where('parent.entity_id = ?', $productId) ->where('t.website_id = ?', $this->storeManager->getStore()->getWebsiteId()) ->where('t.customer_group_id = ?', $this->customerSession->getCustomerGroupId()) ->order('t.min_price ' . Select::SQL_ASC) - ->limit(1)]; + ->limit(1); + $priceSelect = $this->baseSelectProcessor->process($priceSelect); + + return [$priceSelect]; } } diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/DefaultPrice.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/DefaultPrice.php index 289445ae2daf0..2b979ff79fe5c 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/DefaultPrice.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/DefaultPrice.php @@ -368,7 +368,7 @@ protected function prepareFinalPriceDataForType($entityIds, $type) 'select' => $select, 'entity_field' => new \Zend_Db_Expr('e.entity_id'), 'website_field' => new \Zend_Db_Expr('cw.website_id'), - 'store_field' => new \Zend_Db_Expr('cs.store_id') + 'store_field' => new \Zend_Db_Expr('cs.store_id'), ] ); diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/LinkedProductSelectBuilderByBasePrice.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/LinkedProductSelectBuilderByBasePrice.php index d325ab1a9a08d..7caa72b367979 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Product/LinkedProductSelectBuilderByBasePrice.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/LinkedProductSelectBuilderByBasePrice.php @@ -7,6 +7,7 @@ use Magento\Catalog\Api\Data\ProductInterface; use Magento\Catalog\Model\Product; +use Magento\Framework\App\ObjectManager; use Magento\Framework\DB\Select; use Magento\Store\Model\Store; @@ -37,25 +38,34 @@ class LinkedProductSelectBuilderByBasePrice implements LinkedProductSelectBuilde */ private $metadataPool; + /** + * @var BaseSelectProcessorInterface + */ + private $baseSelectProcessor; + /** * @param \Magento\Store\Model\StoreManagerInterface $storeManager * @param \Magento\Framework\App\ResourceConnection $resourceConnection * @param \Magento\Eav\Model\Config $eavConfig * @param \Magento\Catalog\Helper\Data $catalogHelper * @param \Magento\Framework\EntityManager\MetadataPool $metadataPool + * @param BaseSelectProcessorInterface $baseSelectProcessor */ public function __construct( \Magento\Store\Model\StoreManagerInterface $storeManager, \Magento\Framework\App\ResourceConnection $resourceConnection, \Magento\Eav\Model\Config $eavConfig, \Magento\Catalog\Helper\Data $catalogHelper, - \Magento\Framework\EntityManager\MetadataPool $metadataPool + \Magento\Framework\EntityManager\MetadataPool $metadataPool, + BaseSelectProcessorInterface $baseSelectProcessor = null ) { $this->storeManager = $storeManager; $this->resource = $resourceConnection; $this->eavConfig = $eavConfig; $this->catalogHelper = $catalogHelper; $this->metadataPool = $metadataPool; + $this->baseSelectProcessor = (null !== $baseSelectProcessor) + ? $baseSelectProcessor : ObjectManager::getInstance()->get(BaseSelectProcessorInterface::class); } /** @@ -74,18 +84,19 @@ public function build($productId) "link.parent_id = parent.$linkField", [] )->joinInner( - ['child' => $productTable], - "child.entity_id = link.child_id", + [BaseSelectProcessorInterface::PRODUCT_TABLE_ALIAS => $productTable], + sprintf('%s.entity_id = link.child_id', BaseSelectProcessorInterface::PRODUCT_TABLE_ALIAS, $linkField), ['entity_id'] )->joinInner( ['t' => $priceAttribute->getBackendTable()], - "t.$linkField = child.$linkField", + sprintf('t.%s = %s.%1$s', $linkField, BaseSelectProcessorInterface::PRODUCT_TABLE_ALIAS), [] - )->where('parent.entity_id = ? ', $productId) + )->where('parent.entity_id = ?', $productId) ->where('t.attribute_id = ?', $priceAttribute->getAttributeId()) ->where('t.value IS NOT NULL') ->order('t.value ' . Select::SQL_ASC) ->limit(1); + $priceSelect = $this->baseSelectProcessor->process($priceSelect); $priceSelectDefault = clone $priceSelect; $priceSelectDefault->where('t.store_id = ?', Store::DEFAULT_STORE_ID); diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/LinkedProductSelectBuilderBySpecialPrice.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/LinkedProductSelectBuilderBySpecialPrice.php index 792a8f5b86d10..68eaf206e293f 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Product/LinkedProductSelectBuilderBySpecialPrice.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/LinkedProductSelectBuilderBySpecialPrice.php @@ -7,9 +7,13 @@ use Magento\Catalog\Api\Data\ProductInterface; use Magento\Catalog\Model\Product; +use Magento\Framework\App\ObjectManager; use Magento\Framework\DB\Select; use Magento\Store\Model\Store; +/** + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ class LinkedProductSelectBuilderBySpecialPrice implements LinkedProductSelectBuilderInterface { /** @@ -47,6 +51,11 @@ class LinkedProductSelectBuilderBySpecialPrice implements LinkedProductSelectBui */ private $metadataPool; + /** + * @var BaseSelectProcessorInterface + */ + private $baseSelectProcessor; + /** * @param \Magento\Store\Model\StoreManagerInterface $storeManager * @param \Magento\Framework\App\ResourceConnection $resourceConnection @@ -55,6 +64,7 @@ class LinkedProductSelectBuilderBySpecialPrice implements LinkedProductSelectBui * @param \Magento\Framework\Stdlib\DateTime $dateTime * @param \Magento\Framework\Stdlib\DateTime\TimezoneInterface $localeDate * @param \Magento\Framework\EntityManager\MetadataPool $metadataPool + * @param BaseSelectProcessorInterface $baseSelectProcessor */ public function __construct( \Magento\Store\Model\StoreManagerInterface $storeManager, @@ -63,7 +73,8 @@ public function __construct( \Magento\Catalog\Helper\Data $catalogHelper, \Magento\Framework\Stdlib\DateTime $dateTime, \Magento\Framework\Stdlib\DateTime\TimezoneInterface $localeDate, - \Magento\Framework\EntityManager\MetadataPool $metadataPool + \Magento\Framework\EntityManager\MetadataPool $metadataPool, + BaseSelectProcessorInterface $baseSelectProcessor = null ) { $this->storeManager = $storeManager; $this->resource = $resourceConnection; @@ -72,6 +83,8 @@ public function __construct( $this->dateTime = $dateTime; $this->localeDate = $localeDate; $this->metadataPool = $metadataPool; + $this->baseSelectProcessor = (null !== $baseSelectProcessor) + ? $baseSelectProcessor : ObjectManager::getInstance()->get(BaseSelectProcessorInterface::class); } /** @@ -95,12 +108,12 @@ public function build($productId) "link.parent_id = parent.$linkField", [] )->joinInner( - ['child' => $productTable], - "child.entity_id = link.child_id", + [BaseSelectProcessorInterface::PRODUCT_TABLE_ALIAS => $productTable], + sprintf('%s.entity_id = link.child_id', BaseSelectProcessorInterface::PRODUCT_TABLE_ALIAS), ['entity_id'] )->joinInner( ['t' => $specialPriceAttribute->getBackendTable()], - "t.$linkField = child.$linkField", + sprintf('t.%s = %s.%1$s', $linkField, BaseSelectProcessorInterface::PRODUCT_TABLE_ALIAS), [] )->joinLeft( ['special_from' => $specialPriceFromDate->getBackendTable()], @@ -116,7 +129,7 @@ public function build($productId) $specialPriceToDate->getAttributeId() ), '' - )->where('parent.entity_id = ? ', $productId) + )->where('parent.entity_id = ?', $productId) ->where('t.attribute_id = ?', $specialPriceAttribute->getAttributeId()) ->where('t.value IS NOT NULL') ->where( @@ -127,6 +140,7 @@ public function build($productId) $currentDate )->order('t.value ' . Select::SQL_ASC) ->limit(1); + $specialPrice = $this->baseSelectProcessor->process($specialPrice); $specialPriceDefault = clone $specialPrice; $specialPriceDefault->where('t.store_id = ?', Store::DEFAULT_STORE_ID); diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/LinkedProductSelectBuilderByTierPrice.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/LinkedProductSelectBuilderByTierPrice.php index d2d6d89c0a2a5..25bf83f837de7 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Product/LinkedProductSelectBuilderByTierPrice.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/LinkedProductSelectBuilderByTierPrice.php @@ -6,7 +6,7 @@ namespace Magento\Catalog\Model\ResourceModel\Product; use Magento\Catalog\Api\Data\ProductInterface; -use Magento\Catalog\Model\Product; +use Magento\Framework\App\ObjectManager; use Magento\Framework\DB\Select; class LinkedProductSelectBuilderByTierPrice implements LinkedProductSelectBuilderInterface @@ -41,25 +41,34 @@ class LinkedProductSelectBuilderByTierPrice implements LinkedProductSelectBuilde */ private $metadataPool; + /** + * @var BaseSelectProcessorInterface + */ + private $baseSelectProcessor; + /** * @param \Magento\Store\Model\StoreManagerInterface $storeManager * @param \Magento\Framework\App\ResourceConnection $resourceConnection * @param \Magento\Customer\Model\Session $customerSession * @param \Magento\Catalog\Helper\Data $catalogHelper * @param \Magento\Framework\EntityManager\MetadataPool $metadataPool + * @param BaseSelectProcessorInterface $baseSelectProcessor */ public function __construct( \Magento\Store\Model\StoreManagerInterface $storeManager, \Magento\Framework\App\ResourceConnection $resourceConnection, \Magento\Customer\Model\Session $customerSession, \Magento\Catalog\Helper\Data $catalogHelper, - \Magento\Framework\EntityManager\MetadataPool $metadataPool + \Magento\Framework\EntityManager\MetadataPool $metadataPool, + BaseSelectProcessorInterface $baseSelectProcessor = null ) { $this->storeManager = $storeManager; $this->resource = $resourceConnection; $this->customerSession = $customerSession; $this->catalogHelper = $catalogHelper; $this->metadataPool = $metadataPool; + $this->baseSelectProcessor = (null !== $baseSelectProcessor) + ? $baseSelectProcessor : ObjectManager::getInstance()->get(BaseSelectProcessorInterface::class); } /** @@ -77,18 +86,19 @@ public function build($productId) "link.parent_id = parent.$linkField", [] )->joinInner( - ['child' => $productTable], - "child.entity_id = link.child_id", + [BaseSelectProcessorInterface::PRODUCT_TABLE_ALIAS => $productTable], + sprintf('%s.entity_id = link.child_id', BaseSelectProcessorInterface::PRODUCT_TABLE_ALIAS), ['entity_id'] )->joinInner( ['t' => $this->resource->getTableName('catalog_product_entity_tier_price')], - "t.$linkField = child.$linkField", + sprintf('t.%s = %s.%1$s', $linkField, BaseSelectProcessorInterface::PRODUCT_TABLE_ALIAS), [] - )->where('parent.entity_id = ? ', $productId) + )->where('parent.entity_id = ?', $productId) ->where('t.all_groups = 1 OR customer_group_id = ?', $this->customerSession->getCustomerGroupId()) ->where('t.qty = ?', 1) ->order('t.value ' . Select::SQL_ASC) ->limit(1); + $priceSelect = $this->baseSelectProcessor->process($priceSelect); $priceSelectDefault = clone $priceSelect; $priceSelectDefault->where('t.website_id = ?', self::DEFAULT_WEBSITE_ID); diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/StatusBaseSelectProcessor.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/StatusBaseSelectProcessor.php new file mode 100644 index 0000000000000..656998113fdb9 --- /dev/null +++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/StatusBaseSelectProcessor.php @@ -0,0 +1,61 @@ +eavConfig = $eavConfig; + $this->metadataPool = $metadataPool; + } + + /** + * @param Select $select + * @return Select + */ + public function process(Select $select) + { + $linkField = $this->metadataPool->getMetadata(ProductInterface::class)->getLinkField(); + $statusAttribute = $this->eavConfig->getAttribute(Product::ENTITY, ProductInterface::STATUS); + + $select->join( + ['status_attr' => $statusAttribute->getBackendTable()], + sprintf('status_attr.%s = %s.%1$s', $linkField, self::PRODUCT_TABLE_ALIAS), + [] + ) + ->where('status_attr.attribute_id = ?', $statusAttribute->getAttributeId()) + ->where('status_attr.value = ?', Status::STATUS_ENABLED); + + return $select; + } +} diff --git a/app/code/Magento/Catalog/Model/System/Config/Backend/Catalog/Url/Rewrite/Suffix.php b/app/code/Magento/Catalog/Model/System/Config/Backend/Catalog/Url/Rewrite/Suffix.php index 5ed18d15b2f30..c1f37eca1c55c 100644 --- a/app/code/Magento/Catalog/Model/System/Config/Backend/Catalog/Url/Rewrite/Suffix.php +++ b/app/code/Magento/Catalog/Model/System/Config/Backend/Catalog/Url/Rewrite/Suffix.php @@ -40,6 +40,11 @@ class Suffix extends \Magento\Framework\App\Config\Value */ protected $resource; + /** + * @var \Magento\Framework\App\Config\ScopePool + */ + private $scopePool; + /** * @param \Magento\Framework\Model\Context $context * @param \Magento\Framework\Registry $registry @@ -75,6 +80,22 @@ public function __construct( $this->resource = $appResource; } + /** + * Get instance of ScopePool + * + * @return \Magento\Framework\App\Config\ScopePool + * @deprecated + */ + private function getScopePool() + { + if ($this->scopePool === null) { + $this->scopePool = \Magento\Framework\App\ObjectManager::getInstance()->get( + \Magento\Framework\App\Config\ScopePool::class + ); + } + return $this->scopePool; + } + /** * Check url rewrite suffix - whether we can support it * @@ -103,6 +124,24 @@ public function afterSave() return parent::afterSave(); } + /** + * {@inheritdoc} + */ + public function afterDeleteCommit() + { + if ($this->isValueChanged()) { + $this->updateSuffixForUrlRewrites(); + if ($this->isCategorySuffixChanged()) { + $this->cacheTypeList->invalidate([ + \Magento\Framework\App\Cache\Type\Block::TYPE_IDENTIFIER, + \Magento\Framework\App\Cache\Type\Collection::TYPE_IDENTIFIER + ]); + } + } + + return parent::afterDeleteCommit(); + } + /** * Check is category suffix changed * @@ -135,7 +174,12 @@ protected function updateSuffixForUrlRewrites() } $entities = $this->urlFinder->findAllByData($dataFilter); $oldSuffixPattern = '~' . preg_quote($this->getOldValue()) . '$~'; - $suffix = $this->getValue(); + if ($this->getValue() !== null) { + $suffix = $this->getValue(); + } else { + $this->getScopePool()->clean(); + $suffix = $this->_config->getValue($this->getPath()); + } foreach ($entities as $urlRewrite) { $bind = $urlRewrite->getIsAutogenerated() ? [UrlRewrite::REQUEST_PATH => preg_replace($oldSuffixPattern, $suffix, $urlRewrite->getRequestPath())] diff --git a/app/code/Magento/Catalog/Setup/InstallSchema.php b/app/code/Magento/Catalog/Setup/InstallSchema.php index 2ccd8af79a5da..a2ba6aa283f65 100644 --- a/app/code/Magento/Catalog/Setup/InstallSchema.php +++ b/app/code/Magento/Catalog/Setup/InstallSchema.php @@ -18,6 +18,7 @@ class InstallSchema implements InstallSchemaInterface /** * {@inheritdoc} * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + * @throws \Zend_Db_Exception */ public function install(SchemaSetupInterface $setup, ModuleContextInterface $context) { @@ -1853,6 +1854,9 @@ public function install(SchemaSetupInterface $setup, ModuleContextInterface $con $installer->getConnection() ->createTable($table); + $customerGroupTable = $setup->getConnection()->describeTable($setup->getTable('customer_group')); + $customerGroupIdType = $customerGroupTable['customer_group_id']['DATA_TYPE'] == 'int' + ? \Magento\Framework\DB\Ddl\Table::TYPE_INTEGER : $customerGroupTable['customer_group_id']['DATA_TYPE']; /** * Create table 'catalog_product_entity_tier_price' */ @@ -1883,7 +1887,7 @@ public function install(SchemaSetupInterface $setup, ModuleContextInterface $con ) ->addColumn( 'customer_group_id', - \Magento\Framework\DB\Ddl\Table::TYPE_SMALLINT, + $customerGroupIdType, null, ['unsigned' => true, 'nullable' => false, 'default' => '0'], 'Customer Group ID' @@ -2426,7 +2430,6 @@ public function install(SchemaSetupInterface $setup, ModuleContextInterface $con 'option_id', $installer->getTable('catalog_product_option'), 'option_id', - \Magento\Framework\DB\Ddl\Table::ACTION_CASCADE, \Magento\Framework\DB\Ddl\Table::ACTION_CASCADE ) ->setComment( @@ -2937,7 +2940,7 @@ public function install(SchemaSetupInterface $setup, ModuleContextInterface $con ) ->addColumn( 'customer_group_id', - \Magento\Framework\DB\Ddl\Table::TYPE_SMALLINT, + $customerGroupIdType, null, ['unsigned' => true, 'nullable' => false, 'primary' => true], 'Customer Group ID' @@ -3056,7 +3059,7 @@ public function install(SchemaSetupInterface $setup, ModuleContextInterface $con ) ->addColumn( 'customer_group_id', - \Magento\Framework\DB\Ddl\Table::TYPE_SMALLINT, + $customerGroupIdType, null, ['unsigned' => true, 'nullable' => false, 'primary' => true], 'Customer Group ID' diff --git a/app/code/Magento/Catalog/Setup/UpgradeSchema.php b/app/code/Magento/Catalog/Setup/UpgradeSchema.php old mode 100644 new mode 100755 index 9683632f121b6..7fc2ef7d219ba --- a/app/code/Magento/Catalog/Setup/UpgradeSchema.php +++ b/app/code/Magento/Catalog/Setup/UpgradeSchema.php @@ -36,9 +36,81 @@ public function upgrade(SchemaSetupInterface $setup, ModuleContextInterface $con if (version_compare($context->getVersion(), '2.1.0', '<')) { $this->addPercentageValueColumn($setup); } + + if (version_compare($context->getVersion(), '2.1.1', '<')) { + $tables = [ + 'catalog_product_index_price_cfg_opt_agr_idx', + 'catalog_product_index_price_cfg_opt_agr_tmp', + 'catalog_product_index_price_cfg_opt_idx', + 'catalog_product_index_price_cfg_opt_tmp', + 'catalog_product_index_price_final_idx', + 'catalog_product_index_price_final_tmp', + 'catalog_product_index_price_idx', + 'catalog_product_index_price_opt_agr_idx', + 'catalog_product_index_price_opt_agr_tmp', + 'catalog_product_index_price_opt_idx', + 'catalog_product_index_price_opt_tmp', + 'catalog_product_index_price_tmp', + ]; + foreach ($tables as $table) { + $setup->getConnection()->modifyColumn( + $setup->getTable($table), + 'customer_group_id', + ['type' => 'integer', 'nullable' => false] + ); + } + } + + if (version_compare($context->getVersion(), '2.1.2', '<')) { + $this->addSourceEntityIdToProductEavIndex($setup); + } + $setup->endSetup(); } + /** + * Add the column 'source_id' to the Product EAV index tables. + * It allows to identify which entity was used to create value in the index. + * It is useful to identify original entity in a composite products. + * + * @param SchemaSetupInterface $setup + * @return void + */ + private function addSourceEntityIdToProductEavIndex(SchemaSetupInterface $setup) + { + $tables = [ + 'catalog_product_index_eav', + 'catalog_product_index_eav_idx', + 'catalog_product_index_eav_tmp', + 'catalog_product_index_eav_decimal', + 'catalog_product_index_eav_decimal_idx', + 'catalog_product_index_eav_decimal_tmp', + ]; + $connection = $setup->getConnection(); + foreach ($tables as $tableName) { + $tableName = $setup->getTable($tableName); + $connection->addColumn( + $tableName, + 'source_id', + [ + 'type' => \Magento\Framework\DB\Ddl\Table::TYPE_INTEGER, + 'unsigned' => true, + 'nullable' => false, + 'default' => 0, + 'comment' => 'Original entity Id for attribute value', + ] + ); + $connection->dropIndex($tableName, $connection->getPrimaryKeyName($tableName)); + $primaryKeyFields = ['entity_id', 'attribute_id', 'store_id', 'value', 'source_id']; + $setup->getConnection()->addIndex( + $tableName, + $connection->getIndexName($tableName, $primaryKeyFields), + $primaryKeyFields, + \Magento\Framework\DB\Adapter\AdapterInterface::INDEX_TYPE_PRIMARY + ); + } + } + /** * @param SchemaSetupInterface $setup * @return void diff --git a/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Category/Image/UploadTest.php b/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Category/Image/UploadTest.php new file mode 100644 index 0000000000000..387e9a8a6a891 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Category/Image/UploadTest.php @@ -0,0 +1,68 @@ +objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); + } + + public function executeDataProvider() + { + return [ + ['image1', 'image1'], + ['image2', 'image2'], + [null, 'image'], + ]; + } + + /** + * @param string $name + * @param string $savedName + * + * @dataProvider executeDataProvider + */ + public function testExecute($name, $savedName) + { + $request = $this->objectManager->getObject(Request::class); + + $uploader = $this->getMock(ImageUploader::class, ['saveFileToTmpDir'], [], '', false); + + $resultFactory = $this->getMock(ResultFactory::class, ['create'], [], '', false); + + $resultFactory->expects($this->once()) + ->method('create') + ->will($this->returnValue(new DataObject())); + + $model = $this->objectManager->getObject(Model::class, [ + 'request' => $request, + 'resultFactory' => $resultFactory, + 'imageUploader' => $uploader + ]); + + $uploader->expects($this->once()) + ->method('saveFileToTmpDir') + ->with($savedName) + ->will($this->returnValue([])); + + $request->setParam('param_name', $name); + + $model->execute(); + } +} diff --git a/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Category/SaveTest.php b/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Category/SaveTest.php index 51d99f7219575..745d46710053a 100644 --- a/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Category/SaveTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Category/SaveTest.php @@ -5,6 +5,8 @@ */ namespace Magento\Catalog\Test\Unit\Controller\Adminhtml\Category; +use \Magento\Catalog\Controller\Adminhtml\Category\Save as Model; + /** * Class SaveTest * @SuppressWarnings(PHPMD.CouplingBetweenObjects) @@ -14,67 +16,47 @@ class SaveTest extends \PHPUnit_Framework_TestCase /** * @var \Magento\Backend\Model\View\Result\RedirectFactory|\PHPUnit_Framework_MockObject_MockObject */ - protected $resultRedirectFactoryMock; - - /** - * @var \Magento\Framework\Controller\Result\RawFactory|\PHPUnit_Framework_MockObject_MockObject - */ - protected $resultRawFactoryMock; + private $resultRedirectFactoryMock; /** * @var \Magento\Framework\Controller\Result\JsonFactory|\PHPUnit_Framework_MockObject_MockObject */ - protected $resultJsonFactoryMock; + private $resultJsonFactoryMock; /** * @var \Magento\Framework\View\LayoutFactory|\PHPUnit_Framework_MockObject_MockObject */ - protected $layoutFactoryMock; - - /** - * @var \Magento\Backend\App\Action\Context|\PHPUnit_Framework_MockObject_MockObject - */ - protected $contextMock; - - /** - * @var \Magento\Framework\View\Page\Title|\PHPUnit_Framework_MockObject_MockObject - */ - protected $titleMock; + private $layoutFactoryMock; /** * @var \Magento\Framework\App\RequestInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $requestMock; + private $requestMock; /** * @var \Magento\Framework\ObjectManagerInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $objectManagerMock; + private $objectManagerMock; /** * @var \Magento\Framework\Event\ManagerInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $eventManagerMock; - - /** - * @var \Magento\Framework\App\ResponseInterface|\PHPUnit_Framework_MockObject_MockObject - */ - protected $responseMock; + private $eventManagerMock; /** * @var \Magento\Framework\Message\ManagerInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $messageManagerMock; + private $messageManagerMock; /** * @var \Magento\Framework\TestFramework\Unit\Helper\ObjectManager */ - protected $objectManager; + private $objectManager; /** * @var \Magento\Catalog\Controller\Adminhtml\Category\Save */ - protected $save; + private $save; /** * Set up @@ -84,24 +66,7 @@ class SaveTest extends \PHPUnit_Framework_TestCase */ protected function setUp() { - $this->markTestSkipped('Due to MAGETWO-48956'); $this->objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); - - $this->contextMock = $this->getMock( - \Magento\Backend\App\Action\Context::class, - [ - 'getTitle', - 'getRequest', - 'getObjectManager', - 'getEventManager', - 'getResponse', - 'getMessageManager', - 'getResultRedirectFactory' - ], - [], - '', - false - ); $this->resultRedirectFactoryMock = $this->getMock( \Magento\Backend\Model\View\Result\RedirectFactory::class, ['create'], @@ -109,13 +74,6 @@ protected function setUp() '', false ); - $this->resultRawFactoryMock = $this->getMock( - \Magento\Framework\Controller\Result\RawFactory::class, - [], - [], - '', - false - ); $this->resultJsonFactoryMock = $this->getMock( \Magento\Framework\Controller\Result\JsonFactory::class, ['create'], @@ -151,12 +109,6 @@ protected function setUp() true, ['dispatch'] ); - $this->responseMock = $this->getMockForAbstractClass( - \Magento\Framework\App\ResponseInterface::class, - [], - '', - false - ); $this->messageManagerMock = $this->getMockForAbstractClass( \Magento\Framework\Message\ManagerInterface::class, [], @@ -167,23 +119,15 @@ protected function setUp() ['addSuccess', 'getMessages'] ); - $this->contextMock->expects($this->any())->method('getTitle')->willReturn($this->titleMock); - $this->contextMock->expects($this->any())->method('getRequest')->willReturn($this->requestMock); - $this->contextMock->expects($this->any())->method('getObjectManager')->willReturn($this->objectManagerMock); - $this->contextMock->expects($this->any())->method('getEventManager')->willReturn($this->eventManagerMock); - $this->contextMock->expects($this->any())->method('getResponse')->willReturn($this->responseMock); - $this->contextMock->expects($this->any())->method('getMessageManager')->willReturn($this->messageManagerMock); - $this->contextMock->expects($this->any()) - ->method('getResultRedirectFactory') - ->willReturn($this->resultRedirectFactoryMock); - $this->save = $this->objectManager->getObject( \Magento\Catalog\Controller\Adminhtml\Category\Save::class, [ - 'context' => $this->contextMock, - 'resultRawFactory' => $this->resultRawFactoryMock, + 'request' => $this->requestMock, + 'eventManager' => $this->eventManagerMock, + 'messageManager' => $this->messageManagerMock, 'resultJsonFactory' => $this->resultJsonFactoryMock, - 'layoutFactory' => $this->layoutFactoryMock + 'layoutFactory' => $this->layoutFactoryMock, + 'resultRedirectFactory' => $this->resultRedirectFactoryMock ] ); } @@ -201,6 +145,8 @@ protected function setUp() */ public function testExecute($categoryId, $storeId, $parentId) { + $this->markTestSkipped('Due to MAGETWO-48956'); + $rootCategoryId = \Magento\Catalog\Model\Category::TREE_ROOT_ID; $products = [['any_product']]; $postData = [ @@ -577,4 +523,95 @@ public function dataProviderExecute() ] ]; } + + /** + * @return array + */ + public function imagePreprocessingDataProvider() + { + return [ + [['attribute1' => null, 'attribute2' => 123]], + [['attribute2' => 123]] + ]; + } + + /** + * @dataProvider imagePreprocessingDataProvider + * + * @param array $data + */ + public function testImagePreprocessingWithoutValue($data) + { + $eavConfig = $this->getMock(\Magento\Eav\Model\Config::class, ['getEntityType'], [], '', false); + + $imageBackendModel = $this->objectManager->getObject( + \Magento\Catalog\Model\Category\Attribute\Backend\Image::class + ); + + $collection = new \Magento\Framework\DataObject(['attribute_collection' => [ + new \Magento\Framework\DataObject([ + 'attribute_code' => 'attribute1', + 'backend' => $imageBackendModel + ]), + new \Magento\Framework\DataObject([ + 'attribute_code' => 'attribute2', + 'backend' => new \Magento\Framework\DataObject() + ]) + ]]); + + $eavConfig->expects($this->once()) + ->method('getEntityType') + ->with(\Magento\Catalog\Api\Data\CategoryAttributeInterface::ENTITY_TYPE_CODE) + ->will($this->returnValue($collection)); + + $model = $this->objectManager->getObject(\Magento\Catalog\Controller\Adminhtml\Category\Save::class, [ + 'eavConfig' => $eavConfig + ]); + + $result = $model->imagePreprocessing($data); + + $this->assertEquals([ + 'attribute1' => false, + 'attribute2' => 123 + ], $result); + } + + public function testImagePreprocessingWithValue() + { + $eavConfig = $this->getMock(\Magento\Eav\Model\Config::class, ['getEntityType'], [], '', false); + + $imageBackendModel = $this->objectManager->getObject( + \Magento\Catalog\Model\Category\Attribute\Backend\Image::class + ); + + $collection = new \Magento\Framework\DataObject(['attribute_collection' => [ + new \Magento\Framework\DataObject([ + 'attribute_code' => 'attribute1', + 'backend' => $imageBackendModel + ]), + new \Magento\Framework\DataObject([ + 'attribute_code' => 'attribute2', + 'backend' => new \Magento\Framework\DataObject() + ]) + ]]); + + $eavConfig->expects($this->once()) + ->method('getEntityType') + ->with(\Magento\Catalog\Api\Data\CategoryAttributeInterface::ENTITY_TYPE_CODE) + ->will($this->returnValue($collection)); + + $model = $this->objectManager->getObject(Model::class, [ + 'eavConfig' => $eavConfig + ]); + + $result = $model->imagePreprocessing([ + 'attribute1' => 'somevalue', + 'attribute2' => null + ]); + + $this->assertEquals([ + 'attribute1' => 'somevalue', + 'attribute2' => null + ], $result); + } } diff --git a/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/AddAttributeToTemplateTest.php b/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/AddAttributeToTemplateTest.php new file mode 100644 index 0000000000000..a55766f895b62 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/AddAttributeToTemplateTest.php @@ -0,0 +1,247 @@ +objectManager = new ObjectManager($this); + $this->contextMock = $this->getMockBuilder(Context::class) + ->disableOriginalConstructor() + ->getMock(); + $this->productBuilderMock = $this->getMockBuilder(ProductBuilder::class) + ->disableOriginalConstructor() + ->getMock(); + $this->resultJsonFactoryMock = $this->getMockBuilder(JsonFactory::class) + ->disableOriginalConstructor() + ->setMethods(['create']) + ->getMock(); + $this->requestMock = $this->getMockBuilder(\Magento\Framework\App\RequestInterface::class) + ->setMethods(['getParam', 'setParam']) + ->getMockForAbstractClass(); + $this->contextMock->expects($this->once()) + ->method('getRequest') + ->willReturn($this->requestMock); + $this->attributeSetRepositoryMock = $this->getMockBuilder(AttributeSetRepositoryInterface::class) + ->setMethods(['get']) + ->getMockForAbstractClass(); + $this->attributeSetInterfaceMock = $this->getMockBuilder(AttributeSetInterface::class) + ->getMockForAbstractClass(); + $this->searchCriteriaBuilderMock = $this->getMockBuilder(SearchCriteriaBuilder::class) + ->disableOriginalConstructor() + ->setMethods(['addFilter', 'create', 'setPageSize']) + ->getMockForAbstractClass(); + $this->searchCriteriaMock = $this->getMockBuilder(SearchCriteria::class) + ->disableOriginalConstructor() + ->getMock(); + $this->attributeGroupRepositoryMock = $this->getMockBuilder(AttributeGroupRepositoryInterface::class) + ->setMethods(['getList']) + ->getMockForAbstractClass(); + $this->attributeGroupSearchResultsMock = $this->getMockBuilder(AttributeGroupSearchResultsInterface::class) + ->setMethods(['getItems']) + ->getMockForAbstractClass(); + $this->attributeGroupInterfaceFactoryMock = $this->getMockBuilder(AttributeGroupInterfaceFactory::class) + ->setMethods(['create']) + ->disableOriginalConstructor() + ->getMock(); + $this->attributeGroupInterfaceMock = $this->getMockBuilder(AttributeGroupInterface::class) + ->setMethods(['getExtensionAttributes']) + ->getMockForAbstractClass(); + $this->jsonMock = $this->getMockBuilder(Json::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->controller = $this->objectManager->getObject( + AddAttributeToTemplate::class, + [ + 'context' => $this->contextMock, + 'productBuilder' => $this->productBuilderMock, + 'resultJsonFactory' => $this->resultJsonFactoryMock, + ] + ); + + $this->objectManager->setBackwardCompatibleProperty( + $this->controller, + 'attributeSetRepository', + $this->attributeSetRepositoryMock + ); + $this->objectManager->setBackwardCompatibleProperty( + $this->controller, + 'searchCriteriaBuilder', + $this->searchCriteriaBuilderMock + ); + $this->objectManager->setBackwardCompatibleProperty( + $this->controller, + 'attributeGroupRepository', + $this->attributeGroupRepositoryMock + ); + $this->objectManager->setBackwardCompatibleProperty( + $this->controller, + 'attributeGroupFactory', + $this->attributeGroupInterfaceFactoryMock + ); + } + + public function testExecuteWithoutAttributeGroupItems() + { + $groupCode = 'attributes'; + $groupName = 'Attributes'; + $groupSortOrder = '15'; + $templateId = '4'; + $attributeIds = [ + 'selected' => ["178"], + 'total' => '1' + ]; + + $this->requestMock + ->expects($this->any()) + ->method('getParam') + ->willReturnMap( + [ + ['groupCode', null, $groupCode], + ['groupName', null, $groupName], + ['groupSortOrder', null, $groupSortOrder], + ['templateId', null, $templateId], + ['attributeIds', [], $attributeIds] + ] + ); + + $this->attributeSetRepositoryMock->expects($this->once()) + ->method('get') + ->willReturn($this->attributeSetInterfaceMock); + + $this->searchCriteriaBuilderMock->expects($this->any()) + ->method('addFilter') + ->willReturnSelf(); + $this->searchCriteriaBuilderMock->expects($this->any()) + ->method('create') + ->willReturn($this->searchCriteriaMock); + $this->searchCriteriaBuilderMock->expects($this->once()) + ->method('setPageSize') + ->willReturnSelf(); + $this->searchCriteriaBuilderMock->expects($this->never()) + ->method('addSortOrder') + ->willReturnSelf(); + + $this->attributeGroupRepositoryMock->expects($this->once()) + ->method('getList') + ->willReturn($this->attributeGroupSearchResultsMock); + $this->attributeGroupSearchResultsMock->expects($this->once()) + ->method('getItems') + ->willReturn(null); + + $this->attributeGroupInterfaceFactoryMock->expects($this->once()) + ->method('create') + ->willReturn($this->attributeGroupInterfaceMock); + $this->attributeGroupInterfaceMock->expects($this->once()) + ->method('getExtensionAttributes') + ->willThrowException(new LocalizedException(__('Could not get extension attributes'))); + + $this->resultJsonFactoryMock->expects($this->once()) + ->method('create') + ->willReturn($this->jsonMock); + $this->jsonMock->expects($this->once())->method('setJsonData') + ->willReturnSelf(); + + $this->controller->execute(); + } +} diff --git a/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Initialization/HelperTest.php b/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Initialization/HelperTest.php index a4218e92b3486..c67ed25d9c666 100644 --- a/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Initialization/HelperTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Initialization/HelperTest.php @@ -5,18 +5,14 @@ */ namespace Magento\Catalog\Test\Unit\Controller\Adminhtml\Product\Initialization; -use Magento\Catalog\Api\Data\ProductLinkInterfaceFactory; use Magento\Catalog\Controller\Adminhtml\Product\Initialization\Helper; use Magento\Catalog\Controller\Adminhtml\Product\Initialization\StockDataFilter; use Magento\Catalog\Model\Product; use Magento\Catalog\Model\Product\Option; -use Magento\Catalog\Model\ProductRepository; use Magento\Framework\App\RequestInterface; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; -use Magento\Store\Api\Data\StoreInterface; use Magento\Store\Api\Data\WebsiteInterface; use Magento\Store\Model\StoreManagerInterface; -use Magento\Framework\Stdlib\DateTime\Filter\Date as DateFilter; use Magento\Catalog\Api\Data\ProductCustomOptionInterfaceFactory; use Magento\Catalog\Model\Product\Initialization\Helper\ProductLinks; @@ -34,11 +30,6 @@ class HelperTest extends \PHPUnit_Framework_TestCase */ protected $objectManager; - /** - * @var int - */ - protected $websiteId = 1; - /** * @var Helper */ @@ -64,106 +55,54 @@ class HelperTest extends \PHPUnit_Framework_TestCase */ protected $productMock; - /** - * @var StoreInterface|\PHPUnit_Framework_MockObject_MockObject - */ - protected $storeMock; - - /** - * @var WebsiteInterface|\PHPUnit_Framework_MockObject_MockObject - */ - protected $websiteMock; - - /** - * @var DateFilter|\PHPUnit_Framework_MockObject_MockObject - */ - protected $dateFilterMock; - - /** - * @var ProductLinkInterfaceFactory|\PHPUnit_Framework_MockObject_MockObject - */ - protected $productLinkFactoryMock; - - /** - * @var ProductRepository|\PHPUnit_Framework_MockObject_MockObject - */ - protected $productRepositoryMock; - /** * @var ProductCustomOptionInterfaceFactory|\PHPUnit_Framework_MockObject_MockObject */ protected $customOptionFactoryMock; /** - * @var Option|\PHPUnit_Framework_MockObject_MockObject - */ - protected $customOptionMock; - - /** - * @var \PHPUnit_Framework_MockObject_MockObject + * @var \Magento\Catalog\Model\Product\Link\Resolver|\PHPUnit_Framework_MockObject_MockObject */ protected $linkResolverMock; /** - * @var ProductLinks + * @var ProductLinks|\PHPUnit_Framework_MockObject_MockObject */ protected $productLinksMock; protected function setUp() { $this->objectManager = new ObjectManager($this); - $this->productLinkFactoryMock = $this->getMockBuilder(ProductLinkInterfaceFactory::class) - ->disableOriginalConstructor() - ->getMock(); - $this->productRepositoryMock = $this->getMockBuilder(ProductRepository::class) - ->disableOriginalConstructor() - ->getMock(); $this->requestMock = $this->getMockBuilder(RequestInterface::class) ->setMethods(['getPost']) ->getMockForAbstractClass(); - $this->storeMock = $this->getMockBuilder(StoreInterface::class) - ->setMethods(['getWebsite']) - ->getMockForAbstractClass(); - $this->websiteMock = $this->getMockBuilder(WebsiteInterface::class) - ->getMockForAbstractClass(); $this->storeManagerMock = $this->getMockBuilder(StoreManagerInterface::class) ->getMockForAbstractClass(); - $this->dateFilterMock = $this->getMockBuilder(DateFilter::class) - ->disableOriginalConstructor() - ->getMock(); $this->stockFilterMock = $this->getMockBuilder(StockDataFilter::class) ->disableOriginalConstructor() ->getMock(); $this->productMock = $this->getMockBuilder(Product::class) - ->setMethods([ - 'getId', - 'setWebsiteIds', - 'isLockedAttribute', - 'lockAttribute', - 'getAttributes', - 'unlockAttribute', - 'getOptionsReadOnly', - 'setCanSaveCustomOptions', - '__sleep', - '__wakeup', - 'getSku', - 'getProductLinks', - 'getWebsiteIds' - ]) + ->setMethods( + [ + 'getId', + 'isLockedAttribute', + 'lockAttribute', + 'getAttributes', + 'unlockAttribute', + 'getOptionsReadOnly', + 'getSku', + 'getProductLinks', + ] + ) ->disableOriginalConstructor() - ->getMock(); + ->getMockForAbstractClass(); $this->customOptionFactoryMock = $this->getMockBuilder(ProductCustomOptionInterfaceFactory::class) ->disableOriginalConstructor() ->setMethods(['create']) ->getMock(); - $this->customOptionMock = $this->getMockBuilder(Option::class) - ->disableOriginalConstructor() - ->setMethods(null) - ->getMock(); $this->productLinksMock = $this->getMockBuilder(ProductLinks::class) ->disableOriginalConstructor() ->getMock(); - $this->productLinksMock->expects($this->any()) ->method('initializeLinks') ->willReturn($this->productMock); @@ -173,10 +112,7 @@ protected function setUp() 'storeManager' => $this->storeManagerMock, 'stockFilter' => $this->stockFilterMock, 'productLinks' => $this->productLinksMock, - 'dateFilter' => $this->dateFilterMock, 'customOptionFactory' => $this->customOptionFactoryMock, - 'productLinkFactory' => $this->productLinkFactoryMock, - 'productRepository' => $this->productRepositoryMock, ]); $this->linkResolverMock = $this->getMockBuilder(\Magento\Catalog\Model\Product\Link\Resolver::class) @@ -190,9 +126,13 @@ protected function setUp() /** * @covers \Magento\Catalog\Controller\Adminhtml\Product\Initialization\Helper::initialize - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + * @param bool $isSingleStore + * @param array $websiteIds + * @param array $expWebsiteIds + * + * @dataProvider initializeDataProvider */ - public function testInitialize() + public function testInitialize($isSingleStore, $websiteIds, $expWebsiteIds) { $optionsData = [ 'option1' => ['is_delete' => true, 'name' => 'name1', 'price' => 'price1', 'option_id' => ''], @@ -202,6 +142,7 @@ public function testInitialize() $productData = [ 'stock_data' => ['stock_data'], 'options' => $optionsData, + 'website_ids' => $websiteIds ]; $attributeNonDate = $this->getMockBuilder(\Magento\Catalog\Model\ResourceModel\Eav\Attribute::class) ->disableOriginalConstructor() @@ -218,69 +159,39 @@ public function testInitialize() ->disableOriginalConstructor() ->getMock(); - $attributeNonDate->expects($this->any()) - ->method('getBackend') - ->willReturn($attributeNonDateBackEnd); - $attributeDate->expects($this->any()) - ->method('getBackend') - ->willReturn($attributeDateBackEnd); - $this->productMock->expects($this->any()) - ->method('getProductLinks') - ->willReturn([]); - $attributeNonDateBackEnd->expects($this->any()) - ->method('getType') - ->willReturn('non-datetime'); - $attributeDateBackEnd->expects($this->any()) - ->method('getType') - ->willReturn('datetime'); - - $attributesArray = [ - $attributeNonDate, - $attributeDate - ]; + $attributeNonDate->expects($this->any())->method('getBackend')->willReturn($attributeNonDateBackEnd); + $attributeDate->expects($this->any())->method('getBackend')->willReturn($attributeDateBackEnd); + $this->productMock->expects($this->any())->method('getProductLinks')->willReturn([]); + $attributeNonDateBackEnd->expects($this->any())->method('getType')->willReturn('non-datetime'); + $attributeDateBackEnd->expects($this->any())->method('getType')->willReturn('datetime'); $useDefaults = ['attributeCode1', 'attributeCode2']; - $this->requestMock->expects($this->at(0)) - ->method('getPost') - ->with('product') - ->willReturn($productData); - $this->requestMock->expects($this->at(1)) - ->method('getPost') - ->with('use_default') - ->willReturn($useDefaults); + $this->requestMock->expects($this->any())->method('getPost')->willReturnMap( + [ + ['product', [], $productData], + ['use_default', null, $useDefaults] + ] + ); $this->linkResolverMock->expects($this->once())->method('getLinks')->willReturn([]); - $this->stockFilterMock->expects($this->once()) - ->method('filter') - ->with(['stock_data']) + $this->stockFilterMock->expects($this->once())->method('filter')->with(['stock_data']) ->willReturn(['stock_data']); - $this->productMock->expects($this->once()) - ->method('isLockedAttribute') - ->with('media') - ->willReturn(true); - $this->productMock->expects($this->once()) - ->method('unlockAttribute') - ->with('media'); - $this->productMock->expects($this->any()) - ->method('getProductLinks') - ->willReturn([]); - $this->productMock->expects($this->once()) - ->method('lockAttribute') - ->with('media'); - $this->productMock->expects($this->once()) - ->method('getAttributes') - ->willReturn($attributesArray); - - $this->productMock->expects($this->any()) - ->method('getSku') - ->willReturn('sku'); - $this->productMock->expects($this->any()) - ->method('getOptionsReadOnly') - ->willReturn(false); - - $firstExpectedCustomOption = clone $this->customOptionMock; + $this->productMock->expects($this->once())->method('isLockedAttribute')->with('media')->willReturn(true); + $this->productMock->expects($this->once())->method('unlockAttribute')->with('media'); + $this->productMock->expects($this->any())->method('getProductLinks')->willReturn([]); + $this->productMock->expects($this->once())->method('lockAttribute')->with('media'); + $this->productMock->expects($this->once())->method('getAttributes') + ->willReturn([$attributeNonDate, $attributeDate]); + $this->productMock->expects($this->any())->method('getSku')->willReturn('sku'); + $this->productMock->expects($this->any())->method('getOptionsReadOnly')->willReturn(false); + + $customOptionMock = $this->getMockBuilder(Option::class) + ->disableOriginalConstructor() + ->setMethods(null) + ->getMock(); + $firstExpectedCustomOption = clone $customOptionMock; $firstExpectedCustomOption->setData($optionsData['option2']); - $secondExpectedCustomOption = clone $this->customOptionMock; + $secondExpectedCustomOption = clone $customOptionMock; $secondExpectedCustomOption->setData($optionsData['option3']); $this->customOptionFactoryMock->expects($this->any()) ->method('create') @@ -293,8 +204,13 @@ public function testInitialize() $secondExpectedCustomOption ] ]); + $website = $this->getMockBuilder(WebsiteInterface::class)->getMockForAbstractClass(); + $website->expects($this->any())->method('getId')->willReturn(1); + $this->storeManagerMock->expects($this->once())->method('isSingleStoreMode')->willReturn($isSingleStore); + $this->storeManagerMock->expects($this->any())->method('getWebsite')->willReturn($website); $this->assertEquals($this->productMock, $this->helper->initialize($this->productMock)); + $this->assertEquals($expWebsiteIds, $this->productMock->getDataByKey('website_ids')); $productOptions = $this->productMock->getOptions(); $this->assertTrue(2 == count($productOptions)); @@ -305,6 +221,35 @@ public function testInitialize() $this->assertTrue('sku' == $option2->getData('product_sku')); } + /** + * @return array + */ + public function initializeDataProvider() + { + return [ + [ + 'single_store' => false, + 'website_ids' => ['1' => 1, '2' => 1], + 'expected_website_ids' => ['1' => 1, '2' => 1] + ], + [ + 'single_store' => false, + 'website_ids' => ['1' => 1, '2' => 0], + 'expected_website_ids' => ['1' => 1] + ], + [ + 'single_store' => false, + 'website_ids' => ['1' => 0, '2' => 0], + 'expected_website_ids' => [] + ], + [ + 'single_store' => true, + 'website_ids' => [], + 'expected_website_ids' => ['1' => 1] + ], + ]; + } + /** * Data provider for testMergeProductOptions * 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 new file mode 100644 index 0000000000000..aa78fea896687 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Unit/Model/Category/Attribute/Backend/ImageTest.php @@ -0,0 +1,293 @@ +objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); + + $this->attribute = $this->getMockForAbstractClass( + \Magento\Eav\Model\Entity\Attribute\AbstractAttribute::class, + [], + 'TestAttribute', + false, + false, + true, + ['getName'] + ); + + $this->attribute->expects($this->once()) + ->method('getName') + ->will($this->returnValue('test_attribute')); + + $this->logger = $this->getMockForAbstractClass( + \Psr\Log\LoggerInterface::class, + [], + 'TestLogger', + false, + false, + true, + ['critical'] + ); + + $this->imageUploader = $this->getMock( + \Magento\Catalog\Model\ImageUploader::class, + ['moveFileFromTmp'], + [], + '', + false + ); + } + + /** + * @return array + */ + public function deletedValueDataProvider() + { + return [ + [false], + [['delete' => true]] + ]; + } + + /** + * @dataProvider deletedValueDataProvider + * + * @param array $value + */ + public function testBeforeSaveValueDeletion($value) + { + $model = $this->objectManager->getObject(\Magento\Catalog\Model\Category\Attribute\Backend\Image::class); + $model->setAttribute($this->attribute); + + $object = new \Magento\Framework\DataObject([ + 'test_attribute' => $value + ]); + + $model->beforeSave($object); + + $this->assertEquals('', $object->getTestAttribute()); + } + + /** + * @return array + */ + public function invalidValueDataProvider() + { + $closure = function () { + return false; + }; + + return [ + [1234], + [true], + [new \stdClass()], + [$closure], + [['a' => 1, 'b' => 2]] + ]; + } + + /** + * @dataProvider invalidValueDataProvider + * + * @param array $value + */ + public function testBeforeSaveValueInvalid($value) + { + $model = $this->objectManager->getObject(\Magento\Catalog\Model\Category\Attribute\Backend\Image::class); + $model->setAttribute($this->attribute); + + $object = new \Magento\Framework\DataObject([ + 'test_attribute' => $value + ]); + + $model->beforeSave($object); + + $this->assertEquals('', $object->getTestAttribute()); + } + + public function testBeforeSaveAttributeFileName() + { + $model = $this->objectManager->getObject(\Magento\Catalog\Model\Category\Attribute\Backend\Image::class); + $model->setAttribute($this->attribute); + + $object = new \Magento\Framework\DataObject([ + 'test_attribute' => [ + ['name' => 'test123.jpg'] + ] + ]); + + $model->beforeSave($object); + + $this->assertEquals('test123.jpg', $object->getTestAttribute()); + } + + public function testBeforeSaveTemporaryAttribute() + { + $model = $this->objectManager->getObject(\Magento\Catalog\Model\Category\Attribute\Backend\Image::class); + $model->setAttribute($this->attribute); + + $object = new \Magento\Framework\DataObject([ + 'test_attribute' => [ + ['name' => 'test123.jpg', 'tmp_name' => 'abc123', 'url' => 'http://www.example.com/test123.jpg'] + ] + ]); + + $model->beforeSave($object); + + $this->assertEquals([ + ['name' => 'test123.jpg', 'tmp_name' => 'abc123', 'url' => 'http://www.example.com/test123.jpg'] + ], $object->getData('_additional_data_test_attribute')); + } + + public function testBeforeSaveAttributeStringValue() + { + $model = $this->objectManager->getObject(\Magento\Catalog\Model\Category\Attribute\Backend\Image::class); + $model->setAttribute($this->attribute); + + $object = new \Magento\Framework\DataObject([ + 'test_attribute' => 'test123.jpg' + ]); + + $model->beforeSave($object); + + $this->assertEquals('test123.jpg', $object->getTestAttribute()); + $this->assertNull($object->getData('_additional_data_test_attribute')); + } + + /** + * @return \Magento\Catalog\Model\Category\Attribute\Backend\Image + */ + private function setUpModelForAfterSave() + { + $objectManagerMock = $this->getMock( + \Magento\Framework\App\ObjectManager::class, + ['get'], + [], + '', + false + ); + + $imageUploaderMock = $this->imageUploader; + + $objectManagerMock->expects($this->any()) + ->method('get') + ->will($this->returnCallback(function ($class, $params = []) use ($imageUploaderMock) { + if ($class == \Magento\Catalog\CategoryImageUpload::class) { + return $imageUploaderMock; + } + + return $this->objectManager->get($class, $params); + })); + + $model = $this->objectManager->getObject(\Magento\Catalog\Model\Category\Attribute\Backend\Image::class, [ + 'objectManager' => $objectManagerMock, + 'logger' => $this->logger + ]); + $this->objectManager->setBackwardCompatibleProperty($model, 'imageUploader', $this->imageUploader); + + return $model->setAttribute($this->attribute); + } + + public function attributeValueDataProvider() + { + return [ + [[['name' => 'test1234.jpg']]], + ['test1234.jpg'], + [''], + [false] + ]; + } + + /** + * @dataProvider attributeValueDataProvider + * + * @param array $value + */ + public function testAfterSaveWithAdditionalData($value) + { + $model = $this->setUpModelForAfterSave(); + + $this->imageUploader->expects($this->once()) + ->method('moveFileFromTmp') + ->with($this->equalTo('test1234.jpg')); + + $object = new \Magento\Framework\DataObject( + [ + 'test_attribute' => $value, + '_additional_data_test_attribute' => [['name' => 'test1234.jpg']] + ] + ); + + $model->afterSave($object); + } + + /** + * @dataProvider attributeValueDataProvider + * + * @param array $value + */ + public function testAfterSaveWithoutAdditionalData($value) + { + $model = $this->setUpModelForAfterSave(); + + $this->imageUploader->expects($this->never()) + ->method('moveFileFromTmp'); + + $object = new \Magento\Framework\DataObject( + [ + 'test_attribute' => $value + ] + ); + + $model->afterSave($object); + } + + public function testAfterSaveWithExceptions() + { + $model = $this->setUpModelForAfterSave(); + + $exception = new \Exception(); + + $this->imageUploader->expects($this->any()) + ->method('moveFileFromTmp') + ->will($this->throwException($exception)); + + $this->logger->expects($this->once()) + ->method('critical') + ->with($this->equalTo($exception)); + + $object = new \Magento\Framework\DataObject( + [ + '_additional_data_test_attribute' => [['name' => 'test1234.jpg']] + ] + ); + + $model->afterSave($object); + } +} diff --git a/app/code/Magento/Catalog/Test/Unit/Model/CategoryTest.php b/app/code/Magento/Catalog/Test/Unit/Model/CategoryTest.php index 471037f1d3f45..37d0751a81bc0 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/CategoryTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/CategoryTest.php @@ -9,6 +9,7 @@ namespace Magento\Catalog\Test\Unit\Model; use Magento\Catalog\Model\Indexer; +use Magento\Catalog\Model\Category; /** * @SuppressWarnings(PHPMD.TooManyFields) @@ -16,113 +17,131 @@ */ class CategoryTest extends \PHPUnit_Framework_TestCase { - /** @var \Magento\Catalog\Model\Category */ - protected $category; - - /** @var \Magento\Framework\Model\Context|\PHPUnit_Framework_MockObject_MockObject */ - protected $context; - - /** @var \Magento\Framework\Event\ManagerInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $eventManager; + /** + * @var \Magento\Catalog\Model\Category + */ + private $category; - /** @var \Magento\Framework\App\CacheInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $cacheManager; + /** + * @var \Magento\Framework\Registry|\PHPUnit_Framework_MockObject_MockObject + */ + private $registry; - /** @var \Magento\Framework\Registry|\PHPUnit_Framework_MockObject_MockObject */ - protected $registry; + /** + * @var \Magento\Store\Model\StoreManagerInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $storeManager; - /** @var \Magento\Store\Model\StoreManagerInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $storeManager; + /** + * @var \Magento\Catalog\Model\ResourceModel\Category\Tree|\PHPUnit_Framework_MockObject_MockObject + */ + private $categoryTreeResource; - /** @var \Magento\Catalog\Model\ResourceModel\Category\Tree|\PHPUnit_Framework_MockObject_MockObject */ - protected $categoryTreeResource; + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $categoryTreeFactory; - /** @var \PHPUnit_Framework_MockObject_MockObject */ - protected $categoryTreeFactory; + /** + * @var \Magento\Catalog\Api\CategoryRepositoryInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $categoryRepository; - /** @var \Magento\Catalog\Api\CategoryRepositoryInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $categoryRepository; + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $storeCollectionFactory; - /** @var \PHPUnit_Framework_MockObject_MockObject */ - protected $storeCollectionFactory; + /** + * @var \Magento\Framework\UrlInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $url; - /** @var \Magento\Framework\UrlInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $url; + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $productCollectionFactory; - /** @var \PHPUnit_Framework_MockObject_MockObject */ - protected $productCollectionFactory; + /** + * @var \Magento\Catalog\Model\Config|\PHPUnit_Framework_MockObject_MockObject + */ + private $catalogConfig; - /** @var \Magento\Catalog\Model\Config|\PHPUnit_Framework_MockObject_MockObject */ - protected $catalogConfig; + /** + * @var \Magento\Framework\Filter\FilterManager|\PHPUnit_Framework_MockObject_MockObject + */ + private $filterManager; - /** @var \Magento\Framework\Filter\FilterManager|\PHPUnit_Framework_MockObject_MockObject */ - protected $filterManager; + /** + * @var \Magento\Catalog\Model\Indexer\Category\Flat\State|\PHPUnit_Framework_MockObject_MockObject + */ + private $flatState; - /** @var \Magento\Catalog\Model\Indexer\Category\Flat\State|\PHPUnit_Framework_MockObject_MockObject */ - protected $flatState; + /** + * @var \Magento\Framework\Indexer\IndexerInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $flatIndexer; - /** @var \Magento\Framework\Indexer\IndexerInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $flatIndexer; + /** + * @var \Magento\Framework\Indexer\IndexerInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $productIndexer; - /** @var \Magento\Framework\Indexer\IndexerInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $productIndexer; + /** + * @var \Magento\CatalogUrlRewrite\Model\CategoryUrlPathGenerator|\PHPUnit_Framework_MockObject_MockObject + */ + private $categoryUrlPathGenerator; - /** @var \Magento\CatalogUrlRewrite\Model\CategoryUrlPathGenerator|\PHPUnit_Framework_MockObject_MockObject */ - protected $categoryUrlPathGenerator; + /** + * @var \Magento\UrlRewrite\Model\UrlFinderInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $urlFinder; - /** @var \Magento\UrlRewrite\Model\UrlFinderInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $urlFinder; + /** + * @var \Magento\Framework\Model\ResourceModel\AbstractResource|\PHPUnit_Framework_MockObject_MockObject + */ + private $resource; - /** @var \Magento\Framework\Model\ResourceModel\AbstractResource|\PHPUnit_Framework_MockObject_MockObject */ - protected $resource; + /** + * @var \Magento\Framework\Indexer\IndexerRegistry|\PHPUnit_Framework_MockObject_MockObject + */ + private $indexerRegistry; - /** @var \Magento\Framework\Indexer\IndexerRegistry|\PHPUnit_Framework_MockObject_MockObject */ - protected $indexerRegistry; + /** + * @var \Magento\Catalog\Api\CategoryAttributeRepositoryInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $metadataServiceMock; /** * @var \PHPUnit_Framework_MockObject_MockObject */ - protected $metadataServiceMock; + private $attributeValueFactory; /** - * @var \PHPUnit_Framework_MockObject_MockObject + * @var \Magento\Framework\TestFramework\Unit\Helper\ObjectManager */ - protected $attributeValueFactory; + private $objectManager; protected function setUp() { - $this->context = $this->getMock( - \Magento\Framework\Model\Context::class, - ['getEventDispatcher', 'getCacheManager'], + $this->objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); + $this->registry = $this->getMock(\Magento\Framework\Registry::class); + $this->storeManager = $this->getMock(\Magento\Store\Model\StoreManagerInterface::class); + $this->categoryTreeResource = $this->getMock( + \Magento\Catalog\Model\ResourceModel\Category\Tree::class, + [], [], '', false ); - - $this->eventManager = $this->getMock(\Magento\Framework\Event\ManagerInterface::class); - $this->context->expects($this->any())->method('getEventDispatcher') - ->will($this->returnValue($this->eventManager)); - $this->cacheManager = $this->getMock(\Magento\Framework\App\CacheInterface::class); - $this->context->expects($this->any())->method('getCacheManager') - ->will($this->returnValue($this->cacheManager)); - - $this->registry = $this->getMock(\Magento\Framework\Registry::class); - $this->storeManager = $this->getMock(\Magento\Store\Model\StoreManagerInterface::class); - $this->categoryTreeResource = $this->getMock( - \Magento\Catalog\Model\ResourceModel\Category\Tree::class, - [], - [], - '', - false - ); - $this->categoryTreeFactory = $this->getMock( + $this->categoryTreeFactory = $this->getMock( \Magento\Catalog\Model\ResourceModel\Category\TreeFactory::class, ['create'], [], '', false); $this->categoryRepository = $this->getMock(\Magento\Catalog\Api\CategoryRepositoryInterface::class); - $this->storeCollectionFactory = $this->getMock( + $this->storeCollectionFactory = $this->getMock( \Magento\Store\Model\ResourceModel\Store\CollectionFactory::class, ['create'], [], @@ -130,7 +149,7 @@ protected function setUp() false ); $this->url = $this->getMock(\Magento\Framework\UrlInterface::class); - $this->productCollectionFactory = $this->getMock( + $this->productCollectionFactory = $this->getMock( \Magento\Catalog\Model\ResourceModel\Product\CollectionFactory::class, ['create'], [], @@ -138,7 +157,7 @@ protected function setUp() false ); $this->catalogConfig = $this->getMock(\Magento\Catalog\Model\Config::class, [], [], '', false); - $this->filterManager = $this->getMock( + $this->filterManager = $this->getMock( \Magento\Framework\Filter\FilterManager::class, ['translitUrl'], [], @@ -148,7 +167,7 @@ protected function setUp() $this->flatState = $this->getMock(\Magento\Catalog\Model\Indexer\Category\Flat\State::class, [], [], '', false); $this->flatIndexer = $this->getMock(\Magento\Framework\Indexer\IndexerInterface::class); $this->productIndexer = $this->getMock(\Magento\Framework\Indexer\IndexerInterface::class); - $this->categoryUrlPathGenerator = $this->getMock( + $this->categoryUrlPathGenerator = $this->getMock( \Magento\CatalogUrlRewrite\Model\CategoryUrlPathGenerator::class, [], [], @@ -156,19 +175,19 @@ protected function setUp() false ); $this->urlFinder = $this->getMock(\Magento\UrlRewrite\Model\UrlFinderInterface::class); - $this->resource = $this->getMock( + $this->resource = $this->getMock( \Magento\Catalog\Model\ResourceModel\Category::class, [], [], '', false ); - $this->indexerRegistry = $this->getMock( - \Magento\Framework\Indexer\IndexerRegistry::class, - ['get'], - [], - '', - false + $this->indexerRegistry = $this->getMock( + \Magento\Framework\Indexer\IndexerRegistry::class, + ['get'], + [], + '', + false ); $this->metadataServiceMock = $this->getMock(\Magento\Catalog\Api\CategoryAttributeRepositoryInterface::class); @@ -198,7 +217,7 @@ public function testFormatUrlKey() public function testMoveWhenCannotFindParentCategory() { $this->markTestIncomplete('MAGETWO-31165'); - $parentCategory = $this->getMock( + $parentCategory = $this->getMock( \Magento\Catalog\Model\Category::class, ['getId', 'setStoreId', 'load'], [], @@ -223,7 +242,7 @@ public function testMoveWhenCannotFindParentCategory() */ public function testMoveWhenCannotFindNewCategory() { - $parentCategory = $this->getMock( + $parentCategory = $this->getMock( \Magento\Catalog\Model\Category::class, ['getId', 'setStoreId', 'load'], [], @@ -250,7 +269,7 @@ public function testMoveWhenCannotFindNewCategory() public function testMoveWhenParentCategoryIsSameAsChildCategory() { $this->markTestIncomplete('MAGETWO-31165'); - $parentCategory = $this->getMock( + $parentCategory = $this->getMock( \Magento\Catalog\Model\Category::class, ['getId', 'setStoreId', 'load'], [], @@ -277,7 +296,7 @@ public function testMovePrimaryWorkflow() ->method('get') ->with('catalog_category_product') ->will($this->returnValue($indexer)); - $parentCategory = $this->getMock( + $parentCategory = $this->getMock( \Magento\Catalog\Model\Category::class, ['getId', 'setStoreId', 'load'], [], @@ -313,10 +332,9 @@ public function testGetUseFlatResourceTrue() protected function getCategoryModel() { - return (new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this))->getObject( + return $this->objectManager->getObject( \Magento\Catalog\Model\Category::class, [ - 'context' => $this->context, 'registry' => $this->registry, 'storeManager' => $this->storeManager, 'categoryTreeResource' => $this->categoryTreeResource, @@ -487,4 +505,76 @@ public function testGetCustomAttributes() $this->category->getCustomAttribute($descriptionAttributeCode)->getValue() ); } + + /** + * @return array + */ + public function getImageWithAttributeCodeDataProvider() + { + return [ + ['testimage', 'http://www.example.com/catalog/category/testimage'], + [false, false] + ]; + } + + /** + * @param string|bool $value + * @param string|bool $url + * + * @dataProvider getImageWithAttributeCodeDataProvider + */ + public function testGetImageWithAttributeCode($value, $url) + { + $storeManager = $this->getMock(\Magento\Store\Model\StoreManager::class, ['getStore'], [], '', false); + $store = $this->getMock(\Magento\Store\Model\Store::class, ['getBaseUrl'], [], '', false); + + $storeManager->expects($this->any()) + ->method('getStore') + ->will($this->returnValue($store)); + + $store->expects($this->any()) + ->method('getBaseUrl') + ->with(\Magento\Framework\UrlInterface::URL_TYPE_MEDIA) + ->will($this->returnValue('http://www.example.com/')); + + /** @var \Magento\Catalog\Model\Category $model */ + $model = $this->objectManager->getObject( + \Magento\Catalog\Model\Category::class, + [ + 'storeManager' => $storeManager + ] + ); + + $model->setData('attribute1', $value); + + $result = $model->getImageUrl('attribute1'); + + $this->assertEquals($url, $result); + } + + public function testGetImageWithoutAttributeCode() + { + $storeManager = $this->getMock(\Magento\Store\Model\StoreManager::class, ['getStore'], [], '', false); + $store = $this->getMock(\Magento\Store\Model\Store::class, ['getBaseUrl'], [], '', false); + + $storeManager->expects($this->any()) + ->method('getStore') + ->will($this->returnValue($store)); + + $store->expects($this->any()) + ->method('getBaseUrl') + ->with(\Magento\Framework\UrlInterface::URL_TYPE_MEDIA) + ->will($this->returnValue('http://www.example.com/')); + + /** @var \Magento\Catalog\Model\Category $model */ + $model = $this->objectManager->getObject(\Magento\Catalog\Model\Category::class, [ + 'storeManager' => $storeManager + ]); + + $model->setData('image', 'myimage'); + + $result = $model->getImageUrl(); + + $this->assertEquals('http://www.example.com/catalog/category/myimage', $result); + } } diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Product/Attribute/Backend/StockTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Product/Attribute/Backend/StockTest.php index ec9cb624db79a..32d62fd7a998e 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/Product/Attribute/Backend/StockTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/Product/Attribute/Backend/StockTest.php @@ -125,4 +125,17 @@ public function testBeforeSaveQtyIsZero() $stockData = $object->getStockData(); $this->assertEquals(0, $stockData['qty']); } + + public function testBeforeSaveNoStockData() + { + $object = new \Magento\Framework\DataObject( + [ + self::ATTRIBUTE_NAME => ['is_in_stock' => 1, 'qty' => 0] + ] + ); + + $this->model->beforeSave($object); + $this->assertNull($object->getStockData()); + $this->assertNull($object->getData(self::ATTRIBUTE_NAME)); + } } diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Product/ImageTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Product/ImageTest.php index a032ffa33b375..44f7f87cc2c62 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/Product/ImageTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/Product/ImageTest.php @@ -180,7 +180,7 @@ public function testSetGetBaseFile() $this->image->setBaseFile('/somefile.png'); $this->assertEquals('catalog/product/somefile.png', $this->image->getBaseFile()); $this->assertEquals( - 'catalog/product/cache/1//beff4985b56e3afdbeabfc89641a4582/somefile.png', + 'catalog/product/cache//beff4985b56e3afdbeabfc89641a4582/somefile.png', $this->image->getNewFile() ); } @@ -302,7 +302,7 @@ public function testGetUrl() $this->testSetGetBaseFile(); $url = $this->image->getUrl(); $this->assertEquals( - 'http://magento.com/media/catalog/product/cache/1//beff4985b56e3afdbeabfc89641a4582/somefile.png', + 'http://magento.com/media/catalog/product/cache//beff4985b56e3afdbeabfc89641a4582/somefile.png', $url ); } diff --git a/app/code/Magento/Catalog/Test/Unit/Model/ProductRepositoryTest.php b/app/code/Magento/Catalog/Test/Unit/Model/ProductRepositoryTest.php index e91bb469eb112..8c08d0c63bc5e 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/ProductRepositoryTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/ProductRepositoryTest.php @@ -715,6 +715,7 @@ public function testGetList() ->method('process') ->with($searchCriteriaMock, $collectionMock); $collectionMock->expects($this->once())->method('load'); + $collectionMock->expects($this->once())->method('addCategoryIds'); $collectionMock->expects($this->once())->method('getItems')->willReturn([$itemsMock]); $collectionMock->expects($this->once())->method('getSize')->willReturn(128); $searchResultsMock = $this->getMock( diff --git a/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Product/CollectionTest.php b/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Product/CollectionTest.php index 332e775b019b2..b97b7d2f98423 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Product/CollectionTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Product/CollectionTest.php @@ -30,6 +30,26 @@ class CollectionTest extends \PHPUnit_Framework_TestCase */ protected $collection; + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $galleryResourceMock; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $entityMock; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $metadataPoolMock; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $galleryReadHandlerMock; + /** * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ @@ -100,24 +120,49 @@ protected function setUp() ->disableOriginalConstructor() ->getMock(); - $entityMock = $this->getMockBuilder(\Magento\Eav\Model\Entity\AbstractEntity::class) + $this->entityMock = $this->getMockBuilder(\Magento\Eav\Model\Entity\AbstractEntity::class) ->disableOriginalConstructor() ->getMock(); + $this->galleryResourceMock = $this->getMockBuilder( + \Magento\Catalog\Model\ResourceModel\Product\Gallery::class + )->disableOriginalConstructor()->getMock(); + + $this->metadataPoolMock = $this->getMockBuilder( + \Magento\Framework\EntityManager\MetadataPool::class + )->disableOriginalConstructor()->getMock(); + + $this->galleryReadHandlerMock = $this->getMockBuilder( + \Magento\Catalog\Model\Product\Gallery\ReadHandler::class + )->disableOriginalConstructor()->getMock(); + $storeManager->expects($this->any())->method('getId')->willReturn(1); $storeManager->expects($this->any())->method('getStore')->willReturnSelf(); $universalFactory->expects($this->exactly(1))->method('create')->willReturnOnConsecutiveCalls( - $entityMock + $this->entityMock ); - $entityMock->expects($this->once())->method('getConnection')->willReturn($this->connectionMock); - $entityMock->expects($this->once())->method('getDefaultAttributes')->willReturn([]); - $entityMock->expects($this->any())->method('getTable')->willReturnArgument(0); + $this->entityMock->expects($this->once())->method('getConnection')->willReturn($this->connectionMock); + $this->entityMock->expects($this->once())->method('getDefaultAttributes')->willReturn([]); + $this->entityMock->expects($this->any())->method('getTable')->willReturnArgument(0); $this->connectionMock->expects($this->atLeastOnce())->method('select')->willReturn($this->selectMock); $helper = new ObjectManager($this); $this->prepareObjectManager([ - [\Magento\Catalog\Model\ResourceModel\Product\Collection\ProductLimitation::class, + [ + \Magento\Catalog\Model\ResourceModel\Product\Collection\ProductLimitation::class, $this->getMock(\Magento\Catalog\Model\ResourceModel\Product\Collection\ProductLimitation::class) + ], + [ + \Magento\Catalog\Model\ResourceModel\Product\Gallery::class, + $this->galleryResourceMock + ], + [ + \Magento\Framework\EntityManager\MetadataPool::class, + $this->metadataPoolMock + ], + [ + \Magento\Catalog\Model\Product\Gallery\ReadHandler::class, + $this->galleryReadHandlerMock ] ]); $this->collection = $helper->getObject( @@ -150,8 +195,8 @@ protected function setUp() public function testAddProductCategoriesFilter() { - $condition = ['in' => [1,2]]; - $values = [1,2]; + $condition = ['in' => [1, 2]]; + $values = [1, 2]; $conditionType = 'nin'; $preparedSql = "category_id IN(1,2)"; $tableName = "catalog_category_product"; @@ -174,6 +219,47 @@ public function testAddProductCategoriesFilter() $this->collection->addCategoriesFilter([$conditionType => $values]); } + public function testAddMediaGalleryData() + { + $attributeId = 42; + $itemId = 4242; + $linkField = 'entity_id'; + $mediaGalleriesMock = [[$linkField => $itemId]]; + $itemMock = $this->getMockBuilder(\Magento\Catalog\Model\Product::class) + ->disableOriginalConstructor() + ->getMock(); + $attributeMock = $this->getMockBuilder(\Magento\Eav\Model\Entity\Attribute\AbstractAttribute::class) + ->disableOriginalConstructor() + ->getMock(); + $selectMock = $this->getMockBuilder(\Magento\Framework\DB\Select::class) + ->disableOriginalConstructor() + ->getMock(); + $metadataMock = $this->getMockBuilder(\Magento\Framework\EntityManager\EntityMetadataInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $this->collection->addItem($itemMock); + $reflection = new \ReflectionClass(get_class($this->collection)); + $reflectionProperty = $reflection->getProperty('_isCollectionLoaded'); + $reflectionProperty->setAccessible(true); + $reflectionProperty->setValue($this->collection, true); + + $this->galleryResourceMock->expects($this->once())->method('createBatchBaseSelect')->willReturn($selectMock); + $attributeMock->expects($this->once())->method('getAttributeId')->willReturn($attributeId); + $this->entityMock->expects($this->once())->method('getAttribute')->willReturn($attributeMock); + $itemMock->expects($this->atLeastOnce())->method('getId')->willReturn($itemId); + $selectMock->expects($this->once())->method('where')->with('entity.' . $linkField . ' IN (?)', [$itemId]); + $this->metadataPoolMock->expects($this->once())->method('getMetadata')->willReturn($metadataMock); + $metadataMock->expects($this->once())->method('getLinkField')->willReturn($linkField); + + $this->connectionMock->expects($this->once())->method('fetchAll')->with($selectMock)->willReturn( + [['entity_id' => $itemId]] + ); + $this->galleryReadHandlerMock->expects($this->once())->method('addMediaDataToProduct') + ->with($itemMock, $mediaGalleriesMock); + + $this->assertSame($this->collection, $this->collection->addMediaGalleryData()); + } + /** * @param $map */ diff --git a/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Product/CompositeBaseSelectProcessorTest.php b/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Product/CompositeBaseSelectProcessorTest.php new file mode 100644 index 0000000000000..d296474c202f0 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Product/CompositeBaseSelectProcessorTest.php @@ -0,0 +1,54 @@ +objectManager = new ObjectManager($this); + } + + /** + * @expectedException \Magento\Framework\Exception\InputException + */ + public function testInitializeWithWrongProcessorInstance() + { + $processorValid = $this->getMock(BaseSelectProcessorInterface::class); + $processorInvalid = $this->getMock(\stdClass::class); + + $this->objectManager->getObject(CompositeBaseSelectProcessor::class, [ + 'baseSelectProcessors' => [$processorValid, $processorInvalid], + ]); + } + + public function testProcess() + { + $select = $this->getMockBuilder(Select::class)->disableOriginalConstructor()->getMock(); + + $processorFirst = $this->getMock(BaseSelectProcessorInterface::class); + $processorFirst->expects($this->once())->method('process')->with($select)->willReturn($select); + + $processorSecond = $this->getMock(BaseSelectProcessorInterface::class); + $processorSecond->expects($this->once())->method('process')->with($select)->willReturn($select); + + /** @var CompositeBaseSelectProcessor $baseSelectProcessors */ + $baseSelectProcessors = $this->objectManager->getObject(CompositeBaseSelectProcessor::class, [ + 'baseSelectProcessors' => [$processorFirst, $processorSecond], + ]); + $this->assertEquals($select, $baseSelectProcessors->process($select)); + } +} diff --git a/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Product/StatusBaseSelectProcessorTest.php b/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Product/StatusBaseSelectProcessorTest.php new file mode 100644 index 0000000000000..0909f754a01c2 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Product/StatusBaseSelectProcessorTest.php @@ -0,0 +1,102 @@ +eavConfig = $this->getMockBuilder(Config::class)->disableOriginalConstructor()->getMock(); + $this->metadataPool = $this->getMockBuilder(MetadataPool::class)->disableOriginalConstructor()->getMock(); + $this->select = $this->getMockBuilder(Select::class)->disableOriginalConstructor()->getMock(); + + $this->statusBaseSelectProcessor = (new ObjectManager($this))->getObject(StatusBaseSelectProcessor::class, [ + 'eavConfig' => $this->eavConfig, + 'metadataPool' => $this->metadataPool, + ]); + } + + public function testProcess() + { + $linkField = 'link_field'; + $backendTable = 'backend_table'; + $attributeId = 'attribute_id'; + + $metadata = $this->getMock(EntityMetadataInterface::class); + $metadata->expects($this->once()) + ->method('getLinkField') + ->willReturn($linkField); + $this->metadataPool->expects($this->once()) + ->method('getMetadata') + ->with(ProductInterface::class) + ->willReturn($metadata); + + $statusAttribute = $this->getMockBuilder(AttributeInterface::class) + ->setMethods(['getBackendTable', 'getAttributeId']) + ->getMock(); + $statusAttribute->expects($this->once()) + ->method('getBackendTable') + ->willReturn($backendTable); + $statusAttribute->expects($this->once()) + ->method('getAttributeId') + ->willReturn($attributeId); + $this->eavConfig->expects($this->once()) + ->method('getAttribute') + ->with(Product::ENTITY, ProductInterface::STATUS) + ->willReturn($statusAttribute); + + $this->select->expects($this->once()) + ->method('join') + ->with( + ['status_attr' => $backendTable], + sprintf('status_attr.%s = %s.%1$s', $linkField, BaseSelectProcessorInterface::PRODUCT_TABLE_ALIAS), + [] + ) + ->willReturnSelf(); + $this->select->expects($this->at(1)) + ->method('where') + ->with('status_attr.attribute_id = ?', $attributeId) + ->willReturnSelf(); + $this->select->expects($this->at(2)) + ->method('where') + ->with('status_attr.value = ?', Status::STATUS_ENABLED) + ->willReturnSelf(); + + $this->assertEquals($this->select, $this->statusBaseSelectProcessor->process($this->select)); + } +} diff --git a/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Eav.php b/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Eav.php index 1419c17e11b29..e1680ab5a4081 100755 --- a/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Eav.php +++ b/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Eav.php @@ -754,7 +754,10 @@ private function customizeWysiwyg(ProductAttributeInterface $attribute, array $m $meta['arguments']['data']['config']['wysiwyg'] = true; $meta['arguments']['data']['config']['wysiwygConfigData'] = [ 'add_variables' => false, - 'add_widgets' => false + 'add_widgets' => false, + 'add_directives' => true, + 'use_container' => true, + 'container_class' => 'hor-scroll', ]; return $meta; diff --git a/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/General.php b/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/General.php index 0fbba264f7e93..886c7a070adf9 100755 --- a/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/General.php +++ b/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/General.php @@ -315,17 +315,6 @@ protected function customizeNameListeners(array $meta) $importsConfig = [ 'mask' => $this->locator->getStore()->getConfig('catalog/fields_masks/' . $listener), 'component' => 'Magento_Catalog/js/components/import-handler', - 'imports' => [ - 'handleNameChanges' => '${$.provider}:data.product.name', - 'handleDescriptionChanges' => '${$.provider}:data.product.description', - 'handleSkuChanges' => '${$.provider}:data.product.sku', - 'handleColorChanges' => '${$.provider}:data.product.color', - 'handleCountryChanges' => '${$.provider}:data.product.country_of_manufacture', - 'handleGenderChanges' => '${$.provider}:data.product.gender', - 'handleMaterialChanges' => '${$.provider}:data.product.material', - 'handleShortDescriptionChanges' => '${$.provider}:data.product.short_description', - 'handleSizeChanges' => '${$.provider}:data.product.size' - ], 'allowImport' => !$this->locator->getProduct()->getId(), ]; diff --git a/app/code/Magento/Catalog/composer.json b/app/code/Magento/Catalog/composer.json index 64157f6bc8af7..8c77dd77f98b7 100644 --- a/app/code/Magento/Catalog/composer.json +++ b/app/code/Magento/Catalog/composer.json @@ -2,7 +2,7 @@ "name": "magento/module-catalog", "description": "N/A", "require": { - "php": "~5.6.0|7.0.2|7.0.4|~7.0.6", + "php": "~5.6.5|7.0.2|7.0.4|~7.0.6", "magento/module-store": "100.2.*", "magento/module-eav": "100.2.*", "magento/module-cms": "101.1.*", diff --git a/app/code/Magento/Catalog/etc/di.xml b/app/code/Magento/Catalog/etc/di.xml index d191f0332f5f2..6b39520ae021e 100644 --- a/app/code/Magento/Catalog/etc/di.xml +++ b/app/code/Magento/Catalog/etc/di.xml @@ -837,4 +837,12 @@ + + + + + Magento\Catalog\Model\ResourceModel\Product\StatusBaseSelectProcessor + + + diff --git a/app/code/Magento/Catalog/etc/module.xml b/app/code/Magento/Catalog/etc/module.xml index 1250b55b96848..0c9e6bb356fe1 100644 --- a/app/code/Magento/Catalog/etc/module.xml +++ b/app/code/Magento/Catalog/etc/module.xml @@ -6,7 +6,7 @@ */ --> - + diff --git a/app/code/Magento/Catalog/view/adminhtml/web/js/components/import-handler.js b/app/code/Magento/Catalog/view/adminhtml/web/js/components/import-handler.js index e67bde152475f..a0aec918ccda2 100644 --- a/app/code/Magento/Catalog/view/adminhtml/web/js/components/import-handler.js +++ b/app/code/Magento/Catalog/view/adminhtml/web/js/components/import-handler.js @@ -4,148 +4,78 @@ */ define([ + 'Magento_Ui/js/form/element/abstract', 'underscore', - 'Magento_Ui/js/form/element/textarea' -], function (_, Textarea) { + 'uiRegistry' +], function (Abstract, _, registry) { 'use strict'; - return Textarea.extend({ + return Abstract.extend({ defaults: { allowImport: true, autoImportIfEmpty: false, - values: { - 'name': '', - 'description': '', - 'sku': '', - 'color': '', - 'country_of_manufacture': '', - 'gender': '', - 'material': '', - 'short_description': '', - 'size': '' - }, - valueUpdate: 'input', - mask: '' + values: {}, + mask: '', + queryTemplate: 'ns = ${ $.ns }, index = ' }, - /** - * Handle name value changes, if it's allowed - * - * @param {String} newValue - */ - handleNameChanges: function (newValue) { - this.values.name = newValue; - this.updateValue(); - }, - - /** - * Handle description value changes, if it's allowed - * - * @param {String} newValue - */ - handleDescriptionChanges: function (newValue) { - this.values.description = newValue; - this.updateValue(); - }, + /** @inheritdoc */ + initialize: function () { + this._super(); - /** - * Handle sku value changes, if it's allowed - * - * @param {String} newValue - */ - handleSkuChanges: function (newValue) { - if (this.code !== 'sku') { - this.values.sku = newValue; - this.updateValue(); + if (this.allowImport) { + this.setHandlers(); } }, /** - * Handle color value changes, if it's allowed - * - * @param {String} newValue - */ - handleColorChanges: function (newValue) { - this.values.color = newValue; - this.updateValue(); - }, - - /** - * Handle country value changes, if it's allowed - * - * @param {String} newValue + * Split mask placeholder and attach events to placeholder fields. */ - handleCountryChanges: function (newValue) { - this.values.country = newValue; - this.updateValue(); - }, + setHandlers: function () { + var str = this.mask || '', + placeholders; - /** - * Handle gender value changes, if it's allowed - * - * @param {String} newValue - */ - handleGenderChanges: function (newValue) { - this.values.gender = newValue; - this.updateValue(); - }, + placeholders = str.match(/{{(.*?)}}/g); // Get placeholders - /** - * Handle material value changes, if it's allowed - * - * @param {String} newValue - */ - handleMaterialChanges: function (newValue) { - this.values.material = newValue; - this.updateValue(); - }, + _.each(placeholders, function (placeholder) { + placeholder = placeholder.replace(/[{{}}]/g, ''); // Remove curly braces - /** - * Handle short description value changes, if it's allowed - * - * @param {String} newValue - */ - handleShortDescriptionChanges: function (newValue) { - this.values['short_description'] = newValue; - this.updateValue(); + registry.get(this.queryTemplate + placeholder, function (component) { + this.values[placeholder] = component.getPreview(); + component.on('value', this.updateValue.bind(this, placeholder, component)); + component.valueUpdate = 'keyup'; + }.bind(this)); + }, this); }, /** - * Handle size value changes, if it's allowed + * Update field with mask value, if it's allowed. * - * @param {String} newValue + * @param {Object} placeholder + * @param {Object} component */ - handleSizeChanges: function (newValue) { - this.values.size = newValue; - this.updateValue(); - }, + updateValue: function (placeholder, component) { + var string = this.mask || '', + nonEmptyValueFlag = false; - /** - * Update field value, if it's allowed - */ - updateValue: function () { - var str = this.mask || '', - nonEmptyValueFlag = false, - tmpElement; + if (placeholder) { + this.values[placeholder] = component.getPreview() || ''; + } if (!this.allowImport) { return; } - if (str) { - _.each(this.values, function (propertyValue, propertyName) { - str = str.replace('{{' + propertyName + '}}', propertyValue); - nonEmptyValueFlag = nonEmptyValueFlag || !!propertyValue; - }); - } - - // strip tags - tmpElement = document.createElement('div'); - tmpElement.innerHTML = str; - str = tmpElement.textContent || tmpElement.innerText || ''; + _.each(this.values, function (propertyValue, propertyName) { + string = string.replace('{{' + propertyName + '}}', propertyValue); + nonEmptyValueFlag = nonEmptyValueFlag || !!propertyValue; + }); if (nonEmptyValueFlag) { - this.value(str); + string = string.replace(/(<([^>]+)>)/ig, ''); // Remove html tags + this.value(string); + } else { + this.value(''); } }, @@ -169,13 +99,20 @@ define([ * and disallow/allow import value */ userChanges: function () { + + /** + * As userChanges is called before updateValue, + * we forced to get value from component by reference + */ + var actualValue = arguments[1].currentTarget.value; + this._super(); - if (this.value() === '') { + if (actualValue === '') { this.allowImport = true; if (this.autoImportIfEmpty) { - this.updateValue(); + this.updateValue(null, null); } } else { this.allowImport = false; diff --git a/app/code/Magento/Catalog/view/frontend/layout/catalog_category_view.xml b/app/code/Magento/Catalog/view/frontend/layout/catalog_category_view.xml index 4c8ae8eaae952..298cdcc29e953 100644 --- a/app/code/Magento/Catalog/view/frontend/layout/catalog_category_view.xml +++ b/app/code/Magento/Catalog/view/frontend/layout/catalog_category_view.xml @@ -28,53 +28,6 @@ - - product_list_toolbar diff --git a/app/code/Magento/CatalogImportExport/Model/Export/Product.php b/app/code/Magento/CatalogImportExport/Model/Export/Product.php index c9d155177e48a..fa70427296216 100644 --- a/app/code/Magento/CatalogImportExport/Model/Export/Product.php +++ b/app/code/Magento/CatalogImportExport/Model/Export/Product.php @@ -5,7 +5,6 @@ */ namespace Magento\CatalogImportExport\Model\Export; -use Magento\Framework\DB\Ddl\Table; use Magento\ImportExport\Model\Import; use \Magento\Store\Model\Store; use \Magento\CatalogImportExport\Model\Import\Product as ImportProduct; @@ -850,6 +849,24 @@ public function export() return $writer->getContents(); } + /** + * {@inheritdoc} + */ + protected function _prepareEntityCollection(\Magento\Eav\Model\Entity\Collection\AbstractCollection $collection) + { + $exportFilter = !empty($this->_parameters[\Magento\ImportExport\Model\Export::FILTER_ELEMENT_GROUP]) ? + $this->_parameters[\Magento\ImportExport\Model\Export::FILTER_ELEMENT_GROUP] : []; + + if (isset($exportFilter['category_ids']) + && trim($exportFilter['category_ids']) + && $collection instanceof \Magento\Catalog\Model\ResourceModel\Product\Collection + ) { + $collection->addCategoriesFilter(['in' => explode(',', $exportFilter['category_ids'])]); + } + + return parent::_prepareEntityCollection($collection); + } + /** * Get export data for collection * diff --git a/app/code/Magento/CatalogImportExport/Model/Import/Product.php b/app/code/Magento/CatalogImportExport/Model/Import/Product.php index afb2e86b6248a..54208dcdba534 100644 --- a/app/code/Magento/CatalogImportExport/Model/Import/Product.php +++ b/app/code/Magento/CatalogImportExport/Model/Import/Product.php @@ -2304,7 +2304,7 @@ public function validateRow(array $rowData, $rowNum) $sku = $rowData[self::COL_SKU]; - if (isset($this->_oldSku[$sku])) { + if (isset($this->_oldSku[$sku]) && Import::BEHAVIOR_REPLACE !== $this->getBehavior()) { // can we get all necessary data from existent DB product? // check for supported type of existing product if (isset($this->_productTypeModels[$this->_oldSku[$sku]['type_id']])) { @@ -2356,7 +2356,7 @@ public function validateRow(array $rowData, $rowNum) $rowAttributesValid = $productTypeValidator->isRowValid( $rowData, $rowNum, - !isset($this->_oldSku[$sku]) + !(isset($this->_oldSku[$sku]) && Import::BEHAVIOR_REPLACE !== $this->getBehavior()) ); if (!$rowAttributesValid && self::SCOPE_DEFAULT == $rowScope) { // mark SCOPE_DEFAULT row as invalid for future child rows if product not in DB already diff --git a/app/code/Magento/CatalogImportExport/composer.json b/app/code/Magento/CatalogImportExport/composer.json index 7dbd7f4bc5e65..8ed5f43692df5 100644 --- a/app/code/Magento/CatalogImportExport/composer.json +++ b/app/code/Magento/CatalogImportExport/composer.json @@ -2,7 +2,7 @@ "name": "magento/module-catalog-import-export", "description": "N/A", "require": { - "php": "~5.6.0|7.0.2|7.0.4|~7.0.6", + "php": "~5.6.5|7.0.2|7.0.4|~7.0.6", "magento/module-catalog": "101.1.*", "magento/module-catalog-url-rewrite": "100.2.*", "magento/module-eav": "100.2.*", diff --git a/app/code/Magento/CatalogInventory/Model/ResourceModel/Product/StockStatusBaseSelectProcessor.php b/app/code/Magento/CatalogInventory/Model/ResourceModel/Product/StockStatusBaseSelectProcessor.php new file mode 100644 index 0000000000000..829fa8decda7d --- /dev/null +++ b/app/code/Magento/CatalogInventory/Model/ResourceModel/Product/StockStatusBaseSelectProcessor.php @@ -0,0 +1,51 @@ +resource = $resource; + } + + /** + * Add stock item filter to selects + * + * @param Select $select + * @return Select + */ + public function process(Select $select) + { + $stockStatusTable = $this->resource->getTableName('cataloginventory_stock_status'); + + /** @var Select $select */ + $select->join( + ['stock' => $stockStatusTable], + sprintf('stock.product_id = %s.entity_id', BaseSelectProcessorInterface::PRODUCT_TABLE_ALIAS), + [] + ) + ->where('stock.stock_status = ?', Stock::STOCK_IN_STOCK); + return $select; + } +} diff --git a/app/code/Magento/CatalogInventory/Test/Unit/Model/ResourceModel/Product/StockStatusBaseSelectProcessorTest.php b/app/code/Magento/CatalogInventory/Test/Unit/Model/ResourceModel/Product/StockStatusBaseSelectProcessorTest.php new file mode 100644 index 0000000000000..4756e42ffe602 --- /dev/null +++ b/app/code/Magento/CatalogInventory/Test/Unit/Model/ResourceModel/Product/StockStatusBaseSelectProcessorTest.php @@ -0,0 +1,69 @@ +resource = $this->getMockBuilder(ResourceConnection::class)->disableOriginalConstructor()->getMock(); + $this->select = $this->getMockBuilder(Select::class)->disableOriginalConstructor()->getMock(); + + $this->stockStatusBaseSelectProcessor = (new ObjectManager($this))->getObject( + StockStatusBaseSelectProcessor::class, + [ + 'resource' => $this->resource, + ] + ); + } + + public function testProcess() + { + $tableName = 'table_name'; + + $this->resource->expects($this->once()) + ->method('getTableName') + ->with('cataloginventory_stock_status') + ->willReturn($tableName); + + $this->select->expects($this->once()) + ->method('join') + ->with( + ['stock' => $tableName], + sprintf('stock.product_id = %s.entity_id', BaseSelectProcessorInterface::PRODUCT_TABLE_ALIAS), + [] + ) + ->willReturnSelf(); + $this->select->expects($this->once()) + ->method('where') + ->with('stock.stock_status = ?', Stock::STOCK_IN_STOCK) + ->willReturnSelf(); + + $this->stockStatusBaseSelectProcessor->process($this->select); + } +} diff --git a/app/code/Magento/CatalogInventory/composer.json b/app/code/Magento/CatalogInventory/composer.json index 254d8d2db4bf9..684f616ecd6fc 100644 --- a/app/code/Magento/CatalogInventory/composer.json +++ b/app/code/Magento/CatalogInventory/composer.json @@ -2,7 +2,7 @@ "name": "magento/module-catalog-inventory", "description": "N/A", "require": { - "php": "~5.6.0|7.0.2|7.0.4|~7.0.6", + "php": "~5.6.5|7.0.2|7.0.4|~7.0.6", "magento/module-config": "100.2.*", "magento/module-store": "100.2.*", "magento/module-catalog": "101.1.*", diff --git a/app/code/Magento/CatalogInventory/etc/di.xml b/app/code/Magento/CatalogInventory/etc/di.xml index 71b42ef89f73c..e3ca5c01cedab 100644 --- a/app/code/Magento/CatalogInventory/etc/di.xml +++ b/app/code/Magento/CatalogInventory/etc/di.xml @@ -79,4 +79,11 @@ Magento\CatalogInventory\Model\Indexer\Stock\Processor + + + + Magento\CatalogInventory\Model\ResourceModel\Product\StockStatusBaseSelectProcessor + + + diff --git a/app/code/Magento/CatalogRule/Model/ResourceModel/Product/LinkedProductSelectBuilderByCatalogRulePrice.php b/app/code/Magento/CatalogRule/Model/ResourceModel/Product/LinkedProductSelectBuilderByCatalogRulePrice.php index 7d8d44dcbb68a..55b76eb225028 100644 --- a/app/code/Magento/CatalogRule/Model/ResourceModel/Product/LinkedProductSelectBuilderByCatalogRulePrice.php +++ b/app/code/Magento/CatalogRule/Model/ResourceModel/Product/LinkedProductSelectBuilderByCatalogRulePrice.php @@ -6,7 +6,8 @@ namespace Magento\CatalogRule\Model\ResourceModel\Product; use Magento\Catalog\Api\Data\ProductInterface; -use Magento\Catalog\Model\Product; +use Magento\Catalog\Model\ResourceModel\Product\BaseSelectProcessorInterface; +use Magento\Framework\App\ObjectManager; use Magento\Framework\DB\Select; use Magento\Catalog\Model\ResourceModel\Product\LinkedProductSelectBuilderInterface; @@ -42,6 +43,11 @@ class LinkedProductSelectBuilderByCatalogRulePrice implements LinkedProductSelec */ private $metadataPool; + /** + * @var BaseSelectProcessorInterface + */ + private $baseSelectProcessor; + /** * @param \Magento\Store\Model\StoreManagerInterface $storeManager * @param \Magento\Framework\App\ResourceConnection $resourceConnection @@ -49,6 +55,7 @@ class LinkedProductSelectBuilderByCatalogRulePrice implements LinkedProductSelec * @param \Magento\Framework\Stdlib\DateTime $dateTime * @param \Magento\Framework\Stdlib\DateTime\TimezoneInterface $localeDate * @param \Magento\Framework\EntityManager\MetadataPool $metadataPool + * @param BaseSelectProcessorInterface $baseSelectProcessor */ public function __construct( \Magento\Store\Model\StoreManagerInterface $storeManager, @@ -56,7 +63,8 @@ public function __construct( \Magento\Customer\Model\Session $customerSession, \Magento\Framework\Stdlib\DateTime $dateTime, \Magento\Framework\Stdlib\DateTime\TimezoneInterface $localeDate, - \Magento\Framework\EntityManager\MetadataPool $metadataPool + \Magento\Framework\EntityManager\MetadataPool $metadataPool, + BaseSelectProcessorInterface $baseSelectProcessor = null ) { $this->storeManager = $storeManager; $this->resource = $resourceConnection; @@ -64,6 +72,8 @@ public function __construct( $this->dateTime = $dateTime; $this->localeDate = $localeDate; $this->metadataPool = $metadataPool; + $this->baseSelectProcessor = (null !== $baseSelectProcessor) + ? $baseSelectProcessor : ObjectManager::getInstance()->get(BaseSelectProcessorInterface::class); } /** @@ -76,25 +86,28 @@ public function build($productId) $linkField = $this->metadataPool->getMetadata(ProductInterface::class)->getLinkField(); $productTable = $this->resource->getTableName('catalog_product_entity'); - return [$this->resource->getConnection()->select() - ->from(['parent' => $productTable], '') - ->joinInner( - ['link' => $this->resource->getTableName('catalog_product_relation')], - "link.parent_id = parent.$linkField", - [] - )->joinInner( - ['child' => $productTable], - "child.entity_id = link.child_id", - ['entity_id'] - )->joinInner( - ['t' => $this->resource->getTableName('catalogrule_product_price')], - 't.product_id = child.entity_id', - [] - )->where('parent.entity_id = ? ', $productId) + $priceSelect = $this->resource->getConnection()->select() + ->from(['parent' => $productTable], '') + ->joinInner( + ['link' => $this->resource->getTableName('catalog_product_relation')], + "link.parent_id = parent.$linkField", + [] + )->joinInner( + [BaseSelectProcessorInterface::PRODUCT_TABLE_ALIAS => $productTable], + sprintf('%s.entity_id = link.child_id', BaseSelectProcessorInterface::PRODUCT_TABLE_ALIAS), + ['entity_id'] + )->joinInner( + ['t' => $this->resource->getTableName('catalogrule_product_price')], + sprintf('t.product_id = %s.%s', BaseSelectProcessorInterface::PRODUCT_TABLE_ALIAS, $linkField), + [] + )->where('parent.entity_id = ?', $productId) ->where('t.website_id = ?', $this->storeManager->getStore()->getWebsiteId()) ->where('t.customer_group_id = ?', $this->customerSession->getCustomerGroupId()) ->where('t.rule_date = ?', $currentDate) ->order('t.rule_price ' . Select::SQL_ASC) - ->limit(1)]; + ->limit(1); + $priceSelect = $this->baseSelectProcessor->process($priceSelect); + + return [$priceSelect]; } } diff --git a/app/code/Magento/CatalogRule/Setup/InstallSchema.php b/app/code/Magento/CatalogRule/Setup/InstallSchema.php index 70c5724d446de..cb36f6efbdcde 100644 --- a/app/code/Magento/CatalogRule/Setup/InstallSchema.php +++ b/app/code/Magento/CatalogRule/Setup/InstallSchema.php @@ -25,6 +25,9 @@ public function install(SchemaSetupInterface $setup, ModuleContextInterface $con $installer->startSetup(); + $customerGroupTable = $setup->getConnection()->describeTable($setup->getTable('customer_group')); + $customerGroupIdType = $customerGroupTable['customer_group_id']['DATA_TYPE'] == 'int' + ? \Magento\Framework\DB\Ddl\Table::TYPE_INTEGER : $customerGroupTable['customer_group_id']['DATA_TYPE']; /** * Create table 'catalogrule' */ @@ -372,7 +375,7 @@ public function install(SchemaSetupInterface $setup, ModuleContextInterface $con ) ->addColumn( 'customer_group_id', - \Magento\Framework\DB\Ddl\Table::TYPE_SMALLINT, + $customerGroupIdType, null, ['unsigned' => true, 'nullable' => false, 'primary' => true, 'default' => '0'], 'Customer Group Id' @@ -477,7 +480,7 @@ public function install(SchemaSetupInterface $setup, ModuleContextInterface $con ) ->addColumn( 'customer_group_id', - \Magento\Framework\DB\Ddl\Table::TYPE_SMALLINT, + $customerGroupIdType, null, ['unsigned' => true, 'nullable' => false, 'primary' => true], 'Customer Group Id' diff --git a/app/code/Magento/CatalogRule/Setup/UpgradeSchema.php b/app/code/Magento/CatalogRule/Setup/UpgradeSchema.php index 1d576e74c4c80..6bfb927c9c8f5 100644 --- a/app/code/Magento/CatalogRule/Setup/UpgradeSchema.php +++ b/app/code/Magento/CatalogRule/Setup/UpgradeSchema.php @@ -26,6 +26,20 @@ public function upgrade(SchemaSetupInterface $setup, ModuleContextInterface $con $this->removeSubProductDiscounts($setup); } + if (version_compare($context->getVersion(), '2.0.2', '<')) { + $tables = [ + 'catalogrule_product', + 'catalogrule_product_price', + ]; + foreach ($tables as $table) { + $setup->getConnection()->modifyColumn( + $setup->getTable($table), + 'customer_group_id', + ['type' => 'integer'] + ); + } + } + $setup->endSetup(); } diff --git a/app/code/Magento/CatalogRule/composer.json b/app/code/Magento/CatalogRule/composer.json index 71eb5b49db1fc..004ab82a028d3 100644 --- a/app/code/Magento/CatalogRule/composer.json +++ b/app/code/Magento/CatalogRule/composer.json @@ -2,7 +2,7 @@ "name": "magento/module-catalog-rule", "description": "N/A", "require": { - "php": "~5.6.0|7.0.2|7.0.4|~7.0.6", + "php": "~5.6.5|7.0.2|7.0.4|~7.0.6", "magento/module-store": "100.2.*", "magento/module-rule": "100.2.*", "magento/module-catalog": "101.1.*", diff --git a/app/code/Magento/CatalogRule/etc/module.xml b/app/code/Magento/CatalogRule/etc/module.xml index ea6a730279ed5..d7db233e1eb06 100644 --- a/app/code/Magento/CatalogRule/etc/module.xml +++ b/app/code/Magento/CatalogRule/etc/module.xml @@ -6,7 +6,7 @@ */ --> - + diff --git a/app/code/Magento/CatalogRuleConfigurable/composer.json b/app/code/Magento/CatalogRuleConfigurable/composer.json index cc51269e2d972..b930380f7bb02 100644 --- a/app/code/Magento/CatalogRuleConfigurable/composer.json +++ b/app/code/Magento/CatalogRuleConfigurable/composer.json @@ -2,7 +2,7 @@ "name": "magento/module-catalog-rule-configurable", "description": "N/A", "require": { - "php": "~5.6.0|7.0.2|7.0.4|~7.0.6", + "php": "~5.6.5|7.0.2|7.0.4|~7.0.6", "magento/module-configurable-product": "100.2.*", "magento/framework": "100.2.*", "magento/module-catalog": "101.1.*", diff --git a/app/code/Magento/CatalogSearch/Model/Adapter/Mysql/Aggregation/DataProvider.php b/app/code/Magento/CatalogSearch/Model/Adapter/Mysql/Aggregation/DataProvider.php index 9b75e6e6e0c32..ddf86951068ac 100644 --- a/app/code/Magento/CatalogSearch/Model/Adapter/Mysql/Aggregation/DataProvider.php +++ b/app/code/Magento/CatalogSearch/Model/Adapter/Mysql/Aggregation/DataProvider.php @@ -6,6 +6,7 @@ namespace Magento\CatalogSearch\Model\Adapter\Mysql\Aggregation; use Magento\Catalog\Model\Product; +use Magento\CatalogInventory\Model\Stock; use Magento\Customer\Model\Session; use Magento\Eav\Model\Config; use Magento\Framework\App\ResourceConnection; @@ -79,7 +80,13 @@ public function getDataSet( $select = $this->getSelect(); - if ($attribute->getAttributeCode() == 'price') { + $select->joinInner( + ['entities' => $entityIdsTable->getName()], + 'main_table.entity_id = entities.entity_id', + [] + ); + + if ($attribute->getAttributeCode() === 'price') { /** @var \Magento\Store\Model\Store $store */ $store = $this->scopeResolver->getScope($currentScope); if (!$store instanceof \Magento\Store\Model\Store) { @@ -94,19 +101,24 @@ public function getDataSet( $currentScopeId = $this->scopeResolver->getScope($currentScope) ->getId(); $table = $this->resource->getTableName( - 'catalog_product_index_eav' . ($attribute->getBackendType() == 'decimal' ? '_decimal' : '') + 'catalog_product_index_eav' . ($attribute->getBackendType() === 'decimal' ? '_decimal' : '') ); - $select->from(['main_table' => $table], ['value']) + $subSelect = $select; + $subSelect->from(['main_table' => $table], ['main_table.value']) + ->joinLeft( + ['stock_index' => $this->resource->getTableName('cataloginventory_stock_status')], + 'main_table.source_id = stock_index.product_id', + [] + ) ->where('main_table.attribute_id = ?', $attribute->getAttributeId()) - ->where('main_table.store_id = ? ', $currentScopeId); + ->where('main_table.store_id = ? ', $currentScopeId) + ->where('stock_index.stock_status = ?', Stock::STOCK_IN_STOCK) + ->group(['main_table.entity_id', 'main_table.value']); + $parentSelect = $this->getSelect(); + $parentSelect->from(['main_table' => $subSelect], ['main_table.value']); + $select = $parentSelect; } - $select->joinInner( - ['entities' => $entityIdsTable->getName()], - 'main_table.entity_id = entities.entity_id', - [] - ); - return $select; } diff --git a/app/code/Magento/CatalogSearch/Model/Adapter/Mysql/Filter/AliasResolver.php b/app/code/Magento/CatalogSearch/Model/Adapter/Mysql/Filter/AliasResolver.php new file mode 100644 index 0000000000000..7099ce2502b19 --- /dev/null +++ b/app/code/Magento/CatalogSearch/Model/Adapter/Mysql/Filter/AliasResolver.php @@ -0,0 +1,44 @@ +getField(); + switch ($field) { + case 'price': + $alias = 'price_index'; + break; + case 'category_ids': + $alias = 'category_ids_index'; + break; + default: + $alias = $field . RequestGenerator::FILTER_SUFFIX; + break; + } + return $alias; + } +} 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 05205f04f8b99..fb579c1dce29c 100644 --- a/app/code/Magento/CatalogSearch/Model/Adapter/Mysql/Filter/Preprocessor.php +++ b/app/code/Magento/CatalogSearch/Model/Adapter/Mysql/Filter/Preprocessor.php @@ -8,8 +8,11 @@ use Magento\Catalog\Api\Data\ProductInterface; use Magento\Catalog\Model\Product; use Magento\Catalog\Model\ResourceModel\Eav\Attribute; +use Magento\CatalogInventory\Model\Stock; use Magento\CatalogSearch\Model\Search\TableMapper; use Magento\Eav\Model\Config; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\App\ObjectManager; use Magento\Framework\App\ResourceConnection; use Magento\Framework\App\ScopeResolverInterface; use Magento\Framework\DB\Adapter\AdapterInterface; @@ -17,6 +20,7 @@ use Magento\Framework\Search\Adapter\Mysql\ConditionManager; use Magento\Framework\Search\Adapter\Mysql\Filter\PreprocessorInterface; use Magento\Framework\Search\Request\FilterInterface; +use Magento\Store\Model\ScopeInterface; use Magento\Store\Model\Store; /** @@ -60,9 +64,14 @@ class Preprocessor implements PreprocessorInterface private $metadataPool; /** - * @var TableMapper + * @var ScopeConfigInterface */ - private $tableMapper; + private $scopeConfig; + + /** + * @var AliasResolver + */ + private $aliasResolver; /** * @param ConditionManager $conditionManager @@ -71,6 +80,9 @@ class Preprocessor implements PreprocessorInterface * @param ResourceConnection $resource * @param TableMapper $tableMapper * @param string $attributePrefix + * @param ScopeConfigInterface $scopeConfig + * @param AliasResolver $aliasResolver + * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function __construct( ConditionManager $conditionManager, @@ -78,7 +90,9 @@ public function __construct( Config $config, ResourceConnection $resource, TableMapper $tableMapper, - $attributePrefix + $attributePrefix, + ScopeConfigInterface $scopeConfig = null, + AliasResolver $aliasResolver = null ) { $this->conditionManager = $conditionManager; $this->scopeResolver = $scopeResolver; @@ -86,7 +100,16 @@ public function __construct( $this->resource = $resource; $this->connection = $resource->getConnection(); $this->attributePrefix = $attributePrefix; - $this->tableMapper = $tableMapper; + + if (null === $scopeConfig) { + $scopeConfig = ObjectManager::getInstance()->get(ScopeConfigInterface::class); + } + if (null === $aliasResolver) { + $aliasResolver = ObjectManager::getInstance()->get(AliasResolver::class); + } + + $this->scopeConfig = $scopeConfig; + $this->aliasResolver = $aliasResolver; } /** @@ -117,7 +140,7 @@ private function processQueryWithField(FilterInterface $filter, $isNegation, $qu } elseif ($filter->getField() === 'category_ids') { return 'category_ids_index.category_id = ' . (int) $filter->getValue(); } elseif ($attribute->isStatic()) { - $alias = $this->tableMapper->getMappingAlias($filter); + $alias = $this->aliasResolver->getAlias($filter); $resultQuery = str_replace( $this->connection->quoteIdentifier($attribute->getAttributeCode()), $this->connection->quoteIdentifier($alias . '.' . $attribute->getAttributeCode()), @@ -208,7 +231,7 @@ private function processRangeNumeric(FilterInterface $filter, $query, $attribute */ private function processTermSelect(FilterInterface $filter, $isNegation) { - $alias = $this->tableMapper->getMappingAlias($filter); + $alias = $this->aliasResolver->getAlias($filter); if (is_array($filter->getValue())) { $value = sprintf( '%s IN (%s)', @@ -224,9 +247,31 @@ private function processTermSelect(FilterInterface $filter, $isNegation) $value ); + if ($this->isAddStockFilter()) { + $resultQuery = sprintf( + '%1$s AND %2$s%3$s.stock_status = %4$s', + $resultQuery, + $alias, + AliasResolver::STOCK_FILTER_SUFFIX, + Stock::STOCK_IN_STOCK + ); + } + return $resultQuery; } + /** + * @return bool + */ + private function isAddStockFilter() + { + $isShowOutOfStock = $this->scopeConfig->isSetFlag( + 'cataloginventory/options/show_out_of_stock', + ScopeInterface::SCOPE_STORE + ); + return false === $isShowOutOfStock; + } + /** * Get product metadata pool * diff --git a/app/code/Magento/CatalogSearch/Model/Search/FilterMapper/ExclusionStrategy.php b/app/code/Magento/CatalogSearch/Model/Search/FilterMapper/ExclusionStrategy.php new file mode 100644 index 0000000000000..8626b2ea15b07 --- /dev/null +++ b/app/code/Magento/CatalogSearch/Model/Search/FilterMapper/ExclusionStrategy.php @@ -0,0 +1,83 @@ +resourceConnection = $resourceConnection; + $this->storeManager = $storeManager; + $this->aliasResolver = $aliasResolver; + } + + /** + * {@inheritDoc} + */ + public function apply( + \Magento\Framework\Search\Request\FilterInterface $filter, + \Magento\Framework\DB\Select $select + ) { + $isApplied = false; + $field = $filter->getField(); + if ('price' === $field) { + $alias = $this->aliasResolver->getAlias($filter); + $tableName = $this->resourceConnection->getTableName('catalog_product_index_price'); + $select->joinInner( + [ + $alias => $tableName + ], + $this->resourceConnection->getConnection()->quoteInto( + 'search_index.entity_id = price_index.entity_id AND price_index.website_id = ?', + $this->storeManager->getWebsite()->getId() + ), + [] + ); + $isApplied = true; + } elseif ('category_ids' === $field) { + $alias = $this->aliasResolver->getAlias($filter); + $tableName = $this->resourceConnection->getTableName('catalog_category_product_index'); + $select->joinInner( + [ + $alias => $tableName + ], + 'search_index.entity_id = category_ids_index.product_id', + [] + ); + $isApplied = true; + } + return $isApplied; + } +} diff --git a/app/code/Magento/CatalogSearch/Model/Search/FilterMapper/FilterContext.php b/app/code/Magento/CatalogSearch/Model/Search/FilterMapper/FilterContext.php new file mode 100644 index 0000000000000..d244e3d5f7548 --- /dev/null +++ b/app/code/Magento/CatalogSearch/Model/Search/FilterMapper/FilterContext.php @@ -0,0 +1,100 @@ +eavConfig = $eavConfig; + $this->aliasResolver = $aliasResolver; + $this->exclusionStrategy = $exclusionStrategy; + $this->termDropdownStrategy = $termDropdownStrategy; + $this->staticAttributeStrategy = $staticAttributeStrategy; + } + + /** + * {@inheritDoc} + */ + public function apply( + \Magento\Framework\Search\Request\FilterInterface $filter, + \Magento\Framework\DB\Select $select + ) { + $isApplied = $this->exclusionStrategy->apply($filter, $select); + + if (!$isApplied) { + $attribute = $this->getAttributeByCode($filter->getField()); + if ($attribute) { + if ($filter->getType() === \Magento\Framework\Search\Request\FilterInterface::TYPE_TERM + && in_array($attribute->getFrontendInput(), ['select', 'multiselect'], true) + ) { + $isApplied = $this->termDropdownStrategy->apply($filter, $select); + } elseif ($attribute->getBackendType() === AbstractAttribute::TYPE_STATIC) { + $isApplied = $this->staticAttributeStrategy->apply($filter, $select); + } + } + } + + return $isApplied; + } + + /** + * @param string $field + * @return \Magento\Catalog\Model\ResourceModel\Eav\Attribute + * @throws \Magento\Framework\Exception\LocalizedException + */ + private function getAttributeByCode($field) + { + return $this->eavConfig->getAttribute(\Magento\Catalog\Model\Product::ENTITY, $field); + } +} diff --git a/app/code/Magento/CatalogSearch/Model/Search/FilterMapper/FilterStrategyInterface.php b/app/code/Magento/CatalogSearch/Model/Search/FilterMapper/FilterStrategyInterface.php new file mode 100644 index 0000000000000..cf17f7d5132ef --- /dev/null +++ b/app/code/Magento/CatalogSearch/Model/Search/FilterMapper/FilterStrategyInterface.php @@ -0,0 +1,23 @@ +resourceConnection = $resourceConnection; + $this->eavConfig = $eavConfig; + $this->aliasResolver = $aliasResolver; + } + + /** + * {@inheritDoc} + */ + public function apply( + \Magento\Framework\Search\Request\FilterInterface $filter, + \Magento\Framework\DB\Select $select + ) { + $attribute = $this->getAttributeByCode($filter->getField()); + $alias = $this->aliasResolver->getAlias($filter); + $select->joinInner( + [$alias => $attribute->getBackendTable()], + 'search_index.entity_id = ' + . $this->resourceConnection->getConnection()->quoteIdentifier("$alias.entity_id"), + [] + ); + return true; + } + + /** + * @param string $field + * @return \Magento\Catalog\Model\ResourceModel\Eav\Attribute + * @throws \Magento\Framework\Exception\LocalizedException + */ + private function getAttributeByCode($field) + { + return $this->eavConfig->getAttribute(\Magento\Catalog\Model\Product::ENTITY, $field); + } +} diff --git a/app/code/Magento/CatalogSearch/Model/Search/FilterMapper/TermDropdownStrategy.php b/app/code/Magento/CatalogSearch/Model/Search/FilterMapper/TermDropdownStrategy.php new file mode 100644 index 0000000000000..76828fe28f434 --- /dev/null +++ b/app/code/Magento/CatalogSearch/Model/Search/FilterMapper/TermDropdownStrategy.php @@ -0,0 +1,127 @@ +storeManager = $storeManager; + $this->resourceConnection = $resourceConnection; + $this->eavConfig = $eavConfig; + $this->scopeConfig = $scopeConfig; + $this->aliasResolver = $aliasResolver; + } + + /** + * {@inheritDoc} + * @throws \Magento\Framework\Exception\LocalizedException + */ + public function apply( + \Magento\Framework\Search\Request\FilterInterface $filter, + \Magento\Framework\DB\Select $select + ) { + $alias = $this->aliasResolver->getAlias($filter); + $attribute = $this->getAttributeByCode($filter->getField()); + $joinCondition = sprintf( + 'search_index.entity_id = %1$s.entity_id AND %1$s.attribute_id = %2$d AND %1$s.store_id = %3$d', + $alias, + $attribute->getId(), + $this->storeManager->getWebsite()->getId() + ); + $select->joinLeft( + [$alias => $this->resourceConnection->getTableName('catalog_product_index_eav')], + $joinCondition, + [] + ); + if ($this->isAddStockFilter()) { + $stockAlias = $alias . AliasResolver::STOCK_FILTER_SUFFIX; + $select->joinLeft( + [ + $stockAlias => $this->resourceConnection->getTableName('cataloginventory_stock_status'), + ], + sprintf('%2$s.product_id = %1$s.source_id', $alias, $stockAlias), + [] + ); + } + + return true; + } + + /** + * @param string $field + * @return \Magento\Catalog\Model\ResourceModel\Eav\Attribute + * @throws \Magento\Framework\Exception\LocalizedException + */ + private function getAttributeByCode($field) + { + return $this->eavConfig->getAttribute(\Magento\Catalog\Model\Product::ENTITY, $field); + } + + /** + * @return bool + */ + private function isAddStockFilter() + { + $isShowOutOfStock = $this->scopeConfig->isSetFlag( + 'cataloginventory/options/show_out_of_stock', + ScopeInterface::SCOPE_STORE + ); + + return false === $isShowOutOfStock; + } +} diff --git a/app/code/Magento/CatalogSearch/Model/Search/IndexBuilder.php b/app/code/Magento/CatalogSearch/Model/Search/IndexBuilder.php index 1d30bb4a14d23..0e96e4de70025 100644 --- a/app/code/Magento/CatalogSearch/Model/Search/IndexBuilder.php +++ b/app/code/Magento/CatalogSearch/Model/Search/IndexBuilder.php @@ -99,6 +99,7 @@ public function __construct( * * @param RequestInterface $request * @return Select + * @throws \LogicException */ public function build(RequestInterface $request) { @@ -132,7 +133,7 @@ public function build(RequestInterface $request) ), [] ); - $select->where('stock_index.stock_status = ?', Stock::DEFAULT_STOCK_ID); + $select->where('stock_index.stock_status = ?', Stock::STOCK_IN_STOCK); } return $select; diff --git a/app/code/Magento/CatalogSearch/Model/Search/TableMapper.php b/app/code/Magento/CatalogSearch/Model/Search/TableMapper.php index ca7298e1beaac..ac726192856a5 100644 --- a/app/code/Magento/CatalogSearch/Model/Search/TableMapper.php +++ b/app/code/Magento/CatalogSearch/Model/Search/TableMapper.php @@ -6,10 +6,11 @@ namespace Magento\CatalogSearch\Model\Search; -use Magento\Catalog\Model\Product; use Magento\Catalog\Model\ResourceModel\Product\Attribute\CollectionFactory; +use Magento\CatalogSearch\Model\Adapter\Mysql\Filter\AliasResolver; +use Magento\CatalogSearch\Model\Search\FilterMapper\FilterStrategyInterface; use Magento\Eav\Model\Config as EavConfig; -use Magento\Eav\Model\Entity\Attribute\AbstractAttribute; +use Magento\Framework\App\Config\ScopeConfigInterface; use Magento\Framework\App\ObjectManager; use Magento\Framework\App\ResourceConnection as AppResource; use Magento\Framework\DB\Select; @@ -21,12 +22,15 @@ use Magento\Store\Model\StoreManagerInterface; /** + * Responsibility of the TableMapper is to collect all filters from the search query + * and pass them one by one for processing in the FilterContext, + * which will apply them to the Select * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class TableMapper { /** - * @var Resource + * @var AppResource */ private $resource; @@ -40,22 +44,59 @@ class TableMapper */ private $eavConfig; + /** + * @var ScopeConfigInterface + */ + private $scopeConfig; + + /** + * @var FilterStrategyInterface + */ + private $filterStrategy; + + /** + * @var AliasResolver + */ + private $aliasResolver; + /** * @SuppressWarnings(PHPMD.UnusedFormalParameter) * @param AppResource $resource * @param StoreManagerInterface $storeManager * @param CollectionFactory $attributeCollectionFactory * @param EavConfig $eavConfig + * @param ScopeConfigInterface $scopeConfig + * @param FilterStrategyInterface $filterStrategy + * @param AliasResolver $aliasResolver */ public function __construct( AppResource $resource, StoreManagerInterface $storeManager, CollectionFactory $attributeCollectionFactory, - EavConfig $eavConfig = null + EavConfig $eavConfig = null, + ScopeConfigInterface $scopeConfig = null, + FilterStrategyInterface $filterStrategy = null, + AliasResolver $aliasResolver = null ) { $this->resource = $resource; $this->storeManager = $storeManager; - $this->eavConfig = $eavConfig !== null ? $eavConfig : ObjectManager::getInstance()->get(EavConfig::class); + + if (null === $eavConfig) { + $eavConfig = ObjectManager::getInstance()->get(EavConfig::class); + } + if (null === $scopeConfig) { + $scopeConfig = ObjectManager::getInstance()->get(ScopeConfigInterface::class); + } + if (null === $filterStrategy) { + $filterStrategy = ObjectManager::getInstance()->get(FilterStrategyInterface::class); + } + if (null === $aliasResolver) { + $aliasResolver = ObjectManager::getInstance()->get(AliasResolver::class); + } + $this->eavConfig = $eavConfig; + $this->scopeConfig = $scopeConfig; + $this->filterStrategy = $filterStrategy; + $this->aliasResolver = $aliasResolver; } /** @@ -66,111 +107,53 @@ public function __construct( */ public function addTables(Select $select, RequestInterface $request) { - $mappedTables = []; - $filters = $this->getFilters($request->getQuery()); + $appliedFilters = []; + $filters = $this->getFiltersFromQuery($request->getQuery()); foreach ($filters as $filter) { - list($alias, $table, $mapOn, $mappedFields, $joinType) = $this->getMappingData($filter); - if (!array_key_exists($alias, $mappedTables)) { - switch ($joinType) { - case \Magento\Framework\DB\Select::INNER_JOIN: - $select->joinInner( - [$alias => $table], - $mapOn, - $mappedFields - ); - break; - case \Magento\Framework\DB\Select::LEFT_JOIN: - $select->joinLeft( - [$alias => $table], - $mapOn, - $mappedFields - ); - break; - default: - throw new \LogicException(__('Unsupported join type: %1', $joinType)); + $alias = $this->aliasResolver->getAlias($filter); + if (!array_key_exists($alias, $appliedFilters)) { + $isApplied = $this->filterStrategy->apply($filter, $select); + if ($isApplied) { + $appliedFilters[$alias] = true; } - $mappedTables[$alias] = $table; } } return $select; } /** + * This method is deprecated. + * Please use \Magento\CatalogSearch\Model\Adapter\Mysql\Filter\AliasResolver::getAlias() instead. + * + * @deprecated + * @see AliasResolver::getAlias() + * * @param FilterInterface $filter * @return string */ public function getMappingAlias(FilterInterface $filter) { - list($alias) = $this->getMappingData($filter); - return $alias; - } - - /** - * Returns mapping data for field in format: [ - * 'table_alias', - * 'table', - * 'join_condition', - * ['fields'], - * 'joinType' - * ] - * @param FilterInterface $filter - * @return array - */ - private function getMappingData(FilterInterface $filter) - { - $alias = null; - $table = null; - $mapOn = null; - $mappedFields = null; - $field = $filter->getField(); - $joinType = \Magento\Framework\DB\Select::INNER_JOIN; - $fieldToTableMap = $this->getFieldToTableMap($field); - if ($fieldToTableMap) { - list($alias, $table, $mapOn, $mappedFields) = $fieldToTableMap; - $table = $this->resource->getTableName($table); - } elseif ($attribute = $this->getAttributeByCode($field)) { - if ($filter->getType() === FilterInterface::TYPE_TERM - && in_array($attribute->getFrontendInput(), ['select', 'multiselect'], true) - ) { - $joinType = \Magento\Framework\DB\Select::LEFT_JOIN; - $table = $this->resource->getTableName('catalog_product_index_eav'); - $alias = $field . RequestGenerator::FILTER_SUFFIX; - $mapOn = sprintf( - 'search_index.entity_id = %1$s.entity_id AND %1$s.attribute_id = %2$d AND %1$s.store_id = %3$d', - $alias, - $attribute->getId(), - $this->getStoreId() - ); - $mappedFields = []; - } elseif ($attribute->getBackendType() === AbstractAttribute::TYPE_STATIC) { - $table = $attribute->getBackendTable(); - $alias = $field . RequestGenerator::FILTER_SUFFIX; - $mapOn = 'search_index.entity_id = ' . $alias . '.entity_id'; - $mappedFields = null; - } - } - - return [$alias, $table, $mapOn, $mappedFields, $joinType]; + return $this->aliasResolver->getAlias($filter); } /** * @param RequestQueryInterface $query * @return FilterInterface[] */ - private function getFilters($query) + private function getFiltersFromQuery(RequestQueryInterface $query) { $filters = []; switch ($query->getType()) { case RequestQueryInterface::TYPE_BOOL: /** @var \Magento\Framework\Search\Request\Query\BoolExpression $query */ foreach ($query->getMust() as $subQuery) { - $filters = array_merge($filters, $this->getFilters($subQuery)); + $filters = array_merge($filters, $this->getFiltersFromQuery($subQuery)); } foreach ($query->getShould() as $subQuery) { - $filters = array_merge($filters, $this->getFilters($subQuery)); + $filters = array_merge($filters, $this->getFiltersFromQuery($subQuery)); } foreach ($query->getMustNot() as $subQuery) { - $filters = array_merge($filters, $this->getFilters($subQuery)); + $filters = array_merge($filters, $this->getFiltersFromQuery($subQuery)); } break; case RequestQueryInterface::TYPE_FILTER: @@ -219,57 +202,4 @@ private function getFiltersFromBoolFilter(BoolExpression $boolExpression) } return $filters; } - - /** - * @return int - */ - private function getWebsiteId() - { - return $this->storeManager->getWebsite()->getId(); - } - - /** - * @return int - */ - private function getStoreId() - { - return $this->storeManager->getStore()->getId(); - } - - /** - * @param string $field - * @return array|null - */ - private function getFieldToTableMap($field) - { - $fieldToTableMap = [ - 'price' => [ - 'price_index', - 'catalog_product_index_price', - $this->resource->getConnection()->quoteInto( - 'search_index.entity_id = price_index.entity_id AND price_index.website_id = ?', - $this->getWebsiteId() - ), - [] - ], - 'category_ids' => [ - 'category_ids_index', - 'catalog_category_product_index', - 'search_index.entity_id = category_ids_index.product_id', - [] - ] - ]; - return array_key_exists($field, $fieldToTableMap) ? $fieldToTableMap[$field] : null; - } - - /** - * @param string $field - * @return \Magento\Catalog\Model\ResourceModel\Eav\Attribute - * @throws \Magento\Framework\Exception\LocalizedException - */ - private function getAttributeByCode($field) - { - $attribute = $this->eavConfig->getAttribute(Product::ENTITY, $field); - return $attribute; - } } diff --git a/app/code/Magento/CatalogSearch/Test/Unit/Model/Adapter/Mysql/Filter/AliasResolverTest.php b/app/code/Magento/CatalogSearch/Test/Unit/Model/Adapter/Mysql/Filter/AliasResolverTest.php new file mode 100644 index 0000000000000..ab01d2553a3ed --- /dev/null +++ b/app/code/Magento/CatalogSearch/Test/Unit/Model/Adapter/Mysql/Filter/AliasResolverTest.php @@ -0,0 +1,68 @@ +aliasResolver = $objectManagerHelper->getObject( + \Magento\CatalogSearch\Model\Adapter\Mysql\Filter\AliasResolver::class, + [] + ); + } + + /** + * @param string $field + * @param string $expectedAlias + * @dataProvider aliasDataProvider + */ + public function testGetFilterAlias($field, $expectedAlias) + { + $filter = $this->getMockBuilder(\Magento\Framework\Search\Request\Filter\Term::class) + ->setMethods(['getField']) + ->disableOriginalConstructor() + ->getMock(); + $filter->expects($this->once()) + ->method('getField') + ->willReturn($field); + $this->assertSame($expectedAlias, $this->aliasResolver->getAlias($filter)); + } + + /** + * @return array + */ + public function aliasDataProvider() + { + return [ + 'general' => [ + 'field' => 'general', + 'alias' => 'general' . RequestGenerator::FILTER_SUFFIX, + ], + 'price' => [ + 'field' => 'price', + 'alias' => 'price_index', + ], + 'category_ids' => [ + 'field' => 'category_ids', + 'alias' => 'category_ids_index', + ], + ]; + } +} 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 0949bf6469be9..2cd6935586bd8 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 @@ -6,6 +6,7 @@ namespace Magento\CatalogSearch\Test\Unit\Model\Adapter\Mysql\Filter; +use Magento\CatalogSearch\Model\Adapter\Mysql\Filter\AliasResolver; use Magento\Framework\DB\Select; use Magento\Framework\EntityManager\EntityMetadata; use Magento\Framework\Search\Request\FilterInterface; @@ -18,9 +19,9 @@ class PreprocessorTest extends \PHPUnit_Framework_TestCase { /** - * @var \Magento\CatalogSearch\Model\Search\TableMapper|\PHPUnit_Framework_MockObject_MockObject + * @var AliasResolver|\PHPUnit_Framework_MockObject_MockObject */ - private $tableMapper; + private $aliasResolver; /** * @var \Magento\Framework\DB\Adapter\AdapterInterface|MockObject @@ -141,7 +142,7 @@ function ($select) { ) ); - $this->tableMapper = $this->getMockBuilder(\Magento\CatalogSearch\Model\Search\TableMapper::class) + $this->aliasResolver = $this->getMockBuilder(AliasResolver::class) ->disableOriginalConstructor() ->getMock(); $this->metadataPoolMock = $this->getMockBuilder(\Magento\Framework\EntityManager\MetadataPool::class) @@ -164,7 +165,7 @@ function ($select) { 'resource' => $resource, 'attributePrefix' => 'attr_', 'metadataPool' => $this->metadataPoolMock, - 'tableMapper' => $this->tableMapper, + 'aliasResolver' => $this->aliasResolver, ] ); } @@ -234,7 +235,7 @@ public function testProcessStaticAttribute() $this->attribute->method('getAttributeCode') ->willReturn('static_attribute'); - $this->tableMapper->expects($this->once())->method('getMappingAlias') + $this->aliasResolver->expects($this->once())->method('getAlias') ->willReturn('attr_table_alias'); $this->filter->expects($this->exactly(3)) ->method('getField') @@ -272,7 +273,7 @@ public function testProcessTermFilter($frontendInput, $fieldValue, $isNegation, ->method('getFrontendInput') ->willReturn($frontendInput); - $this->tableMapper->expects($this->once())->method('getMappingAlias') + $this->aliasResolver->expects($this->once())->method('getAlias') ->willReturn('termAttrAlias'); $this->filter->expects($this->exactly(3)) diff --git a/app/code/Magento/CatalogSearch/Test/Unit/Model/Search/FilterMapper/FilterContextTest.php b/app/code/Magento/CatalogSearch/Test/Unit/Model/Search/FilterMapper/FilterContextTest.php new file mode 100644 index 0000000000000..9f133b763889b --- /dev/null +++ b/app/code/Magento/CatalogSearch/Test/Unit/Model/Search/FilterMapper/FilterContextTest.php @@ -0,0 +1,236 @@ +eavConfig = $this->getMockBuilder(\Magento\Eav\Model\Config::class) + ->disableOriginalConstructor() + ->setMethods(['getAttribute']) + ->getMock(); + $this->aliasResolver = $this->getMockBuilder( + AliasResolver::class + ) + ->disableOriginalConstructor() + ->setMethods(['getAlias']) + ->getMock(); + $this->exclusionStrategy = $this->getMockBuilder(ExclusionStrategy::class) + ->disableOriginalConstructor() + ->setMethods(['apply']) + ->getMock(); + $this->termDropdownStrategy = $this->getMockBuilder(TermDropdownStrategy::class) + ->disableOriginalConstructor() + ->setMethods(['apply']) + ->getMock(); + $this->staticAttributeStrategy = $this->getMockBuilder(StaticAttributeStrategy::class) + ->disableOriginalConstructor() + ->setMethods(['apply']) + ->getMock(); + $this->select = $this->getMockBuilder(\Magento\Framework\DB\Select::class) + ->disableOriginalConstructor() + ->setMethods([]) + ->getMock(); + $objectManager = new ObjectManager($this); + $this->filterContext = $objectManager->getObject( + FilterContext::class, + [ + 'eavConfig' => $this->eavConfig, + 'aliasResolver' => $this->aliasResolver, + 'exclusionStrategy' => $this->exclusionStrategy, + 'termDropdownStrategy' => $this->termDropdownStrategy, + 'staticAttributeStrategy' => $this->staticAttributeStrategy, + ] + ); + } + + public function testApplyOnExclusionFilter() + { + $filter = $this->createFilterMock(); + $this->exclusionStrategy->expects($this->once()) + ->method('apply') + ->with($filter, $this->select) + ->willReturn(true); + $this->eavConfig->expects($this->never())->method('getAttribute'); + $this->assertTrue($this->filterContext->apply($filter, $this->select)); + } + + public function testApplyFilterWithoutAttribute() + { + $filter = $this->createFilterMock('some_field'); + $this->exclusionStrategy->expects($this->once()) + ->method('apply') + ->with($filter, $this->select) + ->willReturn(false); + $this->eavConfig->expects($this->once()) + ->method('getAttribute') + ->with(\Magento\Catalog\Model\Product::ENTITY, 'some_field') + ->willReturn(null); + $this->assertFalse($this->filterContext->apply($filter, $this->select)); + } + + public function testApplyOnTermFilterBySelect() + { + $filter = $this->createFilterMock('select_field', FilterInterface::TYPE_TERM); + $attribute = $this->createAttributeMock('select'); + $this->eavConfig->expects($this->once()) + ->method('getAttribute') + ->with(\Magento\Catalog\Model\Product::ENTITY, 'select_field') + ->willReturn($attribute); + $this->exclusionStrategy->expects($this->once()) + ->method('apply') + ->with($filter, $this->select) + ->willReturn(false); + $this->termDropdownStrategy->expects($this->once()) + ->method('apply') + ->with($filter, $this->select) + ->willReturn(true); + $this->assertTrue($this->filterContext->apply($filter, $this->select)); + } + + public function testApplyOnTermFilterByMultiSelect() + { + $filter = $this->createFilterMock('multiselect_field', FilterInterface::TYPE_TERM); + $attribute = $this->createAttributeMock('multiselect'); + $this->eavConfig->expects($this->once()) + ->method('getAttribute') + ->with(\Magento\Catalog\Model\Product::ENTITY, 'multiselect_field') + ->willReturn($attribute); + $this->exclusionStrategy->expects($this->once()) + ->method('apply') + ->with($filter, $this->select) + ->willReturn(false); + $this->termDropdownStrategy->expects($this->once()) + ->method('apply') + ->with($filter, $this->select) + ->willReturn(true); + $this->assertTrue($this->filterContext->apply($filter, $this->select)); + } + + public function testApplyOnTermFilterByStaticAttribute() + { + $filter = $this->createFilterMock('multiselect_field', FilterInterface::TYPE_TERM); + $attribute = $this->createAttributeMock('text', AbstractAttribute::TYPE_STATIC); + $this->eavConfig->expects($this->once()) + ->method('getAttribute') + ->with(\Magento\Catalog\Model\Product::ENTITY, 'multiselect_field') + ->willReturn($attribute); + $this->exclusionStrategy->expects($this->once()) + ->method('apply') + ->with($filter, $this->select) + ->willReturn(false); + $this->staticAttributeStrategy->expects($this->once()) + ->method('apply') + ->with($filter, $this->select) + ->willReturn(true); + $this->assertTrue($this->filterContext->apply($filter, $this->select)); + } + + public function testApplyOnTermFilterByUnknownAttributeType() + { + $filter = $this->createFilterMock('multiselect_field', FilterInterface::TYPE_TERM); + $attribute = $this->createAttributeMock('text', 'text'); + $this->eavConfig->expects($this->once()) + ->method('getAttribute') + ->with(\Magento\Catalog\Model\Product::ENTITY, 'multiselect_field') + ->willReturn($attribute); + $this->exclusionStrategy->expects($this->once()) + ->method('apply') + ->with($filter, $this->select) + ->willReturn(false); + $this->assertFalse($this->filterContext->apply($filter, $this->select)); + } + + /** + * @param string $field + * @param string $type + * @return FilterInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private function createFilterMock($field = null, $type = null) + { + $filter = $this->getMockBuilder(FilterInterface::class) + ->setMethods(['getField', 'getType']) + ->getMockForAbstractClass(); + $filter->expects($this->any()) + ->method('getField') + ->willReturn($field); + $filter->expects($this->any()) + ->method('getType') + ->willReturn($type); + + return $filter; + } + + /** + * @param string|null $frontendInput + * @param string|null $backendType + * @return Attribute|\PHPUnit_Framework_MockObject_MockObject + */ + private function createAttributeMock($frontendInput = null, $backendType = null) + { + $attribute = $this->getMockBuilder(Attribute::class) + ->disableOriginalConstructor() + ->setMethods(['getFrontendInput', 'getBackendType']) + ->getMock(); + $attribute->expects($this->any()) + ->method('getFrontendInput') + ->willReturn($frontendInput); + $attribute->expects($this->any()) + ->method('getBackendType') + ->willReturn($backendType); + return $attribute; + } +} diff --git a/app/code/Magento/CatalogSearch/Test/Unit/Model/Search/TableMapperTest.php b/app/code/Magento/CatalogSearch/Test/Unit/Model/Search/TableMapperTest.php index dd9a556146ab3..dad4ad8095f5a 100644 --- a/app/code/Magento/CatalogSearch/Test/Unit/Model/Search/TableMapperTest.php +++ b/app/code/Magento/CatalogSearch/Test/Unit/Model/Search/TableMapperTest.php @@ -6,6 +6,9 @@ namespace Magento\CatalogSearch\Test\Unit\Model\Search; +use Magento\Catalog\Model\ResourceModel\Product\Attribute\Collection; +use Magento\Catalog\Model\ResourceModel\Product\Attribute\CollectionFactory; +use Magento\CatalogSearch\Model\Adapter\Mysql\Filter\AliasResolver; use Magento\Framework\Search\Request\FilterInterface; use Magento\Framework\Search\Request\QueryInterface; use \Magento\Framework\TestFramework\Unit\Helper\ObjectManager; @@ -16,8 +19,10 @@ */ class TableMapperTest extends \PHPUnit_Framework_TestCase { - const WEBSITE_ID = 4512; - const STORE_ID = 2514; + /** + * @var AliasResolver|\PHPUnit_Framework_MockObject_MockObject + */ + private $aliasResolver; /** * @var \Magento\Eav\Model\Config|\PHPUnit_Framework_MockObject_MockObject @@ -25,7 +30,7 @@ class TableMapperTest extends \PHPUnit_Framework_TestCase private $eavConfig; /** - * @var \Magento\Catalog\Model\ResourceModel\Product\Attribute\Collection|\PHPUnit_Framework_MockObject_MockObject + * @var Collection|\PHPUnit_Framework_MockObject_MockObject */ private $attributeCollection; @@ -59,11 +64,6 @@ class TableMapperTest extends \PHPUnit_Framework_TestCase */ private $resource; - /** - * @var \Magento\Store\Api\Data\StoreInterface|\PHPUnit_Framework_MockObject_MockObject - */ - private $store; - /** * @var \Magento\CatalogSearch\Model\Search\TableMapper */ @@ -76,65 +76,49 @@ protected function setUp() $this->connection = $this->getMockBuilder(\Magento\Framework\DB\Adapter\AdapterInterface::class) ->disableOriginalConstructor() ->getMock(); - $this->connection->expects($this->any()) - ->method('quoteInto') - ->willReturnCallback( - function ($query, $expression) { - return str_replace('?', $expression, $query); - } - ); + $this->connection->expects($this->never())->method('quoteInto'); $this->resource = $this->getMockBuilder(\Magento\Framework\App\ResourceConnection::class) ->disableOriginalConstructor() ->getMock(); - $this->resource->method('getTableName') - ->willReturnCallback( - function ($table) { - return 'prefix_' . $table; - } - ); - $this->resource->expects($this->any()) - ->method('getConnection') - ->willReturn($this->connection); + $this->resource->expects($this->never())->method('getTableName'); + $this->resource->expects($this->never())->method('getConnection'); $this->website = $this->getMockBuilder(\Magento\Store\Api\Data\WebsiteInterface::class) ->disableOriginalConstructor() ->getMockForAbstractClass(); - $this->website->expects($this->any()) - ->method('getId') - ->willReturn(self::WEBSITE_ID); - $this->store = $this->getMockBuilder(\Magento\Store\Api\Data\StoreInterface::class) - ->disableOriginalConstructor() - ->getMockForAbstractClass(); - $this->store->expects($this->any()) - ->method('getId') - ->willReturn(self::STORE_ID); + $this->website->expects($this->never())->method('getId'); + $this->storeManager = $this->getMockBuilder(\Magento\Store\Model\StoreManagerInterface::class) ->disableOriginalConstructor() ->getMock(); - $this->storeManager->expects($this->any()) - ->method('getWebsite') - ->willReturn($this->website); - $this->storeManager->expects($this->any()) - ->method('getStore') - ->willReturn($this->store); - $this->attributeCollection = $this->getMockBuilder( - \Magento\Catalog\Model\ResourceModel\Product\Attribute\Collection::class - ) + $this->storeManager->expects($this->never())->method('getWebsite'); + $this->storeManager->expects($this->never())->method('getStore'); + + $this->attributeCollection = $this->getMockBuilder(Collection::class) ->disableOriginalConstructor() ->getMock(); - $attributeCollectionFactory = $this->getMockBuilder( - \Magento\Catalog\Model\ResourceModel\Product\Attribute\CollectionFactory::class - ) + $attributeCollectionFactory = $this->getMockBuilder(CollectionFactory::class) ->setMethods(['create']) ->disableOriginalConstructor() ->getMock(); $attributeCollectionFactory->expects($this->never()) ->method('create'); + $this->eavConfig = $this->getMockBuilder(\Magento\Eav\Model\Config::class) ->setMethods(['getAttribute']) ->disableOriginalConstructor() ->getMock(); + + $this->aliasResolver = $this->getMockBuilder(AliasResolver::class) + ->disableOriginalConstructor() + ->getMock(); + $this->aliasResolver->expects($this->any()) + ->method('getAlias') + ->willReturnCallback(function (FilterInterface $filter) { + return $filter->getField() . '_alias'; + }); + $this->target = $objectManager->getObject( \Magento\CatalogSearch\Model\Search\TableMapper::class, [ @@ -142,6 +126,7 @@ function ($table) { 'storeManager' => $this->storeManager, 'attributeCollectionFactory' => $attributeCollectionFactory, 'eavConfig' => $this->eavConfig, + 'aliasResolver' => $this->aliasResolver, ] ); @@ -160,14 +145,7 @@ public function testAddPriceFilter() $this->request->expects($this->once()) ->method('getQuery') ->willReturn($query); - $this->select->expects($this->once()) - ->method('joinInner') - ->with( - ['price_index' => 'prefix_catalog_product_index_price'], - 'search_index.entity_id = price_index.entity_id AND price_index.website_id = ' . self::WEBSITE_ID, - [] - ) - ->willReturnSelf(); + $select = $this->target->addTables($this->select, $this->request); $this->assertEquals($this->select, $select, 'Returned results isn\'t equal to passed select'); } @@ -176,18 +154,10 @@ public function testAddStaticAttributeFilter() { $priceFilter = $this->createRangeFilter('static'); $query = $this->createFilterQuery($priceFilter); - $this->createAttributeMock('static', 'static', 'backend_table', 0, 'select'); $this->request->expects($this->once()) ->method('getQuery') ->willReturn($query); - $this->select->expects($this->once()) - ->method('joinInner') - ->with( - ['static_filter' => 'backend_table'], - 'search_index.entity_id = static_filter.entity_id', - null - ) - ->willReturnSelf(); + $select = $this->target->addTables($this->select, $this->request); $this->assertEquals($this->select, $select, 'Returned results isn\'t equal to passed select'); } @@ -199,46 +169,25 @@ public function testAddCategoryIds() $this->request->expects($this->once()) ->method('getQuery') ->willReturn($query); - $this->select->expects($this->once()) - ->method('joinInner') - ->with( - ['category_ids_index' => 'prefix_catalog_category_product_index'], - 'search_index.entity_id = category_ids_index.product_id', - [] - ) - ->willReturnSelf(); + $select = $this->target->addTables($this->select, $this->request); $this->assertEquals($this->select, $select, 'Returned results isn\'t equal to passed select'); } public function testAddTermFilter() { - $this->createAttributeMock('color', null, null, 132, 'select', 0); $categoryIdsFilter = $this->createTermFilter('color'); $query = $this->createFilterQuery($categoryIdsFilter); $this->request->expects($this->once()) ->method('getQuery') ->willReturn($query); - $this->select->expects($this->once()) - ->method('joinLeft') - ->with( - ['color_filter' => 'prefix_catalog_product_index_eav'], - 'search_index.entity_id = color_filter.entity_id' - . ' AND color_filter.attribute_id = 132' - . ' AND color_filter.store_id = 2514', - [] - ) - ->willReturnSelf(); + $select = $this->target->addTables($this->select, $this->request); $this->assertEquals($this->select, $select, 'Returned results isn\'t equal to passed select'); } public function testAddBoolQueryWithTermFiltersInside() { - $this->createAttributeMock('must1', null, null, 101, 'select', 0); - $this->createAttributeMock('should1', null, null, 102, 'select', 1); - $this->createAttributeMock('mustNot1', null, null, 103, 'select', 2); - $query = $this->createBoolQuery( [ $this->createFilterQuery($this->createTermFilter('must1')), @@ -253,45 +202,13 @@ public function testAddBoolQueryWithTermFiltersInside() $this->request->expects($this->once()) ->method('getQuery') ->willReturn($query); - $this->select->expects($this->at(0)) - ->method('joinLeft') - ->with( - ['must1_filter' => 'prefix_catalog_product_index_eav'], - 'search_index.entity_id = must1_filter.entity_id' - . ' AND must1_filter.attribute_id = 101' - . ' AND must1_filter.store_id = 2514', - [] - ) - ->willReturnSelf(); - $this->select->expects($this->at(1)) - ->method('joinLeft') - ->with( - ['should1_filter' => 'prefix_catalog_product_index_eav'], - 'search_index.entity_id = should1_filter.entity_id' - . ' AND should1_filter.attribute_id = 102' - . ' AND should1_filter.store_id = 2514', - [] - ) - ->willReturnSelf(); - $this->select->expects($this->at(2)) - ->method('joinLeft') - ->with( - ['mustNot1_filter' => 'prefix_catalog_product_index_eav'], - 'search_index.entity_id = mustNot1_filter.entity_id' - . ' AND mustNot1_filter.attribute_id = 103' - . ' AND mustNot1_filter.store_id = 2514', - [] - ) - ->willReturnSelf(); + $select = $this->target->addTables($this->select, $this->request); $this->assertEquals($this->select, $select, 'Returned results isn\'t equal to passed select'); } public function testAddBoolQueryWithTermAndPriceFiltersInside() { - $this->createAttributeMock('must1', null, null, 101, 'select', 0); - $this->createAttributeMock('should1', null, null, 102, 'select', 1); - $this->createAttributeMock('mustNot1', null, null, 103, 'select', 2); $query = $this->createBoolQuery( [ $this->createFilterQuery($this->createTermFilter('must1')), @@ -307,53 +224,13 @@ public function testAddBoolQueryWithTermAndPriceFiltersInside() $this->request->expects($this->once()) ->method('getQuery') ->willReturn($query); - $this->select->expects($this->at(0)) - ->method('joinLeft') - ->with( - ['must1_filter' => 'prefix_catalog_product_index_eav'], - 'search_index.entity_id = must1_filter.entity_id' - . ' AND must1_filter.attribute_id = 101' - . ' AND must1_filter.store_id = 2514', - [] - ) - ->willReturnSelf(); - $this->select->expects($this->at(1)) - ->method('joinInner') - ->with( - ['price_index' => 'prefix_catalog_product_index_price'], - 'search_index.entity_id = price_index.entity_id AND price_index.website_id = ' . self::WEBSITE_ID, - [] - ) - ->willReturnSelf(); - $this->select->expects($this->at(2)) - ->method('joinLeft') - ->with( - ['should1_filter' => 'prefix_catalog_product_index_eav'], - 'search_index.entity_id = should1_filter.entity_id' - . ' AND should1_filter.attribute_id = 102' - . ' AND should1_filter.store_id = 2514', - [] - ) - ->willReturnSelf(); - $this->select->expects($this->at(3)) - ->method('joinLeft') - ->with( - ['mustNot1_filter' => 'prefix_catalog_product_index_eav'], - 'search_index.entity_id = mustNot1_filter.entity_id' - . ' AND mustNot1_filter.attribute_id = 103' - . ' AND mustNot1_filter.store_id = 2514', - [] - ) - ->willReturnSelf(); + $select = $this->target->addTables($this->select, $this->request); $this->assertEquals($this->select, $select, 'Returned results isn\'t equal to passed select'); } public function testAddBoolFilterWithTermFiltersInside() { - $this->createAttributeMock('must1', null, null, 101, 'select', 0); - $this->createAttributeMock('should1', null, null, 102, 'select', 1); - $this->createAttributeMock('mustNot1', null, null, 103, 'select', 2); $query = $this->createFilterQuery( $this->createBoolFilter( [ @@ -370,45 +247,13 @@ public function testAddBoolFilterWithTermFiltersInside() $this->request->expects($this->once()) ->method('getQuery') ->willReturn($query); - $this->select->expects($this->at(0)) - ->method('joinLeft') - ->with( - ['must1_filter' => 'prefix_catalog_product_index_eav'], - 'search_index.entity_id = must1_filter.entity_id' - . ' AND must1_filter.attribute_id = 101' - . ' AND must1_filter.store_id = 2514', - [] - ) - ->willReturnSelf(); - $this->select->expects($this->at(1)) - ->method('joinLeft') - ->with( - ['should1_filter' => 'prefix_catalog_product_index_eav'], - 'search_index.entity_id = should1_filter.entity_id' - . ' AND should1_filter.attribute_id = 102' - . ' AND should1_filter.store_id = 2514', - [] - ) - ->willReturnSelf(); - $this->select->expects($this->at(2)) - ->method('joinLeft') - ->with( - ['mustNot1_filter' => 'prefix_catalog_product_index_eav'], - 'search_index.entity_id = mustNot1_filter.entity_id' - . ' AND mustNot1_filter.attribute_id = 103' - . ' AND mustNot1_filter.store_id = 2514', - [] - ) - ->willReturnSelf(); + $select = $this->target->addTables($this->select, $this->request); $this->assertEquals($this->select, $select, 'Returned results isn\'t equal to passed select'); } public function testAddBoolFilterWithBoolFiltersInside() { - $this->createAttributeMock('must1', null, null, 101, 'select', 0); - $this->createAttributeMock('should1', null, null, 102, 'select', 1); - $this->createAttributeMock('mustNot1', null, null, 103, 'select', 2); $query = $this->createFilterQuery( $this->createBoolFilter( [ @@ -425,36 +270,7 @@ public function testAddBoolFilterWithBoolFiltersInside() $this->request->expects($this->once()) ->method('getQuery') ->willReturn($query); - $this->select->expects($this->at(0)) - ->method('joinLeft') - ->with( - ['must1_filter' => 'prefix_catalog_product_index_eav'], - 'search_index.entity_id = must1_filter.entity_id' - . ' AND must1_filter.attribute_id = 101' - . ' AND must1_filter.store_id = 2514', - [] - ) - ->willReturnSelf(); - $this->select->expects($this->at(1)) - ->method('joinLeft') - ->with( - ['should1_filter' => 'prefix_catalog_product_index_eav'], - 'search_index.entity_id = should1_filter.entity_id' - . ' AND should1_filter.attribute_id = 102' - . ' AND should1_filter.store_id = 2514', - [] - ) - ->willReturnSelf(); - $this->select->expects($this->at(2)) - ->method('joinLeft') - ->with( - ['mustNot1_filter' => 'prefix_catalog_product_index_eav'], - 'search_index.entity_id = mustNot1_filter.entity_id' - . ' AND mustNot1_filter.attribute_id = 103' - . ' AND mustNot1_filter.store_id = 2514', - [] - ) - ->willReturnSelf(); + $select = $this->target->addTables($this->select, $this->request); $this->assertEquals($this->select, $select, 'Returned results isn\'t equal to passed select'); } @@ -472,6 +288,7 @@ private function createFilterQuery($filter) ->willReturn(QueryInterface::TYPE_FILTER); $query->method('getReference') ->willReturn($filter); + return $query; } @@ -495,6 +312,7 @@ private function createBoolQuery(array $must, array $should, array $mustNot) ->willReturn($should); $query->method('getMustNot') ->willReturn($mustNot); + return $query; } @@ -518,6 +336,7 @@ private function createBoolFilter(array $must, array $should, array $mustNot) ->willReturn($should); $query->method('getMustNot') ->willReturn($mustNot); + return $query; } @@ -532,6 +351,7 @@ private function createRangeFilter($field) FilterInterface::TYPE_RANGE, $field ); + return $filter; } @@ -546,6 +366,7 @@ private function createTermFilter($field) FilterInterface::TYPE_TERM, $field ); + return $filter; } @@ -564,40 +385,7 @@ private function createFilterMock($class, $type, $field) ->willReturn($type); $filter->method('getField') ->willReturn($field); - return $filter; - } - /** - * @param string $code - * @param string $backendType - * @param string $backendTable - * @param int $attributeId - * @param string $frontendInput - * @param int $positionInCollection - */ - private function createAttributeMock( - $code, - $backendType = null, - $backendTable = null, - $attributeId = 120, - $frontendInput = 'select', - $positionInCollection = 0 - ) { - $attribute = $this->getMockBuilder(\Magento\Catalog\Model\ResourceModel\Eav\Attribute::class) - ->setMethods(['getBackendType', 'getBackendTable', 'getId', 'getFrontendInput']) - ->disableOriginalConstructor() - ->getMock(); - $attribute->method('getId') - ->willReturn($attributeId); - $attribute->method('getBackendType') - ->willReturn($backendType); - $attribute->method('getBackendTable') - ->willReturn($backendTable); - $attribute->method('getFrontendInput') - ->willReturn($frontendInput); - $this->eavConfig->expects($this->at($positionInCollection)) - ->method('getAttribute') - ->with(\Magento\Catalog\Model\Product::ENTITY, $code) - ->willReturn($attribute); + return $filter; } } diff --git a/app/code/Magento/CatalogSearch/composer.json b/app/code/Magento/CatalogSearch/composer.json index 49756420bd230..313cc99881a31 100644 --- a/app/code/Magento/CatalogSearch/composer.json +++ b/app/code/Magento/CatalogSearch/composer.json @@ -2,7 +2,7 @@ "name": "magento/module-catalog-search", "description": "N/A", "require": { - "php": "~5.6.0|7.0.2|7.0.4|~7.0.6", + "php": "~5.6.5|7.0.2|7.0.4|~7.0.6", "magento/module-store": "100.2.*", "magento/module-catalog": "101.1.*", "magento/module-search": "100.2.*", diff --git a/app/code/Magento/CatalogSearch/etc/di.xml b/app/code/Magento/CatalogSearch/etc/di.xml index f62b4e47767a2..7e9451f9b83e7 100644 --- a/app/code/Magento/CatalogSearch/etc/di.xml +++ b/app/code/Magento/CatalogSearch/etc/di.xml @@ -11,6 +11,7 @@ + Magento\CatalogSearch\Model\ResourceModel\EngineInterface::CONFIG_ENGINE_PATH diff --git a/app/code/Magento/CatalogUrlRewrite/composer.json b/app/code/Magento/CatalogUrlRewrite/composer.json index 8fd54cbc8ee83..a5f66cd09dda4 100644 --- a/app/code/Magento/CatalogUrlRewrite/composer.json +++ b/app/code/Magento/CatalogUrlRewrite/composer.json @@ -2,7 +2,7 @@ "name": "magento/module-catalog-url-rewrite", "description": "N/A", "require": { - "php": "~5.6.0|7.0.2|7.0.4|~7.0.6", + "php": "~5.6.5|7.0.2|7.0.4|~7.0.6", "magento/module-backend": "100.2.*", "magento/module-catalog": "101.1.*", "magento/module-catalog-import-export": "100.2.*", diff --git a/app/code/Magento/CatalogWidget/composer.json b/app/code/Magento/CatalogWidget/composer.json index 330f46176286c..198e54db32d88 100644 --- a/app/code/Magento/CatalogWidget/composer.json +++ b/app/code/Magento/CatalogWidget/composer.json @@ -2,7 +2,7 @@ "name": "magento/module-catalog-widget", "description": "N/A", "require": { - "php": "~5.6.0|7.0.2|7.0.4|~7.0.6", + "php": "~5.6.5|7.0.2|7.0.4|~7.0.6", "magento/module-catalog": "101.1.*", "magento/module-widget": "100.2.*", "magento/module-backend": "100.2.*", diff --git a/app/code/Magento/Checkout/Controller/Cart/Add.php b/app/code/Magento/Checkout/Controller/Cart/Add.php index fddfa63b5983e..bfbf0008bf589 100644 --- a/app/code/Magento/Checkout/Controller/Cart/Add.php +++ b/app/code/Magento/Checkout/Controller/Cart/Add.php @@ -84,6 +84,7 @@ public function execute() } $params = $this->getRequest()->getParams(); + try { if (isset($params['qty'])) { $filter = new \Zend_Filter_LocalizedToNormalized( diff --git a/app/code/Magento/Checkout/Model/Cart.php b/app/code/Magento/Checkout/Model/Cart.php index 4ce23c9c7f709..236c716f572d9 100644 --- a/app/code/Magento/Checkout/Model/Cart.php +++ b/app/code/Magento/Checkout/Model/Cart.php @@ -91,6 +91,11 @@ class Cart extends DataObject implements CartInterface */ protected $productRepository; + /** + * @var \Magento\Checkout\Model\Cart\RequestInfoFilterInterface + */ + private $requestInfoFilter; + /** * @param \Magento\Framework\Event\ManagerInterface $eventManager * @param \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig @@ -315,6 +320,7 @@ protected function _getProduct($productInfo) * * @param \Magento\Framework\DataObject|int|array $requestInfo * @return \Magento\Framework\DataObject + * @throws \Magento\Framework\Exception\LocalizedException */ protected function _getProductRequest($requestInfo) { @@ -322,11 +328,14 @@ protected function _getProductRequest($requestInfo) $request = $requestInfo; } elseif (is_numeric($requestInfo)) { $request = new \Magento\Framework\DataObject(['qty' => $requestInfo]); - } else { + } elseif (is_array($requestInfo)) { $request = new \Magento\Framework\DataObject($requestInfo); + } else { + throw new \Magento\Framework\Exception\LocalizedException( + __('We found an invalid request for adding product to quote.') + ); } - - !$request->hasFormKey() ?: $request->unsFormKey(); + $this->getRequestInfoFilter()->filter($request); return $request; } @@ -722,4 +731,19 @@ public function updateItem($itemId, $requestInfo = null, $updatingParams = null) $this->_checkoutSession->setLastAddedProductId($productId); return $result; } + + /** + * Getter for RequestInfoFilter + * + * @deprecated + * @return \Magento\Checkout\Model\Cart\RequestInfoFilterInterface + */ + private function getRequestInfoFilter() + { + if ($this->requestInfoFilter === null) { + $this->requestInfoFilter = \Magento\Framework\App\ObjectManager::getInstance() + ->get(\Magento\Checkout\Model\Cart\RequestInfoFilterInterface::class); + } + return $this->requestInfoFilter; + } } diff --git a/app/code/Magento/Checkout/Model/Cart/RequestInfoFilter.php b/app/code/Magento/Checkout/Model/Cart/RequestInfoFilter.php new file mode 100644 index 0000000000000..10f3b81386b8f --- /dev/null +++ b/app/code/Magento/Checkout/Model/Cart/RequestInfoFilter.php @@ -0,0 +1,44 @@ +filterList = $filterList; + } + + /** + * Filters the data with values from filterList + * + * @param \Magento\Framework\DataObject $params + * @return $this + */ + public function filter(\Magento\Framework\DataObject $params) + { + foreach ($this->filterList as $filterKey) { + /** @var string $filterKey */ + if ($params->hasData($filterKey)) { + $params->unsetData($filterKey); + } + } + return $this; + } +} diff --git a/app/code/Magento/Checkout/Model/Cart/RequestInfoFilterComposite.php b/app/code/Magento/Checkout/Model/Cart/RequestInfoFilterComposite.php new file mode 100644 index 0000000000000..2ef24c0a5f28a --- /dev/null +++ b/app/code/Magento/Checkout/Model/Cart/RequestInfoFilterComposite.php @@ -0,0 +1,41 @@ +filters = $filters; + } + + /** + * Loops through all leafs of the composite and calls filter method + * + * @param \Magento\Framework\DataObject $params + * @return $this + */ + public function filter(\Magento\Framework\DataObject $params) + { + foreach ($this->filters as $filter) { + $filter->filter($params); + } + return $this; + } +} diff --git a/app/code/Magento/Checkout/Model/Cart/RequestInfoFilterInterface.php b/app/code/Magento/Checkout/Model/Cart/RequestInfoFilterInterface.php new file mode 100644 index 0000000000000..4bd268f6c896e --- /dev/null +++ b/app/code/Magento/Checkout/Model/Cart/RequestInfoFilterInterface.php @@ -0,0 +1,21 @@ +savePaymentInformation($cartId, $email, $paymentMethod, $billingAddress); try { $orderId = $this->cartManagement->placeOrder($cartId); + } catch (\Magento\Framework\Exception\LocalizedException $e) { + throw new CouldNotSaveException( + __($e->getMessage()), + $e + ); } catch (\Exception $e) { + $this->getLogger()->critical($e); throw new CouldNotSaveException( __('An error occurred on the server. Please try to place the order again.'), $e @@ -117,4 +131,18 @@ public function getPaymentInformation($cartId) $quoteIdMask = $this->quoteIdMaskFactory->create()->load($cartId, 'masked_id'); return $this->paymentInformationManagement->getPaymentInformation($quoteIdMask->getQuoteId()); } + + /** + * Get logger instance + * + * @return \Psr\Log\LoggerInterface + * @deprecated + */ + private function getLogger() + { + if (!$this->logger) { + $this->logger = \Magento\Framework\App\ObjectManager::getInstance()->get(\Psr\Log\LoggerInterface::class); + } + return $this->logger; + } } diff --git a/app/code/Magento/Checkout/Model/PaymentInformationManagement.php b/app/code/Magento/Checkout/Model/PaymentInformationManagement.php index 140917dcdfeff..79e76feb43661 100644 --- a/app/code/Magento/Checkout/Model/PaymentInformationManagement.php +++ b/app/code/Magento/Checkout/Model/PaymentInformationManagement.php @@ -7,6 +7,9 @@ use Magento\Framework\Exception\CouldNotSaveException; +/** + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ class PaymentInformationManagement implements \Magento\Checkout\Api\PaymentInformationManagementInterface { /** @@ -34,6 +37,11 @@ class PaymentInformationManagement implements \Magento\Checkout\Api\PaymentInfor */ protected $cartTotalsRepository; + /** + * @var \Psr\Log\LoggerInterface + */ + private $logger; + /** * @param \Magento\Quote\Api\BillingAddressManagementInterface $billingAddressManagement * @param \Magento\Quote\Api\PaymentMethodManagementInterface $paymentMethodManagement @@ -67,7 +75,13 @@ public function savePaymentInformationAndPlaceOrder( $this->savePaymentInformation($cartId, $paymentMethod, $billingAddress); try { $orderId = $this->cartManagement->placeOrder($cartId); + } catch (\Magento\Framework\Exception\LocalizedException $e) { + throw new CouldNotSaveException( + __($e->getMessage()), + $e + ); } catch (\Exception $e) { + $this->getLogger()->critical($e); throw new CouldNotSaveException( __('An error occurred on the server. Please try to place the order again.'), $e @@ -102,4 +116,18 @@ public function getPaymentInformation($cartId) $paymentDetails->setTotals($this->cartTotalsRepository->get($cartId)); return $paymentDetails; } + + /** + * Get logger instance + * + * @return \Psr\Log\LoggerInterface + * @deprecated + */ + private function getLogger() + { + if (!$this->logger) { + $this->logger = \Magento\Framework\App\ObjectManager::getInstance()->get(\Psr\Log\LoggerInterface::class); + } + return $this->logger; + } } diff --git a/app/code/Magento/Checkout/Test/Unit/Model/Cart/RequestInfoFilterCompositeTest.php b/app/code/Magento/Checkout/Test/Unit/Model/Cart/RequestInfoFilterCompositeTest.php new file mode 100644 index 0000000000000..6c758cf4661fd --- /dev/null +++ b/app/code/Magento/Checkout/Test/Unit/Model/Cart/RequestInfoFilterCompositeTest.php @@ -0,0 +1,73 @@ +objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); + + $requestInfoFilterMock1 = $this->getMock( + \Magento\Checkout\Model\Cart\RequestInfoFilter::class, + ['filter'], + [], + '', + false + ); + $requestInfoFilterMock2 = $this->getMock( + \Magento\Checkout\Model\Cart\RequestInfoFilter::class, + ['filter'], + [], + '', + false + ); + + $requestInfoFilterMock1->expects($this->atLeastOnce()) + ->method('filter'); + $requestInfoFilterMock2->expects($this->atLeastOnce()) + ->method('filter'); + + $filterList = [ $requestInfoFilterMock1, $requestInfoFilterMock2]; + + $this->model = $this->objectManager->getObject( + \Magento\Checkout\Model\Cart\RequestInfoFilterComposite::class, + [ + 'filters' => $filterList, + ] + ); + } + + /** + * Test Filter method + */ + public function testFilter() + { + /** @var \Magento\Framework\DataObject $params */ + $params = $this->objectManager->getObject( + \Magento\Framework\DataObject::class, + ['data' => ['abc' => 1, 'efg' => 1, 'xyz' => 1]] + ); + $result = $this->model->filter($params); + $this->assertEquals($this->model, $result); + } +} diff --git a/app/code/Magento/Checkout/Test/Unit/Model/Cart/RequestInfoFilterTest.php b/app/code/Magento/Checkout/Test/Unit/Model/Cart/RequestInfoFilterTest.php new file mode 100644 index 0000000000000..cd6ca330da1b2 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Unit/Model/Cart/RequestInfoFilterTest.php @@ -0,0 +1,52 @@ +objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); + + $this->model = $this->objectManager->getObject( + \Magento\Checkout\Model\Cart\RequestInfoFilter::class, + [ + 'filterList' => ['efg', 'xyz'], + ] + ); + } + + /** + * Test Filter method + */ + public function testFilter() + { + /** @var \Magento\Framework\DataObject $params */ + $params = $this->objectManager->getObject( + \Magento\Framework\DataObject::class, + ['data' => ['abc' => 1, 'efg' => 1, 'xyz' => 1]] + ); + $result = $this->model->filter($params); + $this->assertEquals($this->model, $result); + $this->assertEquals(['abc' => 1], $params->convertToArray()); + } +} diff --git a/app/code/Magento/Checkout/Test/Unit/Model/CartTest.php b/app/code/Magento/Checkout/Test/Unit/Model/CartTest.php index 199e6692e68d1..9984fe12d3792 100644 --- a/app/code/Magento/Checkout/Test/Unit/Model/CartTest.php +++ b/app/code/Magento/Checkout/Test/Unit/Model/CartTest.php @@ -29,7 +29,7 @@ class CartTest extends \PHPUnit_Framework_TestCase */ protected $customerSessionMock; - /** @var \Magento\CatalogInventory\Api\StockItem|\PHPUnit_Framework_MockObject_MockObject */ + /** @var \Magento\CatalogInventory\Api\Data\StockItemInterface|\PHPUnit_Framework_MockObject_MockObject */ protected $stockItemMock; /** @@ -57,16 +57,39 @@ class CartTest extends \PHPUnit_Framework_TestCase */ protected $stockState; + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $storeManagerMock; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $storeMock; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $productRepository; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $requestInfoFilterMock; + protected function setUp() { $this->checkoutSessionMock = $this->getMock(\Magento\Checkout\Model\Session::class, [], [], '', false); $this->customerSessionMock = $this->getMock(\Magento\Customer\Model\Session::class, [], [], '', false); $this->scopeConfigMock = $this->getMock(\Magento\Framework\App\Config\ScopeConfigInterface::class); + $this->quoteMock = $this->getMock(\Magento\Quote\Model\Quote::class, [], [], '', false); + $this->eventManagerMock = $this->getMock(\Magento\Framework\Event\ManagerInterface::class); + $this->storeManagerMock = $this->getMock(\Magento\Store\Model\StoreManagerInterface::class); + $this->productRepository = $this->getMock(\Magento\Catalog\Api\ProductRepositoryInterface::class); $this->stockRegistry = $this->getMockBuilder(\Magento\CatalogInventory\Model\StockRegistry::class) ->disableOriginalConstructor() ->setMethods(['getStockItem', '__wakeup']) ->getMock(); - $this->stockItemMock = $this->getMock( \Magento\CatalogInventory\Model\Stock\Item::class, ['getMinSaleQty', '__wakeup'], @@ -74,7 +97,6 @@ protected function setUp() '', false ); - $this->stockState = $this->getMock( \Magento\CatalogInventory\Model\StockState::class, ['suggestQty', '__wakeup'], @@ -82,12 +104,22 @@ protected function setUp() '', false ); + $this->storeMock = + $this->getMock(\Magento\Store\Model\Store::class, ['getWebsiteId', 'getId', '__wakeup'], [], '', false); + $this->requestInfoFilterMock = $this->getMock(\Magento\Checkout\Model\Cart\RequestInfoFilterInterface::class); $this->stockRegistry->expects($this->any()) ->method('getStockItem') ->will($this->returnValue($this->stockItemMock)); - $this->quoteMock = $this->getMock(\Magento\Quote\Model\Quote::class, [], [], '', false); - $this->eventManagerMock = $this->getMock(\Magento\Framework\Event\ManagerInterface::class); + $this->storeMock->expects($this->any()) + ->method('getWebsiteId') + ->will($this->returnValue(10)); + $this->storeMock->expects($this->any()) + ->method('getId') + ->will($this->returnValue(10)); + $this->storeManagerMock->expects($this->any()) + ->method('getStore') + ->will($this->returnValue($this->storeMock)); $this->objectManagerHelper = new ObjectManagerHelper($this); $this->cart = $this->objectManagerHelper->getObject( @@ -98,9 +130,14 @@ protected function setUp() 'stockRegistry' => $this->stockRegistry, 'stockState' => $this->stockState, 'customerSession' => $this->customerSessionMock, - 'eventManager' => $this->eventManagerMock + 'eventManager' => $this->eventManagerMock, + 'storeManager' => $this->storeManagerMock, + 'productRepository' => $this->productRepository ] ); + + $this->objectManagerHelper + ->setBackwardCompatibleProperty($this->cart, 'requestInfoFilter', $this->requestInfoFilterMock); } public function testSuggestItemsQty() @@ -169,10 +206,17 @@ public function testUpdateItems() */ public function prepareQuoteItemMock($itemId) { - $store = $this->getMock(\Magento\Store\Model\Store::class, ['getWebsiteId', '__wakeup'], [], '', false); + $store = $this->getMock(\Magento\Store\Model\Store::class, ['getId', '__wakeup'], [], '', false); $store->expects($this->any()) ->method('getWebsiteId') ->will($this->returnValue(10)); + $store->expects($this->any()) + ->method('getId') + ->will($this->returnValue(10)); + $this->storeManagerMock->expects($this->any()) + ->method('getStore') + ->will($this->returnValue($store)); + switch ($itemId) { case 2: $product = $this->getMock( @@ -255,4 +299,162 @@ public function useQtyDataProvider() ['useQty' => false] ]; } + + /** + * Test successful scenarios for AddProduct + * + * @param int|\Magento\Catalog\Model\Product $productInfo + * @param \Magento\Framework\DataObject|int|array $requestInfo + * @dataProvider addProductDataProvider + */ + public function testAddProduct($productInfo, $requestInfo) + { + $product = $this->getMock( + \Magento\Catalog\Model\Product::class, + ['getStore', 'getWebsiteIds', 'getProductUrl', 'getId', '__wakeup'], + [], + '', + false + ); + $product->expects($this->any()) + ->method('getId') + ->will($this->returnValue(4)); + $product->expects($this->once()) + ->method('getStore') + ->will($this->returnValue($this->storeMock)); + $product->expects($this->any()) + ->method('getWebsiteIds') + ->will($this->returnValue([10])); + $product->expects($this->any()) + ->method('getProductUrl') + ->will($this->returnValue('url')); + $this->productRepository->expects($this->any()) + ->method('getById') + ->will($this->returnValue($product)); + $this->quoteMock->expects($this->once()) + ->method('addProduct') + ->will($this->returnValue(1)); + $this->checkoutSessionMock->expects($this->once()) + ->method('getQuote') + ->will($this->returnValue($this->quoteMock)); + + $this->eventManagerMock->expects($this->at(0))->method('dispatch')->with( + 'checkout_cart_product_add_after', + ['quote_item' => 1, 'product' => $product] + ); + + if (!$productInfo) { + $productInfo = $product; + } + $result = $this->cart->addProduct($productInfo, $requestInfo); + $this->assertSame($this->cart, $result); + } + + /** + * Test exception on adding product for AddProduct + * + * @throws \Magento\Framework\Exception\LocalizedException + */ + public function testAddProductException() + { + $product = $this->getMock( + \Magento\Catalog\Model\Product::class, + ['getStore', 'getWebsiteIds', 'getProductUrl', 'getId', '__wakeup'], + [], + '', + false + ); + $product->expects($this->any()) + ->method('getId') + ->will($this->returnValue(4)); + $product->expects($this->once()) + ->method('getStore') + ->will($this->returnValue($this->storeMock)); + $product->expects($this->any()) + ->method('getWebsiteIds') + ->will($this->returnValue([10])); + $product->expects($this->any()) + ->method('getProductUrl') + ->will($this->returnValue('url')); + $this->productRepository->expects($this->any()) + ->method('getById') + ->will($this->returnValue($product)); + $this->quoteMock->expects($this->once()) + ->method('addProduct') + ->will($this->returnValue('error')); + $this->checkoutSessionMock->expects($this->once()) + ->method('getQuote') + ->will($this->returnValue($this->quoteMock)); + + $this->eventManagerMock->expects($this->never())->method('dispatch')->with( + 'checkout_cart_product_add_after', + ['quote_item' => 1, 'product' => $product] + ); + $this->setExpectedException(\Magento\Framework\Exception\LocalizedException::class); + $this->cart->addProduct(4, 4); + } + + /** + * Test bad parameters on adding product for AddProduct + * + * @throws \Magento\Framework\Exception\LocalizedException + */ + public function testAddProductExceptionBadParams() + { + $product = $this->getMock( + \Magento\Catalog\Model\Product::class, + ['getWebsiteIds', 'getId', '__wakeup'], + [], + '', + false + ); + $product->expects($this->any()) + ->method('getId') + ->will($this->returnValue(4)); + $product->expects($this->any()) + ->method('getWebsiteIds') + ->will($this->returnValue([10])); + $this->productRepository->expects($this->any()) + ->method('getById') + ->will($this->returnValue($product)); + + $this->eventManagerMock->expects($this->never())->method('dispatch')->with( + 'checkout_cart_product_add_after', + ['quote_item' => 1, 'product' => $product] + ); + $this->setExpectedException(\Magento\Framework\Exception\LocalizedException::class); + $this->cart->addProduct(4, 'bad'); + } + + /** + * Data provider for testAddProduct + * + * @return array + */ + public function addProductDataProvider() + { + $obj = new ObjectManagerHelper($this) ; + $data = ['qty' => 5.5, 'sku' => 'prod']; + + return [ + 'prod_int_info_int' => [4, 4], + 'prod_int_info_array' => [ 4, $data], + 'prod_int_info_object' => [ + 4, + $obj->getObject( + \Magento\Framework\DataObject::class, + ['data' => $data] + ) + ], + 'prod_obj_info_int' => [null, 4], + 'prod_obj_info_array' => [ null, $data], + 'prod_obj_info_object' => [ + null, + $obj->getObject( + \Magento\Framework\DataObject::class, + ['data' => $data] + ) + ] + ]; + } } diff --git a/app/code/Magento/Checkout/Test/Unit/Model/GuestPaymentInformationManagementTest.php b/app/code/Magento/Checkout/Test/Unit/Model/GuestPaymentInformationManagementTest.php index 093bbf4a5accc..76cbafb48ebd4 100644 --- a/app/code/Magento/Checkout/Test/Unit/Model/GuestPaymentInformationManagementTest.php +++ b/app/code/Magento/Checkout/Test/Unit/Model/GuestPaymentInformationManagementTest.php @@ -42,6 +42,11 @@ class GuestPaymentInformationManagementTest extends \PHPUnit_Framework_TestCase */ protected $model; + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $loggerMock; + protected function setUp() { $objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); @@ -61,6 +66,7 @@ protected function setUp() '', false ); + $this->loggerMock = $this->getMock(\Psr\Log\LoggerInterface::class); $this->model = $objectManager->getObject( \Magento\Checkout\Model\GuestPaymentInformationManagement::class, [ @@ -71,6 +77,7 @@ protected function setUp() 'quoteIdMaskFactory' => $this->quoteIdMaskFactoryMock ] ); + $objectManager->setBackwardCompatibleProperty($this->model, 'logger', $this->loggerMock); } public function testSavePaymentInformationAndPlaceOrder() @@ -112,7 +119,7 @@ public function testSavePaymentInformationAndPlaceOrderException() ->method('assign') ->with($cartId, $billingAddressMock); $this->paymentMethodManagementMock->expects($this->once())->method('set')->with($cartId, $paymentMock); - $exception = new CouldNotSaveException(__('DB exception')); + $exception = new \Exception(__('DB exception')); $this->cartManagementMock->expects($this->once())->method('placeOrder')->willThrowException($exception); $this->model->savePaymentInformationAndPlaceOrder($cartId, $email, $paymentMock, $billingAddressMock); @@ -161,4 +168,29 @@ public function testSavePaymentInformationWithoutBillingAddress() $billingAddressMock->expects($this->once())->method('setEmail')->with($email); $this->assertTrue($this->model->savePaymentInformation($cartId, $email, $paymentMock)); } + + /** + * @expectedExceptionMessage DB exception + * @expectedException \Magento\Framework\Exception\CouldNotSaveException + */ + public function testSavePaymentInformationAndPlaceOrderWithLocolizedException() + { + $cartId = 100; + $email = 'email@magento.com'; + $paymentMock = $this->getMock(\Magento\Quote\Api\Data\PaymentInterface::class); + $billingAddressMock = $this->getMock(\Magento\Quote\Api\Data\AddressInterface::class); + + $billingAddressMock->expects($this->once())->method('setEmail')->with($email)->willReturnSelf(); + + $this->billingAddressManagementMock->expects($this->once()) + ->method('assign') + ->with($cartId, $billingAddressMock); + $this->paymentMethodManagementMock->expects($this->once())->method('set')->with($cartId, $paymentMock); + $phrase = new \Magento\Framework\Phrase(__('DB exception')); + $exception = new \Magento\Framework\Exception\LocalizedException($phrase); + $this->loggerMock->expects($this->never())->method('critical'); + $this->cartManagementMock->expects($this->once())->method('placeOrder')->willThrowException($exception); + + $this->model->savePaymentInformationAndPlaceOrder($cartId, $email, $paymentMock, $billingAddressMock); + } } diff --git a/app/code/Magento/Checkout/Test/Unit/Model/PaymentInformationManagementTest.php b/app/code/Magento/Checkout/Test/Unit/Model/PaymentInformationManagementTest.php index 496054b2c7b1e..8da67a1fcb715 100644 --- a/app/code/Magento/Checkout/Test/Unit/Model/PaymentInformationManagementTest.php +++ b/app/code/Magento/Checkout/Test/Unit/Model/PaymentInformationManagementTest.php @@ -29,6 +29,11 @@ class PaymentInformationManagementTest extends \PHPUnit_Framework_TestCase */ protected $model; + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $loggerMock; + protected function setUp() { $objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); @@ -40,6 +45,8 @@ protected function setUp() ); $this->cartManagementMock = $this->getMock(\Magento\Quote\Api\CartManagementInterface::class); + $this->loggerMock = $this->getMock(\Psr\Log\LoggerInterface::class); + $this->model = $objectManager->getObject( \Magento\Checkout\Model\PaymentInformationManagement::class, [ @@ -48,6 +55,7 @@ protected function setUp() 'cartManagement' => $this->cartManagementMock ] ); + $objectManager->setBackwardCompatibleProperty($this->model, 'logger', $this->loggerMock); } public function testSavePaymentInformationAndPlaceOrder() @@ -83,7 +91,8 @@ public function testSavePaymentInformationAndPlaceOrderException() ->method('assign') ->with($cartId, $billingAddressMock); $this->paymentMethodManagementMock->expects($this->once())->method('set')->with($cartId, $paymentMock); - $exception = new CouldNotSaveException(__('DB exception')); + $exception = new \Exception(__('DB exception')); + $this->loggerMock->expects($this->once())->method('critical'); $this->cartManagementMock->expects($this->once())->method('placeOrder')->willThrowException($exception); $this->model->savePaymentInformationAndPlaceOrder($cartId, $paymentMock, $billingAddressMock); @@ -129,4 +138,26 @@ public function testSavePaymentInformationWithoutBillingAddress() $this->assertTrue($this->model->savePaymentInformation($cartId, $paymentMock)); } + + /** + * @expectedExceptionMessage DB exception + * @expectedException \Magento\Framework\Exception\CouldNotSaveException + */ + public function testSavePaymentInformationAndPlaceOrderWithLocolizedException() + { + $cartId = 100; + $paymentMock = $this->getMock(\Magento\Quote\Api\Data\PaymentInterface::class); + $billingAddressMock = $this->getMock(\Magento\Quote\Api\Data\AddressInterface::class); + + $this->billingAddressManagementMock->expects($this->once()) + ->method('assign') + ->with($cartId, $billingAddressMock); + $this->paymentMethodManagementMock->expects($this->once())->method('set')->with($cartId, $paymentMock); + $phrase = new \Magento\Framework\Phrase(__('DB exception')); + $exception = new \Magento\Framework\Exception\LocalizedException($phrase); + $this->loggerMock->expects($this->never())->method('critical'); + $this->cartManagementMock->expects($this->once())->method('placeOrder')->willThrowException($exception); + + $this->model->savePaymentInformationAndPlaceOrder($cartId, $paymentMock, $billingAddressMock); + } } diff --git a/app/code/Magento/Checkout/composer.json b/app/code/Magento/Checkout/composer.json index 5545e409b65ab..60919d9fa2ca9 100644 --- a/app/code/Magento/Checkout/composer.json +++ b/app/code/Magento/Checkout/composer.json @@ -2,7 +2,7 @@ "name": "magento/module-checkout", "description": "N/A", "require": { - "php": "~5.6.0|7.0.2|7.0.4|~7.0.6", + "php": "~5.6.5|7.0.2|7.0.4|~7.0.6", "magento/module-store": "100.2.*", "magento/module-sales": "100.2.*", "magento/module-backend": "100.2.*", diff --git a/app/code/Magento/Checkout/etc/di.xml b/app/code/Magento/Checkout/etc/di.xml index c55598fdff5e7..a2243b33a04ed 100644 --- a/app/code/Magento/Checkout/etc/di.xml +++ b/app/code/Magento/Checkout/etc/di.xml @@ -26,4 +26,20 @@ + + + + + form_key + + + + + + + Magento\Checkout\Model\Cart\RequestInfoFilter + + + diff --git a/app/code/Magento/Checkout/etc/frontend/di.xml b/app/code/Magento/Checkout/etc/frontend/di.xml index bccf81bcb6ee8..6fb9058c3b768 100644 --- a/app/code/Magento/Checkout/etc/frontend/di.xml +++ b/app/code/Magento/Checkout/etc/frontend/di.xml @@ -72,4 +72,12 @@ + + + + form_key + custom_price + + + diff --git a/app/code/Magento/Checkout/view/frontend/web/js/model/address-converter.js b/app/code/Magento/Checkout/view/frontend/web/js/model/address-converter.js index 2d6fa460ef66d..3ec34cd96574b 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/model/address-converter.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/model/address-converter.js @@ -44,6 +44,13 @@ define( addressData.region.region_code = region['code']; addressData.region.region = region['name']; } + } else if ( + !addressData.region_id + && countryData()[addressData.country_id] + && countryData()[addressData.country_id]['regions'] + ) { + addressData.region.region_code = ''; + addressData.region.region = ''; } delete addressData.region_id; diff --git a/app/code/Magento/Checkout/view/frontend/web/js/model/full-screen-loader.js b/app/code/Magento/Checkout/view/frontend/web/js/model/full-screen-loader.js index ca9d6493a1674..9005015d2460e 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/model/full-screen-loader.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/model/full-screen-loader.js @@ -24,7 +24,7 @@ define([ /** * Stop full page loader action * - * @param {Boolean} forceStop + * @param {Boolean} [forceStop] */ stopLoader: function (forceStop) { var $elem = $(containerId), diff --git a/app/code/Magento/Checkout/view/frontend/web/js/model/new-customer-address.js b/app/code/Magento/Checkout/view/frontend/web/js/model/new-customer-address.js index bee4480b0dfb1..9a3685b212b43 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/model/new-customer-address.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/model/new-customer-address.js @@ -22,7 +22,7 @@ define([], function () { return { email: addressData.email, countryId: addressData['country_id'] || addressData.countryId || window.checkoutConfig.defaultCountryId, - regionId: regionId, + regionId: regionId || addressData.regionId, regionCode: (addressData.region) ? addressData.region.region_code : null, region: (addressData.region) ? addressData.region.region : null, customerId: addressData.customer_id, @@ -30,7 +30,7 @@ define([], function () { company: addressData.company, telephone: addressData.telephone, fax: addressData.fax, - postcode: addressData.postcode ? addressData.postcode : window.checkoutConfig.defaultPostcode, + postcode: addressData.postcode ? addressData.postcode : window.checkoutConfig.defaultPostcode || undefined, city: addressData.city, firstname: addressData.firstname, lastname: addressData.lastname, diff --git a/app/code/Magento/Checkout/view/frontend/web/js/model/payment/method-group.js b/app/code/Magento/Checkout/view/frontend/web/js/model/payment/method-group.js new file mode 100644 index 0000000000000..4236a215d7359 --- /dev/null +++ b/app/code/Magento/Checkout/view/frontend/web/js/model/payment/method-group.js @@ -0,0 +1,31 @@ +/** + * Copyright © 2016 Magento. All rights reserved. + * See COPYING.txt for license details. + */ + +define([ + 'uiElement', + 'mage/translate' +], function (Element, $t) { + 'use strict'; + + var DEFAULT_GROUP_ALIAS = 'default'; + + return Element.extend({ + defaults: { + alias: DEFAULT_GROUP_ALIAS, + title: $t('Payment Method'), + sortOrder: 100, + displayArea: 'payment-methods-items-${ $.alias }' + }, + + /** + * Checks if group instance is default + * + * @returns {Boolean} + */ + isDefault: function () { + return this.alias === DEFAULT_GROUP_ALIAS; + } + }); +}); diff --git a/app/code/Magento/Checkout/view/frontend/web/js/model/shipping-rates-validator.js b/app/code/Magento/Checkout/view/frontend/web/js/model/shipping-rates-validator.js index 420d50b83478a..c49960ecfb91d 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/model/shipping-rates-validator.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/model/shipping-rates-validator.js @@ -176,7 +176,7 @@ define( address; if (this.validateAddressData(addressFlat)) { - addressFlat = $.extend(true, {}, quote.shippingAddress(), addressFlat); + addressFlat = uiRegistry.get('checkoutProvider').shippingAddress; address = addressConverter.formAddressDataToQuoteAddress(addressFlat); selectShippingAddress(address); } diff --git a/app/code/Magento/Checkout/view/frontend/web/js/view/payment/list.js b/app/code/Magento/Checkout/view/frontend/web/js/view/payment/list.js index 18f6b8479c229..918d305ee031b 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/view/payment/list.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/view/payment/list.js @@ -10,14 +10,22 @@ define([ 'Magento_Checkout/js/model/payment/method-list', 'Magento_Checkout/js/model/payment/renderer-list', 'uiLayout', - 'Magento_Checkout/js/model/checkout-data-resolver' -], function (_, ko, utils, Component, paymentMethods, rendererList, layout, checkoutDataResolver) { + 'Magento_Checkout/js/model/checkout-data-resolver', + 'mage/translate', + 'uiRegistry' +], function (_, ko, utils, Component, paymentMethods, rendererList, layout, checkoutDataResolver, $t, registry) { 'use strict'; return Component.extend({ defaults: { template: 'Magento_Checkout/payment-methods/list', - visible: paymentMethods().length > 0 + visible: paymentMethods().length > 0, + configDefaultGroup: { + name: 'methodGroup', + component: 'Magento_Checkout/js/model/payment/method-group' + }, + paymentGroupsList: [], + defaultGroupTitle: $t('Select a new payment method') }, /** @@ -26,7 +34,7 @@ define([ * @returns {Component} Chainable. */ initialize: function () { - this._super().initChildren(); + this._super().initDefaulGroup().initChildren(); paymentMethods.subscribe( function (changes) { checkoutDataResolver.resolvePaymentMethod(); @@ -47,6 +55,27 @@ define([ return this; }, + /** @inheritdoc */ + initObservable: function () { + this._super(). + observe(['paymentGroupsList']); + + return this; + }, + + /** + * Creates default group + * + * @returns {Component} Chainable. + */ + initDefaulGroup: function() { + layout([ + this.configDefaultGroup + ]); + + return this; + }, + /** * Create renders for child payment methods. * @@ -77,7 +106,7 @@ define([ rendererTemplate = { parent: '${ $.$data.parentName }', name: '${ $.$data.name }', - displayArea: 'payment-method-items', + displayArea: payment.displayArea, component: payment.component }; rendererComponent = utils.template(rendererTemplate, templateData); @@ -95,49 +124,105 @@ define([ * @param {Object} paymentMethodData */ createRenderer: function (paymentMethodData) { - var isRendererForMethod = false; + var isRendererForMethod = false, + currentGroup; + + registry.get(this.configDefaultGroup.name, function (defaultGroup) { + _.each(rendererList(), function (renderer) { - _.find(rendererList(), function (renderer) { + if (renderer.hasOwnProperty('typeComparatorCallback') && + typeof renderer.typeComparatorCallback == 'function' + ) { + isRendererForMethod = renderer.typeComparatorCallback(renderer.type, paymentMethodData.method); + } else { + isRendererForMethod = renderer.type === paymentMethodData.method; + } - if (renderer.hasOwnProperty('typeComparatorCallback') && - typeof renderer.typeComparatorCallback == 'function' - ) { - isRendererForMethod = renderer.typeComparatorCallback(renderer.type, paymentMethodData.method); - } else { - isRendererForMethod = renderer.type === paymentMethodData.method; - } + if (isRendererForMethod) { + currentGroup = renderer.group ? renderer.group : defaultGroup; - if (isRendererForMethod) { - layout( - [ + this.collectPaymentGroups(currentGroup); + + layout([ this.createComponent( { config: renderer.config, component: renderer.component, name: renderer.type, method: paymentMethodData.method, - item: paymentMethodData + item: paymentMethodData, + displayArea: currentGroup.displayArea } - ) - ] - ); - } + )]); + } + }.bind(this)); }.bind(this)); }, + /** + * Collects unique groups of available payment methods + * + * @param {Object} group + */ + collectPaymentGroups: function (group) { + var groupsList = this.paymentGroupsList(), + isGroupExists = _.some(groupsList, function (existsGroup) { + return existsGroup.alias === group.alias; + }); + + if (!isGroupExists) { + groupsList.push(group); + groupsList = _.sortBy(groupsList, function (existsGroup) { + return existsGroup.sortOrder; + }); + this.paymentGroupsList(groupsList); + } + }, + + /** + * Returns payment group title + * + * @param {Object} group + * @returns {String} + */ + getGroupTitle: function (group) { + var title = group().title; + + if (group().isDefault() && this.paymentGroupsList().length > 1) { + title = this.defaultGroupTitle; + } + + return title + ':'; + }, + + /** + * Checks if at least one payment method available + * + * @returns {String} + */ + isPaymentMethodsAvailable: function () { + return _.some(this.paymentGroupsList(), function (group) { + return this.getRegion(group.displayArea)().length; + }, this); + }, + /** * Remove view renderer. * * @param {String} paymentMethodCode */ removeRenderer: function (paymentMethodCode) { - var items = this.getRegion('payment-method-items'); + var items; + + _.each(this.paymentGroupsList(), function (group) { + items = this.getRegion(group.displayArea); - _.find(items(), function (value) { - if (value.item.method.indexOf(paymentMethodCode) === 0) { - value.disposeSubscriptions(); - value.destroy(); - } + _.find(items(), function (value) { + if (value.item.method.indexOf(paymentMethodCode) === 0) { + value.disposeSubscriptions(); + value.destroy(); + } + }); }, this); } }); diff --git a/app/code/Magento/Checkout/view/frontend/web/js/view/shipping.js b/app/code/Magento/Checkout/view/frontend/web/js/view/shipping.js index ddb62ceb37b37..8910e41731d11 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/view/shipping.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/view/shipping.js @@ -179,7 +179,7 @@ define( newShippingAddress; this.source.set('params.invalid', false); - this.source.trigger('shippingAddress.data.validate'); + this.triggerShippingDataValidateEvent(); if (!this.source.get('params.invalid')) { addressData = this.source.get('shippingAddress'); @@ -254,12 +254,7 @@ define( if (this.isFormInline) { this.source.set('params.invalid', false); - this.source.trigger('shippingAddress.data.validate'); - - if (this.source.get('shippingAddress.custom_attributes')) { - this.source.trigger('shippingAddress.custom_attributes.data.validate'); - } - + this.triggerShippingDataValidateEvent(); if (emailValidationResult && this.source.get('params.invalid') || !quote.shippingMethod().method_code || @@ -304,6 +299,18 @@ define( } return true; + }, + + /** + * Trigger Shipping data Validate Event. + * + * @return {void} + */ + triggerShippingDataValidateEvent: function () { + this.source.trigger('shippingAddress.data.validate'); + if (this.source.get('shippingAddress.custom_attributes')) { + this.source.trigger('shippingAddress.custom_attributes.data.validate'); + } } }); } diff --git a/app/code/Magento/Checkout/view/frontend/web/template/billing-address/details.html b/app/code/Magento/Checkout/view/frontend/web/template/billing-address/details.html index f45115181fe4c..da625c51d4b77 100644 --- a/app/code/Magento/Checkout/view/frontend/web/template/billing-address/details.html +++ b/app/code/Magento/Checkout/view/frontend/web/template/billing-address/details.html @@ -13,6 +13,11 @@
+ + + + + +
@@ -40,6 +40,13 @@ $hints = [

{{$state.current.header}}

+
+ {{validateUrl.failed}} +
+