diff --git a/CHANGELOG.md b/CHANGELOG.md index 8c31ec98beae2..ea40937204ddf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,111 @@ +2.1.10-dev +============= +* GitHub issues: + * [#6718](https://github.com/magento/magento2/issues/6718) -- Custom composer modules break Component Manager (fixed in [#9692](https://github.com/magento/magento2/pull/9692)) + * [#4170](https://github.com/magento/magento2/issues/4170) -- Magento2 Mini Cart Items Issue (fixed in [#10050](https://github.com/magento/magento2/pull/10050)) + * [#5377](https://github.com/magento/magento2/issues/5377) -- "No items" in minicart in 2.1 (fixed in [#10050](https://github.com/magento/magento2/pull/10050)) + * [#6999](https://github.com/magento/magento2/issues/6999) -- Performance: getConfigurableAttributes cache is broken (fixed in [#9809](https://github.com/magento/magento2/pull/9809)) + * [#6882](https://github.com/magento/magento2/issues/6882) -- Minicart empty if FPC disabled in Magneto 2.1.1 (fixed in [#10050](https://github.com/magento/magento2/pull/10050)) + * [#4731](https://github.com/magento/magento2/issues/4731) -- developer mode throws an exception, but production mode is good (fixed in [#9718](https://github.com/magento/magento2/pull/9718)) + * [#7827](https://github.com/magento/magento2/issues/7827) -- DOM schema validation error (fixed in [#9718](https://github.com/magento/magento2/pull/9718)) + * [#3872](https://github.com/magento/magento2/issues/3872) -- Slash as category URL suffix gives 404 error on all category pages (fixed in [#10164](https://github.com/magento/magento2/pull/10164)) + * [#4660](https://github.com/magento/magento2/issues/4660) -- Multiple URLs causes duplicated content (fixed in [#10164](https://github.com/magento/magento2/pull/10164)) + * [#4876](https://github.com/magento/magento2/issues/4876) -- Product URL Suffix "/" results in 404 error (fixed in [#10164](https://github.com/magento/magento2/pull/10164)) + * [#8264](https://github.com/magento/magento2/issues/8264) -- Custom URL Rewrite where the request path ends with a forward slash is not matched (fixed in [#10164](https://github.com/magento/magento2/pull/10164)) + * [#1980](https://github.com/magento/magento2/issues/1980) -- Product attributes' labels are not translated on product edit page (fixed in [#10184](https://github.com/magento/magento2/pull/10184)) + * [#6818](https://github.com/magento/magento2/issues/6818) -- PageCache gives error "Uncaught TypeError: element.prop is not a function" when there is an iframe (fixed in [#10218](https://github.com/magento/magento2/pull/10218)) + * [#6175](https://github.com/magento/magento2/issues/6175) -- Unable to generate unsecure URL if current URL is secure (fixed in [#10188](https://github.com/magento/magento2/pull/10188)) + * [#5651](https://github.com/magento/magento2/issues/5651) -- Purchase date on admin screen is always *:07:00 (fixed in [#10260](https://github.com/magento/magento2/pull/10260)) + * [#9619](https://github.com/magento/magento2/issues/9619) -- Impossible to create Text Swatch 0 (Zero) (fixed in [#10282](https://github.com/magento/magento2/pull/10282)) + * [#10266](https://github.com/magento/magento2/issues/10266) -- Product Attributes - Size 0 (fixed in [#10282](https://github.com/magento/magento2/pull/10282)) +* GitHub pull requests: + * [#9692](https://github.com/magento/magento2/pull/9692) -- Backport of MAGETWO-59256 for 2.1: Custom composer modules break Component Manager #6718 (by @JTimNolan) + * [#9809](https://github.com/magento/magento2/pull/9809) -- Fix issue #6999: Configurable attribute cache was never hit (by @thlassche) + * [#10050](https://github.com/magento/magento2/pull/10050) -- [2.1-backport] Customer-data is not updates after login when full page cache disabled (by @ihor-sviziev) + * [#10075](https://github.com/magento/magento2/pull/10075) -- Fix date format in adminhtml order grid (by @alessandroniciforo) + * [#9718](https://github.com/magento/magento2/pull/9718) -- ported fix from 237e54d - MAGETWO-55684: Fix XSD schema (by @pixelhed) + * [#10159](https://github.com/magento/magento2/pull/10159) -- Fix labels tranlation on category page (by @fernandofauth) + * [#10164](https://github.com/magento/magento2/pull/10164) -- [2.1-backport] Fix trailing slash used in url rewrites (by @ihor-sviziev) + * [#10184](https://github.com/magento/magento2/pull/10184) -- Fixed: Product attributes labels are not translated on product edit page (by @fernandofauth) + * [#10211](https://github.com/magento/magento2/pull/10211) -- Add clarification about deprecated methods in Abstract model (by @ihor-sviziev) + * [#10218](https://github.com/magento/magento2/pull/10218) -- Backport 1b55a64 to 2.1 - Fixes #6818 (by @ajpevers) + * [#10188](https://github.com/magento/magento2/pull/10188) -- magento/magento2:#6175 Fixed Unable to generate unsecure URL if current URL is secure (by @arshadpkm) + * [#10260](https://github.com/magento/magento2/pull/10260) -- Fix order date format in Orders Grid (by @ihor-sviziev) + * [#10282](https://github.com/magento/magento2/pull/10282) -- 2.1 - Allow to use text swatch 0 (by @ihor-sviziev) + +2.1.8 +============= +* GitHub issues: + * [#5627](https://github.com/magento/magento2/issues/5627) -- main.CRITICAL: Broken reference (Magento CE v2.1) (fixed in [#9092](https://github.com/magento/magento2/pull/9092)) + * [#4232](https://github.com/magento/magento2/issues/4232) -- UTF-8 special character issue in widgets (fixed in [#9333](https://github.com/magento/magento2/pull/9333)) + * [#4427](https://github.com/magento/magento2/issues/4427) -- SEO/HEAD - Meta title is null when breadcrumb section is removed via XML (fixed in [#9324](https://github.com/magento/magento2/pull/9324)) + * [#4868](https://github.com/magento/magento2/issues/4868) -- Checkout page very large and quite slow. (fixed in [#9364](https://github.com/magento/magento2/pull/9364) and [#9365](https://github.com/magento/magento2/pull/9365)) + * [#6997](https://github.com/magento/magento2/issues/6997) -- Remove unneeded region definitions from the /checkout page. (fixed in [#9364](https://github.com/magento/magento2/pull/9364)) + * [#6451](https://github.com/magento/magento2/issues/6451) -- Login Popup broken on iPad portrait (fixed in [#9396](https://github.com/magento/magento2/pull/9396)) + * [#7497](https://github.com/magento/magento2/issues/7497) -- Shipping method radios become disabled when checkout page refreshed (fixed in [#9485](https://github.com/magento/magento2/pull/9485)) + * [#4828](https://github.com/magento/magento2/issues/4828) -- Show/hide Editor not working sometimes (fixed in [#9499](https://github.com/magento/magento2/pull/9499)) + * [#6222](https://github.com/magento/magento2/issues/6222) -- [2.1.0] Sometimes WYSIWYG editor does not show. (fixed in [#9499](https://github.com/magento/magento2/pull/9499)) + * [#6815](https://github.com/magento/magento2/issues/6815) -- wysiwyg Editor problem (fixed in [#9499](https://github.com/magento/magento2/pull/9499)) + * [#6866](https://github.com/magento/magento2/issues/6866) -- Products in wishlist show $0.00 price (fixed in [#9571](https://github.com/magento/magento2/pull/9571)) + * [#8607](https://github.com/magento/magento2/issues/8607) -- Interface constructor if present will break Magento compilation (fixed in [#9524](https://github.com/magento/magento2/pull/9524)) + * [#5352](https://github.com/magento/magento2/issues/5352) -- Magento 2.1 email logo image function does not work (fixed in [#9590](https://github.com/magento/magento2/pull/9590)) + * [#5916](https://github.com/magento/magento2/issues/5916) -- Magento 2.1 transactional email uploaded logo not showing in admin. (fixed in [#9590](https://github.com/magento/magento2/pull/9590)) + * [#5633](https://github.com/magento/magento2/issues/5633) -- Magento 2.1 fails to load email_logo.png (fixed in [#9590](https://github.com/magento/magento2/pull/9590)) + * [#6420](https://github.com/magento/magento2/issues/6420) -- New order email header logo not showing correctly v2.1 (fixed in [#9590](https://github.com/magento/magento2/pull/9590)) + * [#6275](https://github.com/magento/magento2/issues/6275) -- Transactional Email Logo Not Getting Updated (fixed in [#9590](https://github.com/magento/magento2/pull/9590)) + * [#6502](https://github.com/magento/magento2/issues/6502) -- Can't save Logo Image to Transactional Emails (fixed in [#9590](https://github.com/magento/magento2/pull/9590)) + * [#7985](https://github.com/magento/magento2/issues/7985) -- Logo email (fixed in [#9590](https://github.com/magento/magento2/pull/9590)) + * [#7853](https://github.com/magento/magento2/issues/7853) -- Transactional email logo wrong location (fixed in [#9590](https://github.com/magento/magento2/pull/9590)) + * [#8728](https://github.com/magento/magento2/issues/8728) -- Transactional Emails Logo (fixed in [#9590](https://github.com/magento/magento2/pull/9590)) + * [#8626](https://github.com/magento/magento2/issues/8626) -- Magento 2.1.2 - 2.1.4 email logo image function does not work (fixed in [#9590](https://github.com/magento/magento2/pull/9590)) + * [#8489](https://github.com/magento/magento2/issues/8489) -- Magento 2.1.4 - Asking Why Email Logo Never been fixed on all Magento releases (fixed in [#9590](https://github.com/magento/magento2/pull/9590)) + * [#8961](https://github.com/magento/magento2/issues/8961) -- email logo error (fixed in [#9590](https://github.com/magento/magento2/pull/9590)) + * [#9118](https://github.com/magento/magento2/issues/9118) -- transactional email (fixed in [#9590](https://github.com/magento/magento2/pull/9590)) + * [#9428](https://github.com/magento/magento2/issues/9428) -- 2.1.6 Receive 500 error when want export Low Stock Report. (fixed in [#9487](https://github.com/magento/magento2/pull/9487)) + * [#3640](https://github.com/magento/magento2/issues/3640) -- CartItemInterface cannot add extension_attributes (fixed in [#9647](https://github.com/magento/magento2/pull/9647)) + * [#9646](https://github.com/magento/magento2/issues/9646) -- CartTotalRepository cannot handle extension attributes in quote addresses (fixed in [#9647](https://github.com/magento/magento2/pull/9647)) + * [#700](https://github.com/magento/magento2/issues/700) -- suggestion: revise WYSIWYG editor (fixed in [#9655](https://github.com/magento/magento2/pull/9655)) + * [#2312](https://github.com/magento/magento2/issues/2312) -- Media Browser loses PNG transparency for the thumbnails (fixed in [#9662](https://github.com/magento/magento2/pull/9662)) + * [#5401](https://github.com/magento/magento2/issues/5401) -- Transparency of .png image gone (fixed in [#9662](https://github.com/magento/magento2/pull/9662)) + * [#7149](https://github.com/magento/magento2/issues/7149) -- Admin WYSIWYG upgrade to latest Tiny MCE 4.* (fixed in [#9655](https://github.com/magento/magento2/pull/9655)) + * [#8874](https://github.com/magento/magento2/issues/8874) -- tinyMCE is disabled (fixed in [#9655](https://github.com/magento/magento2/pull/9655)) + * [#9518](https://github.com/magento/magento2/issues/9518) -- Chrome version 58 causes problems with selections in the tinymce editor (fixed in [#9655](https://github.com/magento/magento2/pull/9655)) + * [#7959](https://github.com/magento/magento2/issues/7959) -- JS error on product page Cannot read property 'oldPrice' of undefined (fixed in [#9776](https://github.com/magento/magento2/pull/9776)) + * [#9679](https://github.com/magento/magento2/issues/9679) -- Translation for layered navigation attribute option not working (fixed in [#9704](https://github.com/magento/magento2/pull/9704)) + * [#6746](https://github.com/magento/magento2/issues/6746) -- Magento 2.1.1 Problem with change currency (fixed in [#9841](https://github.com/magento/magento2/pull/9841)) + * [#9562](https://github.com/magento/magento2/issues/9562) -- ItemZone on product detail is not set correctly when chaning products via related/upsell products list (fixed in [#9841](https://github.com/magento/magento2/pull/9841)) + * [#7279](https://github.com/magento/magento2/issues/7279) -- Bill-to Name and Ship-to Name trancated to 20 characters in backend (fixed in [#10011](https://github.com/magento/magento2/pull/10011)) + * [#9139](https://github.com/magento/magento2/issues/9139) -- Unable to set negative product's quantity (fixed in [#9770](https://github.com/magento/magento2/pull/9770)) +* GitHub pull requests: + * [#9092](https://github.com/magento/magento2/pull/9092) -- Issue #5627: main.CRITICAL: Broken reference (Magento CE v2.1) (by @malachy-mcconnnell) + * [#8880](https://github.com/magento/magento2/pull/8880) -- Update design_config_form.xml (by @WaPoNe) + * [#9332](https://github.com/magento/magento2/pull/9332) -- Backport of MAGETWO-54401 for Magento 2.1 - Unable to click "Insert image" twice (by @hostep) + * [#9333](https://github.com/magento/magento2/pull/9333) -- Backport of MAGETWO-52850 for Magento 2.1 - [GitHub] UTF-8 special character issue in widgets #4232 (by @hostep) + * [#9324](https://github.com/magento/magento2/pull/9324) -- Page meta title fix in case breadcrumb section is removed via XML (by @latenights) + * [#9364](https://github.com/magento/magento2/pull/9364) -- Backport of MAGETWO-59685 for Magento 2.1 - Checkout pages very slow … (by @hostep) + * [#9376](https://github.com/magento/magento2/pull/9376) -- Fix a bug resulting in incorrect offsets with dynamic row drag-n-drop functionality (by @navarr) + * [#9365](https://github.com/magento/magento2/pull/9365) -- Backport of MAGETWO-60351 for Magento 2.1 - Unnecessary disabled paym… (by @hostep) + * [#9396](https://github.com/magento/magento2/pull/9396) -- [2.1-backport] Fix Login Popup broken on iPad portrait (by @ihor-sviziev) + * [#9485](https://github.com/magento/magento2/pull/9485) -- Shipping method radios become disabled when checkout page refreshed (by @rachkulik) + * [#9500](https://github.com/magento/magento2/pull/9500) -- Backport of MAGETWO-54798 For Magento 2.1: One page checkout - Street Address should highlight red when data is missing (by @hostep) + * [#9499](https://github.com/magento/magento2/pull/9499) -- Backport of MAGETWO-57675 for Magento 2.1: WYSIWYG editor does not show. #6222 #4828 #6815 (by @hostep) + * [#9571](https://github.com/magento/magento2/pull/9571) -- Backport of MAGETWO-59512 for Magento 2.1: Products in wishlist show $0.00 price #6866 (by @hostep) + * [#9524](https://github.com/magento/magento2/pull/9524) -- magento/magento2#8607: Interface constructor if present will break Magento compilation (by @LoganayakiK) + * [#9590](https://github.com/magento/magento2/pull/9590) -- Backport of MAGETWO-53010 for Magento 2.1: Saving a custom transactional email logo, failed. (by @hostep) + * [#9487](https://github.com/magento/magento2/pull/9487) -- magento/magento2#9428: 2.1.6 Fixed 500 error while getting Low Stock Reports (by @mikebox) + * [#9653](https://github.com/magento/magento2/pull/9653) -- Allow X-Forwarded-For to have multiple values [2.1 backport] (by @kassner) + * [#9647](https://github.com/magento/magento2/pull/9647) -- Fix for #9646 (by @ekuusela) + * [#9662](https://github.com/magento/magento2/pull/9662) -- Keep transparency when resizing images [2.1 backport] (by @kassner) + * [#9661](https://github.com/magento/magento2/pull/9661) -- Add configurations for change email templates [2.1 backport] (by @kassner) + * [#9660](https://github.com/magento/magento2/pull/9660) -- Do not di:compile tests/ folder [2.1 backport] (by @kassner) + * [#9655](https://github.com/magento/magento2/pull/9655) -- Backport of MAGETWO-69152: Removed workaround for old Webkit bug in t… (by @hostep) + * [#9776](https://github.com/magento/magento2/pull/9776) -- #7959 - Fix for JS error on Swatch Renderer for undefined oldPrice (by @dreamworkers) + * [#9601](https://github.com/magento/magento2/pull/9601) -- Do not hardcode product link types [2.1 backport] (by @kassner) + * [#9704](https://github.com/magento/magento2/pull/9704) -- Fixes regression bug introduced in Magento 2.1.6 where the layered navigation options are sometimes being cached using the wrong store id. (by @hostep) + * [#9841](https://github.com/magento/magento2/pull/9841) -- Backport of MAGETWO-59089 for Magento 2.1: Magento 2.1.1 Problem with change currency (by @hostep) + * [#10011](https://github.com/magento/magento2/pull/10011) -- Backport 7279 to 2.1 (by @lazyguru) + * [#9770](https://github.com/magento/magento2/pull/9770) -- #9139 Unable to set negative product's quantity fixes commit. (by @poongud) + 2.1.1 ============= To get detailed information about changes in Magento 2.1.1, please visit [Magento Community Edition (CE) Release Notes](http://devdocs.magento.com/guides/v2.1/release-notes/ReleaseNotes2.1.1CE.html "Magento Community Edition (CE) Release Notes") diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0634653275a9a..2839ac5ee9d32 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -29,3 +29,8 @@ If you are a new GitHub user, we recommend that you create your own [free github 3. Create and test your work. 4. Fork the Magento 2 repository according to [Fork a repository instructions](http://devdocs.magento.com/guides/v2.0/contributor-guide/contributing.html#fork) and when you are ready to send us a pull request – follow [Create a pull request instructions](http://devdocs.magento.com/guides/v2.0/contributor-guide/contributing.html#pull_request). 5. Once your contribution is received, Magento 2 development team will review the contribution and collaborate with you as needed to improve the quality of the contribution. + +## Code of Conduct + +Please note that this project is released with a Contributor Code of Conduct. We expect you to agree to its terms when participating in this project. +The full text is available in the repository [Wiki](https://github.com/magento/magento2/wiki/Magento-Code-of-Conduct). diff --git a/app/code/Magento/AdvancedPricingImportExport/Model/Import/AdvancedPricing.php b/app/code/Magento/AdvancedPricingImportExport/Model/Import/AdvancedPricing.php index 821f9619b1a79..ffe49d9c151b6 100644 --- a/app/code/Magento/AdvancedPricingImportExport/Model/Import/AdvancedPricing.php +++ b/app/code/Magento/AdvancedPricingImportExport/Model/Import/AdvancedPricing.php @@ -542,19 +542,24 @@ protected function retrieveOldSkus() */ protected function processCountExistingPrices($prices, $table) { + $oldSkus = $this->retrieveOldSkus(); + $existProductIds = array_intersect_key($oldSkus, $prices); + if (!count($existProductIds)) { + return $this; + } + $tableName = $this->_resourceFactory->create()->getTable($table); $productEntityLinkField = $this->getProductEntityLinkField(); $existingPrices = $this->_connection->fetchAssoc( $this->_connection->select()->from( $tableName, ['value_id', $productEntityLinkField, 'all_groups', 'customer_group_id'] - ) + )->where($productEntityLinkField . ' IN (?)', $existProductIds) ); - $oldSkus = $this->retrieveOldSkus(); foreach ($existingPrices as $existingPrice) { - foreach ($oldSkus as $sku => $productId) { - if ($existingPrice[$productEntityLinkField] == $productId && isset($prices[$sku])) { - $this->incrementCounterUpdated($prices[$sku], $existingPrice); + foreach ($prices as $sku => $skuPrices) { + if (isset($oldSkus[$sku]) && $existingPrice[$productEntityLinkField] == $oldSkus[$sku]) { + $this->incrementCounterUpdated($skuPrices, $existingPrice); } } } diff --git a/app/code/Magento/AdvancedPricingImportExport/composer.json b/app/code/Magento/AdvancedPricingImportExport/composer.json index b9228eb4803e0..b52a4b8c3e532 100644 --- a/app/code/Magento/AdvancedPricingImportExport/composer.json +++ b/app/code/Magento/AdvancedPricingImportExport/composer.json @@ -13,7 +13,7 @@ "magento/framework": "100.1.*" }, "type": "magento2-module", - "version": "100.1.2", + "version": "100.1.3", "license": [ "OSL-3.0", "AFL-3.0" diff --git a/app/code/Magento/Backend/view/adminhtml/ui_component/design_config_form.xml b/app/code/Magento/Backend/view/adminhtml/ui_component/design_config_form.xml index deb3a05f352c3..44a5fe307eff1 100644 --- a/app/code/Magento/Backend/view/adminhtml/ui_component/design_config_form.xml +++ b/app/code/Magento/Backend/view/adminhtml/ui_component/design_config_form.xml @@ -43,7 +43,7 @@ dynamicRows record Remove - Add New User Agent Rule + Add New User Agent Rule false false @@ -67,7 +67,7 @@ text search false - Search String + Search String false @@ -80,7 +80,7 @@ text value false - Theme Name + Theme Name false @@ -91,7 +91,7 @@ actionDelete text false - Actions + Actions 50 data-grid-actions-cell Magento_Backend/dynamic-rows/cells/action-delete 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 3c53eaaf1444e..dda3bb7831f42 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/Block/Product/View/Attributes.php b/app/code/Magento/Catalog/Block/Product/View/Attributes.php index 6960ff0b8e5a7..2ec831083f675 100644 --- a/app/code/Magento/Catalog/Block/Product/View/Attributes.php +++ b/app/code/Magento/Catalog/Block/Product/View/Attributes.php @@ -15,6 +15,9 @@ use Magento\Framework\Phrase; use Magento\Framework\Pricing\PriceCurrencyInterface; +/** + * Product attributes block. + */ class Attributes extends \Magento\Framework\View\Element\Template { /** @@ -59,6 +62,7 @@ public function getProduct() if (!$this->_product) { $this->_product = $this->_coreRegistry->registry('product'); } + return $this->_product; } @@ -96,6 +100,7 @@ public function getAdditionalData(array $excludeAttr = []) } } } + return $data; } } diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Category.php b/app/code/Magento/Catalog/Controller/Adminhtml/Category.php index 4e376463260d7..464498edcd787 100644 --- a/app/code/Magento/Catalog/Controller/Adminhtml/Category.php +++ b/app/code/Magento/Catalog/Controller/Adminhtml/Category.php @@ -31,16 +31,16 @@ abstract class Category extends \Magento\Backend\App\Action */ protected function _initCategory($getRootInstead = false) { - $categoryId = (int)$this->getRequest()->getParam('id', false); + $categoryId = $this->resolveCategoryId(); $storeId = (int)$this->getRequest()->getParam('store'); - $category = $this->_objectManager->create('Magento\Catalog\Model\Category'); + $category = $this->_objectManager->create(\Magento\Catalog\Model\Category::class); $category->setStoreId($storeId); if ($categoryId) { $category->load($categoryId); if ($storeId) { $rootId = $this->_objectManager->get( - 'Magento\Store\Model\StoreManagerInterface' + \Magento\Store\Model\StoreManagerInterface::class )->getStore( $storeId )->getRootCategoryId(); @@ -55,13 +55,25 @@ protected function _initCategory($getRootInstead = false) } } - $this->_objectManager->get('Magento\Framework\Registry')->register('category', $category); - $this->_objectManager->get('Magento\Framework\Registry')->register('current_category', $category); - $this->_objectManager->get('Magento\Cms\Model\Wysiwyg\Config') + $this->_objectManager->get(\Magento\Framework\Registry::class)->register('category', $category); + $this->_objectManager->get(\Magento\Framework\Registry::class)->register('current_category', $category); + $this->_objectManager->get(\Magento\Cms\Model\Wysiwyg\Config::class) ->setStoreId($this->getRequest()->getParam('store')); return $category; } + /** + * Resolve Category Id (from get or from post). + * + * @return int + */ + private function resolveCategoryId() + { + $categoryId = (int)$this->getRequest()->getParam('id', false); + + return $categoryId ?: (int)$this->getRequest()->getParam('entity_id', false); + } + /** * Build response for ajax request * @@ -79,7 +91,7 @@ protected function ajaxRequestResponse($category, $resultPage) if (empty($breadcrumbsPath)) { // but if no category, and it is deleted - prepare breadcrumbs from path, saved in session $breadcrumbsPath = $this->_objectManager->get( - 'Magento\Backend\Model\Auth\Session' + \Magento\Backend\Model\Auth\Session::class )->getDeletedPath( true ); @@ -107,7 +119,7 @@ protected function ajaxRequestResponse($category, $resultPage) ['response' => $eventResponse, 'controller' => $this] ); /** @var \Magento\Framework\Controller\Result\Json $resultJson */ - $resultJson = $this->_objectManager->get('Magento\Framework\Controller\Result\Json'); + $resultJson = $this->_objectManager->get(\Magento\Framework\Controller\Result\Json::class); $resultJson->setHeader('Content-type', 'application/json', true); $resultJson->setData($eventResponse->getData()); return $resultJson; 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 e016e0a2ee84f..689569aefd94c 100644 --- a/app/code/Magento/Catalog/Controller/Adminhtml/Category/Image/Upload.php +++ b/app/code/Magento/Catalog/Controller/Adminhtml/Category/Image/Upload.php @@ -44,14 +44,15 @@ protected function _isAllowed() } /** - * Upload file controller action + * Upload file controller action. * * @return \Magento\Framework\Controller\ResultInterface */ 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 39154f4c24ff1..256e5b9760f1a 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,13 @@ class Save extends \Magento\Catalog\Controller\Adminhtml\Category */ private $storeManager; + /** + * Config instance holder. + * + * @var \Magento\Eav\Model\Config + */ + private $eavConfig; + /** * Constructor * @@ -72,8 +80,9 @@ public function __construct( } /** - * Filter category data + * Filter category data. * + * @deprecated * @param array $rawData * @return array */ @@ -126,7 +135,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,21 +257,48 @@ 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) + public function imagePreprocessing(array $data) { - if (empty($data['image'])) { - unset($data['image']); - $data['image']['delete'] = true; + $emptyImageAttributes = $this->getEmptyImageAttributes($data); + $attributeCodes = array_keys($emptyImageAttributes); + foreach ($attributeCodes as $attributeCode) { + $data[$attributeCode] = false; } + return $data; } + /** + * Get image attributes without value. + * + * @param array $data + * @return array + */ + private function getEmptyImageAttributes(array $data) + { + $result = []; + $entityType = $this->getConfig()->getEntityType(CategoryAttributeInterface::ENTITY_TYPE_CODE); + foreach ($entityType->getAttributeCollection() as $attribute) { + $attributeCode = $attribute->getAttributeCode(); + $backendModel = $attribute->getBackend(); + if (isset($data[$attributeCode])) { + continue; + } + if (!$backendModel instanceof \Magento\Catalog\Model\Category\Attribute\Backend\Image) { + continue; + } + $result[$attributeCode] = $attribute; + } + + return $result; + } + /** * Converting inputs from string to boolean * @@ -346,4 +382,20 @@ protected function getRedirectParams($isNewCategory, $hasError, $categoryId, $pa } return ['path' => $path, 'params' => $params]; } + + /** + * Get Config instance. + * + * @return \Magento\Eav\Model\Config + */ + private function getConfig() + { + if (null === $this->eavConfig) { + $this->eavConfig = \Magento\Framework\App\ObjectManager::getInstance()->get( + \Magento\Eav\Model\Config::class + ); + } + + return $this->eavConfig; + } } diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Attribute/Validate.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Attribute/Validate.php index cb4d370cf51e8..94fad64f5ed96 100644 --- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Attribute/Validate.php +++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Attribute/Validate.php @@ -68,7 +68,7 @@ public function execute() $attributeCode = $attributeCode ?: $this->generateCode($frontendLabel[0]); $attributeId = $this->getRequest()->getParam('attribute_id'); $attribute = $this->_objectManager->create( - 'Magento\Catalog\Model\ResourceModel\Eav\Attribute' + \Magento\Catalog\Model\ResourceModel\Eav\Attribute::class )->loadByCode( $this->_entityTypeId, $attributeCode @@ -87,10 +87,10 @@ public function execute() if ($this->getRequest()->has('new_attribute_set_name')) { $setName = $this->getRequest()->getParam('new_attribute_set_name'); /** @var $attributeSet \Magento\Eav\Model\Entity\Attribute\Set */ - $attributeSet = $this->_objectManager->create('Magento\Eav\Model\Entity\Attribute\Set'); + $attributeSet = $this->_objectManager->create(\Magento\Eav\Model\Entity\Attribute\Set::class); $attributeSet->setEntityTypeId($this->_entityTypeId)->load($setName, 'attribute_set_name'); if ($attributeSet->getId()) { - $setName = $this->_objectManager->get('Magento\Framework\Escaper')->escapeHtml($setName); + $setName = $this->_objectManager->get(\Magento\Framework\Escaper::class)->escapeHtml($setName); $this->messageManager->addError(__('An attribute set named \'%1\' already exists.', $setName)); $layout = $this->layoutFactory->create(); @@ -149,10 +149,16 @@ private function setMessageToResponse($response, $messages) /** * @param DataObject $response * @param array|null $options + * + * @return void */ private function checkUniqueOption(DataObject $response, array $options = null) { - if (is_array($options) && !$this->isUniqueAdminValues($options['value'], $options['delete'])) { + if (is_array($options) + && !empty($options['value']) + && !empty($options['delete']) + && !$this->isUniqueAdminValues($options['value'], $options['delete']) + ) { $this->setMessageToResponse($response, [__('The value of Admin must be unique.')]); $response->setError(true); } 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 6aca274680999..52addeb6d6280 100644 --- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Initialization/Helper.php +++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Initialization/Helper.php @@ -74,11 +74,6 @@ class Helper * @var \Magento\Framework\Stdlib\DateTime\Filter\DateTime */ private $dateTimeFilter; - - /** - * @var \Magento\Catalog\Model\Product\LinkTypeProvider - */ - private $linkTypeProvider; /** * Helper constructor. @@ -88,7 +83,6 @@ class Helper * @param ProductLinks $productLinks * @param \Magento\Backend\Helper\Js $jsHelper * @param \Magento\Framework\Stdlib\DateTime\Filter\Date $dateFilter - * @param \Magento\Catalog\Model\Product\LinkTypeProvider $linkTypeProvider */ public function __construct( \Magento\Framework\App\RequestInterface $request, @@ -96,8 +90,7 @@ public function __construct( StockDataFilter $stockFilter, \Magento\Catalog\Model\Product\Initialization\Helper\ProductLinks $productLinks, \Magento\Backend\Helper\Js $jsHelper, - \Magento\Framework\Stdlib\DateTime\Filter\Date $dateFilter, - \Magento\Catalog\Model\Product\LinkTypeProvider $linkTypeProvider = null + \Magento\Framework\Stdlib\DateTime\Filter\Date $dateFilter ) { $this->request = $request; $this->storeManager = $storeManager; @@ -105,8 +98,6 @@ public function __construct( $this->productLinks = $productLinks; $this->jsHelper = $jsHelper; $this->dateFilter = $dateFilter; - $this->linkTypeProvider = $linkTypeProvider ?: \Magento\Framework\App\ObjectManager::getInstance() - ->get(\Magento\Catalog\Model\Product\LinkTypeProvider::class); } /** @@ -256,17 +247,11 @@ protected function setProductLinks(\Magento\Catalog\Model\Product $product) $product = $this->productLinks->initializeLinks($product, $links); $productLinks = $product->getProductLinks(); - $linkTypes = []; - - /** @var \Magento\Catalog\Api\Data\ProductLinkTypeInterface $linkTypeObject */ - foreach ($this->linkTypeProvider->getItems() as $linkTypeObject) { - $linkTypes[$linkTypeObject->getName()] = $product->getData($linkTypeObject->getName() . '_readonly'); - } - - // skip linkTypes that were already processed on initializeLinks plugins - foreach ($productLinks as $productLink) { - unset($linkTypes[$productLink->getLinkType()]); - } + $linkTypes = [ + 'related' => $product->getRelatedReadonly(), + 'upsell' => $product->getUpsellReadonly(), + 'crosssell' => $product->getCrosssellReadonly() + ]; foreach ($linkTypes as $linkType => $readonly) { if (isset($links[$linkType]) && !$readonly) { diff --git a/app/code/Magento/Catalog/Model/Category.php b/app/code/Magento/Catalog/Model/Category.php index d24169bc14ba0..a560e7e58e6de 100644 --- a/app/code/Magento/Catalog/Model/Category.php +++ b/app/code/Magento/Catalog/Model/Category.php @@ -652,14 +652,16 @@ public function formatUrlKey($str) } /** - * Retrieve image URL + * Get image url by attribute code. * + * @param string $attributeCode * @return 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( @@ -671,6 +673,7 @@ public function getImageUrl() ); } } + return $url; } 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 be448053342a0..a8bc510817f1b 100644 --- a/app/code/Magento/Catalog/Model/Category/Attribute/Backend/Image.php +++ b/app/code/Magento/Catalog/Model/Category/Attribute/Backend/Image.php @@ -70,7 +70,45 @@ public function __construct( } /** - * Get image uploader + * 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); + $imageName = $this->getUploadedImageName($value); + + if ($imageName) { + $object->setData($attributeName, $imageName); + } else if (!is_string($value)) { + $object->setData($attributeName, ''); + } + + return parent::beforeSave($object); + } + + /** + * Gets image name from $value array. + * Will return empty string in case $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 ''; + } + + /** + * Get image uploader. * * @return \Magento\Catalog\Model\ImageUploader * @@ -79,26 +117,25 @@ public function __construct( private function getImageUploader() { if ($this->imageUploader === null) { - $this->imageUploader = \Magento\Framework\App\ObjectManager::getInstance()->get( - 'Magento\Catalog\CategoryImageUpload' - ); + $this->imageUploader = \Magento\Framework\App\ObjectManager::getInstance() + ->get(\Magento\Catalog\CategoryImageUpload::class); } + return $this->imageUploader; } /** - * Save uploaded file and set its name to category + * Save uploaded file and set its name to category. * * @param \Magento\Framework\DataObject $object * @return \Magento\Catalog\Model\Category\Attribute\Backend\Image */ public function afterSave($object) { - $image = $object->getData($this->getAttribute()->getName(), null); - - if ($image !== null) { + $imageName = $object->getData($this->getAttribute()->getName(), null); + if ($imageName) { try { - $this->getImageUploader()->moveFileFromTmp($image); + $this->getImageUploader()->moveFileFromTmp($imageName); } catch (\Exception $e) { $this->_logger->critical($e); } diff --git a/app/code/Magento/Catalog/Model/Category/DataProvider.php b/app/code/Magento/Catalog/Model/Category/DataProvider.php index 4b91d0ac68f5f..3fdb5020a6cc8 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,16 +207,55 @@ 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; } + /** + * Converts category image data to acceptable format for rendering. + * + * @param \Magento\Catalog\Model\Category $category + * @param array $categoryData + * @return array + */ + private function convertValues(Category $category, array $categoryData) + { + $imageAttributes = $this->getImageAttributes($category, $categoryData); + $attributeCodes = array_keys($imageAttributes); + foreach ($attributeCodes as $attributeCode) { + unset($categoryData[$attributeCode]); + $categoryData[$attributeCode][0]['name'] = $category->getData($attributeCode); + $categoryData[$attributeCode][0]['url'] = $category->getImageUrl($attributeCode); + } + + return $categoryData; + } + + /** + * Get all category image attributes. + * + * @param \Magento\Catalog\Model\Category $category + * @param array $categoryData + * @return array + */ + private function getImageAttributes(Category $category, array $categoryData) + { + $imageAttributes = []; + foreach ($category->getAttributes() as $attributeCode => $attribute) { + if (!isset($categoryData[$attributeCode])) { + continue; + } + if ($attribute->getBackend() instanceof ImageBackendModel) { + $imageAttributes[$attributeCode] = $attribute; + } + } + + return $imageAttributes; + } + /** * Get attributes meta * diff --git a/app/code/Magento/Catalog/Model/Indexer/Category/Product/AbstractAction.php b/app/code/Magento/Catalog/Model/Indexer/Category/Product/AbstractAction.php index feb9f3bf8c343..250ea1d477a34 100644 --- a/app/code/Magento/Catalog/Model/Indexer/Category/Product/AbstractAction.php +++ b/app/code/Magento/Catalog/Model/Indexer/Category/Product/AbstractAction.php @@ -8,6 +8,7 @@ namespace Magento\Catalog\Model\Indexer\Category\Product; +use Magento\Framework\DB\Query\Generator as QueryGenerator; use Magento\Framework\App\ResourceConnection; use Magento\Framework\EntityManager\MetadataPool; @@ -102,20 +103,29 @@ abstract class AbstractAction */ protected $tempTreeIndexTableName; + /** + * @var QueryGenerator + */ + private $queryGenerator; + /** * @param ResourceConnection $resource * @param \Magento\Store\Model\StoreManagerInterface $storeManager * @param \Magento\Catalog\Model\Config $config + * @param QueryGenerator $queryGenerator */ public function __construct( \Magento\Framework\App\ResourceConnection $resource, \Magento\Store\Model\StoreManagerInterface $storeManager, - \Magento\Catalog\Model\Config $config + \Magento\Catalog\Model\Config $config, + QueryGenerator $queryGenerator = null ) { $this->resource = $resource; $this->connection = $resource->getConnection(); $this->storeManager = $storeManager; $this->config = $config; + $this->queryGenerator = $queryGenerator ?: \Magento\Framework\App\ObjectManager::getInstance() + ->get(QueryGenerator::class); } /** @@ -302,22 +312,35 @@ protected function isRangingNeeded() } /** - * Return selects cut by min and max + * Return selects cut by min and max. * * @param \Magento\Framework\DB\Select $select * @param string $field * @param int $range * @return \Magento\Framework\DB\Select[] */ - protected function prepareSelectsByRange(\Magento\Framework\DB\Select $select, $field, $range = self::RANGE_CATEGORY_STEP) - { - return $this->isRangingNeeded() ? $this->connection->selectsByRange( - $field, - $select, - $range - ) : [ - $select - ]; + protected function prepareSelectsByRange( + \Magento\Framework\DB\Select $select, + $field, + $range = self::RANGE_CATEGORY_STEP + ) { + if ($this->isRangingNeeded()) { + $iterator = $this->queryGenerator->generate( + $field, + $select, + $range, + \Magento\Framework\DB\Query\BatchIteratorInterface::NON_UNIQUE_FIELD_ITERATOR + ); + + $queries = []; + foreach ($iterator as $query) { + $queries[] = $query; + } + + return $queries; + } + + return [$select]; } /** diff --git a/app/code/Magento/Catalog/Model/Product/Attribute/Repository.php b/app/code/Magento/Catalog/Model/Product/Attribute/Repository.php index a6fe00107622f..b7d592263163a 100644 --- a/app/code/Magento/Catalog/Model/Product/Attribute/Repository.php +++ b/app/code/Magento/Catalog/Model/Product/Attribute/Repository.php @@ -8,6 +8,7 @@ use Magento\Framework\Exception\InputException; use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Eav\Api\Data\AttributeInterface; /** * @SuppressWarnings(PHPMD.CouplingBetweenObjects) @@ -49,11 +50,6 @@ class Repository implements \Magento\Catalog\Api\ProductAttributeRepositoryInter */ protected $searchCriteriaBuilder; - /** - * @var \Magento\Catalog\Api\ProductAttributeOptionManagementInterface - */ - private $optionManagement; - /** * @param \Magento\Catalog\Model\ResourceModel\Attribute $attributeResource * @param \Magento\Catalog\Helper\Product $productHelper @@ -117,12 +113,17 @@ public function save(\Magento\Catalog\Api\Data\ProductAttributeInterface $attrib throw NoSuchEntityException::singleField('attribute_code', $existingModel->getAttributeCode()); } + // Attribute code must not be changed after attribute creation + $attribute->setAttributeCode($existingModel->getAttributeCode()); $attribute->setAttributeId($existingModel->getAttributeId()); $attribute->setIsUserDefined($existingModel->getIsUserDefined()); $attribute->setFrontendInput($existingModel->getFrontendInput()); if (is_array($attribute->getFrontendLabels())) { - $frontendLabel[0] = $existingModel->getDefaultFrontendLabel(); + $defaultFrontendLabel = $attribute->getDefaultFrontendLabel(); + $frontendLabel[0] = !empty($defaultFrontendLabel) + ? $defaultFrontendLabel + : $existingModel->getDefaultFrontendLabel(); foreach ($attribute->getFrontendLabels() as $item) { $frontendLabel[$item->getStoreId()] = $item->getLabel(); } @@ -171,10 +172,31 @@ public function save(\Magento\Catalog\Api\Data\ProductAttributeInterface $attrib ); $attribute->setIsUserDefined(1); } - $this->attributeResource->save($attribute); - foreach ($attribute->getOptions() as $option) { - $this->getOptionManagement()->add($attribute->getAttributeCode(), $option); + if (!empty($attribute->getData(AttributeInterface::OPTIONS))) { + $options = []; + $sortOrder = 0; + $default = []; + $optionIndex = 0; + foreach ($attribute->getOptions() as $option) { + $optionIndex++; + $optionId = $option->getValue() ?: 'option_' . $optionIndex; + $options['value'][$optionId][0] = $option->getLabel(); + $options['order'][$optionId] = $option->getSortOrder() ?: $sortOrder++; + if (is_array($option->getStoreLabels())) { + foreach ($option->getStoreLabels() as $label) { + $options['value'][$optionId][$label->getStoreId()] = $label->getLabel(); + } + } + if ($option->getIsDefault()) { + $default[] = $optionId; + } + } + $attribute->setDefault($default); + if (count($options)) { + $attribute->setOption($options); + } } + $this->attributeResource->save($attribute); return $this->get($attribute->getAttributeCode()); } @@ -253,16 +275,4 @@ protected function validateFrontendInput($frontendInput) throw InputException::invalidFieldValue('frontend_input', $frontendInput); } } - - /** - * @return \Magento\Catalog\Api\ProductAttributeOptionManagementInterface - */ - private function getOptionManagement() - { - if (null === $this->optionManagement) { - $this->optionManagement = \Magento\Framework\App\ObjectManager::getInstance() - ->get('Magento\Catalog\Api\ProductAttributeOptionManagementInterface'); - } - return $this->optionManagement; - } } diff --git a/app/code/Magento/Catalog/Model/Product/Copier.php b/app/code/Magento/Catalog/Model/Product/Copier.php index 7095cc038ed39..0af477af9b9d7 100644 --- a/app/code/Magento/Catalog/Model/Product/Copier.php +++ b/app/code/Magento/Catalog/Model/Product/Copier.php @@ -9,6 +9,9 @@ use Magento\Catalog\Api\Data\ProductInterface; +/** + * Catalog product copier. + */ class Copier { /** @@ -54,12 +57,15 @@ public function copy(\Magento\Catalog\Model\Product $product) $product->getWebsiteIds(); $product->getCategoryIds(); + /** @var \Magento\Framework\EntityManager\EntityMetadataInterface $metadata */ + $metadata = $this->getMetadataPool()->getMetadata(ProductInterface::class); + /** @var \Magento\Catalog\Model\Product $duplicate */ $duplicate = $this->productFactory->create(); $duplicate->setData($product->getData()); $duplicate->setOptions([]); $duplicate->setIsDuplicate(true); - $duplicate->setOriginalId($product->getEntityId()); + $duplicate->setOriginalLinkId($product->getData($metadata->getLinkField())); $duplicate->setStatus(\Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_DISABLED); $duplicate->setCreatedAt(null); $duplicate->setUpdatedAt(null); @@ -81,11 +87,11 @@ public function copy(\Magento\Catalog\Model\Product $product) } } while (!$isDuplicateSaved); $this->getOptionRepository()->duplicate($product, $duplicate); - $metadata = $this->getMetadataPool()->getMetadata(ProductInterface::class); $product->getResource()->duplicate( $product->getData($metadata->getLinkField()), $duplicate->getData($metadata->getLinkField()) ); + return $duplicate; } @@ -97,8 +103,9 @@ private function getOptionRepository() { if (null === $this->optionRepository) { $this->optionRepository = \Magento\Framework\App\ObjectManager::getInstance() - ->get('Magento\Catalog\Model\Product\Option\Repository'); + ->get(\Magento\Catalog\Model\Product\Option\Repository::class); } + return $this->optionRepository; } @@ -110,8 +117,9 @@ private function getMetadataPool() { if (null === $this->metadataPool) { $this->metadataPool = \Magento\Framework\App\ObjectManager::getInstance() - ->get('Magento\Framework\EntityManager\MetadataPool'); + ->get(\Magento\Framework\EntityManager\MetadataPool::class); } + return $this->metadataPool; } } diff --git a/app/code/Magento/Catalog/Model/Product/Gallery/CreateHandler.php b/app/code/Magento/Catalog/Model/Product/Gallery/CreateHandler.php index f95865eb5f33b..f2c0d82e4b73f 100644 --- a/app/code/Magento/Catalog/Model/Product/Gallery/CreateHandler.php +++ b/app/code/Magento/Catalog/Model/Product/Gallery/CreateHandler.php @@ -278,6 +278,8 @@ protected function processNewImage($product, array &$image) } /** + * Duplicate product media gallery data. + * * @param \Magento\Catalog\Model\Product $product * @return $this */ @@ -294,7 +296,7 @@ protected function duplicate($product) $this->resourceModel->duplicate( $this->getAttribute()->getAttributeId(), isset($mediaGalleryData['duplicate']) ? $mediaGalleryData['duplicate'] : [], - $product->getOriginalId(), + $product->getOriginalLinkId(), $product->getData($this->metadata->getLinkField()) ); diff --git a/app/code/Magento/Catalog/Model/Product/Gallery/UpdateHandler.php b/app/code/Magento/Catalog/Model/Product/Gallery/UpdateHandler.php index 8c2d4a53a7722..03c175e5ffc73 100644 --- a/app/code/Magento/Catalog/Model/Product/Gallery/UpdateHandler.php +++ b/app/code/Magento/Catalog/Model/Product/Gallery/UpdateHandler.php @@ -29,7 +29,10 @@ protected function processDeletedImages($product, array &$images) if (!empty($image['removed'])) { if (!empty($image['value_id']) && !isset($picturesInOtherStores[$image['file']])) { $recordsToDelete[] = $image['value_id']; - $filesToDelete[] = ltrim($image['file'], '/'); + // only delete physical files if they are not used by any other products + if (!($this->resourceModel->countImageUses($image['file']) > 1)) { + $filesToDelete[] = ltrim($image['file'], '/'); + } } } } diff --git a/app/code/Magento/Catalog/Model/ProductRepository.php b/app/code/Magento/Catalog/Model/ProductRepository.php index 1d0bc6e866ea1..c8ba17ff966e2 100644 --- a/app/code/Magento/Catalog/Model/ProductRepository.php +++ b/app/code/Magento/Catalog/Model/ProductRepository.php @@ -278,6 +278,7 @@ protected function getCacheKey($data) */ protected function initializeProductData(array $productData, $createNew) { + unset($productData['media_gallery']); if ($createNew) { $product = $this->productFactory->create(); if ($this->storeManager->hasSingleStore()) { @@ -420,8 +421,15 @@ private function processLinks(\Magento\Catalog\Api\Data\ProductInterface $produc } /** - * @param ProductInterface $product - * @param array $mediaGalleryEntries + * Process Media gallery data before save product. + * + * Compare Media Gallery Entries Data with existing Media Gallery + * * If Media entry has not value_id set it as new + * * If Existing entry 'value_id' absent in Media Gallery set 'removed' flag + * * Merge Existing and new media gallery + * + * @param ProductInterface $product contains only existing media gallery items + * @param array $mediaGalleryEntries array which contains all media gallery items * @return $this * @throws InputException * @throws StateException @@ -431,11 +439,10 @@ protected function processMediaGallery(ProductInterface $product, $mediaGalleryE { $existingMediaGallery = $product->getMediaGallery('images'); $newEntries = []; + $entriesById = []; if (!empty($existingMediaGallery)) { - $entriesById = []; foreach ($mediaGalleryEntries as $entry) { - if (isset($entry['id'])) { - $entry['value_id'] = $entry['id']; + if (isset($entry['value_id'])) { $entriesById[$entry['value_id']] = $entry; } else { $newEntries[] = $entry; @@ -444,6 +451,9 @@ protected function processMediaGallery(ProductInterface $product, $mediaGalleryE foreach ($existingMediaGallery as $key => &$existingEntry) { if (isset($entriesById[$existingEntry['value_id']])) { $updatedEntry = $entriesById[$existingEntry['value_id']]; + if ($updatedEntry['file'] === null) { + unset($updatedEntry['file']); + } $existingMediaGallery[$key] = array_merge($existingEntry, $updatedEntry); } else { //set the removed flag @@ -471,11 +481,18 @@ protected function processMediaGallery(ProductInterface $product, $mediaGalleryE } /** @var ImageContentInterface $contentDataObject */ $contentDataObject = $this->contentFactory->create() - ->setName($newEntry['content'][ImageContentInterface::NAME]) - ->setBase64EncodedData($newEntry['content'][ImageContentInterface::BASE64_ENCODED_DATA]) - ->setType($newEntry['content'][ImageContentInterface::TYPE]); + ->setName($newEntry['content']['data'][ImageContentInterface::NAME]) + ->setBase64EncodedData($newEntry['content']['data'][ImageContentInterface::BASE64_ENCODED_DATA]) + ->setType($newEntry['content']['data'][ImageContentInterface::TYPE]); $newEntry['content'] = $contentDataObject; $this->processNewMediaGalleryEntry($product, $newEntry); + + $finalGallery = $product->getData('media_gallery'); + $newEntryId = key(array_diff_key($product->getData('media_gallery')['images'], $entriesById)); + $newEntry = array_replace_recursive($newEntry, $finalGallery['images'][$newEntryId]); + $entriesById[$newEntryId] = $newEntry; + $finalGallery['images'][$newEntryId] = $newEntry; + $product->setData('media_gallery', $finalGallery); } return $this; } @@ -503,8 +520,6 @@ public function save(\Magento\Catalog\Api\Data\ProductInterface $product, $saveO $productDataArray = $this->extensibleDataObjectConverter ->toNestedArray($product, [], 'Magento\Catalog\Api\Data\ProductInterface'); $productDataArray = array_replace($productDataArray, $product->getData()); - unset($productDataArray['media_gallery']); - $ignoreLinksFlag = $product->getData('ignore_links_flag'); $productLinks = null; if (!$ignoreLinksFlag && $ignoreLinksFlag !== null) { @@ -514,8 +529,8 @@ public function save(\Magento\Catalog\Api\Data\ProductInterface $product, $saveO $product = $this->initializeProductData($productDataArray, empty($existingProduct)); $this->processLinks($product, $productLinks); - if (isset($productDataArray['media_gallery_entries'])) { - $this->processMediaGallery($product, $productDataArray['media_gallery_entries']); + if (isset($productDataArray['media_gallery'])) { + $this->processMediaGallery($product, $productDataArray['media_gallery']['images']); } if (!$product->getOptionsReadonly()) { diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Category.php b/app/code/Magento/Catalog/Model/ResourceModel/Category.php index 24b7ffd0881ab..7d3939096fafa 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Category.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Category.php @@ -33,6 +33,13 @@ class Category extends AbstractResource */ protected $_categoryProductTable; + /** + * Entities where attribute is filled. + * + * @var array[] + */ + private $entitiesWhereAttributesIs; + /** * Id of 'is_active' category attribute * @@ -575,22 +582,29 @@ public function getIsActiveAttributeId() */ public function findWhereAttributeIs($entityIdsFilter, $attribute, $expectedValue) { - $linkField = $this->getLinkField(); - $bind = ['attribute_id' => $attribute->getId(), 'value' => $expectedValue]; - $selectEntities = $this->getConnection()->select()->from( - ['ce' => $this->getTable('catalog_category_entity')], - ['entity_id'] - )->joinLeft( - ['ci' => $attribute->getBackend()->getTable()], - "ci.{$linkField} = ce.{$linkField} AND attribute_id = :attribute_id", - ['value'] - )->where( - 'ci.value = :value' - )->where( - 'ce.entity_id IN (?)', - $entityIdsFilter - ); - return $this->getConnection()->fetchCol($selectEntities, $bind); + $entityIdsFilterHash = md5(serialize($entityIdsFilter)); + + if (!isset($this->entitiesWhereAttributesIs[$entityIdsFilterHash][$attribute->getId()][$expectedValue])) { + $linkField = $this->getLinkField(); + $bind = ['attribute_id' => $attribute->getId(), 'value' => $expectedValue]; + $selectEntities = $this->getConnection()->select()->from( + ['ce' => $this->getTable('catalog_category_entity')], + ['entity_id'] + )->joinLeft( + ['ci' => $attribute->getBackend()->getTable()], + "ci.{$linkField} = ce.{$linkField} AND attribute_id = :attribute_id", + ['value'] + )->where( + 'ci.value = :value' + )->where( + 'ce.entity_id IN (?)', + $entityIdsFilter + ); + $this->entitiesWhereAttributesIs[$entityIdsFilterHash][$attribute->getId()][$expectedValue] = + $this->getConnection()->fetchCol($selectEntities, $bind); + } + + return $this->entitiesWhereAttributesIs[$entityIdsFilterHash][$attribute->getId()][$expectedValue]; } /** @@ -1035,7 +1049,7 @@ private function getEntityManager() { if (null === $this->entityManager) { $this->entityManager = \Magento\Framework\App\ObjectManager::getInstance() - ->get('Magento\Framework\EntityManager\EntityManager'); + ->get(\Magento\Framework\EntityManager\EntityManager::class); } return $this->entityManager; } @@ -1047,7 +1061,7 @@ private function getAggregateCount() { if (null === $this->aggregateCount) { $this->aggregateCount = \Magento\Framework\App\ObjectManager::getInstance() - ->get('Magento\Catalog\Model\ResourceModel\Category\AggregateCount'); + ->get(\Magento\Catalog\Model\ResourceModel\Category\AggregateCount::class); } return $this->aggregateCount; } diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/Collection.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/Collection.php index 66b45112c28db..fadfedc211d6e 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Product/Collection.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/Collection.php @@ -2213,16 +2213,24 @@ public function addMediaGalleryData() $linkField = $this->getMetadataPool()->getMetadata(ProductInterface::class)->getLinkField(); $items = $this->getItems(); - $select->where('entity.' . $linkField . ' IN (?)', array_map(function ($item) { - return $item->getId(); - }, $items)); + $select->where( + 'entity.' . $linkField . ' IN (?)', + array_map( + function ($item) use ($linkField) { + return $item->getData($linkField); + }, + $items + ) + ); foreach ($this->getConnection()->fetchAll($select) as $row) { $mediaGalleries[$row[$linkField]][] = $row; } foreach ($items as $item) { - $mediaEntries = isset($mediaGalleries[$item->getId()]) ? $mediaGalleries[$item->getId()] : []; + $mediaEntries = isset($mediaGalleries[$item->getData($linkField)]) + ? $mediaGalleries[$item->getData($linkField)] + : []; $this->getGalleryReadHandler()->addMediaDataToProduct($item, $mediaEntries); } diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/Gallery.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/Gallery.php index ad733d510b315..156f5672351fc 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Product/Gallery.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/Gallery.php @@ -449,4 +449,22 @@ public function getProductImages($product, $storeIds) return $this->getConnection()->fetchAll($select); } + + /** + * Counts uses of this image. + * + * @param string $image + * @return int + */ + public function countImageUses($image) + { + $select = $this->getConnection()->select() + ->from( + [$this->getMainTableAlias() => $this->getMainTable()], + 'count(*)' + ) + ->where('value = ?', $image); + + return $this->getConnection()->fetchOne($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 227b47d572e01..7cd1c8c09fc17 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 4a45401630020..76127b02d5fb9 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 74e5147804eb8..e7cf48c378ca0 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,12 +317,43 @@ 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; } + /** + * Prepares data from select to save. + * + * @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); + } + /** * Retrieve temporary source index table name * 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 41456945191fa..8a97d43a18d7e 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 @@ -235,11 +235,30 @@ protected function _prepareFinalPriceData($entityIds = null) * @param string|null $type product type, all if null * @return $this * @throws \Magento\Framework\Exception\LocalizedException - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ protected function prepareFinalPriceDataForType($entityIds, $type) { $this->_prepareDefaultFinalPriceTable(); + + $select = $this->getSelect($entityIds, $type); + $query = $select->insertFromSelect($this->_getDefaultFinalPriceTable(), [], false); + $this->getConnection()->query($query); + return $this; + } + + /** + * Forms Select for collecting price related data for final price index table + * Next types of prices took into account: default, special, tier price + * Moved to protected for possible reusing + * + * @param int|array $entityIds Ids for filtering output result + * @param string|null $type Type for filtering output result by specified product type (all if null) + * @return \Magento\Framework\DB\Select + * @throws \Magento\Framework\Exception\LocalizedException + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + protected function getSelect($entityIds = null, $type = null) + { $metadata = $this->getMetadataPool()->getMetadata(\Magento\Catalog\Api\Data\ProductInterface::class); $connection = $this->getConnection(); $select = $connection->select()->from( @@ -368,13 +387,10 @@ 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'), ] ); - - $query = $select->insertFromSelect($this->_getDefaultFinalPriceTable(), [], false); - $connection->query($query); - return $this; + return $select; } /** diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/Option/Value.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/Option/Value.php index 0acfed1411196..a3df7bbc083fd 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Product/Option/Value.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/Option/Value.php @@ -33,6 +33,11 @@ class Value extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb */ protected $_config; + /** + * @var \Magento\Framework\Locale\FormatInterface + */ + private $localeFormat; + /** * Class constructor * @@ -81,7 +86,7 @@ protected function _afterSave(\Magento\Framework\Model\AbstractModel $object) } /** - * Save option value price data + * Save option value price data. * * @param \Magento\Framework\Model\AbstractModel $object * @return void @@ -91,8 +96,9 @@ protected function _afterSave(\Magento\Framework\Model\AbstractModel $object) protected function _saveValuePrices(\Magento\Framework\Model\AbstractModel $object) { $priceTable = $this->getTable('catalog_product_option_type_price'); + $formattedPrice = $this->getLocaleFormatter()->getNumber($object->getPrice()); - $price = (double)sprintf('%F', $object->getPrice()); + $price = (double)sprintf('%F', $formattedPrice); $priceType = $object->getPriceType(); if ($object->getPrice() && $priceType) { @@ -410,4 +416,20 @@ public function duplicate(\Magento\Catalog\Model\Product\Option\Value $object, $ return $object; } + + /** + * Get FormatInterface to convert price from string to number format. + * + * @return \Magento\Framework\Locale\FormatInterface + * @deprecated + */ + private function getLocaleFormatter() + { + if ($this->localeFormat === null) { + $this->localeFormat = \Magento\Framework\App\ObjectManager::getInstance() + ->get(\Magento\Framework\Locale\FormatInterface::class); + } + + return $this->localeFormat; + } } diff --git a/app/code/Magento/Catalog/Pricing/Price/MinimalPriceCalculatorInterface.php b/app/code/Magento/Catalog/Pricing/Price/MinimalPriceCalculatorInterface.php new file mode 100644 index 0000000000000..de31b1d5b51a9 --- /dev/null +++ b/app/code/Magento/Catalog/Pricing/Price/MinimalPriceCalculatorInterface.php @@ -0,0 +1,32 @@ +calculator = $calculator; + } + + /** + * Get raw value of "as low as" as a minimal among tier prices. + * + * @param SaleableInterface $saleableItem + * @return float|null + */ + public function getValue(SaleableInterface $saleableItem) + { + /** @var TierPrice $price */ + $price = $saleableItem->getPriceInfo()->getPrice(TierPrice::PRICE_CODE); + $tierPriceList = $price->getTierPriceList(); + + $tierPrices = []; + foreach ($tierPriceList as $tierPrice) { + /** @var AmountInterface $price */ + $price = $tierPrice['price']; + $tierPrices[] = $price->getValue(); + } + + return $tierPrices ? min($tierPrices) : null; + } + + /** + * Return calculated amount object that keeps "as low as" value. + * + * @param SaleableInterface $saleableItem + * @return AmountInterface|null + */ + public function getAmount(SaleableInterface $saleableItem) + { + $value = $this->getValue($saleableItem); + + return $value === null ? null : $this->calculator->getAmount($value, $saleableItem); + } +} diff --git a/app/code/Magento/Catalog/Pricing/Render/FinalPriceBox.php b/app/code/Magento/Catalog/Pricing/Render/FinalPriceBox.php index bab93e08f2753..afe3e0f7374a7 100644 --- a/app/code/Magento/Catalog/Pricing/Render/FinalPriceBox.php +++ b/app/code/Magento/Catalog/Pricing/Render/FinalPriceBox.php @@ -8,40 +8,49 @@ use Magento\Catalog\Pricing\Price; use Magento\Framework\App\ObjectManager; -use Magento\Framework\Module\Manager; -use Magento\Framework\Pricing\Render; use Magento\Framework\Pricing\Render\PriceBox as BasePriceBox; -use Magento\Msrp\Pricing\Price\MsrpPrice; -use Magento\Catalog\Model\Product\Pricing\Renderer\SalableResolverInterface; -use Magento\Framework\View\Element\Template\Context; use Magento\Framework\Pricing\SaleableInterface; use Magento\Framework\Pricing\Price\PriceInterface; use Magento\Framework\Pricing\Render\RendererPool; +use Magento\Msrp\Pricing\Price\MsrpPrice; +use Magento\Framework\View\Element\Template\Context; /** - * Class for final_price rendering + * Class for final_price rendering. * * @method bool getUseLinkForAsLowAs() * @method bool getDisplayMinimalPrice() - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class FinalPriceBox extends BasePriceBox { /** - * @var SalableResolverInterface + * Interface resolver provided to check is product available for sale. + * + * @var \Magento\Catalog\Model\Product\Pricing\Renderer\SalableResolverInterface */ private $salableResolver; - /** @var Manager */ + /** + * Module statuses manager. + * + * @var \Magento\Framework\Module\Manager + */ private $moduleManager; + /** + * Shows minimal value of Tier Prices. + * + * @var \Magento\Catalog\Pricing\Price\MinimalPriceCalculatorInterface + */ + private $minimalPriceCalculator; + /** * @param Context $context * @param SaleableInterface $saleableItem * @param PriceInterface $price * @param RendererPool $rendererPool * @param array $data - * @param SalableResolverInterface $salableResolver + * @param \Magento\Catalog\Model\Product\Pricing\Renderer\SalableResolverInterface $salableResolver */ public function __construct( Context $context, @@ -49,11 +58,11 @@ public function __construct( PriceInterface $price, RendererPool $rendererPool, array $data = [], - SalableResolverInterface $salableResolver = null + \Magento\Catalog\Model\Product\Pricing\Renderer\SalableResolverInterface $salableResolver = null ) { parent::__construct($context, $saleableItem, $price, $rendererPool, $data); - $this->salableResolver = $salableResolver ?: \Magento\Framework\App\ObjectManager::getInstance() - ->get(SalableResolverInterface::class); + $this->salableResolver = $salableResolver ?: ObjectManager::getInstance() + ->get(\Magento\Catalog\Model\Product\Pricing\Renderer\SalableResolverInterface::class); } /** @@ -116,7 +125,7 @@ private function isMsrpPriceApplicable() } /** - * Wrap with standard required container + * Wrap with standard required container. * * @param string $html * @return string @@ -130,17 +139,21 @@ protected function wrapResult($html) } /** - * Render minimal amount + * Render minimal amount. * * @return string */ public function renderAmountMinimal() { - /** @var \Magento\Catalog\Pricing\Price\FinalPrice $price */ - $price = $this->getPriceType(\Magento\Catalog\Pricing\Price\FinalPrice::PRICE_CODE); $id = $this->getPriceId() ? $this->getPriceId() : 'product-minimal-price-' . $this->getSaleableItem()->getId(); + $amount = $this->getMinimalPriceCalculator()->getAmount($this->getSaleableItem()); + + if ($amount === null) { + return ''; + } + return $this->renderAmount( - $price->getMinimalPrice(), + $amount, [ 'display_label' => __('As low as'), 'price_id' => $id, @@ -151,7 +164,7 @@ public function renderAmountMinimal() } /** - * Define if the special price should be shown + * Define if the special price should be shown. * * @return bool */ @@ -163,23 +176,25 @@ public function hasSpecialPrice() } /** - * Define if the minimal price should be shown + * Define if the minimal price should be shown. * * @return bool */ public function showMinimalPrice() { + $minTierPrice = $this->getMinimalPriceCalculator()->getValue($this->getSaleableItem()); + /** @var Price\FinalPrice $finalPrice */ $finalPrice = $this->getPriceType(Price\FinalPrice::PRICE_CODE); $finalPriceValue = $finalPrice->getAmount()->getValue(); - $minimalPriceAValue = $finalPrice->getMinimalPrice()->getValue(); + return $this->getDisplayMinimalPrice() - && $minimalPriceAValue - && $minimalPriceAValue < $finalPriceValue; + && $minTierPrice !== null + && $minTierPrice < $finalPriceValue; } /** - * Get Key for caching block content + * Get Key for caching block content. * * @return string */ @@ -203,19 +218,19 @@ public function getCacheKeyInfo() /** * @deprecated - * @return Manager + * @return \Magento\Framework\Module\Manager */ private function getModuleManager() { if ($this->moduleManager === null) { - $this->moduleManager = ObjectManager::getInstance()->get(Manager::class); + $this->moduleManager = ObjectManager::getInstance()->get(\Magento\Framework\Module\Manager::class); } return $this->moduleManager; } /** - * Get flag that price rendering should be done for the list of products - * By default (if flag is not set) is false + * Get flag that price rendering should be done for the list of products. + * By default (if flag is not set) is false. * * @return bool */ @@ -224,4 +239,18 @@ public function isProductList() $isProductList = $this->getData('is_product_list'); return $isProductList === true; } + + /** + * @deprecated + * @return \Magento\Catalog\Pricing\Price\MinimalPriceCalculatorInterface + */ + private function getMinimalPriceCalculator() + { + if ($this->minimalPriceCalculator == null) { + $this->minimalPriceCalculator = ObjectManager::getInstance() + ->get(\Magento\Catalog\Pricing\Price\MinimalPriceCalculatorInterface::class); + } + + return $this->minimalPriceCalculator; + } } diff --git a/app/code/Magento/Catalog/Setup/InstallSchema.php b/app/code/Magento/Catalog/Setup/InstallSchema.php index 206bd820e59fb..4e164d53e317d 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) { @@ -2426,7 +2427,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( diff --git a/app/code/Magento/Catalog/Setup/UpgradeSchema.php b/app/code/Magento/Catalog/Setup/UpgradeSchema.php index aef2501aa9aaa..213cbe8b0cf7c 100644 --- a/app/code/Magento/Catalog/Setup/UpgradeSchema.php +++ b/app/code/Magento/Catalog/Setup/UpgradeSchema.php @@ -32,9 +32,59 @@ public function upgrade(SchemaSetupInterface $setup, ModuleContextInterface $con if (version_compare($context->getVersion(), '2.0.6', '<')) { $this->addUniqueKeyToCategoryProductTable($setup); } + + if (version_compare($context->getVersion(), '2.1.4', '<')) { + $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/Block/Product/View/AttributesTest.php b/app/code/Magento/Catalog/Test/Unit/Block/Product/View/AttributesTest.php new file mode 100644 index 0000000000000..091711d5cb064 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Unit/Block/Product/View/AttributesTest.php @@ -0,0 +1,292 @@ +context = $this->getMockBuilder(\Magento\Catalog\Block\Product\Context::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->registry = $this->getMockBuilder(\Magento\Framework\Registry::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->context->expects($this->any()) + ->method('getRegistry') + ->willReturn($this->registry); + + $this->priceCurrency = $this->getMockBuilder(\Magento\Framework\Pricing\PriceCurrencyInterface::class) + ->disableOriginalConstructor() + ->setMethods([]) + ->getMockForAbstractClass(); + + $this->product = $this->getMockBuilder(\Magento\Catalog\Model\Product::class) + ->disableOriginalConstructor() + ->setMethods(['getAttributes', 'hasData']) + ->getMock(); + + $this->block = new \Magento\Catalog\Block\Product\View\Attributes( + $this->context, + $this->registry, + $this->priceCurrency + ); + + $this->getObjectManager()->setBackwardCompatibleProperty($this->block, '_product', $this->product); + } + + /** + * @covers \Magento\Catalog\Block\Product\View\Attributes::getAdditionalData + * @dataProvider getAdditionalDataProvider + * + * @param array $attributes + * @param bool $productHasAttributeValue + * @param array $excludedAttributes + * @param array $expectedResult + * @return void + */ + public function testGetAdditionalData( + $attributes, + $productHasAttributeValue, + $excludedAttributes, + $expectedResult + ) { + $this->product->expects(self::once())->method('getAttributes') + ->willReturn($attributes); + + $this->product->expects(self::any())->method('hasData') + ->with('attribute') + ->willReturn($productHasAttributeValue); + + $this->priceCurrency->expects(self::any()) + ->method('convertAndFormat') + ->withAnyParameters() + ->willReturn('test'); + + self::assertEquals($expectedResult, $this->block->getAdditionalData($excludedAttributes)); + } + + /** + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + * + * @return array + */ + public function getAdditionalDataProvider() + { + return [ + 'No Attributes' => [ + [], + false, + [], + [] + ], + 'With Invisible On Frontend Attribute' => [ + [ + $this->prepareAttributeMock(['is_visible_on_front' => false]) + ], + false, + [], + [] + ], + 'With Excluded On Frontend Attribute' => [ + [ + $this->prepareAttributeMock( + [ + 'attribute_code' => 'excluded_attribute', + 'is_visible_on_front' => false + ] + ) + ], + false, + ['excluded_attribute'], + [] + ], + 'Product Has No Attribute Value' => [ + [ + $this->prepareAttributeMock( + [ + 'attribute_code' => 'attribute', + 'store_label' => 'Test Attribute', + 'is_visible_on_front' => true, + ] + ) + ], + false, + [], + [ + 'attribute' => [ + 'label' => 'Test Attribute', + 'value' => 'N/A', + 'code' => 'attribute', + ] + ] + ], + 'Product With Null Attribute Value' => [ + [ + $this->prepareAttributeMock( + [ + 'attribute_code' => 'attribute', + 'store_label' => 'Test Attribute', + 'is_visible_on_front' => true, + 'value' => null + ] + ) + ], + true, + [], + [ + 'attribute' => [ + 'label' => 'Test Attribute', + 'value' => 'No', + 'code' => 'attribute', + ] + ] + ], + 'Product With Price Attribute' => [ + [ + $this->prepareAttributeMock( + [ + 'attribute_code' => 'attribute', + 'store_label' => 'Test Attribute', + 'is_visible_on_front' => true, + 'frontend_input' => 'price', + 'value' => '2.1' + ] + ) + ], + true, + [], + [ + 'attribute' => [ + 'label' => 'Test Attribute', + 'value' => 'test', + 'code' => 'attribute', + ] + ] + ], + 'Product With Phrase Attribute Value' => [ + [ + $this->prepareAttributeMock( + [ + 'attribute_code' => 'attribute', + 'store_label' => 'Test Attribute', + 'is_visible_on_front' => true, + 'frontend_input' => 'price', + 'value' => __('test') + ] + ) + ], + true, + [], + [ + 'attribute' => [ + 'label' => 'Test Attribute', + 'value' => 'test', + 'code' => 'attribute', + ] + ] + ], + ]; + } + + /** + * Return object manager. + * + * @return ObjectManager + */ + private function getObjectManager() + { + if ($this->objectManager === null) { + $this->objectManager = new ObjectManager($this); + } + + return $this->objectManager; + } + + /** + * Prepare attribute mock. + * + * @param array $data + * @return \Magento\Eav\Model\Entity\Attribute + */ + private function prepareAttributeMock($data = []) + { + $attributeValue = isset($data['value']) ? $data['value']: null; + + /** @var \PHPUnit_Framework_MockObject_MockObject $frontendModel */ + $frontendModel = $this->getMockBuilder( + \Magento\Eav\Model\Entity\Attribute\Frontend\DefaultFrontend::class + ) + ->disableOriginalConstructor() + ->setMethods(['getValue']) + ->getMock(); + + $frontendModel->expects(self::any()) + ->method('getValue') + ->willReturn($attributeValue); + + $attribute = $this->getObjectManager()->getObject( + \Magento\Eav\Model\Entity\Attribute::class, + ['data' => $data] + ); + $this->getObjectManager()->setBackwardCompatibleProperty($attribute, '_frontend', $frontendModel); + + return $attribute; + } +} 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..e4fb125d6c347 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Category/Image/UploadTest.php @@ -0,0 +1,120 @@ +objectManager = new ObjectManager($this); + } + + /** + * Test Uploader::execute() handle request and move image into tmp dir. + * + * @param string $name + * @param string $savedName + * + * @dataProvider executeDataProvider + */ + public function testExecute($name, $savedName) + { + $cookieName = 'testName'; + $sessionId = 'testSessionId'; + $lifetime = 'testLifetime'; + $path = 'testPath'; + $domain = 'testDomain'; + $data = [ + 'cookie' => [ + 'name' => $cookieName, + 'value' => $sessionId, + 'lifetime' => $lifetime, + 'path' => $path, + 'domain' => $domain + ] + ]; + $request = $this->objectManager->getObject(Request::class); + $uploader = $this->getMockBuilder(ImageUploader::class) + ->disableOriginalConstructor() + ->setMethods(['saveFileToTmpDir']) + ->getMock(); + $resultFactory = $this->getMockBuilder(ResultFactory::class) + ->disableOriginalConstructor() + ->setMethods(['create']) + ->getMock(); + $resultFactory->expects($this->once()) + ->method('create') + ->will($this->returnValue(new DataObject())); + $session = $this->getMockBuilder(\Magento\Backend\Model\Session::class) + ->disableOriginalConstructor() + ->getMock(); + $session->expects($this->once()) + ->method('getName') + ->willReturn($cookieName); + $session->expects($this->once()) + ->method('getSessionId') + ->willReturn($sessionId); + $session->expects($this->once()) + ->method('getCookieLifeTime') + ->willReturn($lifetime); + $session->expects($this->once()) + ->method('getCookiePath') + ->willReturn($path); + $session->expects($this->once()) + ->method('getCookieDomain') + ->willReturn($domain); + $model = $this->objectManager->getObject(Model::class, [ + 'request' => $request, + 'resultFactory' => $resultFactory, + 'imageUploader' => $uploader, + '_session' => $session + ]); + $uploader->expects($this->once()) + ->method('saveFileToTmpDir') + ->with($savedName) + ->will($this->returnValue([])); + $request->setParam('param_name', $name); + $result = $model->execute(); + $this->assertSame($data, $result->getData()); + } + + /** + * Data for testExecute. + * + * @return array + */ + public function executeDataProvider() + { + return [ + ['image1', 'image1'], + ['image2', 'image2'], + [null, 'image'], + ]; + } +} 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 e46d517e881d5..51b1cde5917b8 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 @@ -3,8 +3,16 @@ * Copyright © 2013-2017 Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Catalog\Test\Unit\Controller\Adminhtml\Category; +use Magento\Catalog\Api\Data\CategoryAttributeInterface; +use Magento\Catalog\Controller\Adminhtml\Category\Save; +use Magento\Eav\Model\Config; +use Magento\Eav\Model\Entity\Type; +use Magento\Store\Model\Store; +use Magento\Store\Model\StoreManager; + /** * Class SaveTest * @SuppressWarnings(PHPMD.CouplingBetweenObjects) @@ -14,67 +22,81 @@ class SaveTest extends \PHPUnit_Framework_TestCase /** * @var \Magento\Backend\Model\View\Result\RedirectFactory|\PHPUnit_Framework_MockObject_MockObject */ - protected $resultRedirectFactoryMock; + private $resultRedirectFactoryMock; /** * @var \Magento\Framework\Controller\Result\RawFactory|\PHPUnit_Framework_MockObject_MockObject */ - protected $resultRawFactoryMock; + private $resultRawFactoryMock; /** * @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; + private $layoutFactoryMock; /** * @var \Magento\Backend\App\Action\Context|\PHPUnit_Framework_MockObject_MockObject */ - protected $contextMock; + private $contextMock; /** * @var \Magento\Framework\View\Page\Title|\PHPUnit_Framework_MockObject_MockObject */ - protected $titleMock; + private $titleMock; /** * @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; + private $eventManagerMock; /** * @var \Magento\Framework\App\ResponseInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $responseMock; + private $responseMock; /** * @var \Magento\Framework\Message\ManagerInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $messageManagerMock; + private $messageManagerMock; /** * @var \Magento\Framework\TestFramework\Unit\Helper\ObjectManager */ - protected $objectManager; + private $objectManager; + + /** + * Config mock holder. + * + * @var Config|\PHPUnit_Framework_MockObject_MockObject + */ + private $eavCongig; /** - * @var \Magento\Catalog\Controller\Adminhtml\Category\Save + * StoreManager mock holder. + * + * @var StoreManager|\PHPUnit_Framework_MockObject_MockObject + */ + private $storeManager; + + /** + * @var Save */ - protected $save; + private $save; /** * Set up @@ -84,11 +106,10 @@ 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', + \Magento\Backend\App\Action\Context::class, [ 'getTitle', 'getRequest', @@ -103,35 +124,35 @@ protected function setUp() false ); $this->resultRedirectFactoryMock = $this->getMock( - 'Magento\Backend\Model\View\Result\RedirectFactory', + \Magento\Backend\Model\View\Result\RedirectFactory::class, ['create'], [], '', false ); $this->resultRawFactoryMock = $this->getMock( - 'Magento\Framework\Controller\Result\RawFactory', + \Magento\Framework\Controller\Result\RawFactory::class, [], [], '', false ); $this->resultJsonFactoryMock = $this->getMock( - 'Magento\Framework\Controller\Result\JsonFactory', + \Magento\Framework\Controller\Result\JsonFactory::class, ['create'], [], '', false ); $this->layoutFactoryMock = $this->getMock( - 'Magento\Framework\View\LayoutFactory', + \Magento\Framework\View\LayoutFactory::class, ['create'], [], '', false ); $this->requestMock = $this->getMockForAbstractClass( - 'Magento\Framework\App\RequestInterface', + \Magento\Framework\App\RequestInterface::class, [], '', false, @@ -139,11 +160,11 @@ protected function setUp() true, ['getParam', 'getPost', 'getPostValue'] ); - $this->objectManagerMock = $this->getMockBuilder('Magento\Framework\ObjectManagerInterface') + $this->objectManagerMock = $this->getMockBuilder(\Magento\Framework\ObjectManagerInterface::class) ->disableOriginalConstructor() ->getMock(); $this->eventManagerMock = $this->getMockForAbstractClass( - 'Magento\Framework\Event\ManagerInterface', + \Magento\Framework\Event\ManagerInterface::class, [], '', false, @@ -152,13 +173,13 @@ protected function setUp() ['dispatch'] ); $this->responseMock = $this->getMockForAbstractClass( - 'Magento\Framework\App\ResponseInterface', + \Magento\Framework\App\ResponseInterface::class, [], '', false ); $this->messageManagerMock = $this->getMockForAbstractClass( - 'Magento\Framework\Message\ManagerInterface', + \Magento\Framework\Message\ManagerInterface::class, [], '', false, @@ -177,15 +198,24 @@ protected function setUp() ->method('getResultRedirectFactory') ->willReturn($this->resultRedirectFactoryMock); + $this->storeManager = $this->getMockBuilder(StoreManager::class) + ->disableOriginalConstructor() + ->getMock(); + $this->save = $this->objectManager->getObject( - 'Magento\Catalog\Controller\Adminhtml\Category\Save', + Save::class, [ 'context' => $this->contextMock, 'resultRawFactory' => $this->resultRawFactoryMock, 'resultJsonFactory' => $this->resultJsonFactoryMock, - 'layoutFactory' => $this->layoutFactoryMock + 'layoutFactory' => $this->layoutFactoryMock, + 'storeManager' => $this->storeManager ] ); + $this->eavCongig = $this->getMockBuilder(Config::class) + ->disableOriginalConstructor() + ->getMock(); + $this->objectManager->setBackwardCompatibleProperty($this->save, 'eavConfig', $this->eavCongig); } /** @@ -217,7 +247,7 @@ public function testExecute($categoryId, $storeId, $parentId) * |\PHPUnit_Framework_MockObject_MockObject $resultRedirectMock */ $resultRedirectMock = $this->getMock( - 'Magento\Backend\Model\View\Result\Redirect', + \Magento\Backend\Model\View\Result\Redirect::class, [], [], '', @@ -228,7 +258,7 @@ public function testExecute($categoryId, $storeId, $parentId) * |\PHPUnit_Framework_MockObject_MockObject $blockMock */ $blockMock = $this->getMock( - 'Magento\Framework\View\Element\Messages', + \Magento\Framework\View\Element\Messages::class, ['setMessages', 'getGroupedHtml'], [], '', @@ -239,7 +269,7 @@ public function testExecute($categoryId, $storeId, $parentId) * |\PHPUnit_Framework_MockObject_MockObject $categoryMock */ $categoryMock = $this->getMock( - 'Magento\Catalog\Model\Category', + \Magento\Catalog\Model\Category::class, [ 'setStoreId', 'load', @@ -249,6 +279,7 @@ public function testExecute($categoryId, $storeId, $parentId) 'setParentId', 'setData', 'addData', + 'getAttributes', 'setAttributeSetId', 'getDefaultAttributeSetId', 'getProductsReadonly', @@ -268,7 +299,7 @@ public function testExecute($categoryId, $storeId, $parentId) * |\PHPUnit_Framework_MockObject_MockObject $parentCategoryMock */ $parentCategoryMock = $this->getMock( - 'Magento\Catalog\Model\Category', + \Magento\Catalog\Model\Category::class, [ 'setStoreId', 'load', @@ -292,7 +323,7 @@ public function testExecute($categoryId, $storeId, $parentId) * |\PHPUnit_Framework_MockObject_MockObject $sessionMock */ $sessionMock = $this->getMock( - 'Magento\Backend\Model\Auth\Session', + \Magento\Backend\Model\Auth\Session::class, [], [], '', @@ -303,7 +334,7 @@ public function testExecute($categoryId, $storeId, $parentId) * |\PHPUnit_Framework_MockObject_MockObject $registryMock */ $registryMock = $this->getMock( - 'Magento\Framework\Registry', + \Magento\Framework\Registry::class, ['register'], [], '', @@ -314,7 +345,7 @@ public function testExecute($categoryId, $storeId, $parentId) * |\PHPUnit_Framework_MockObject_MockObject $wysiwygConfigMock */ $wysiwygConfigMock = $this->getMock( - 'Magento\Cms\Model\Wysiwyg\Config', + \Magento\Cms\Model\Wysiwyg\Config::class, ['setStoreId'], [], '', @@ -325,7 +356,7 @@ public function testExecute($categoryId, $storeId, $parentId) * |\PHPUnit_Framework_MockObject_MockObject $storeManagerMock */ $storeManagerMock = $this->getMockForAbstractClass( - 'Magento\Store\Model\StoreManagerInterface', + \Magento\Store\Model\StoreManagerInterface::class, [], '', false, @@ -338,7 +369,7 @@ public function testExecute($categoryId, $storeId, $parentId) * |\PHPUnit_Framework_MockObject_MockObject $layoutMock */ $layoutMock = $this->getMockForAbstractClass( - 'Magento\Framework\View\Layout', + \Magento\Framework\View\Layout::class, [], '', false, @@ -351,7 +382,7 @@ public function testExecute($categoryId, $storeId, $parentId) * |\PHPUnit_Framework_MockObject_MockObject $resultJsonMock */ $resultJsonMock = $this->getMock( - 'Magento\Cms\Model\Wysiwyg\Config', + \Magento\Cms\Model\Wysiwyg\Config::class, ['setData'], [], '', @@ -362,7 +393,7 @@ public function testExecute($categoryId, $storeId, $parentId) * |\PHPUnit_Framework_MockObject_MockObject $messagesMock */ $messagesMock = $this->getMock( - 'Magento\Framework\Message\Collection', + \Magento\Framework\Message\Collection::class, [], [], '', @@ -395,10 +426,10 @@ public function testExecute($categoryId, $storeId, $parentId) ->will( $this->returnValueMap( [ - ['Magento\Backend\Model\Auth\Session', $sessionMock], - ['Magento\Framework\Registry', $registryMock], - ['Magento\Cms\Model\Wysiwyg\Config', $wysiwygConfigMock], - ['Magento\Store\Model\StoreManagerInterface', $storeManagerMock], + [\Magento\Backend\Model\Auth\Session::class, $sessionMock], + [\Magento\Framework\Registry::class, $registryMock], + [\Magento\Cms\Model\Wysiwyg\Config::class, $wysiwygConfigMock], + [\Magento\Store\Model\StoreManagerInterface::class, $storeManagerMock], ] ) ); @@ -433,14 +464,15 @@ public function testExecute($categoryId, $storeId, $parentId) ->method('getPostValue') ->willReturn($postData); $addData = $postData; - $addData['image'] = ['delete' => true]; $categoryMock->expects($this->once()) ->method('addData') ->with($addData); $categoryMock->expects($this->any()) ->method('getId') ->will($this->returnValue($categoryId)); - + $categoryMock->expects($this->once()) + ->method('getAttributes') + ->willReturn([]); if (!$parentId) { if ($storeId) { $storeManagerMock->expects($this->once()) @@ -499,7 +531,7 @@ public function testExecute($categoryId, $storeId, $parentId) ); $categoryResource = $this->getMock( - 'Magento\Catalog\Model\ResourceModel\Category', + \Magento\Catalog\Model\ResourceModel\Category::class, [], [], '', @@ -537,9 +569,28 @@ public function testExecute($categoryId, $storeId, $parentId) $blockMock->expects($this->once()) ->method('getGroupedHtml') ->will($this->returnValue('grouped-html')); + $entityType = $this->getMockBuilder(Type::class) + ->disableOriginalConstructor() + ->getMock(); + $entityType->expects($this->once()) + ->method('getAttributeCollection') + ->willReturn([]); $this->resultJsonFactoryMock->expects($this->once()) ->method('create') ->will($this->returnValue($resultJsonMock)); + $this->eavCongig->expects($this->once()) + ->method('getEntityType') + ->with(CategoryAttributeInterface::ENTITY_TYPE_CODE) + ->willReturn($entityType); + $store = $this->getMockBuilder(Store::class) + ->disableOriginalConstructor() + ->getMock(); + $store->expects($this->once()) + ->method('getCode') + ->willReturn('testCode'); + $this->storeManager->expects($this->once()) + ->method('getStore') + ->willReturn($store); $categoryMock->expects($this->once()) ->method('toArray') ->will($this->returnValue(['category-data'])); @@ -577,4 +628,96 @@ public function dataProviderExecute() ] ]; } + + /** + * Test Save::ImagePreprocessing() does set image attribute data to false if there are no value(image was removed). + * + * @dataProvider imagePreprocessingDataProvider + * @param array $data + * @return void + */ + 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(Save::class, [ + 'eavConfig' => $eavConfig + ]); + $result = $model->imagePreprocessing($data); + $this->assertEquals([ + 'attribute1' => false, + 'attribute2' => 123 + ], $result); + } + + /** + * Test Save::ImagePreprocessing() doesn't set image attribute data to false if image wasn't removed(value exists). + * + * @return void + */ + 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(Save::class, [ + 'eavConfig' => $eavConfig + ]); + $result = $model->imagePreprocessing([ + 'attribute1' => 'somevalue', + 'attribute2' => null + ]); + $this->assertEquals([ + 'attribute1' => 'somevalue', + 'attribute2' => null + ], $result); + } + + /** + * Test data for testImagePreprocessingWithoutValue. + * + * @return array + */ + public function imagePreprocessingDataProvider() + { + return [ + [['attribute1' => null, 'attribute2' => 123]], + [['attribute2' => 123]] + ]; + } } diff --git a/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Attribute/ValidateTest.php b/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Attribute/ValidateTest.php index eb0149544323a..64cab9578c654 100644 --- a/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Attribute/ValidateTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Attribute/ValidateTest.php @@ -198,8 +198,16 @@ public function testUniqueValidation(array $options, $isError) public function provideUniqueData() { return [ - // valid options - [ + 'no values' => [ + [ + 'delete' => [ + "option_0" => "", + "option_1" => "", + "option_2" => "", + ] + ], false + ], + 'valid options' => [ [ 'value' => [ "option_0" => [1, 0], @@ -213,8 +221,7 @@ public function provideUniqueData() ] ], false ], - //with duplicate - [ + 'duplicate options' => [ [ 'value' => [ "option_0" => [1, 0], @@ -228,8 +235,7 @@ public function provideUniqueData() ] ], true ], - //with duplicate but deleted - [ + 'duplicate and deleted' => [ [ 'value' => [ "option_0" => [1, 0], 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 8bc397b301df5..ef7f6b7f96abe 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 @@ -6,7 +6,7 @@ namespace Magento\Catalog\Test\Unit\Controller\Adminhtml\Product\Initialization; use Magento\Catalog\Api\Data\ProductLinkInterfaceFactory; -use Magento\Catalog\Api\ProductRepositoryInterface as ProductRepository; +use Magento\Catalog\Api\ProductRepositoryInterface\Proxy as ProductRepository; use Magento\Catalog\Controller\Adminhtml\Product\Initialization\Helper; use Magento\Catalog\Controller\Adminhtml\Product\Initialization\StockDataFilter; use Magento\Catalog\Model\Product; @@ -19,9 +19,6 @@ use Magento\Catalog\Api\Data\ProductCustomOptionInterfaceFactory; use Magento\Catalog\Api\Data\ProductCustomOptionInterface; use Magento\Catalog\Model\Product\Initialization\Helper\ProductLinks; -use Magento\Catalog\Model\Product\LinkTypeProvider; -use Magento\Catalog\Api\Data\ProductLinkTypeInterface; -use Magento\Catalog\Model\ProductLink\Link as ProductLink; /** * Class HelperTest @@ -107,11 +104,6 @@ class HelperTest extends \PHPUnit_Framework_TestCase */ protected $linkResolverMock; - /** - * @var \Magento\Catalog\Model\Product\LinkTypeProvider|\PHPUnit_Framework_MockObject_MockObject - */ - protected $linkTypeProviderMock; - /** * @var ProductLinks|\PHPUnit_Framework_MockObject_MockObject */ @@ -121,7 +113,6 @@ protected function setUp() { $this->objectManager = new ObjectManager($this); $this->productLinkFactoryMock = $this->getMockBuilder(ProductLinkInterfaceFactory::class) - ->setMethods(['create']) ->disableOriginalConstructor() ->getMock(); $this->productRepositoryMock = $this->getMockBuilder(ProductRepository::class) @@ -159,6 +150,7 @@ protected function setUp() '__sleep', '__wakeup', 'getSku', + 'getProductLinks', 'getWebsiteIds' ]) ->disableOriginalConstructor() @@ -176,9 +168,6 @@ protected function setUp() $this->productLinksMock->expects($this->any()) ->method('initializeLinks') ->willReturn($this->productMock); - $this->linkTypeProviderMock = $this->getMockBuilder(LinkTypeProvider::class) - ->disableOriginalConstructor() - ->getMock(); $this->helper = $this->objectManager->getObject(Helper::class, [ 'request' => $this->requestMock, @@ -189,7 +178,6 @@ protected function setUp() 'customOptionFactory' => $this->customOptionFactoryMock, 'productLinkFactory' => $this->productLinkFactoryMock, 'productRepository' => $this->productRepositoryMock, - 'linkTypeProvider' => $this->linkTypeProviderMock, ]); $this->linkResolverMock = $this->getMockBuilder(\Magento\Catalog\Model\Product\Link\Resolver::class) @@ -202,10 +190,10 @@ protected function setUp() } /** + * @covers \Magento\Catalog\Controller\Adminhtml\Product\Initialization\Helper::initialize * @SuppressWarnings(PHPMD.ExcessiveMethodLength) - * @param array $links */ - private function assembleProductMock($links = []) + public function testInitialize() { $this->customOptionMock->expects($this->once()) ->method('setProductSku'); @@ -241,6 +229,9 @@ private function assembleProductMock($links = []) $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'); @@ -263,7 +254,7 @@ private function assembleProductMock($links = []) ->method('getPost') ->with('use_default') ->willReturn($useDefaults); - $this->linkResolverMock->expects($this->once())->method('getLinks')->willReturn($links); + $this->linkResolverMock->expects($this->once())->method('getLinks')->willReturn([]); $this->stockFilterMock->expects($this->once()) ->method('filter') ->with(['stock_data']) @@ -275,6 +266,9 @@ private function assembleProductMock($links = []) $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'); @@ -289,7 +283,7 @@ private function assembleProductMock($links = []) $this->productMock->expects($this->once()) ->method('addData') ->with($productData); - $this->productMock->expects($this->any()) + $this->productMock->expects($this->once()) ->method('getSku') ->willReturn('sku'); $this->productMock->expects($this->any()) @@ -303,193 +297,10 @@ private function assembleProductMock($links = []) $this->productMock->expects($this->once()) ->method('setOptions') ->with([$this->customOptionMock]); - } - - /** - * @covers \Magento\Catalog\Controller\Adminhtml\Product\Initialization\Helper::initialize - */ - public function testInitialize() - { - $this->assembleProductMock(); - $this->linkTypeProviderMock->expects($this->once()) - ->method('getItems') - ->willReturn($this->assembleLinkTypes(['related', 'upsell', 'crosssell'])); $this->assertEquals($this->productMock, $this->helper->initialize($this->productMock)); } - /** - * @covers \Magento\Catalog\Controller\Adminhtml\Product\Initialization\Helper::initialize - * @dataProvider initializeWithLinksDataProvider - */ - public function testInitializeWithLinks($links, $linkTypes, $expectedLinks) - { - $this->productLinkFactoryMock->expects($this->any()) - ->method('create') - ->willReturnCallback(function () { - return $this->getMockBuilder(ProductLink::class) - ->setMethods(null) - ->disableOriginalConstructor() - ->getMock(); - }); - - $this->linkTypeProviderMock->expects($this->once()) - ->method('getItems') - ->willReturn($this->assembleLinkTypes($linkTypes)); - - $this->assembleProductRepositoryMock($links); - $this->assembleProductMock($links); - - $this->assertEquals($this->productMock, $this->helper->initialize($this->productMock)); - - $productLinks = $this->productMock->getProductLinks(); - $this->assertCount(count($expectedLinks), $productLinks); - $resultLinks = []; - - foreach ($productLinks as $link) { - $this->assertInstanceOf(ProductLink::class, $link); - $this->assertEquals('sku', $link->getSku()); - $resultLinks[] = ['type' => $link->getLinkType(), 'linked_product_sku' => $link->getLinkedProductSku()]; - } - - $this->assertEquals($expectedLinks, $resultLinks); - } - - /** - * @return array - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) - */ - public function initializeWithLinksDataProvider() - { - return [ - // No links - [ - 'links' => [], - 'linkTypes' => ['related', 'upsell', 'crosssell'], - 'expected_links' => [], - ], - - // Related links - [ - 'links' => [ - 'related' => [ - 0 => [ - 'id' => 1, - 'thumbnail' => 'http://magento.dev/media/no-image.jpg', - 'name' => 'Test', - 'status' => 'Enabled', - 'attribute_set' => 'Default', - 'sku' => 'Test', - 'price' => 1.00, - 'position' => 1, - 'record_id' => 1, - ] - ] - ], - 'linkTypes' => ['related', 'upsell', 'crosssell'], - 'expected_links' => [ - ['type' => 'related', 'linked_product_sku' => 'Test'], - ], - ], - - // Custom link - [ - 'links' => [ - 'customlink' => [ - 0 => [ - 'id' => 4, - 'thumbnail' => 'http://magento.dev/media/no-image.jpg', - 'name' => 'Test Custom', - 'status' => 'Enabled', - 'attribute_set' => 'Default', - 'sku' => 'Testcustom', - 'price' => 1.00, - 'position' => 1, - 'record_id' => 1, - ], - ], - ], - 'linkTypes' => ['related', 'upsell', 'crosssell', 'customlink'], - 'expected_links' => [ - ['type' => 'customlink', 'linked_product_sku' => 'Testcustom'], - ], - ], - - // Both links - [ - 'links' => [ - 'related' => [ - 0 => [ - 'id' => 1, - 'thumbnail' => 'http://magento.dev/media/no-image.jpg', - 'name' => 'Test', - 'status' => 'Enabled', - 'attribute_set' => 'Default', - 'sku' => 'Test', - 'price' => 1.00, - 'position' => 1, - 'record_id' => 1, - ], - ], - 'customlink' => [ - 0 => [ - 'id' => 4, - 'thumbnail' => 'http://magento.dev/media/no-image.jpg', - 'name' => 'Test Custom', - 'status' => 'Enabled', - 'attribute_set' => 'Default', - 'sku' => 'Testcustom', - 'price' => 2.00, - 'position' => 2, - 'record_id' => 1, - ], - ], - ], - 'linkTypes' => ['related', 'upsell', 'crosssell', 'customlink'], - 'expected_links' => [ - ['type' => 'related', 'linked_product_sku' => 'Test'], - ['type' => 'customlink', 'linked_product_sku' => 'Testcustom'], - ], - ], - - // Undefined link type - [ - 'links' => [ - 'related' => [ - 0 => [ - 'id' => 1, - 'thumbnail' => 'http://magento.dev/media/no-image.jpg', - 'name' => 'Test', - 'status' => 'Enabled', - 'attribute_set' => 'Default', - 'sku' => 'Test', - 'price' => 1.00, - 'position' => 1, - 'record_id' => 1, - ], - ], - 'customlink' => [ - 0 => [ - 'id' => 4, - 'thumbnail' => 'http://magento.dev/media/no-image.jpg', - 'name' => 'Test Custom', - 'status' => 'Enabled', - 'attribute_set' => 'Default', - 'sku' => 'Testcustom', - 'price' => 2.00, - 'position' => 2, - 'record_id' => 1, - ], - ], - ], - 'linkTypes' => ['related', 'upsell', 'crosssell'], - 'expected_links' => [ - ['type' => 'related', 'linked_product_sku' => 'Test'], - ], - ], - ]; - } - /** * Data provider for testMergeProductOptions * @@ -579,55 +390,4 @@ public function testMergeProductOptions($productOptions, $defaultOptions, $expec $result = $this->helper->mergeProductOptions($productOptions, $defaultOptions); $this->assertEquals($expectedResults, $result); } - - /** - * @param array $types - * @return array - */ - private function assembleLinkTypes($types) - { - $linkTypes = []; - $linkTypeCode = 1; - - foreach ($types as $typeName) { - $linkType = $this->getMock(ProductLinkTypeInterface::class); - $linkType->method('getCode')->willReturn($linkTypeCode++); - $linkType->method('getName')->willReturn($typeName); - - $linkTypes[] = $linkType; - } - - return $linkTypes; - } - - /** - * @param array $links - */ - private function assembleProductRepositoryMock($links) - { - $repositoryReturnMap = []; - - foreach ($links as $linkType) { - foreach ($linkType as $link) { - $mockLinkedProduct = $this->getMockBuilder(Product::class) - ->disableOriginalConstructor() - ->getMock(); - - $mockLinkedProduct->expects($this->any()) - ->method('getId') - ->willReturn($link['id']); - - $mockLinkedProduct->expects($this->any()) - ->method('getSku') - ->willReturn($link['sku']); - - // Even optional arguments need to be provided for returnMapValue - $repositoryReturnMap[] = [$link['id'], false, null, false, $mockLinkedProduct]; - } - } - - $this->productRepositoryMock->expects($this->any()) - ->method('getById') - ->will($this->returnValueMap($repositoryReturnMap)); - } } 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..9ad566cf93e36 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Unit/Model/Category/Attribute/Backend/ImageTest.php @@ -0,0 +1,293 @@ +objectManager = new 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( + LoggerInterface::class, + [], + 'TestLogger', + false, + false, + true, + ['critical'] + ); + $this->imageUploader = $this->getMockBuilder(ImageUploader::class) + ->disableOriginalConstructor() + ->setMethods(['moveFileFromTmp']) + ->getMock(); + } + + /** + * Test Image::beforeSave() returns empty string on attribute removal. + * + * @dataProvider deletedValueDataProvider + * @param array $value + * @return void + */ + public function testBeforeSaveValueDeletion($value) + { + $model = $this->objectManager->getObject(Image::class); + $model->setAttribute($this->attribute); + $object = new \Magento\Framework\DataObject([ + 'test_attribute' => $value + ]); + $model->beforeSave($object); + $this->assertEquals('', $object->getTestAttribute()); + } + + /** + * Test Image::beforeSave() with invalid attribute value returns empty string. + * + * @dataProvider invalidValueDataProvider + * @param array $value + * @return void + */ + public function testBeforeSaveValueInvalid($value) + { + $model = $this->objectManager->getObject(Image::class); + $model->setAttribute($this->attribute); + $object = new \Magento\Framework\DataObject([ + 'test_attribute' => $value + ]); + $model->beforeSave($object); + $this->assertEquals('', $object->getTestAttribute()); + } + + /** + * Test Image::beforeSave() save attribute image name. + * + * @return void + */ + public function testBeforeSaveAttributeFileName() + { + $model = $this->objectManager->getObject(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()); + } + + /** + * Test Image::beforeSave() can handle attribute value as string. + * + * @return void + */ + public function testBeforeSaveAttributeStringValue() + { + $model = $this->objectManager->getObject(Image::class); + $model->setAttribute($this->attribute); + $object = new \Magento\Framework\DataObject([ + 'test_attribute' => 'test123.jpg' + ]); + $model->beforeSave($object); + $this->assertEquals('test123.jpg', $object->getTestAttribute()); + } + + /** + * Test Image::afterSave(). + * + * @return void + */ + public function testAfterSave() + { + $model = $this->setUpModelForAfterSave(); + $this->imageUploader->expects($this->once()) + ->method('moveFileFromTmp') + ->with($this->equalTo('test1234.jpg')); + + $object = new \Magento\Framework\DataObject( + [ + 'test_attribute' => 'test1234.jpg' + ] + ); + $model->afterSave($object); + } + + /** + * Test Image::afterSave() with invalid attribute value. + * + * @dataProvider invalidValueDataProviderForAfterSave + * @param array $value + * @return void + */ + public function testAfterSaveValueInvalid($value) + { + $model = $this->setUpModelForAfterSave(); + $this->imageUploader->expects($this->never()) + ->method('moveFileFromTmp'); + $object = new \Magento\Framework\DataObject( + [ + 'test_attribute' => $value + ] + ); + $model->afterSave($object); + } + + /** + * Test Image::afterSave() log error on exception. + * + * @return void + */ + 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( + [ + 'test_attribute' => 'test1234.jpg' + ] + ); + $model->afterSave($object); + } + + /** + * Prepare Image for Image::afterSave(). + * + * @return Image + */ + private function setUpModelForAfterSave() + { + $objectManagerMock = $this->getMockBuilder(\Magento\Framework\App\ObjectManager::class) + ->disableOriginalConstructor() + ->setMethods(['get']) + ->getMock(); + $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(Image::class, [ + 'objectManager' => $objectManagerMock, + 'logger' => $this->logger + ]); + $this->objectManager->setBackwardCompatibleProperty($model, 'imageUploader', $this->imageUploader); + + return $model->setAttribute($this->attribute); + } + + /** + * Test data for testAfterSaveValueInvalid(). + * + * @return array + */ + public function invalidValueDataProviderForAfterSave() + { + return [ + [''], + [false] + ]; + } + + /** + * Test data for testBeforeSaveValueDeletion. + * + * @return array + */ + public function deletedValueDataProvider() + { + return [ + [false], + [['delete' => true]] + ]; + } + + /** + * Test data for testBeforeSaveValueInvalid. + * + * @return array + */ + public function invalidValueDataProvider() + { + $closure = function () { + return false; + }; + + return [ + [1234], + [true], + [new \stdClass()], + [$closure], + [['a' => 1, 'b' => 2]] + ]; + } +} diff --git a/app/code/Magento/Catalog/Test/Unit/Model/CategoryTest.php b/app/code/Magento/Catalog/Test/Unit/Model/CategoryTest.php index 6aae050b784b2..f363a96e11b82 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/CategoryTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/CategoryTest.php @@ -8,6 +8,7 @@ namespace Magento\Catalog\Test\Unit\Model; +use Magento\Catalog\Model\Category; use Magento\Catalog\Model\Indexer; /** @@ -16,153 +17,216 @@ */ class CategoryTest extends \PHPUnit_Framework_TestCase { - /** @var \Magento\Catalog\Model\Category */ - protected $category; + /** + * @var \Magento\Catalog\Model\Category + */ + private $category; - /** @var \Magento\Framework\Model\Context|\PHPUnit_Framework_MockObject_MockObject */ - protected $context; + /** + * @var \Magento\Framework\Model\Context|\PHPUnit_Framework_MockObject_MockObject + */ + private $context; - /** @var \Magento\Framework\Event\ManagerInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $eventManager; + /** + * @var \Magento\Framework\Event\ManagerInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $eventManager; - /** @var \Magento\Framework\App\CacheInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $cacheManager; + /** + * @var \Magento\Framework\App\CacheInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $cacheManager; - /** @var \Magento\Framework\Registry|\PHPUnit_Framework_MockObject_MockObject */ - protected $registry; + /** + * @var \Magento\Framework\Registry|\PHPUnit_Framework_MockObject_MockObject + */ + private $registry; - /** @var \Magento\Store\Model\StoreManagerInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $storeManager; + /** + * @var \Magento\Store\Model\StoreManagerInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $storeManager; - /** @var \Magento\Catalog\Model\ResourceModel\Category\Tree|\PHPUnit_Framework_MockObject_MockObject */ - protected $categoryTreeResource; + /** + * @var \Magento\Catalog\Model\ResourceModel\Category\Tree|\PHPUnit_Framework_MockObject_MockObject + */ + private $categoryTreeResource; - /** @var \PHPUnit_Framework_MockObject_MockObject */ - protected $categoryTreeFactory; + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $categoryTreeFactory; - /** @var \Magento\Catalog\Api\CategoryRepositoryInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $categoryRepository; + /** + * @var \Magento\Catalog\Api\CategoryRepositoryInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $categoryRepository; - /** @var \PHPUnit_Framework_MockObject_MockObject */ - protected $storeCollectionFactory; + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $storeCollectionFactory; - /** @var \Magento\Framework\UrlInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $url; + /** + * @var \Magento\Framework\UrlInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $url; - /** @var \PHPUnit_Framework_MockObject_MockObject */ - protected $productCollectionFactory; + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $productCollectionFactory; - /** @var \Magento\Catalog\Model\Config|\PHPUnit_Framework_MockObject_MockObject */ - protected $catalogConfig; + /** + * @var \Magento\Catalog\Model\Config|\PHPUnit_Framework_MockObject_MockObject + */ + private $catalogConfig; - /** @var \Magento\Framework\Filter\FilterManager|\PHPUnit_Framework_MockObject_MockObject */ - protected $filterManager; + /** + * @var \Magento\Framework\Filter\FilterManager|\PHPUnit_Framework_MockObject_MockObject + */ + private $filterManager; - /** @var \Magento\Catalog\Model\Indexer\Category\Flat\State|\PHPUnit_Framework_MockObject_MockObject */ - protected $flatState; + /** + * @var \Magento\Catalog\Model\Indexer\Category\Flat\State|\PHPUnit_Framework_MockObject_MockObject + */ + private $flatState; - /** @var \Magento\Framework\Indexer\IndexerInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $flatIndexer; + /** + * @var \Magento\Framework\Indexer\IndexerInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $flatIndexer; - /** @var \Magento\Framework\Indexer\IndexerInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $productIndexer; + /** + * @var \Magento\Framework\Indexer\IndexerInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $productIndexer; - /** @var \Magento\CatalogUrlRewrite\Model\CategoryUrlPathGenerator|\PHPUnit_Framework_MockObject_MockObject */ - protected $categoryUrlPathGenerator; + /** + * @var \Magento\CatalogUrlRewrite\Model\CategoryUrlPathGenerator|\PHPUnit_Framework_MockObject_MockObject + */ + private $categoryUrlPathGenerator; - /** @var \Magento\UrlRewrite\Model\UrlFinderInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $urlFinder; + /** + * @var \Magento\UrlRewrite\Model\UrlFinderInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $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->objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); $this->context = $this->getMock( - 'Magento\Framework\Model\Context', + \Magento\Framework\Model\Context::class, ['getEventDispatcher', 'getCacheManager'], [], '', false ); - - $this->eventManager = $this->getMock('Magento\Framework\Event\ManagerInterface'); + $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'); + $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'); - $this->storeManager = $this->getMock('Magento\Store\Model\StoreManagerInterface'); - $this->categoryTreeResource = $this->getMock('Magento\Catalog\Model\ResourceModel\Category\Tree', [], [], '', false); + $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( - 'Magento\Catalog\Model\ResourceModel\Category\TreeFactory', + \Magento\Catalog\Model\ResourceModel\Category\TreeFactory::class, ['create'], [], '', false); - $this->categoryRepository = $this->getMock('Magento\Catalog\Api\CategoryRepositoryInterface'); + $this->categoryRepository = $this->getMock(\Magento\Catalog\Api\CategoryRepositoryInterface::class); $this->storeCollectionFactory = $this->getMock( - 'Magento\Store\Model\ResourceModel\Store\CollectionFactory', + \Magento\Store\Model\ResourceModel\Store\CollectionFactory::class, ['create'], [], '', false ); - $this->url = $this->getMock('Magento\Framework\UrlInterface'); + $this->url = $this->getMock(\Magento\Framework\UrlInterface::class); $this->productCollectionFactory = $this->getMock( - 'Magento\Catalog\Model\ResourceModel\Product\CollectionFactory', + \Magento\Catalog\Model\ResourceModel\Product\CollectionFactory::class, ['create'], [], '', false ); - $this->catalogConfig = $this->getMock('Magento\Catalog\Model\Config', [], [], '', false); + $this->catalogConfig = $this->getMock(\Magento\Catalog\Model\Config::class, [], [], '', false); $this->filterManager = $this->getMock( - 'Magento\Framework\Filter\FilterManager', + \Magento\Framework\Filter\FilterManager::class, ['translitUrl'], [], '', false ); - $this->flatState = $this->getMock('Magento\Catalog\Model\Indexer\Category\Flat\State', [], [], '', false); - $this->flatIndexer = $this->getMock('Magento\Framework\Indexer\IndexerInterface'); - $this->productIndexer = $this->getMock('Magento\Framework\Indexer\IndexerInterface'); + $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( - 'Magento\CatalogUrlRewrite\Model\CategoryUrlPathGenerator', + \Magento\CatalogUrlRewrite\Model\CategoryUrlPathGenerator::class, [], [], '', false ); - $this->urlFinder = $this->getMock('Magento\UrlRewrite\Model\UrlFinderInterface'); + $this->urlFinder = $this->getMock(\Magento\UrlRewrite\Model\UrlFinderInterface::class); $this->resource = $this->getMock( - 'Magento\Catalog\Model\ResourceModel\Category', + \Magento\Catalog\Model\ResourceModel\Category::class, [], [], '', false ); - $this->indexerRegistry = $this->getMock('Magento\Framework\Indexer\IndexerRegistry', ['get'], [], '', false); - - $this->metadataServiceMock = $this->getMock('\Magento\Catalog\Api\CategoryAttributeRepositoryInterface'); - $this->attributeValueFactory = $this->getMockBuilder('Magento\Framework\Api\AttributeValueFactory') + $this->indexerRegistry = $this->getMock( + \Magento\Framework\Indexer\IndexerRegistry::class, + ['get'], + [], + '', + false + ); + $this->metadataServiceMock = $this->getMock(\Magento\Catalog\Api\CategoryAttributeRepositoryInterface::class); + $this->attributeValueFactory = $this->getMockBuilder(\Magento\Framework\Api\AttributeValueFactory::class) ->disableOriginalConstructor()->getMock(); - $this->category = $this->getCategoryModel(); } @@ -187,7 +251,7 @@ public function testMoveWhenCannotFindParentCategory() { $this->markTestIncomplete('MAGETWO-31165'); $parentCategory = $this->getMock( - 'Magento\Catalog\Model\Category', + \Magento\Catalog\Model\Category::class, ['getId', 'setStoreId', 'load'], [], '', @@ -197,7 +261,7 @@ public function testMoveWhenCannotFindParentCategory() $parentCategory->expects($this->any())->method('load')->will($this->returnSelf()); $this->categoryRepository->expects($this->any())->method('get')->will($this->returnValue($parentCategory)); - $store = $this->getMock('Magento\Store\Model\Store', [], [], '', false); + $store = $this->getMock(\Magento\Store\Model\Store::class, [], [], '', false); $this->storeManager->expects($this->any())->method('getStore')->will($this->returnValue($store)); $this->category->move(1, 2); @@ -212,7 +276,7 @@ public function testMoveWhenCannotFindParentCategory() public function testMoveWhenCannotFindNewCategory() { $parentCategory = $this->getMock( - 'Magento\Catalog\Model\Category', + \Magento\Catalog\Model\Category::class, ['getId', 'setStoreId', 'load'], [], '', @@ -223,7 +287,7 @@ public function testMoveWhenCannotFindNewCategory() $parentCategory->expects($this->any())->method('load')->will($this->returnSelf()); $this->categoryRepository->expects($this->any())->method('get')->will($this->returnValue($parentCategory)); - $store = $this->getMock('Magento\Store\Model\Store', [], [], '', false); + $store = $this->getMock(\Magento\Store\Model\Store::class, [], [], '', false); $this->storeManager->expects($this->any())->method('getStore')->will($this->returnValue($store)); $this->category->move(1, 2); @@ -239,7 +303,7 @@ public function testMoveWhenParentCategoryIsSameAsChildCategory() { $this->markTestIncomplete('MAGETWO-31165'); $parentCategory = $this->getMock( - 'Magento\Catalog\Model\Category', + \Magento\Catalog\Model\Category::class, ['getId', 'setStoreId', 'load'], [], '', @@ -250,7 +314,7 @@ public function testMoveWhenParentCategoryIsSameAsChildCategory() $parentCategory->expects($this->any())->method('load')->will($this->returnSelf()); $this->categoryRepository->expects($this->any())->method('get')->will($this->returnValue($parentCategory)); - $store = $this->getMock('Magento\Store\Model\Store', [], [], '', false); + $store = $this->getMock(\Magento\Store\Model\Store::class, [], [], '', false); $this->storeManager->expects($this->any())->method('getStore')->will($this->returnValue($store)); $this->category->setId(5); @@ -266,7 +330,7 @@ public function testMovePrimaryWorkflow() ->with('catalog_category_product') ->will($this->returnValue($indexer)); $parentCategory = $this->getMock( - 'Magento\Catalog\Model\Category', + \Magento\Catalog\Model\Category::class, ['getId', 'setStoreId', 'load'], [], '', @@ -277,7 +341,7 @@ public function testMovePrimaryWorkflow() $parentCategory->expects($this->any())->method('load')->will($this->returnSelf()); $this->categoryRepository->expects($this->any())->method('get')->will($this->returnValue($parentCategory)); - $store = $this->getMock('Magento\Store\Model\Store', [], [], '', false); + $store = $this->getMock(\Magento\Store\Model\Store::class, [], [], '', false); $this->storeManager->expects($this->any())->method('getStore')->will($this->returnValue($store)); $this->category->setId(3); @@ -299,10 +363,17 @@ public function testGetUseFlatResourceTrue() $this->assertEquals(true, $category->getUseFlatResource()); } - protected function getCategoryModel() + /** + * Create \Magento\Catalog\Model\Category instance. + * + * @return \Magento\Catalog\Model\Category + */ + private function getCategoryModel() { - return (new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this))->getObject( - 'Magento\Catalog\Model\Category', + $objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); + /** @var Category $category */ + $category = $objectManager->getObject( + \Magento\Catalog\Model\Category::class, [ 'context' => $this->context, 'registry' => $this->registry, @@ -326,8 +397,15 @@ protected function getCategoryModel() 'customAttributeFactory' => $this->attributeValueFactory, ] ); + + return $category; } + /** + * Test data for testReindexFlatEnabled. + * + * @return array + */ public function reindexFlatEnabledTestDataProvider() { return [ @@ -377,6 +455,11 @@ public function testReindexFlatEnabled($flatScheduled, $productScheduled, $expec $this->category->reindex(); } + /** + * Test data for testReindexFlatDisabled. + * + * @return array + */ public function reindexFlatDisabledTestDataProvider() { return [ @@ -438,11 +521,11 @@ public function testGetCustomAttributes() { $nameAttributeCode = 'name'; $descriptionAttributeCode = 'description'; - $interfaceAttribute = $this->getMock('\Magento\Framework\Api\MetadataObjectInterface'); + $interfaceAttribute = $this->getMock(\Magento\Framework\Api\MetadataObjectInterface::class); $interfaceAttribute->expects($this->once()) ->method('getAttributeCode') ->willReturn($nameAttributeCode); - $descriptionAttribute = $this->getMock('\Magento\Framework\Api\MetadataObjectInterface'); + $descriptionAttribute = $this->getMock(\Magento\Framework\Api\MetadataObjectInterface::class); $descriptionAttribute->expects($this->once()) ->method('getAttributeCode') ->willReturn($descriptionAttributeCode); @@ -475,4 +558,86 @@ public function testGetCustomAttributes() $this->category->getCustomAttribute($descriptionAttributeCode)->getValue() ); } + + /** + * Test get image url by attribute code. + * + * @param string|bool $value + * @param string|bool $url + * @return void + * + * @dataProvider getImageWithAttributeCodeDataProvider + */ + public function testGetImageWithAttributeCode($value, $url) + { + $storeManager = $this->getMockBuilder(\Magento\Store\Model\StoreManager::class) + ->disableOriginalConstructor() + ->setMethods(['getStore']) + ->getMock(); + $store = $this->getMockBuilder(\Magento\Store\Model\Store::class) + ->disableOriginalConstructor() + ->setMethods(['getBaseUrl']) + ->getMock(); + $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); + } + + /** + * Test data for testGetImageWithAttributeCode. + * + * @return array + */ + public function getImageWithAttributeCodeDataProvider() + { + return [ + ['testimage', 'http://www.example.com/catalog/category/testimage'], + [false, false] + ]; + } + + /** + * Test get image url without specifying attribute code. + * + * @return void + */ + public function testGetImageWithoutAttributeCode() + { + $storeManager = $this->getMockBuilder(\Magento\Store\Model\StoreManager::class) + ->disableOriginalConstructor() + ->setMethods(['getStore']) + ->getMock(); + $store = $this->getMockBuilder(\Magento\Store\Model\Store::class) + ->disableOriginalConstructor() + ->setMethods(['getBaseUrl']) + ->getMock(); + $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/CopierTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Product/CopierTest.php index 211eae79e439e..a3e2663fc41d2 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/Product/CopierTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/Product/CopierTest.php @@ -7,6 +7,9 @@ use \Magento\Catalog\Model\Product\Copier; +/** + * Test class for \Magento\Catalog\Model\Product\Copier. + */ class CopierTest extends \PHPUnit_Framework_TestCase { /** @@ -20,17 +23,17 @@ class CopierTest extends \PHPUnit_Framework_TestCase protected $_model; /** - * @var \PHPUnit_Framework_MockObject_MockObject + * @var \Magento\Catalog\Model\Product\CopyConstructorInterface|\PHPUnit_Framework_MockObject_MockObject */ protected $copyConstructorMock; /** - * @var \PHPUnit_Framework_MockObject_MockObject + * @var \Magento\Catalog\Model\ProductFactory|\PHPUnit_Framework_MockObject_MockObject */ protected $productFactoryMock; /** - * @var \PHPUnit_Framework_MockObject_MockObject + * @var \Magento\Catalog\Model\Product|\PHPUnit_Framework_MockObject_MockObject */ protected $productMock; @@ -41,29 +44,29 @@ class CopierTest extends \PHPUnit_Framework_TestCase protected function setUp() { - $this->copyConstructorMock = $this->getMock('\Magento\Catalog\Model\Product\CopyConstructorInterface'); + $this->copyConstructorMock = $this->getMock(\Magento\Catalog\Model\Product\CopyConstructorInterface::class); $this->productFactoryMock = $this->getMock( - '\Magento\Catalog\Model\ProductFactory', + \Magento\Catalog\Model\ProductFactory::class, ['create'], [], '', false ); $this->optionRepositoryMock = $this->getMock( - 'Magento\Catalog\Model\Product\Option\Repository', + \Magento\Catalog\Model\Product\Option\Repository::class, [], [], '', false ); $this->optionRepositoryMock; - $this->productMock = $this->getMock('\Magento\Catalog\Model\Product', [], [], '', false); + $this->productMock = $this->getMock(\Magento\Catalog\Model\Product::class, [], [], '', false); $this->productMock->expects($this->any())->method('getEntityId')->willReturn(1); - $this->metadata = $this->getMockBuilder('Magento\Framework\EntityManager\EntityMetadata') + $this->metadata = $this->getMockBuilder(\Magento\Framework\EntityManager\EntityMetadata::class) ->disableOriginalConstructor() ->getMock(); - $metadataPool = $this->getMockBuilder('Magento\Framework\EntityManager\MetadataPool') + $metadataPool = $this->getMockBuilder(\Magento\Framework\EntityManager\MetadataPool::class) ->disableOriginalConstructor() ->getMock(); $metadataPool->expects($this->any())->method('getMetadata')->willReturn($this->metadata); @@ -78,6 +81,11 @@ protected function setUp() ]); } + /** + * @covers \Magento\Catalog\Model\Product\Copier::copy + * + * @return void + */ public function testCopy() { $this->productMock->expects($this->atLeastOnce())->method('getWebsiteIds'); @@ -87,18 +95,18 @@ public function testCopy() ['linkField', null, '1'], ]); - $resourceMock = $this->getMock('\Magento\Catalog\Model\ResourceModel\Product', [], [], '', false); + $resourceMock = $this->getMock(\Magento\Catalog\Model\ResourceModel\Product::class, [], [], '', false); $this->productMock->expects($this->once())->method('getResource')->will($this->returnValue($resourceMock)); $duplicateMock = $this->getMock( - '\Magento\Catalog\Model\Product', + \Magento\Catalog\Model\Product::class, [ '__wakeup', 'setData', 'setOptions', 'getData', 'setIsDuplicate', - 'setOriginalId', + 'setOriginalLinkId', 'setStatus', 'setCreatedAt', 'setUpdatedAt', @@ -117,7 +125,7 @@ public function testCopy() $duplicateMock->expects($this->once())->method('setOptions')->with([]); $duplicateMock->expects($this->once())->method('setIsDuplicate')->with(true); - $duplicateMock->expects($this->once())->method('setOriginalId')->with(1); + $duplicateMock->expects($this->once())->method('setOriginalLinkId')->with(1); $duplicateMock->expects( $this->once() )->method( @@ -154,8 +162,11 @@ public function testCopy() } /** + * Set object non-public properties. + * * @param $object * @param array $properties + * @return void */ private function setProperties($object, $properties = []) { diff --git a/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/AbstractTest.php b/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/AbstractTest.php index f30457a53ddbd..1dabd8b47f894 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/AbstractTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/AbstractTest.php @@ -47,11 +47,13 @@ public function testWalkAttributes() $code = 'test_attr'; $set = 10; + $storeId = 100; $object = $this->getMock('Magento\Catalog\Model\Product', ['__wakeup'], [], '', false); $object->setData('test_attr', 'test_attr'); $object->setData('attribute_set_id', $set); + $object->setData('store_id', $storeId); $entityType = new \Magento\Framework\DataObject(); $entityType->setEntityTypeCode('test'); diff --git a/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/CategoryTest.php b/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/CategoryTest.php new file mode 100644 index 0000000000000..2c91a91042b24 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/CategoryTest.php @@ -0,0 +1,146 @@ +selectMock = $this->getMockBuilder(Select::class)->disableOriginalConstructor()->getMock(); + $this->selectMock->expects($this->at(2))->method('where')->willReturnSelf(); + $this->selectMock->expects($this->once())->method('from')->willReturnSelf(); + $this->selectMock->expects($this->once())->method('joinLeft')->willReturnSelf(); + $this->connectionMock = $this->getMockBuilder(Adapter::class)->getMockForAbstractClass(); + $this->connectionMock->expects($this->once())->method('select')->willReturn($this->selectMock); + $this->resourceMock = $this->getMockBuilder(ResourceConnection::class)->disableOriginalConstructor()->getMock(); + $this->resourceMock->expects($this->any())->method('getConnection')->willReturn($this->connectionMock); + $this->connectionMock->expects($this->any())->method('getTableName')->willReturn('TableName'); + $this->resourceMock->expects($this->any())->method('getTableName')->willReturn('TableName'); + $this->contextMock = $this->getMockBuilder(Context::class)->disableOriginalConstructor()->getMock(); + $this->eavConfigMock = $this->getMockBuilder(Config::class)->disableOriginalConstructor()->getMock(); + $this->entityType = $this->getMockBuilder(Type::class)->disableOriginalConstructor()->getMock(); + $this->eavConfigMock->expects($this->any())->method('getEntityType')->willReturn($this->entityType); + $this->contextMock->expects($this->any())->method('getEavConfig')->willReturn($this->eavConfigMock); + $this->contextMock->expects($this->any())->method('getResource')->willReturn($this->resourceMock); + $this->storeManagerMock = $this->getMockBuilder(StoreManagerInterface::class)->getMock(); + $this->factoryMock = $this->getMockBuilder(Factory::class)->disableOriginalConstructor()->getMock(); + $this->managerMock = $this->getMockBuilder(ManagerInterface::class)->getMock(); + $this->treeFactoryMock = $this->getMockBuilder(Category\TreeFactory::class) + ->disableOriginalConstructor() + ->getMock(); + $this->collectionFactoryMock = $this->getMockBuilder(CollectionFactory::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->category = new Category( + $this->contextMock, + $this->storeManagerMock, + $this->factoryMock, + $this->managerMock, + $this->treeFactoryMock, + $this->collectionFactoryMock, + [] + ); + } + + /** + * @return void + */ + public function testFindWhereAttributeIs() + { + $entityIdsFilter = [1, 2]; + $expectedValue = 123; + $attribute = $this->getMockBuilder(Attribute::class)->disableOriginalConstructor()->getMock(); + $backendModel = $this->getMockBuilder(AbstractBackend::class)->disableOriginalConstructor()->getMock(); + + $attribute->expects($this->any())->method('getBackend')->willReturn($backendModel); + $this->connectionMock->expects($this->once())->method('fetchCol')->willReturn(['result']); + + $result = $this->category->findWhereAttributeIs($entityIdsFilter, $attribute, $expectedValue); + $this->assertEquals(['result'], $result); + } +} 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 2fa57eed2b253..37aaae9b1b15a 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 @@ -10,6 +10,8 @@ /** * Class CollectionTest + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class CollectionTest extends \PHPUnit_Framework_TestCase { @@ -53,68 +55,68 @@ class CollectionTest extends \PHPUnit_Framework_TestCase */ protected function setUp() { - $entityFactory = $this->getMock('Magento\Framework\Data\Collection\EntityFactory', [], [], '', false); - $logger = $this->getMockBuilder('Psr\Log\LoggerInterface') + $entityFactory = $this->getMock(\Magento\Framework\Data\Collection\EntityFactory::class, [], [], '', false); + $logger = $this->getMockBuilder(\Psr\Log\LoggerInterface::class) ->disableOriginalConstructor() ->getMockForAbstractClass(); - $fetchStrategy = $this->getMockBuilder('Magento\Framework\Data\Collection\Db\FetchStrategyInterface') + $fetchStrategy = $this->getMockBuilder(\Magento\Framework\Data\Collection\Db\FetchStrategyInterface::class) ->disableOriginalConstructor() ->getMockForAbstractClass(); - $eventManager = $this->getMockBuilder('Magento\Framework\Event\ManagerInterface') + $eventManager = $this->getMockBuilder(\Magento\Framework\Event\ManagerInterface::class) ->disableOriginalConstructor() ->getMockForAbstractClass(); - $eavConfig = $this->getMockBuilder('Magento\Eav\Model\Config') + $eavConfig = $this->getMockBuilder(\Magento\Eav\Model\Config::class) ->disableOriginalConstructor() ->getMock(); - $resource = $this->getMockBuilder('Magento\Framework\App\ResourceConnection') + $resource = $this->getMockBuilder(\Magento\Framework\App\ResourceConnection::class) ->disableOriginalConstructor() ->getMock(); - $eavEntityFactory = $this->getMockBuilder('Magento\Eav\Model\EntityFactory') + $eavEntityFactory = $this->getMockBuilder(\Magento\Eav\Model\EntityFactory::class) ->disableOriginalConstructor() ->getMock(); - $resourceHelper = $this->getMockBuilder('Magento\Catalog\Model\ResourceModel\Helper') + $resourceHelper = $this->getMockBuilder(\Magento\Catalog\Model\ResourceModel\Helper::class) ->disableOriginalConstructor() ->getMock(); - $universalFactory = $this->getMockBuilder('Magento\Framework\Validator\UniversalFactory') + $universalFactory = $this->getMockBuilder(\Magento\Framework\Validator\UniversalFactory::class) ->disableOriginalConstructor() ->getMock(); - $storeManager = $this->getMockBuilder('Magento\Store\Model\StoreManagerInterface') + $storeManager = $this->getMockBuilder(\Magento\Store\Model\StoreManagerInterface::class) ->disableOriginalConstructor() ->setMethods(['getStore', 'getId']) ->getMockForAbstractClass(); - $moduleManager = $this->getMockBuilder('Magento\Framework\Module\Manager') + $moduleManager = $this->getMockBuilder(\Magento\Framework\Module\Manager::class) ->disableOriginalConstructor() ->getMock(); - $catalogProductFlatState = $this->getMockBuilder('Magento\Catalog\Model\Indexer\Product\Flat\State') + $catalogProductFlatState = $this->getMockBuilder(\Magento\Catalog\Model\Indexer\Product\Flat\State::class) ->disableOriginalConstructor() ->getMock(); - $scopeConfig = $this->getMockBuilder('Magento\Framework\App\Config\ScopeConfigInterface') + $scopeConfig = $this->getMockBuilder(\Magento\Framework\App\Config\ScopeConfigInterface::class) ->disableOriginalConstructor() ->getMockForAbstractClass(); - $productOptionFactory = $this->getMockBuilder('Magento\Catalog\Model\Product\OptionFactory') + $productOptionFactory = $this->getMockBuilder(\Magento\Catalog\Model\Product\OptionFactory::class) ->disableOriginalConstructor() ->getMock(); - $catalogUrl = $this->getMockBuilder('Magento\Catalog\Model\ResourceModel\Url') + $catalogUrl = $this->getMockBuilder(\Magento\Catalog\Model\ResourceModel\Url::class) ->disableOriginalConstructor() ->getMock(); - $localeDate = $this->getMockBuilder('Magento\Framework\Stdlib\DateTime\TimezoneInterface') + $localeDate = $this->getMockBuilder(\Magento\Framework\Stdlib\DateTime\TimezoneInterface::class) ->disableOriginalConstructor() ->getMockForAbstractClass(); - $customerSession = $this->getMockBuilder('Magento\Customer\Model\Session') + $customerSession = $this->getMockBuilder(\Magento\Customer\Model\Session::class) ->disableOriginalConstructor() ->getMock(); - $dateTime = $this->getMockBuilder('Magento\Framework\Stdlib\DateTime') + $dateTime = $this->getMockBuilder(\Magento\Framework\Stdlib\DateTime::class) ->disableOriginalConstructor() ->getMock(); - $groupManagement = $this->getMockBuilder('Magento\Customer\Api\GroupManagementInterface') + $groupManagement = $this->getMockBuilder(\Magento\Customer\Api\GroupManagementInterface::class) ->disableOriginalConstructor() ->getMockForAbstractClass(); - $this->connectionMock = $this->getMockBuilder('Magento\Framework\DB\Adapter\AdapterInterface') + $this->connectionMock = $this->getMockBuilder(\Magento\Framework\DB\Adapter\AdapterInterface::class) ->disableOriginalConstructor() ->getMockForAbstractClass(); - $this->selectMock = $this->getMockBuilder('Magento\Framework\DB\Select') + $this->selectMock = $this->getMockBuilder(\Magento\Framework\DB\Select::class) ->disableOriginalConstructor() ->getMock(); @@ -147,8 +149,8 @@ protected function setUp() $this->prepareObjectManager([ [ - 'Magento\Catalog\Model\ResourceModel\Product\Collection\ProductLimitation', - $this->getMock('Magento\Catalog\Model\ResourceModel\Product\Collection\ProductLimitation') + \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, @@ -164,7 +166,7 @@ protected function setUp() ] ]); $this->collection = $helper->getObject( - 'Magento\Catalog\Model\ResourceModel\Product\Collection', + \Magento\Catalog\Model\ResourceModel\Product\Collection::class, [ 'entityFactory' => $entityFactory, 'logger' => $logger, @@ -217,14 +219,19 @@ public function testAddProductCategoriesFilter() $this->collection->addCategoriesFilter([$conditionType => $values]); } - public function testAddMediaGalleryData() + /** + * @dataProvider addMediaGalleryDataDataProvider + * @param string $linkField + * @param int $linkFieldId + * @throws \Magento\Framework\Exception\LocalizedException + */ + public function testAddMediaGalleryData($linkField, $linkFieldId) { $attributeId = 42; - $itemId = 4242; - $linkField = 'entity_id'; - $mediaGalleriesMock = [[$linkField => $itemId]]; + $mediaGalleriesMock = [[$linkField => $linkFieldId]]; $itemMock = $this->getMockBuilder(\Magento\Catalog\Model\Product::class) ->disableOriginalConstructor() + ->setMethods(['getData']) ->getMock(); $attributeMock = $this->getMockBuilder(\Magento\Eav\Model\Entity\Attribute\AbstractAttribute::class) ->disableOriginalConstructor() @@ -244,13 +251,13 @@ public function testAddMediaGalleryData() $this->galleryResourceMock->expects($this->once())->method('createBatchBaseSelect')->willReturn($selectMock); $attributeMock->expects($this->once())->method('getAttributeId')->willReturn($attributeId); $this->entityMock->expects($this->once())->method('getAttribute')->willReturn($attributeMock); - $itemMock->expects($this->atLeastOnce())->method('getId')->willReturn($itemId); - $selectMock->expects($this->once())->method('where')->with('entity.' . $linkField . ' IN (?)', [$itemId]); + $itemMock->expects($this->atLeastOnce())->method('getData')->willReturn($linkFieldId); + $selectMock->expects($this->once())->method('where')->with('entity.' . $linkField . ' IN (?)', [$linkFieldId]); $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]] + [[$linkField => $linkFieldId]] ); $this->galleryReadHandlerMock->expects($this->once())->method('addMediaDataToProduct') ->with($itemMock, $mediaGalleriesMock); @@ -258,17 +265,28 @@ public function testAddMediaGalleryData() $this->assertSame($this->collection, $this->collection->addMediaGalleryData()); } + /** + * @return array + */ + public function addMediaGalleryDataDataProvider() + { + return [ + ['entity_id', 4242], + ['row_id', 4] + ]; + } + /** * @param $map */ private function prepareObjectManager($map) { - $objectManagerMock = $this->getMock('Magento\Framework\ObjectManagerInterface'); + $objectManagerMock = $this->getMock(\Magento\Framework\ObjectManagerInterface::class); $objectManagerMock->expects($this->any())->method('getInstance')->willReturnSelf(); $objectManagerMock->expects($this->any()) ->method('get') ->will($this->returnValueMap($map)); - $reflectionClass = new \ReflectionClass('Magento\Framework\App\ObjectManager'); + $reflectionClass = new \ReflectionClass(\Magento\Framework\App\ObjectManager::class); $reflectionProperty = $reflectionClass->getProperty('_instance'); $reflectionProperty->setAccessible(true); $reflectionProperty->setValue($objectManagerMock); diff --git a/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Product/GalleryTest.php b/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Product/GalleryTest.php index 0518cc9ac5066..ce0b2fe6b36c4 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Product/GalleryTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Product/GalleryTest.php @@ -53,7 +53,7 @@ protected function setUp() $objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); $this->connection = $this->getMock( - 'Magento\Framework\DB\Adapter\Pdo\Mysql', + \Magento\Framework\DB\Adapter\Pdo\Mysql::class, [], [], '', @@ -63,7 +63,7 @@ protected function setUp() ->method('setCacheAdapter'); $metadata = $this->getMock( - 'Magento\Framework\EntityManager\EntityMetadata', + \Magento\Framework\EntityManager\EntityMetadata::class, [], [], '', @@ -77,7 +77,7 @@ protected function setUp() ->willReturn($this->connection); $metadataPool = $this->getMock( - 'Magento\Framework\EntityManager\MetadataPool', + \Magento\Framework\EntityManager\MetadataPool::class, [], [], '', @@ -85,21 +85,27 @@ protected function setUp() ); $metadataPool->expects($this->once()) ->method('getMetadata') - ->with('Magento\Catalog\Api\Data\ProductInterface') + ->with(\Magento\Catalog\Api\Data\ProductInterface::class) ->willReturn($metadata); - $resource = $this->getMock('Magento\Framework\App\ResourceConnection', [], [], '', false); + $resource = $this->getMock(\Magento\Framework\App\ResourceConnection::class, [], [], '', false); $resource->expects($this->any())->method('getTableName')->willReturn('table'); $this->resource = $objectManager->getObject( - 'Magento\Catalog\Model\ResourceModel\Product\Gallery', + \Magento\Catalog\Model\ResourceModel\Product\Gallery::class, [ 'metadataPool' => $metadataPool, 'resource' => $resource ] ); - $this->product = $this->getMock('Magento\Catalog\Model\Product', [], [], '', false); - $this->select = $this->getMock('Magento\Framework\DB\Select', [], [], '', false); - $this->attribute = $this->getMock('Magento\Eav\Model\Entity\Attribute\AbstractAttribute', [], [], '', false); + $this->product = $this->getMock(\Magento\Catalog\Model\Product::class, [], [], '', false); + $this->select = $this->getMock(\Magento\Framework\DB\Select::class, [], [], '', false); + $this->attribute = $this->getMock( + \Magento\Eav\Model\Entity\Attribute\AbstractAttribute::class, + [], + [], + '', + false + ); } public function testLoadDataFromTableByValueId() @@ -437,4 +443,32 @@ public function testDeleteGalleryValueInStore() $this->resource->deleteGalleryValueInStore($valueId, $entityId, $storeId); } + + public function testCountImageUses() + { + $results = [ + [ + 'value_id' => '1', + 'attribute_id' => 90, + 'value' => '/d/o/download_7.jpg', + 'media_type' => 'image', + 'disabled' => '0', + ], + ]; + + $this->connection->expects($this->once())->method('select')->will($this->returnValue($this->select)); + $this->select->expects($this->at(0)) + ->method('from') + ->with(['main' => 'table'], 'count(*)') + ->willReturnSelf(); + $this->select->expects($this->at(1)) + ->method('where') + ->with('value = ?', 1) + ->willReturnSelf(); + $this->connection->expects($this->once()) + ->method('fetchOne') + ->with($this->select) + ->willReturn(count($results)); + $this->assertEquals($this->resource->countImageUses(1), count($results)); + } } diff --git a/app/code/Magento/Catalog/Test/Unit/Pricing/Price/MinimalTierPriceCalculatorTest.php b/app/code/Magento/Catalog/Test/Unit/Pricing/Price/MinimalTierPriceCalculatorTest.php new file mode 100644 index 0000000000000..a649d3500c965 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Unit/Pricing/Price/MinimalTierPriceCalculatorTest.php @@ -0,0 +1,112 @@ +price = $this->getMock(TierPrice::class, [], [], '', false); + $this->priceInfo = $this->getMockForAbstractClass(PriceInfoInterface::class); + $this->saleable = $this->getMockForAbstractClass(SaleableInterface::class); + + $this->objectManager = new ObjectManager($this); + + $this->calculator = $this->getMockForAbstractClass(CalculatorInterface::class); + $this->object = $this->objectManager->getObject( + MinimalTierPriceCalculator::class, + ['calculator' => $this->calculator] + ); + } + + private function getValueTierPricesExistShouldReturnMinTierPrice() + { + $minPrice = 5; + $notMinPrice = 10; + + $minAmount = $this->getMockForAbstractClass(AmountInterface::class); + $minAmount->expects($this->once())->method('getValue')->willReturn($minPrice); + + $notMinAmount = $this->getMockForAbstractClass(AmountInterface::class); + $notMinAmount->expects($this->once())->method('getValue')->willReturn($notMinPrice); + $tierPriceList = [ + [ + 'price' => $minAmount + ], + [ + 'price' => $notMinAmount + ] + ]; + + $this->price->expects($this->once())->method('getTierPriceList')->willReturn($tierPriceList); + + $this->priceInfo->expects($this->once())->method('getPrice')->with(TierPrice::PRICE_CODE) + ->willReturn($this->price); + + $this->saleable->expects($this->once())->method('getPriceInfo')->willReturn($this->priceInfo); + + return $minPrice; + } + + public function testGetValueTierPricesExistShouldReturnMinTierPrice() + { + $minPrice = $this->getValueTierPricesExistShouldReturnMinTierPrice(); + $this->assertEquals($minPrice, $this->object->getValue($this->saleable)); + } + + public function testGetGetAmountMinTierPriceExistShouldReturnAmountObject() + { + $minPrice = $this->getValueTierPricesExistShouldReturnMinTierPrice(); + + $amount = $this->getMockForAbstractClass(AmountInterface::class); + + $this->calculator->expects($this->once()) + ->method('getAmount') + ->with($minPrice, $this->saleable) + ->willReturn($amount); + + $this->assertSame($amount, $this->object->getAmount($this->saleable)); + } +} diff --git a/app/code/Magento/Catalog/Test/Unit/Pricing/Render/FinalPriceBoxTest.php b/app/code/Magento/Catalog/Test/Unit/Pricing/Render/FinalPriceBoxTest.php index f2455661cf2d6..0b1690c8f73f1 100644 --- a/app/code/Magento/Catalog/Test/Unit/Pricing/Render/FinalPriceBoxTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Pricing/Render/FinalPriceBoxTest.php @@ -9,6 +9,10 @@ use Magento\Catalog\Model\Product\Pricing\Renderer\SalableResolverInterface; use Magento\Framework\Module\Manager; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\Catalog\Pricing\Price\MinimalPriceCalculatorInterface; +use Magento\Framework\Pricing\Amount\AmountInterface; +use Magento\Framework\Pricing\Render\Amount; +use Magento\Catalog\Pricing\Price\FinalPrice; /** * Class FinalPriceBoxTest @@ -72,6 +76,11 @@ class FinalPriceBoxTest extends \PHPUnit_Framework_TestCase /** @var Manager|\PHPUnit_Framework_MockObject_MockObject */ private $moduleManager; + /** + * @var MinimalPriceCalculatorInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $minimalPriceCalculator; + /** * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ @@ -168,7 +177,7 @@ protected function setUp() $this->price = $this->getMock(\Magento\Framework\Pricing\Price\PriceInterface::class); $this->price->expects($this->any()) ->method('getPriceCode') - ->will($this->returnValue(\Magento\Catalog\Pricing\Price\FinalPrice::PRICE_CODE)); + ->will($this->returnValue(FinalPrice::PRICE_CODE)); $this->objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); @@ -176,6 +185,10 @@ protected function setUp() ->disableOriginalConstructor() ->getMockForAbstractClass(); + $this->minimalPriceCalculator =$this->getMockBuilder(MinimalPriceCalculatorInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + $this->object = $this->objectManager->getObject( \Magento\Catalog\Pricing\Render\FinalPriceBox::class, [ @@ -184,7 +197,8 @@ protected function setUp() 'rendererPool' => $this->rendererPool, 'price' => $this->price, 'data' => ['zone' => 'test_zone', 'list_category_page' => true], - 'salableResolver' => $this->salableResolverMock + 'salableResolver' => $this->salableResolverMock, + 'minimalPriceCalculator' => $this->minimalPriceCalculator ] ); @@ -316,12 +330,18 @@ public function testRenderMsrpNotRegisteredException() public function testRenderAmountMinimal() { - $priceType = $this->getMock(\Magento\Catalog\Pricing\Price\FinalPrice::class, [], [], '', false); - $amount = $this->getMockForAbstractClass(\Magento\Framework\Pricing\Amount\AmountInterface::class); $priceId = 'price_id'; $html = 'html'; $this->object->setData('price_id', $priceId); + $this->product->expects($this->never())->method('getId'); + + $amount = $this->getMockForAbstractClass(AmountInterface::class); + + $this->minimalPriceCalculator->expects($this->once())->method('getAmount') + ->with($this->product) + ->willReturn($amount); + $arguments = [ 'zone' => 'test_zone', 'list_category_page' => true, @@ -331,24 +351,15 @@ public function testRenderAmountMinimal() 'skip_adjustments' => true, ]; - $amountRender = $this->getMock(\Magento\Framework\Pricing\Render\Amount::class, ['toHtml'], [], '', false); + $amountRender = $this->getMock(Amount::class, ['toHtml'], [], '', false); $amountRender->expects($this->once()) ->method('toHtml') - ->will($this->returnValue($html)); - - $this->priceInfo->expects($this->once()) - ->method('getPrice') - ->with(\Magento\Catalog\Pricing\Price\FinalPrice::PRICE_CODE) - ->will($this->returnValue($priceType)); - - $priceType->expects($this->once()) - ->method('getMinimalPrice') - ->will($this->returnValue($amount)); + ->willReturn($html); $this->rendererPool->expects($this->once()) ->method('createAmountRender') ->with($amount, $this->product, $this->price, $arguments) - ->will($this->returnValue($amountRender)); + ->willReturn($amountRender); $this->assertEquals($html, $this->object->renderAmountMinimal()); } @@ -362,9 +373,9 @@ public function testRenderAmountMinimal() public function testHasSpecialPrice($regularPrice, $finalPrice, $expectedResult) { $regularPriceType = $this->getMock(\Magento\Catalog\Pricing\Price\RegularPrice::class, [], [], '', false); - $finalPriceType = $this->getMock(\Magento\Catalog\Pricing\Price\FinalPrice::class, [], [], '', false); - $regularPriceAmount = $this->getMockForAbstractClass(\Magento\Framework\Pricing\Amount\AmountInterface::class); - $finalPriceAmount = $this->getMockForAbstractClass(\Magento\Framework\Pricing\Amount\AmountInterface::class); + $finalPriceType = $this->getMock(FinalPrice::class, [], [], '', false); + $regularPriceAmount = $this->getMockForAbstractClass(AmountInterface::class); + $finalPriceAmount = $this->getMockForAbstractClass(AmountInterface::class); $regularPriceAmount->expects($this->once()) ->method('getValue') @@ -386,7 +397,7 @@ public function testHasSpecialPrice($regularPrice, $finalPrice, $expectedResult) ->will($this->returnValue($regularPriceType)); $this->priceInfo->expects($this->at(1)) ->method('getPrice') - ->with(\Magento\Catalog\Pricing\Price\FinalPrice::PRICE_CODE) + ->with(FinalPrice::PRICE_CODE) ->will($this->returnValue($finalPriceType)); $this->assertEquals($expectedResult, $this->object->hasSpecialPrice()); @@ -403,35 +414,30 @@ public function hasSpecialPriceProvider() public function testShowMinimalPrice() { - $finalPrice = 10.0; $minimalPrice = 5.0; - $displayMininmalPrice = 2.0; - - $this->object->setDisplayMinimalPrice($displayMininmalPrice); + $finalPrice = 10.0; + $displayMininmalPrice = true; - $finalPriceType = $this->getMock(\Magento\Catalog\Pricing\Price\FinalPrice::class, [], [], '', false); + $this->minimalPriceCalculator->expects($this->once())->method('getValue')->with($this->product) + ->willReturn($minimalPrice); - $finalPriceAmount = $this->getMockForAbstractClass(\Magento\Framework\Pricing\Amount\AmountInterface::class); - $minimalPriceAmount = $this->getMockForAbstractClass(\Magento\Framework\Pricing\Amount\AmountInterface::class); + $finalPriceAmount = $this->getMockForAbstractClass(AmountInterface::class); $finalPriceAmount->expects($this->once()) ->method('getValue') ->will($this->returnValue($finalPrice)); - $minimalPriceAmount->expects($this->once()) - ->method('getValue') - ->will($this->returnValue($minimalPrice)); - $finalPriceType->expects($this->at(0)) + $finalPriceType = $this->getMock(FinalPrice::class, [], [], '', false); + $finalPriceType->expects($this->once()) ->method('getAmount') ->will($this->returnValue($finalPriceAmount)); - $finalPriceType->expects($this->at(1)) - ->method('getMinimalPrice') - ->will($this->returnValue($minimalPriceAmount)); $this->priceInfo->expects($this->once()) ->method('getPrice') - ->with(\Magento\Catalog\Pricing\Price\FinalPrice::PRICE_CODE) - ->will($this->returnValue($finalPriceType)); + ->with(FinalPrice::PRICE_CODE) + ->willReturn($finalPriceType); + + $this->object->setDisplayMinimalPrice($displayMininmalPrice); $this->assertTrue($this->object->showMinimalPrice()); } diff --git a/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/EavTest.php b/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/EavTest.php index d9aa1e32b044d..ff1a7042aa224 100755 --- a/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/EavTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/EavTest.php @@ -5,33 +5,36 @@ */ namespace Magento\Catalog\Test\Unit\Ui\DataProvider\Product\Form\Modifier; -use Magento\Catalog\Model\Product\Type; +use Magento\Catalog\Api\Data\ProductAttributeInterface; +use Magento\Catalog\Api\ProductAttributeGroupRepositoryInterface; +use Magento\Catalog\Api\ProductAttributeRepositoryInterface; +use Magento\Catalog\Model\ResourceModel\Eav\Attribute; +use Magento\Catalog\Model\ResourceModel\Eav\Attribute as EavAttribute; +use Magento\Catalog\Model\ResourceModel\Eav\AttributeFactory as EavAttributeFactory; use Magento\Catalog\Ui\DataProvider\Product\Form\Modifier\Eav; +use Magento\Eav\Api\Data\AttributeGroupInterface; use Magento\Eav\Model\Config; -use Magento\Framework\App\RequestInterface; -use Magento\Store\Model\StoreManagerInterface; -use Magento\Store\Api\Data\StoreInterface; -use Magento\Ui\DataProvider\EavValidationRules; -use Magento\Eav\Model\ResourceModel\Entity\Attribute\Group\Collection as GroupCollection; -use Magento\Eav\Model\ResourceModel\Entity\Attribute\Group\CollectionFactory as GroupCollectionFactory; use Magento\Eav\Model\Entity\Attribute\Group; -use Magento\Catalog\Model\ResourceModel\Eav\Attribute as EavAttribute; use Magento\Eav\Model\Entity\Type as EntityType; use Magento\Eav\Model\ResourceModel\Entity\Attribute\Collection as AttributeCollection; -use Magento\Ui\DataProvider\Mapper\FormElement as FormElementMapper; -use Magento\Ui\DataProvider\Mapper\MetaProperties as MetaPropertiesMapper; -use Magento\Framework\Api\SearchCriteriaBuilder; -use Magento\Catalog\Api\ProductAttributeGroupRepositoryInterface; +use Magento\Eav\Model\ResourceModel\Entity\Attribute\Group\Collection as GroupCollection; +use Magento\Eav\Model\ResourceModel\Entity\Attribute\Group\CollectionFactory as GroupCollectionFactory; +use Magento\Framework\Api\AttributeInterface; use Magento\Framework\Api\SearchCriteria; -use Magento\Framework\Api\SortOrderBuilder; -use Magento\Catalog\Api\ProductAttributeRepositoryInterface; +use Magento\Framework\Api\SearchCriteriaBuilder; use Magento\Framework\Api\SearchResultsInterface; -use Magento\Catalog\Api\Data\ProductAttributeInterface; -use Magento\Eav\Api\Data\AttributeGroupInterface; -use Magento\Catalog\Model\ResourceModel\Eav\Attribute; +use Magento\Framework\Api\SortOrderBuilder; +use Magento\Framework\App\RequestInterface; use Magento\Framework\Currency; +use Magento\Framework\Event\ManagerInterface; use Magento\Framework\Locale\Currency as CurrencyLocale; -Use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\Framework\Stdlib\ArrayManager; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\Store\Api\Data\StoreInterface; +use Magento\Store\Model\StoreManagerInterface; +use Magento\Ui\DataProvider\EavValidationRules; +use Magento\Ui\DataProvider\Mapper\FormElement as FormElementMapper; +use Magento\Ui\DataProvider\Mapper\MetaProperties as MetaPropertiesMapper; /** * Class EavTest @@ -157,6 +160,26 @@ class EavTest extends AbstractModifierTest */ protected $currencyLocaleMock; + /** + * @var ProductAttributeInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $productAttributeMock; + + /** + * @var ArrayManager|\PHPUnit_Framework_MockObject_MockObject + */ + protected $arrayManagerMock; + + /** + * @var EavAttributeFactory|\PHPUnit_Framework_MockObject_MockObject + */ + private $eavAttributeFactoryMock; + + /** + * @var ManagerInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $eventManagerMock; + /** * @var ObjectManager */ @@ -167,6 +190,9 @@ class EavTest extends AbstractModifierTest */ protected $eav; + /** + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ protected function setUp() { parent::setUp(); @@ -228,10 +254,24 @@ protected function setUp() $this->searchResultsMock = $this->getMockBuilder(SearchResultsInterface::class) ->getMockForAbstractClass(); $this->eavAttributeMock = $this->getMockBuilder(Attribute::class) - ->setMethods(['getAttributeGroupCode', 'getApplyTo', 'getFrontendInput', 'getAttributeCode']) + ->setMethods(['load', 'getAttributeGroupCode', 'getApplyTo', 'getFrontendInput', 'getAttributeCode']) + ->disableOriginalConstructor() + ->getMock(); + $this->productAttributeMock = $this->getMockBuilder(ProductAttributeInterface::class) + ->getMock(); + $this->arrayManagerMock = $this->getMockBuilder(ArrayManager::class) + ->getMock(); + $this->eavAttributeFactoryMock = $this->getMockBuilder(EavAttributeFactory::class) + ->disableOriginalConstructor() + ->setMethods(['create']) + ->getMock(); + $this->eventManagerMock = $this->getMockBuilder(ManagerInterface::class) ->disableOriginalConstructor() ->getMock(); + $this->eavAttributeFactoryMock->expects($this->any()) + ->method('create') + ->willReturn($this->eavAttributeMock); $this->groupCollectionFactoryMock->expects($this->any()) ->method('create') ->willReturn($this->groupCollectionMock); @@ -277,7 +317,10 @@ protected function setUp() ->disableOriginalConstructor() ->setMethods(['getCurrency']) ->getMock(); - + $this->eavAttributeMock->expects($this->any()) + ->method('load') + ->willReturnSelf(); + $this->eav =$this->getModel(); $this->objectManager->setBackwardCompatibleProperty( $this->eav, @@ -304,6 +347,9 @@ protected function createModel() 'attributeGroupRepository' => $this->attributeGroupRepositoryMock, 'sortOrderBuilder' => $this->sortOrderBuilderMock, 'attributeRepository' => $this->attributeRepositoryMock, + 'arrayManager' => $this->arrayManagerMock, + 'eavAttributeFactory' => $this->eavAttributeFactoryMock, + '_eventManager' => $this->eventManagerMock, ]); } @@ -399,4 +445,184 @@ public function testModifyData() $this->assertEquals($sourceData, $this->eav->modifyData([])); } + + /** + * @param int $productId + * @param array $attributeData + * @param string $attrValue + * @param array $expected + * @covers \Magento\Catalog\Ui\DataProvider\Product\Form\Modifier\Eav::isProductExists + * @covers \Magento\Catalog\Ui\DataProvider\Product\Form\Modifier\Eav::setupAttributeMeta + * @dataProvider setupAttributeMetaDataProvider + */ + public function testSetupAttributeMetaDefaultAttribute($productId, array $attributeData, $attrValue, $expected) + { + $configPath = 'arguments/data/config'; + $groupCode = 'product-details'; + $sortOrder = '0'; + + $this->productMock->expects($this->any()) + ->method('getId') + ->willReturn($productId); + + $this->initDataMock($this->productAttributeMock, $attributeData); + + $this->productAttributeMock->expects($this->any()) + ->method('getDefaultValue') + ->willReturn('required_value'); + + $this->productAttributeMock->expects($this->any()) + ->method('getAttributeCode') + ->willReturn('code'); + + $this->productAttributeMock->expects($this->any()) + ->method('getValue') + ->willReturn('value'); + + $attributeMock = $this->getMockBuilder(AttributeInterface::class) + ->disableOriginalConstructor() + ->getMock(); + + $attributeMock->expects($this->any()) + ->method('getValue') + ->willReturn($attrValue); + + $this->productMock->expects($this->any()) + ->method('getCustomAttribute') + ->willReturn($attributeMock); + + $this->arrayManagerMock->expects($this->any()) + ->method('set') + ->with( + $configPath, + [], + $expected + ) + ->willReturn($expected); + + $this->arrayManagerMock->expects($this->any()) + ->method('merge') + ->willReturn($expected); + + $this->arrayManagerMock->expects($this->any()) + ->method('get') + ->willReturn([]); + + $this->arrayManagerMock->expects($this->any()) + ->method('exists'); + + $this->assertEquals( + $expected, + $this->eav->setupAttributeMeta($this->productAttributeMock, $groupCode, $sortOrder) + ); + } + + /** + * Setup attribute meta data provider. + * + * @return array + */ + public function setupAttributeMetaDataProvider() + { + return [ + 'default_null_prod_not_new_and_required' => [ + 'productId' => 1, + 'attributeData' => [ + 'is_required' => true, + 'default_frontend_label' => 'text', + ], + 'attrValue' => 'val', + 'expected' => [ + 'dataType' => null, + 'formElement' => null, + 'visible' => null, + 'required' => true, + 'notice' => null, + 'default' => null, + 'label' => __('text'), + 'code' => 'code', + 'source' => 'product-details', + 'scopeLabel' => '', + 'globalScope' => false, + 'sortOrder' => 0, + ], + ], + 'default_null_prod_not_new_and_not_required' => [ + 'productId' => 1, + 'attributeData' => [ + 'productRequired' => false, + 'default_frontend_label' => 'text', + ], + 'attrValue' => 'val', + 'expected' => [ + 'dataType' => null, + 'formElement' => null, + 'visible' => null, + 'required' => false, + 'notice' => null, + 'default' => null, + 'label' => __('text'), + 'code' => 'code', + 'source' => 'product-details', + 'scopeLabel' => '', + 'globalScope' => false, + 'sortOrder' => 0, + ], + ], + 'default_null_prod_new_and_not_required' => [ + 'productId' => null, + 'attributeData' => [ + 'productRequired' => false, + ], + 'attrValue' => null, + 'expected' => [ + 'dataType' => null, + 'formElement' => null, + 'visible' => null, + 'required' => false, + 'notice' => null, + 'default' => 'required_value', + 'label' => __(null), + 'code' => 'code', + 'source' => 'product-details', + 'scopeLabel' => '', + 'globalScope' => false, + 'sortOrder' => 0, + ], + ], + 'default_null_prod_new_and_required' => [ + 'productId' => null, + 'attributeData' => [ + 'productRequired' => false, + ], + 'attrValue' => null, + 'expected' => [ + 'dataType' => null, + 'formElement' => null, + 'visible' => null, + 'required' => false, + 'notice' => null, + 'default' => 'required_value', + 'label' => __(null), + 'code' => 'code', + 'source' => 'product-details', + 'scopeLabel' => '', + 'globalScope' => false, + 'sortOrder' => 0, + ], + ] + ]; + } + + /** + * @param \PHPUnit_Framework_MockObject_MockObject $mock + * @param array $data + */ + private function initDataMock(\PHPUnit_Framework_MockObject_MockObject $mock, array $data) + { + foreach ($data as $key => $value) { + $mock->method('get' . implode(explode('_', ucwords($key, "_")))) + ->willReturn($value); + } + } } 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 176e578d7a73f..de1d780fbf4fb 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 @@ -244,7 +244,7 @@ public function modifyMeta(array $meta) if ($attributes) { $meta[$groupCode]['children'] = $this->getAttributesMeta($attributes, $groupCode); $meta[$groupCode]['arguments']['data']['config']['componentType'] = Fieldset::NAME; - $meta[$groupCode]['arguments']['data']['config']['label'] = __('%1', $group->getAttributeGroupName()); + $meta[$groupCode]['arguments']['data']['config']['label'] = __('%1', __($group->getAttributeGroupName())); $meta[$groupCode]['arguments']['data']['config']['collapsible'] = true; $meta[$groupCode]['arguments']['data']['config']['dataScope'] = self::DATA_SCOPE_PRODUCT; $meta[$groupCode]['arguments']['data']['config']['sortOrder'] = @@ -524,6 +524,16 @@ private function getPreviousSetAttributes() return $this->prevSetAttributes; } + /** + * Check is product already new or we trying to create one. + * + * @return bool + */ + private function isProductExists() + { + return (bool) $this->locator->getProduct()->getId(); + } + /** * Initial meta setup * @@ -533,6 +543,7 @@ private function getPreviousSetAttributes() * @return array * @throws \Magento\Framework\Exception\LocalizedException * @SuppressWarnings(PHPMD.CyclomaticComplexity) + * @SuppressWarnings(PHPMD.NPathComplexity) * @api */ public function setupAttributeMeta(ProductAttributeInterface $attribute, $groupCode, $sortOrder) @@ -545,8 +556,8 @@ public function setupAttributeMeta(ProductAttributeInterface $attribute, $groupC 'visible' => $attribute->getIsVisible(), 'required' => $attribute->getIsRequired(), 'notice' => $attribute->getNote(), - 'default' => $attribute->getDefaultValue(), - 'label' => $attribute->getDefaultFrontendLabel(), + 'default' => (!$this->isProductExists()) ? $attribute->getDefaultValue() : null, + 'label' => __($attribute->getDefaultFrontendLabel()), 'code' => $attribute->getAttributeCode(), 'source' => $groupCode, 'scopeLabel' => $this->getScopeLabel($attribute), @@ -650,7 +661,7 @@ public function setupAttributeContainerMeta(ProductAttributeInterface $attribute 'formElement' => 'container', 'componentType' => 'container', 'breakLine' => false, - 'label' => $attribute->getDefaultFrontendLabel(), + 'label' => __($attribute->getDefaultFrontendLabel()), 'required' => $attribute->getIsRequired(), ] ); @@ -745,7 +756,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/composer.json b/app/code/Magento/Catalog/composer.json index 286735a99d368..39ab8352f8c0b 100644 --- a/app/code/Magento/Catalog/composer.json +++ b/app/code/Magento/Catalog/composer.json @@ -33,7 +33,7 @@ "magento/module-catalog-sample-data": "Sample Data version:100.1.*" }, "type": "magento2-module", - "version": "101.0.7", + "version": "101.0.8", "license": [ "OSL-3.0", "AFL-3.0" diff --git a/app/code/Magento/Catalog/etc/di.xml b/app/code/Magento/Catalog/etc/di.xml index 075e33107d43b..793c6351ffe73 100644 --- a/app/code/Magento/Catalog/etc/di.xml +++ b/app/code/Magento/Catalog/etc/di.xml @@ -805,4 +805,5 @@ + diff --git a/app/code/Magento/Catalog/etc/eav_attributes.xml b/app/code/Magento/Catalog/etc/eav_attributes.xml index 133849a28e048..005402937232f 100644 --- a/app/code/Magento/Catalog/etc/eav_attributes.xml +++ b/app/code/Magento/Catalog/etc/eav_attributes.xml @@ -30,6 +30,7 @@ + diff --git a/app/code/Magento/Catalog/etc/module.xml b/app/code/Magento/Catalog/etc/module.xml index ffbd5bb6b206d..571176694547a 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/ui_component/category_form.xml b/app/code/Magento/Catalog/view/adminhtml/ui_component/category_form.xml index b87bdba972321..46d15e3fcc31d 100644 --- a/app/code/Magento/Catalog/view/adminhtml/ui_component/category_form.xml +++ b/app/code/Magento/Catalog/view/adminhtml/ui_component/category_form.xml @@ -167,6 +167,7 @@ Magento\Catalog\Ui\Component\Category\Form\Element\Wysiwyg + Description wysiwyg @@ -217,6 +218,7 @@ 70 string select + Display Mode @@ -253,6 +255,7 @@ admin__field-default multiselect category + Available Product Listing Sort By @@ -290,6 +293,7 @@ admin__field-default select category + Default Product Listing Sort By @@ -325,6 +329,7 @@ admin__field-small + Layered Navigation Price Step input category $ diff --git a/app/code/Magento/Catalog/view/adminhtml/ui_component/product_listing.xml b/app/code/Magento/Catalog/view/adminhtml/ui_component/product_listing.xml index f3285ba03962b..5220c962ad0f5 100644 --- a/app/code/Magento/Catalog/view/adminhtml/ui_component/product_listing.xml +++ b/app/code/Magento/Catalog/view/adminhtml/ui_component/product_listing.xml @@ -24,6 +24,9 @@ Magento_Ui/js/grid/provider + + filters.store_id + @@ -34,7 +37,17 @@ true - + + + + Magento_Ui/js/grid/controls/bookmarks/bookmarks + dataGridActions + + localStorage + + + + 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 3d3c30e4797ae..a6c71e7d4b660 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,8 +4,9 @@ */ define([ + 'underscore', 'Magento_Ui/js/form/element/textarea' -], function (Textarea) { +], function (_, Textarea) { 'use strict'; return Textarea.extend({ @@ -123,24 +124,21 @@ define([ * Update field value, if it's allowed */ updateValue: function () { - var str = this.mask, + var str = this.mask || '', nonEmptyValueFlag = false, - placeholder, - property, tmpElement; if (!this.allowImport) { return; } - for (property in this.values) { - if (this.values.hasOwnProperty(property)) { - placeholder = ''; - placeholder = placeholder.concat('{{', property, '}}'); - str = str.replace(placeholder, this.values[property]); - nonEmptyValueFlag = nonEmptyValueFlag || !!this.values[property]; - } + 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; diff --git a/app/code/Magento/CatalogImportExport/Model/Export/Product.php b/app/code/Magento/CatalogImportExport/Model/Export/Product.php index 57a3e56153d26..b3f266728e952 100644 --- a/app/code/Magento/CatalogImportExport/Model/Export/Product.php +++ b/app/code/Magento/CatalogImportExport/Model/Export/Product.php @@ -944,7 +944,7 @@ protected function collectRawData() if ($storeId != Store::DEFAULT_STORE_ID && isset($data[$itemId][Store::DEFAULT_STORE_ID][$fieldName]) - && $data[$itemId][Store::DEFAULT_STORE_ID][$fieldName] == $attrValue + && $data[$itemId][Store::DEFAULT_STORE_ID][$fieldName] == htmlspecialchars_decode($attrValue) ) { continue; } @@ -983,7 +983,7 @@ protected function collectRawData() $data[$itemId][$storeId][self::COL_ATTR_SET] = $this->_attrSetIdToName[$attrSetId]; $data[$itemId][$storeId][self::COL_TYPE] = $item->getTypeId(); } - $data[$itemId][$storeId][self::COL_SKU] = $item->getSku(); + $data[$itemId][$storeId][self::COL_SKU] = htmlspecialchars_decode($item->getSku()); $data[$itemId][$storeId]['store_id'] = $storeId; $data[$itemId][$storeId]['product_id'] = $itemId; $data[$itemId][$storeId]['product_link_id'] = $productLinkId; diff --git a/app/code/Magento/CatalogImportExport/Model/Import/Product.php b/app/code/Magento/CatalogImportExport/Model/Import/Product.php index cbcbbd66897b1..bdc73b8fb22f7 100644 --- a/app/code/Magento/CatalogImportExport/Model/Import/Product.php +++ b/app/code/Magento/CatalogImportExport/Model/Import/Product.php @@ -568,7 +568,10 @@ class Product extends \Magento\ImportExport\Model\Import\Entity\AbstractEntity /** @var array */ protected $productUrlSuffix = []; - /** @var array */ + /** + * @var array + * @deprecated + */ protected $productUrlKeys = []; /** @@ -1039,8 +1042,8 @@ protected function _initTypeModels() if (!$model instanceof \Magento\CatalogImportExport\Model\Import\Product\Type\AbstractType) { throw new \Magento\Framework\Exception\LocalizedException( __( - 'Entity type model must be an instance of ' - . 'Magento\CatalogImportExport\Model\Import\Product\Type\AbstractType' + "Entity type model must be an instance of '%1'", + \Magento\CatalogImportExport\Model\Import\Product\Type\AbstractType::class ) ); } @@ -1249,7 +1252,7 @@ protected function _saveProductAttributes(array $attributesData) $linkId = $this->_connection->fetchOne( $this->_connection->select() ->from($this->getResource()->getTable('catalog_product_entity')) - ->where('sku = ?', $sku) + ->where('sku = ?', (string)$sku) ->columns($this->getProductEntityLinkField()) ); @@ -1521,6 +1524,10 @@ protected function _saveProducts() } $rowScope = $this->getRowScope($rowData); + if (empty($rowData[self::URL_KEY])) { + $rowData[self::URL_KEY] = $this->getUrlKey($rowData); + } + $rowSku = $rowData[self::COL_SKU]; if (null === $rowSku) { @@ -2636,18 +2643,22 @@ protected function getProductUrlSuffix($storeId = null) } /** + * Return url key if specified, or generate one from product name otherwise. + * * @param array $rowData * @return string */ protected function getUrlKey($rowData) { if (!empty($rowData[self::URL_KEY])) { - $this->productUrlKeys[$rowData[self::COL_SKU]] = $rowData[self::URL_KEY]; + return $rowData[self::URL_KEY]; } - $urlKey = !empty($this->productUrlKeys[$rowData[self::COL_SKU]]) - ? $this->productUrlKeys[$rowData[self::COL_SKU]] - : $this->productUrl->formatUrlKey($rowData[self::COL_NAME]); - return $urlKey; + + if (!empty($rowData[self::COL_NAME])) { + return $this->productUrl->formatUrlKey($rowData[self::COL_NAME]); + } + + return ''; } /** diff --git a/app/code/Magento/CatalogImportExport/composer.json b/app/code/Magento/CatalogImportExport/composer.json index f444a64338dc9..84bf4c07c1e8c 100644 --- a/app/code/Magento/CatalogImportExport/composer.json +++ b/app/code/Magento/CatalogImportExport/composer.json @@ -16,7 +16,7 @@ "ext-ctype": "*" }, "type": "magento2-module", - "version": "100.1.4", + "version": "100.1.5", "license": [ "OSL-3.0", "AFL-3.0" diff --git a/app/code/Magento/CatalogInventory/Model/Quote/Item/QuantityValidator.php b/app/code/Magento/CatalogInventory/Model/Quote/Item/QuantityValidator.php index 8d25488dee51c..58cf3ff72c8ec 100644 --- a/app/code/Magento/CatalogInventory/Model/Quote/Item/QuantityValidator.php +++ b/app/code/Magento/CatalogInventory/Model/Quote/Item/QuantityValidator.php @@ -7,9 +7,11 @@ */ namespace Magento\CatalogInventory\Model\Quote\Item; -use Magento\CatalogInventory\Api\StockRegistryInterface; -use Magento\CatalogInventory\Api\StockStateInterface; +use Magento\CatalogInventory\Model\Stock; +/** + * Quantity validation. + */ class QuantityValidator { /** @@ -23,26 +25,30 @@ class QuantityValidator protected $stockItemInitializer; /** - * @var StockRegistryInterface + * Stock registry. + * + * @var \Magento\CatalogInventory\Api\StockRegistryInterface */ protected $stockRegistry; /** - * @var StockStateInterface + * Stock state. + * + * @var \Magento\CatalogInventory\Api\StockStateInterface */ protected $stockState; /** * @param QuantityValidator\Initializer\Option $optionInitializer * @param QuantityValidator\Initializer\StockItem $stockItemInitializer - * @param StockRegistryInterface $stockRegistry - * @param StockStateInterface $stockState + * @param \Magento\CatalogInventory\Api\StockRegistryInterface $stockRegistry + * @param \Magento\CatalogInventory\Api\StockStateInterface $stockState */ public function __construct( QuantityValidator\Initializer\Option $optionInitializer, QuantityValidator\Initializer\StockItem $stockItemInitializer, - StockRegistryInterface $stockRegistry, - StockStateInterface $stockState + \Magento\CatalogInventory\Api\StockRegistryInterface $stockRegistry, + \Magento\CatalogInventory\Api\StockStateInterface $stockState ) { $this->optionInitializer = $optionInitializer; $this->stockItemInitializer = $stockItemInitializer; @@ -65,7 +71,6 @@ public function validate(\Magento\Framework\Event\Observer $observer) { /* @var $quoteItem \Magento\Quote\Model\Quote\Item */ $quoteItem = $observer->getEvent()->getItem(); - if (!$quoteItem || !$quoteItem->getProductId() || !$quoteItem->getQuote() || @@ -73,7 +78,7 @@ public function validate(\Magento\Framework\Event\Observer $observer) ) { return; } - + $product = $quoteItem->getProduct(); $qty = $quoteItem->getQty(); /** @var \Magento\CatalogInventory\Model\Stock\Item $stockItem */ @@ -81,26 +86,32 @@ public function validate(\Magento\Framework\Event\Observer $observer) $quoteItem->getProduct()->getId(), $quoteItem->getProduct()->getStore()->getWebsiteId() ); - /* @var $stockItem \Magento\CatalogInventory\Api\Data\StockItemInterface */ + if (!$stockItem instanceof \Magento\CatalogInventory\Api\Data\StockItemInterface) { throw new \Magento\Framework\Exception\LocalizedException(__('The stock item for Product is not valid.')); } - $parentStockItem = false; + /** @var \Magento\CatalogInventory\Api\Data\StockStatusInterface $stockStatus */ + $stockStatus = $this->stockRegistry->getStockStatus($product->getId(), $product->getStore()->getWebsiteId()); + + /** @var \Magento\CatalogInventory\Api\Data\StockStatusInterface|bool $parentStockStatus */ + $parentStockStatus = false; /** * Check if product in stock. For composite products check base (parent) item stock status */ if ($quoteItem->getParentItem()) { $product = $quoteItem->getParentItem()->getProduct(); - $parentStockItem = $this->stockRegistry->getStockItem( + $parentStockStatus = $this->stockRegistry->getStockStatus( $product->getId(), $product->getStore()->getWebsiteId() ); } - if ($stockItem) { - if (!$stockItem->getIsInStock() || $parentStockItem && !$parentStockItem->getIsInStock()) { + if ($stockStatus) { + if ($stockStatus->getStockStatus() == Stock::STOCK_OUT_OF_STOCK + || $parentStockStatus && $parentStockStatus->getStockStatus() == Stock::STOCK_OUT_OF_STOCK + ) { $quoteItem->addErrorInfo( 'cataloginventory', \Magento\CatalogInventory\Helper\Data::ERROR_QTY, @@ -123,13 +134,13 @@ public function validate(\Magento\Framework\Event\Observer $observer) * Check item for options */ if (($options = $quoteItem->getQtyOptions()) && $qty > 0) { - $qty = $quoteItem->getProduct()->getTypeInstance()->prepareQuoteItemQty($qty, $quoteItem->getProduct()); + $qty = $product->getTypeInstance()->prepareQuoteItemQty($qty, $product); $quoteItem->setData('qty', $qty); - if ($stockItem) { + if ($stockStatus) { $result = $this->stockState->checkQtyIncrements( - $quoteItem->getProduct()->getId(), + $product->getId(), $qty, - $quoteItem->getProduct()->getStore()->getWebsiteId() + $product->getStore()->getWebsiteId() ); if ($result->getHasError()) { $quoteItem->addErrorInfo( @@ -172,7 +183,10 @@ public function validate(\Magento\Framework\Event\Observer $observer) ); } else { // Delete error from item and its quote, if it was set due to qty lack - $this->_removeErrorsFromQuoteAndItem($quoteItem, \Magento\CatalogInventory\Helper\Data::ERROR_QTY); + $this->_removeErrorsFromQuoteAndItem( + $quoteItem, + \Magento\CatalogInventory\Helper\Data::ERROR_QTY + ); } } } else { diff --git a/app/code/Magento/CatalogInventory/Test/Unit/Model/Quote/Item/QuantityValidator/Initializer/QuantityValidatorTest.php b/app/code/Magento/CatalogInventory/Test/Unit/Model/Quote/Item/QuantityValidator/Initializer/QuantityValidatorTest.php new file mode 100644 index 0000000000000..a21036eceeab3 --- /dev/null +++ b/app/code/Magento/CatalogInventory/Test/Unit/Model/Quote/Item/QuantityValidator/Initializer/QuantityValidatorTest.php @@ -0,0 +1,453 @@ +stockRegistryMock = $this->getMock( + StockRegistry::class, + [], + [], + '', + false + ); + + $this->stockStatusMock = $this->getMock(Status::class, [], [], '', false); + + $this->optionInitializer = $this->getMock(Option::class, [], [], '', false); + $this->stockItemInitializer = $this->getMock(StockItem::class, [], [], '', false); + $this->stockState = $this->getMock(StockState::class, [], [], '', false); + $this->quantityValidator = $objectManagerHelper->getObject( + QuantityValidator::class, + [ + 'optionInitializer' => $this->optionInitializer, + 'stockItemInitializer' => $this->stockItemInitializer, + 'stockRegistry' => $this->stockRegistryMock, + 'stockState' => $this->stockState + ] + ); + $this->observerMock = $this->getMock(Observer::class, [], [], '', false); + $this->eventMock = $this->getMock(Event::class, ['getItem'], [], '', false); + $this->quoteMock = $this->getMock(Quote::class, [], [], '', false); + $this->storeMock = $this->getMock(Store::class, [], [], '', false); + $this->quoteItemMock = $this->getMock( + Item::class, + ['getProductId', 'getQuote', 'getQty', 'getProduct', 'getParentItem', + 'addErrorInfo', 'setData', 'getQtyOptions'], + [], + '', + false + ); + $this->parentItemMock = $this->getMock( + Item::class, + ['getProduct', 'getId', 'getStore'], + [], + '', + false + ); + $this->productMock = $this->getMock(Product::class, [], [], '', false); + $this->stockItemMock = $this->getMock(StockMock::class, [], [], '', false); + $this->parentStockItemMock = $this->getMock(StockMock::class, ['getStockStatus'], [], '', false); + + $this->typeInstanceMock = $this->getMock(Type::class, [], [], '', false); + + $this->resultMock = $this->getMock( + DataObject::class, + ['checkQtyIncrements', 'getMessage', 'getQuoteMessage', 'getHasError'], + [], + '', + false + ); + } + + /** + * This tests the scenario when item is not in stock + * + * @return void + */ + public function testValidateOutOfStock() + { + $this->createInitialStub(0); + $this->stockRegistryMock->expects($this->at(0)) + ->method('getStockItem') + ->willReturn($this->stockItemMock); + + $this->stockRegistryMock->expects($this->atLeastOnce()) + ->method('getStockStatus') + ->willReturn($this->stockStatusMock); + + $this->quoteItemMock->expects($this->once()) + ->method('addErrorInfo') + ->with( + 'cataloginventory', + Data::ERROR_QTY, + __('This product is out of stock.') + ); + $this->quoteMock->expects($this->once()) + ->method('addErrorInfo') + ->with( + 'stock', + 'cataloginventory', + Data::ERROR_QTY, + __('Some of the products are out of stock.') + ); + $this->quantityValidator->validate($this->observerMock); + } + + /** + * This tests the scenario when item is in stock but parent is not in stock + * + * @return void + */ + public function testValidateInStock() + { + $this->createInitialStub(1); + $this->stockRegistryMock->expects($this->at(0)) + ->method('getStockItem') + ->willReturn($this->stockItemMock); + + $this->stockRegistryMock->expects($this->at(1)) + ->method('getStockStatus') + ->willReturn($this->stockStatusMock); + + $this->quoteItemMock->expects($this->any()) + ->method('getParentItem') + ->willReturn($this->parentItemMock); + + $this->stockRegistryMock->expects($this->at(2)) + ->method('getStockStatus') + ->willReturn($this->parentStockItemMock); + + $this->parentStockItemMock->expects($this->once()) + ->method('getStockStatus') + ->willReturn(false); + + $this->stockStatusMock->expects($this->atLeastOnce()) + ->method('getStockStatus') + ->willReturn(true); + + $this->quoteItemMock->expects($this->once()) + ->method('addErrorInfo') + ->with( + 'cataloginventory', + Data::ERROR_QTY, + __('This product is out of stock.') + ); + $this->quoteMock->expects($this->once()) + ->method('addErrorInfo') + ->with( + 'stock', + 'cataloginventory', + Data::ERROR_QTY, + __('Some of the products are out of stock.') + ); + $this->quantityValidator->validate($this->observerMock); + } + + /** + * This tests the scenario when item is in stock and has options + * + * @return void + */ + public function testValidateWithOptions() + { + $optionMock = $this->getMockBuilder(OptionItem::class) + ->disableOriginalConstructor() + ->setMethods(['setHasError']) + ->getMock(); + $this->stockRegistryMock->expects($this->at(0)) + ->method('getStockItem') + ->willReturn($this->stockItemMock); + $this->stockRegistryMock->expects($this->at(1)) + ->method('getStockStatus') + ->willReturn($this->stockStatusMock); + $options = [$optionMock]; + $this->createInitialStub(1); + $this->setUpStubForQuantity(1, true); + $this->setUpStubForRemoveError(); + $this->parentStockItemMock->expects($this->any()) + ->method('getStockStatus') + ->willReturn(true); + $this->stockStatusMock->expects($this->once()) + ->method('getStockStatus') + ->willReturn(true); + $this->quoteItemMock->expects($this->any()) + ->method('getQtyOptions') + ->willReturn($options); + $this->optionInitializer->expects($this->any()) + ->method('initialize') + ->willReturn($this->resultMock); + $optionMock->expects($this->never()) + ->method('setHasError'); + $this->quantityValidator->validate($this->observerMock); + } + + /** + * This tests the scenario with options but has errors + * + * @return void + */ + public function testValidateWithOptionsAndError() + { + $optionMock = $this->getMockBuilder(OptionItem::class) + ->disableOriginalConstructor() + ->setMethods(['setHasError']) + ->getMock(); + $this->stockRegistryMock->expects($this->at(0)) + ->method('getStockItem') + ->willReturn($this->stockItemMock); + $this->stockRegistryMock->expects($this->at(1)) + ->method('getStockStatus') + ->willReturn($this->stockStatusMock); + $options = [$optionMock]; + $this->createInitialStub(1); + $this->setUpStubForQuantity(1, true); + $this->setUpStubForRemoveError(); + $this->parentStockItemMock->expects($this->any()) + ->method('getStockStatus') + ->willReturn(true); + $this->stockStatusMock->expects($this->once()) + ->method('getStockStatus') + ->willReturn(true); + $this->quoteItemMock->expects($this->any()) + ->method('getQtyOptions') + ->willReturn($options); + $this->optionInitializer->expects($this->any()) + ->method('initialize') + ->willReturn($this->resultMock); + $optionMock->expects($this->never()) + ->method('setHasError'); + $this->quantityValidator->validate($this->observerMock); + } + + /** + * @param int $qty + * @param bool $hasError + * @return void + */ + private function setUpStubForQuantity($qty, $hasError) + { + $this->productMock->expects($this->any()) + ->method('getTypeInstance') + ->willReturn($this->typeInstanceMock); + $this->typeInstanceMock->expects($this->any()) + ->method('prepareQuoteItemQty') + ->willReturn($qty); + $this->quoteItemMock->expects($this->any()) + ->method('setData'); + $this->productMock->expects($this->any()) + ->method('getId') + ->willReturn(1); + $this->stockState->expects($this->any()) + ->method('checkQtyIncrements') + ->willReturn($this->resultMock); + $this->resultMock->expects($this->any()) + ->method('getHasError') + ->willReturn($hasError); + $this->resultMock->expects($this->any()) + ->method('getMessage') + ->willReturn(''); + $this->resultMock->expects($this->any()) + ->method('getQuoteMessage') + ->willReturn(''); + $this->resultMock->expects($this->any()) + ->method('getQuoteMessageIndex') + ->willReturn(''); + } + + /** + * @param int $qty + * @return void + */ + private function createInitialStub($qty) + { + $this->storeMock->expects($this->any()) + ->method('getWebsiteId') + ->willReturn(1); + $this->quoteMock->expects($this->any()) + ->method('getIsSuperMode') + ->willReturn(0); + $this->productMock->expects($this->any()) + ->method('getId') + ->willReturn(1); + $this->productMock->expects($this->any()) + ->method('getStore') + ->willReturn($this->storeMock); + $this->quoteItemMock->expects($this->any()) + ->method('getProductId') + ->willReturn(1); + $this->quoteItemMock->expects($this->any()) + ->method('getQuote') + ->willReturn($this->quoteMock); + $this->quoteItemMock->expects($this->once()) + ->method('getQty') + ->willReturn($qty); + $this->quoteItemMock->expects($this->any()) + ->method('getProduct') + ->willReturn($this->productMock); + $this->eventMock->expects($this->any()) + ->method('getItem') + ->willReturn($this->quoteItemMock); + $this->observerMock->expects($this->any()) + ->method('getEvent') + ->willReturn($this->eventMock); + $this->parentItemMock->expects($this->any()) + ->method('getProduct') + ->willReturn($this->productMock); + $this->parentStockItemMock->expects($this->any()) + ->method('getIsInStock') + ->willReturn(false); + $this->storeMock->expects($this->any()) + ->method('getWebsiteId') + ->willReturn(1); + $this->quoteItemMock->expects($this->any()) + ->method('getQuote') + ->willReturn($this->quoteMock); + $this->quoteMock->expects($this->any()) + ->method('getQuote') + ->willReturn($this->quoteMock); + $this->quoteItemMock->expects($this->any()) + ->method('addErrorInfo'); + $this->quoteMock->expects($this->any()) + ->method('addErrorInfo'); + $this->setUpStubForQuantity(0, false); + $this->stockItemInitializer->expects($this->any()) + ->method('initialize') + ->willReturn($this->resultMock); + } + + /** + * @return void + */ + private function setUpStubForRemoveError() + { + $quoteItems = [$this->quoteItemMock]; + $this->quoteItemMock->expects($this->any()) + ->method('getHasError') + ->willReturn(false); + $this->quoteMock->expects($this->any()) + ->method('getItemsCollection') + ->willReturn($quoteItems); + $this->quoteMock->expects($this->any()) + ->method('getHasError') + ->willReturn(false); + } +} diff --git a/app/code/Magento/CatalogInventory/Ui/DataProvider/Product/Form/Modifier/AdvancedInventory.php b/app/code/Magento/CatalogInventory/Ui/DataProvider/Product/Form/Modifier/AdvancedInventory.php index 94ded128ec06f..6fd92299ac75b 100644 --- a/app/code/Magento/CatalogInventory/Ui/DataProvider/Product/Form/Modifier/AdvancedInventory.php +++ b/app/code/Magento/CatalogInventory/Ui/DataProvider/Product/Form/Modifier/AdvancedInventory.php @@ -213,7 +213,7 @@ private function prepareMeta() 'dataScope' => 'qty', 'validation' => [ 'validate-number' => true, - 'validate-digits' => true, + 'validate-integer' => true, 'less-than-equals-to' => StockDataFilter::MAX_QTY_VALUE, ], 'imports' => [ diff --git a/app/code/Magento/CatalogInventory/composer.json b/app/code/Magento/CatalogInventory/composer.json index e0675f94a7cd2..9092ffada021e 100644 --- a/app/code/Magento/CatalogInventory/composer.json +++ b/app/code/Magento/CatalogInventory/composer.json @@ -13,7 +13,7 @@ "magento/module-ui": "100.1.*" }, "type": "magento2-module", - "version": "100.1.5", + "version": "100.1.6", "license": [ "OSL-3.0", "AFL-3.0" diff --git a/app/code/Magento/CatalogInventory/view/adminhtml/ui_component/product_form.xml b/app/code/Magento/CatalogInventory/view/adminhtml/ui_component/product_form.xml index af7ad35e0df54..b72b88d2d7269 100644 --- a/app/code/Magento/CatalogInventory/view/adminhtml/ui_component/product_form.xml +++ b/app/code/Magento/CatalogInventory/view/adminhtml/ui_component/product_form.xml @@ -95,7 +95,7 @@ quantity_and_stock_status.qty true - true + true 99999999 200 diff --git a/app/code/Magento/CatalogInventory/view/adminhtml/web/js/components/qty-validator-changer.js b/app/code/Magento/CatalogInventory/view/adminhtml/web/js/components/qty-validator-changer.js index f11e020e72337..3123784e64e71 100644 --- a/app/code/Magento/CatalogInventory/view/adminhtml/web/js/components/qty-validator-changer.js +++ b/app/code/Magento/CatalogInventory/view/adminhtml/web/js/components/qty-validator-changer.js @@ -19,8 +19,7 @@ define([ handleChanges: function (value) { var isDigits = value !== 1; - this.validation['validate-number'] = !isDigits; - this.validation['validate-digits'] = isDigits; + this.validation['validate-integer'] = isDigits; this.validation['less-than-equals-to'] = isDigits ? 99999999 : 99999999.9999; this.validate(); } diff --git a/app/code/Magento/CatalogRule/view/adminhtml/ui_component/catalog_rule_form.xml b/app/code/Magento/CatalogRule/view/adminhtml/ui_component/catalog_rule_form.xml index d5f0f0db2d2e6..66fc7bdffc382 100644 --- a/app/code/Magento/CatalogRule/view/adminhtml/ui_component/catalog_rule_form.xml +++ b/app/code/Magento/CatalogRule/view/adminhtml/ui_component/catalog_rule_form.xml @@ -72,7 +72,7 @@ - Rule Name + Rule Name true text input @@ -87,7 +87,7 @@ - Description + Description true text textarea @@ -99,7 +99,7 @@ - Status + Status true number select @@ -121,7 +121,7 @@ - Websites + Websites number multiselect @@ -140,7 +140,7 @@ - Customer Groups + Customer Groups number multiselect @@ -155,7 +155,7 @@ - From + From true text date @@ -170,7 +170,7 @@ - To + To true text date @@ -185,7 +185,7 @@ - Priority + Priority text input catalog_rule @@ -205,7 +205,7 @@ - Apply + Apply number select catalog_rule @@ -274,7 +274,7 @@ - Discount Amount + Discount Amount text input catalog_rule @@ -288,7 +288,7 @@ - Discard subsequent rules + Discard subsequent rules bool number select @@ -309,4 +309,4 @@ - + \ No newline at end of file diff --git a/app/code/Magento/CatalogSearch/Block/Advanced/Result.php b/app/code/Magento/CatalogSearch/Block/Advanced/Result.php index 76e15ecd6a525..36e60fe4626af 100644 --- a/app/code/Magento/CatalogSearch/Block/Advanced/Result.php +++ b/app/code/Magento/CatalogSearch/Block/Advanced/Result.php @@ -14,26 +14,26 @@ use Magento\Framework\View\Element\Template\Context; /** - * Advanced search result + * Advanced search result. */ class Result extends Template { /** - * Url factory + * Url factory. * * @var UrlFactory */ protected $_urlFactory; /** - * Catalog layer + * Catalog layer. * * @var \Magento\Catalog\Model\Layer */ protected $_catalogLayer; /** - * Catalog search advanced + * Catalog search advanced. * * @var Advanced */ @@ -64,6 +64,7 @@ public function __construct( */ protected function _prepareLayout() { + $this->pageConfig->getTitle()->set($this->getPageTitle()); $breadcrumbs = $this->getLayout()->getBlock('breadcrumbs'); if ($breadcrumbs) { $breadcrumbs->addCrumb( @@ -81,11 +82,22 @@ protected function _prepareLayout() ['label' => __('Results')] ); } + return parent::_prepareLayout(); } /** - * Set order options + * Get page title. + * + * @return \Magento\Framework\Phrase + */ + private function getPageTitle() + { + return __('Advanced Search Results'); + } + + /** + * Set order options. * * @return void */ @@ -101,7 +113,7 @@ public function setListOrders() } /** - * Set view mode options + * Set view mode options. * * @return void */ @@ -111,6 +123,8 @@ public function setListModes() } /** + * Set search result collection. + * * @return void */ public function setListCollection() @@ -119,6 +133,8 @@ public function setListCollection() } /** + * Return product collection. + * * @return Collection */ protected function _getProductCollection() @@ -127,6 +143,8 @@ protected function _getProductCollection() } /** + * Return search model. + * * @return Advanced */ public function getSearchModel() @@ -135,6 +153,8 @@ public function getSearchModel() } /** + * Return results count. + * * @return mixed */ public function getResultCount() @@ -143,10 +163,13 @@ public function getResultCount() $size = $this->getSearchModel()->getProductCollection()->getSize(); $this->setResultCount($size); } + return $this->getData('result_count'); } /** + * Return search product listing html. + * * @return string */ public function getProductListHtml() @@ -155,6 +178,8 @@ public function getProductListHtml() } /** + * Return form url. + * * @return string */ public function getFormUrl() @@ -168,6 +193,8 @@ public function getFormUrl() } /** + * Return search criteria. + * * @return array */ public function getSearchCriterias() 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 d4f16f7f012f3..495b22048b4d5 100644 --- a/app/code/Magento/CatalogSearch/Model/Adapter/Mysql/Aggregation/DataProvider.php +++ b/app/code/Magento/CatalogSearch/Model/Adapter/Mysql/Aggregation/DataProvider.php @@ -15,6 +15,7 @@ use Magento\Framework\DB\Select; use Magento\Framework\Search\Adapter\Mysql\Aggregation\DataProviderInterface; use Magento\Framework\Search\Request\BucketInterface; +use Magento\CatalogInventory\Model\Stock; /** * @SuppressWarnings(PHPMD.CouplingBetweenObjects) @@ -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..52a2c6ff40c99 --- /dev/null +++ b/app/code/Magento/CatalogSearch/Model/Adapter/Mysql/Filter/AliasResolver.php @@ -0,0 +1,47 @@ +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 63a98b3a6455b..056524452645e 100644 --- a/app/code/Magento/CatalogSearch/Model/Adapter/Mysql/Filter/Preprocessor.php +++ b/app/code/Magento/CatalogSearch/Model/Adapter/Mysql/Filter/Preprocessor.php @@ -18,6 +18,10 @@ use Magento\Framework\Search\Adapter\Mysql\Filter\PreprocessorInterface; use Magento\Framework\Search\Request\FilterInterface; use Magento\Store\Model\Store; +use Magento\CatalogInventory\Model\Stock; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\App\ObjectManager; +use Magento\Store\Model\ScopeInterface; /** * @SuppressWarnings(PHPMD.CouplingBetweenObjects) @@ -60,10 +64,26 @@ class Preprocessor implements PreprocessorInterface private $metadataPool; /** + * @deprecated + * * @var TableMapper */ private $tableMapper; + /** + * Scope config. + * + * @var ScopeConfigInterface + */ + private $scopeConfig; + + /** + * Resolving table alias for Search Request filter. + * + * @var AliasResolver + */ + private $aliasResolver; + /** * @param ConditionManager $conditionManager * @param ScopeResolverInterface $scopeResolver @@ -71,6 +91,8 @@ class Preprocessor implements PreprocessorInterface * @param ResourceConnection $resource * @param TableMapper $tableMapper * @param string $attributePrefix + * @param ScopeConfigInterface $scopeConfig + * @param AliasResolver $aliasResolver */ public function __construct( ConditionManager $conditionManager, @@ -78,7 +100,9 @@ public function __construct( Config $config, ResourceConnection $resource, TableMapper $tableMapper, - $attributePrefix + $attributePrefix, + ScopeConfigInterface $scopeConfig = null, + AliasResolver $aliasResolver = null ) { $this->conditionManager = $conditionManager; $this->scopeResolver = $scopeResolver; @@ -87,6 +111,8 @@ public function __construct( $this->connection = $resource->getConnection(); $this->attributePrefix = $attributePrefix; $this->tableMapper = $tableMapper; + $this->scopeConfig = $scopeConfig ?: ObjectManager::getInstance()->get(ScopeConfigInterface::class); + $this->aliasResolver = $aliasResolver ?: ObjectManager::getInstance()->get(AliasResolver::class); } /** @@ -117,7 +143,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 +234,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 +250,34 @@ 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; } + /** + * Checks if it is necessary to show out of stock products. + * + * @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/Indexer/Fulltext/Action/Full.php b/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Action/Full.php index 43a9549c46293..e08954806da29 100644 --- a/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Action/Full.php +++ b/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Action/Full.php @@ -6,9 +6,12 @@ namespace Magento\CatalogSearch\Model\Indexer\Fulltext\Action; use Magento\CatalogSearch\Model\Indexer\Fulltext; +use Magento\Framework\App\ObjectManager; use Magento\Framework\App\ResourceConnection; /** + * Class provides iterator through number of products suitable for fulltext indexation + * * @SuppressWarnings(PHPMD.TooManyFields) * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ @@ -30,6 +33,8 @@ class Full * Index values separator * * @var string + * @deprecated Moved to \Magento\CatalogSearch\Model\Indexer\Fulltext\Action\DataProvider + * @see \Magento\CatalogSearch\Model\Indexer\Fulltext\Action\DataProvider::$separator */ protected $separator = ' | '; @@ -37,6 +42,7 @@ class Full * Array of \DateTime objects per store * * @var \DateTime[] + * @deprecated Not used anymore */ protected $dates = []; @@ -44,6 +50,8 @@ class Full * Product Type Instances cache * * @var array + * @deprecated Moved to \Magento\CatalogSearch\Model\Indexer\Fulltext\Action\DataProvider + * @see \Magento\CatalogSearch\Model\Indexer\Fulltext\Action\DataProvider::$productTypes */ protected $productTypes = []; @@ -51,6 +59,8 @@ class Full * Product Emulators cache * * @var array + * @deprecated Moved to \Magento\CatalogSearch\Model\Indexer\Fulltext\Action\DataProvider + * @see \Magento\CatalogSearch\Model\Indexer\Fulltext\Action\DataProvider::$productEmulators */ protected $productEmulators = []; @@ -77,6 +87,8 @@ class Full * Catalog product type * * @var \Magento\Catalog\Model\Product\Type + * @deprecated Moved to \Magento\CatalogSearch\Model\Indexer\Fulltext\Action\DataProvider + * @see \Magento\CatalogSearch\Model\Indexer\Fulltext\Action\DataProvider::$catalogProductType */ protected $catalogProductType; @@ -91,6 +103,7 @@ class Full * Core store config * * @var \Magento\Framework\App\Config\ScopeConfigInterface + * @deprecated Not used anymore */ protected $scopeConfig; @@ -98,6 +111,8 @@ class Full * Store manager * * @var \Magento\Store\Model\StoreManagerInterface + * @deprecated Moved to \Magento\CatalogSearch\Model\Indexer\Fulltext\Action\DataProvider + * @see \Magento\CatalogSearch\Model\Indexer\Fulltext\Action\DataProvider::$storeManager */ protected $storeManager; @@ -108,21 +123,25 @@ class Full /** * @var \Magento\Framework\Indexer\SaveHandler\IndexerInterface + * @deprecated As part of self::cleanIndex() */ protected $indexHandler; /** * @var \Magento\Framework\Stdlib\DateTime + * @deprecated Not used anymore */ protected $dateTime; /** * @var \Magento\Framework\Locale\ResolverInterface + * @deprecated Not used anymore */ protected $localeResolver; /** * @var \Magento\Framework\Stdlib\DateTime\TimezoneInterface + * @deprecated Not used anymore */ protected $localeDate; @@ -133,16 +152,19 @@ class Full /** * @var \Magento\CatalogSearch\Model\ResourceModel\Fulltext + * @deprecated Not used anymore */ protected $fulltextResource; /** * @var \Magento\Framework\Search\Request\Config + * @deprecated As part of self::reindexAll() */ protected $searchRequestConfig; /** * @var \Magento\Framework\Search\Request\DimensionFactory + * @deprecated As part of self::cleanIndex() */ private $dimensionFactory; @@ -153,6 +175,8 @@ class Full /** * @var \Magento\CatalogSearch\Model\Indexer\Fulltext\Action\IndexIteratorFactory + * @deprecated DataProvider used directly without IndexIterator + * @see self::$dataProvider */ private $iteratorFactory; @@ -161,6 +185,11 @@ class Full */ private $metadataPool; + /** + * @var DataProvider + */ + private $dataProvider; + /** * @param ResourceConnection $resource * @param \Magento\Catalog\Model\Product\Type $catalogProductType @@ -181,6 +210,7 @@ class Full * @param \Magento\Framework\Indexer\ConfigInterface $indexerConfig * @param \Magento\CatalogSearch\Model\Indexer\Fulltext\Action\IndexIteratorFactory $indexIteratorFactory * @param \Magento\Framework\EntityManager\MetadataPool $metadataPool + * @param DataProvider $dataProvider * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -202,7 +232,8 @@ public function __construct( \Magento\Framework\Search\Request\DimensionFactory $dimensionFactory, \Magento\Framework\Indexer\ConfigInterface $indexerConfig, \Magento\CatalogSearch\Model\Indexer\Fulltext\Action\IndexIteratorFactory $indexIteratorFactory, - \Magento\Framework\EntityManager\MetadataPool $metadataPool = null + \Magento\Framework\EntityManager\MetadataPool $metadataPool = null, + DataProvider $dataProvider = null ) { $this->resource = $resource; $this->connection = $resource->getConnection(); @@ -223,13 +254,16 @@ public function __construct( $this->fulltextResource = $fulltextResource; $this->dimensionFactory = $dimensionFactory; $this->iteratorFactory = $indexIteratorFactory; - $this->metadataPool = $metadataPool ?: \Magento\Framework\App\ObjectManager::getInstance() + $this->metadataPool = $metadataPool ?: ObjectManager::getInstance() ->get(\Magento\Framework\EntityManager\MetadataPool::class); + $this->dataProvider = $dataProvider ?: ObjectManager::getInstance()->get(DataProvider::class); } /** * Rebuild whole fulltext index for all stores * + * @deprecated Please use \Magento\CatalogSearch\Model\Indexer\Fulltext::executeFull instead + * @see \Magento\CatalogSearch\Model\Indexer\Fulltext::executeFull * @return void */ public function reindexAll() @@ -282,12 +316,14 @@ protected function getProductIdsFromParents(array $entityIds) /** * Regenerate search index for specific store * + * To be suitable for indexation product must meet set of requirements: + * - to be visible on frontend + * - to be enabled + * - in case product is composite at least one sub product must be enabled + * * @param int $storeId Store View Id - * @param int|array $productIds Product Entity Id + * @param int[] $productIds Product Entity Id * @return \Generator - * - * @SuppressWarnings(PHPMD.CyclomaticComplexity) - * @SuppressWarnings(PHPMD.NPathComplexity) */ public function rebuildStoreIndex($storeId, $productIds = null) { @@ -307,27 +343,129 @@ public function rebuildStoreIndex($storeId, $productIds = null) 'datetime' => array_keys($this->getSearchableAttributes('datetime')), ]; - // status and visibility filter + $lastProductId = 0; + do { + $products = $this->dataProvider + ->getSearchableProducts($storeId, $staticFields, $productIds, $lastProductId); + + $productsIds = array_column($products, 'entity_id'); + $relatedProducts = $this->getRelatedProducts($products); + $productsIds = array_merge($productsIds, array_values($relatedProducts)); + + $productsAttributes = $this->dataProvider->getProductAttributes($storeId, $productsIds, $dynamicFields); + + foreach ($products as $productData) { + $lastProductId = $productData['entity_id']; + + if (!$this->isProductVisible($productData['entity_id'], $productsAttributes) || + !$this->isProductEnabled($productData['entity_id'], $productsAttributes) + ) { + continue; + } + + $productIndex = [$productData['entity_id'] => $productsAttributes[$productData['entity_id']]]; + if (isset($relatedProducts[$productData['entity_id']])) { + $childProductsIndex = $this->getChildProductsIndex( + $productData['entity_id'], + $relatedProducts, + $productsAttributes + ); + if (empty($childProductsIndex)) { + continue; + } + $productIndex = $productIndex + $childProductsIndex; + } + + $index = $this->dataProvider->prepareProductIndex($productIndex, $productData, $storeId); + yield $productData['entity_id'] => $index; + } + } while (count($products) > 0); + } + + /** + * Get related (child) products ids + * + * Load related (child) products ids for composite product type + * + * @param array $products + * @return array + */ + private function getRelatedProducts($products) + { + $relatedProducts = []; + foreach ($products as $productData) { + $relatedProducts[$productData['entity_id']] = $this->dataProvider->getProductChildIds( + $productData['entity_id'], + $productData['type_id'] + ); + } + return array_filter($relatedProducts); + } + + /** + * Performs check that product is visible on Store Front + * + * Check that product is visible on Store Front using visibility attribute + * and allowed visibility values. + * + * @param int $productId + * @param array $productsAttributes + * @return bool + */ + private function isProductVisible($productId, array $productsAttributes) + { $visibility = $this->getSearchableAttribute('visibility'); - $status = $this->getSearchableAttribute('status'); - $statusIds = $this->catalogProductStatus->getVisibleStatusIds(); $allowedVisibility = $this->engine->getAllowedVisibility(); + return isset($productsAttributes[$productId]) && + isset($productsAttributes[$productId][$visibility->getId()]) && + in_array($productsAttributes[$productId][$visibility->getId()], $allowedVisibility); + } - return $this->iteratorFactory->create([ - 'storeId' => $storeId, - 'productIds' => $productIds, - 'staticFields' => $staticFields, - 'dynamicFields' => $dynamicFields, - 'visibility' => $visibility, - 'allowedVisibility' => $allowedVisibility, - 'status' => $status, - 'statusIds' => $statusIds - ]); + /** + * Performs check that product is enabled on Store Front + * + * Check that product is enabled on Store Front using status attribute + * and statuses allowed to be visible on Store Front. + * + * @param int $productId + * @param array $productsAttributes + * @return bool + */ + private function isProductEnabled($productId, array $productsAttributes) + { + $status = $this->getSearchableAttribute('status'); + $allowedStatuses = $this->catalogProductStatus->getVisibleStatusIds(); + return isset($productsAttributes[$productId]) && + isset($productsAttributes[$productId][$status->getId()]) && + in_array($productsAttributes[$productId][$status->getId()], $allowedStatuses); + } + + /** + * Get data for index using related(child) products data + * + * Build data for index using child products(if any). + * Use only enabled child products {@see isProductEnabled}. + * + * @param int $parentId + * @param array $relatedProducts + * @param array $productsAttributes + * @return array + */ + private function getChildProductsIndex($parentId, array $relatedProducts, array $productsAttributes) + { + $productIndex = []; + foreach ($relatedProducts[$parentId] as $productChildId) { + if ($this->isProductEnabled($productChildId, $productsAttributes)) { + $productIndex[$productChildId] = $productsAttributes[$productChildId]; + } + } + return $productIndex; } /** * Clean search index data for store * + * @deprecated As part of self::reindexAll() * @param int $storeId * @return void */ @@ -341,6 +479,7 @@ protected function cleanIndex($storeId) * Retrieve EAV Config Singleton * * @return \Magento\Eav\Model\Config + * @deprecated Use $self::$eavConfig directly */ protected function getEavConfig() { @@ -369,7 +508,7 @@ protected function getSearchableAttributes($backendType = null) ['engine' => $this->engine, 'attributes' => $attributes] ); - $entity = $this->getEavConfig()->getEntityType(\Magento\Catalog\Model\Product::ENTITY)->getEntity(); + $entity = $this->eavConfig->getEntityType(\Magento\Catalog\Model\Product::ENTITY)->getEntity(); foreach ($attributes as $attribute) { $attribute->setEntity($entity); @@ -413,12 +552,14 @@ protected function getSearchableAttribute($attribute) } } - return $this->getEavConfig()->getAttribute(\Magento\Catalog\Model\Product::ENTITY, $attribute); + return $this->eavConfig->getAttribute(\Magento\Catalog\Model\Product::ENTITY, $attribute); } /** * Returns expression for field unification * + * @deprecated Moved to \Magento\CatalogSearch\Model\Indexer\Fulltext\Action\DataProvider + * @see \Magento\CatalogSearch\Model\Indexer\Fulltext\Action\DataProvider::unifyField() * @param string $field * @param string $backendType * @return \Zend_Db_Expr @@ -436,6 +577,8 @@ protected function unifyField($field, $backendType = 'varchar') /** * Retrieve Product Type Instance * + * @deprecated Moved to \Magento\CatalogSearch\Model\Indexer\Fulltext\Action\DataProvider + * @see \Magento\CatalogSearch\Model\Indexer\Fulltext\Action\DataProvider::getProductTypeInstance() * @param string $typeId * @return \Magento\Catalog\Model\Product\Type\AbstractType */ @@ -452,6 +595,8 @@ protected function getProductTypeInstance($typeId) /** * Retrieve Product Emulator (Magento Object) * + * @deprecated Moved to \Magento\CatalogSearch\Model\Indexer\Fulltext\Action\DataProvider + * @see \Magento\CatalogSearch\Model\Indexer\Fulltext\Action\DataProvider::getProductEmulator() * @param string $typeId * @return \Magento\Framework\DataObject */ diff --git a/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Action/IndexIterator.php b/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Action/IndexIterator.php index ef223a774cb97..7f4b5f517ac9d 100644 --- a/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Action/IndexIterator.php +++ b/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Action/IndexIterator.php @@ -10,6 +10,8 @@ * @SuppressWarnings(PHPMD.TooManyFields) * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) + * @deprecated No more used + * @see \Magento\CatalogSearch\Model\Indexer\Fulltext\Action\Full */ class IndexIterator implements \Iterator { @@ -130,9 +132,10 @@ public function __construct( $this->statusIds = $statusIds; } - /** * {@inheritDoc} + * + * @deprecated Since class is deprecated */ public function current() { @@ -141,6 +144,8 @@ public function current() /** * {@inheritDoc} + * + * @deprecated Since class is deprecated */ public function next() { @@ -237,6 +242,8 @@ public function next() /** * {@inheritDoc} + * + * @deprecated Since class is deprecated */ public function key() { @@ -245,6 +252,8 @@ public function key() /** * {@inheritDoc} + * + * @deprecated Since class is deprecated */ public function valid() { @@ -253,6 +262,8 @@ public function valid() /** * {@inheritDoc} + * + * @deprecated Since class is deprecated */ public function rewind() { 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..03675f27cac21 --- /dev/null +++ b/app/code/Magento/CatalogSearch/Model/Search/FilterMapper/ExclusionStrategy.php @@ -0,0 +1,86 @@ +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..e1901afce4ffd --- /dev/null +++ b/app/code/Magento/CatalogSearch/Model/Search/FilterMapper/FilterContext.php @@ -0,0 +1,114 @@ +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; + } + + /** + * Returns attribute by attribute_code. + * + * @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..0e41e0cd69f9a --- /dev/null +++ b/app/code/Magento/CatalogSearch/Model/Search/FilterMapper/FilterStrategyInterface.php @@ -0,0 +1,26 @@ +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; + } + + /** + * Returns attribute by attribute_code. + * + * @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..a0f7f706b4105 --- /dev/null +++ b/app/code/Magento/CatalogSearch/Model/Search/FilterMapper/TermDropdownStrategy.php @@ -0,0 +1,149 @@ +storeManager = $storeManager; + $this->resourceConnection = $resourceConnection; + $this->eavConfig = $eavConfig; + $this->scopeConfig = $scopeConfig; + $this->aliasResolver = $aliasResolver; + } + + /** + * Applies filter. + * + * @param \Magento\Framework\Search\Request\FilterInterface $filter + * @param \Magento\Framework\DB\Select $select + * + * @return bool is filter was applied + * + * @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; + } + + /** + * Returns attribute by attribute code. + * + * @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); + } + + /** + * Check if it is necessary to show out of stock products. + * + * @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 333c44af3424f..03129b0d69859 100644 --- a/app/code/Magento/CatalogSearch/Model/Search/IndexBuilder.php +++ b/app/code/Magento/CatalogSearch/Model/Search/IndexBuilder.php @@ -21,7 +21,7 @@ use Magento\Framework\App\ScopeResolverInterface; /** - * Build base Query for Index + * Build base Query for Index. * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class IndexBuilder implements IndexBuilderInterface @@ -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; @@ -147,7 +148,7 @@ private function getStockConfiguration() { if ($this->stockConfiguration === null) { $this->stockConfiguration = \Magento\Framework\App\ObjectManager::getInstance() - ->get('Magento\CatalogInventory\Api\StockConfigurationInterface'); + ->get(\Magento\CatalogInventory\Api\StockConfigurationInterface::class); } return $this->stockConfiguration; } diff --git a/app/code/Magento/CatalogSearch/Model/Search/RequestGenerator.php b/app/code/Magento/CatalogSearch/Model/Search/RequestGenerator.php index 609b48956b81e..9a16f95a17847 100644 --- a/app/code/Magento/CatalogSearch/Model/Search/RequestGenerator.php +++ b/app/code/Magento/CatalogSearch/Model/Search/RequestGenerator.php @@ -8,10 +8,14 @@ use Magento\Catalog\Api\Data\EavAttributeInterface; use Magento\Catalog\Model\Entity\Attribute; use Magento\Catalog\Model\ResourceModel\Product\Attribute\CollectionFactory; -use Magento\Framework\Search\Request\BucketInterface; +use Magento\CatalogSearch\Model\Search\RequestGenerator\GeneratorResolver; +use Magento\Framework\App\ObjectManager; use Magento\Framework\Search\Request\FilterInterface; use Magento\Framework\Search\Request\QueryInterface; +/** + * Search request generator. + */ class RequestGenerator { /** Filter name suffix */ @@ -21,20 +25,34 @@ class RequestGenerator const BUCKET_SUFFIX = '_bucket'; /** + * Product attribute collection factory. + * * @var CollectionFactory */ private $productAttributeCollectionFactory; + /** + * Search generator resolver. + * + * @var GeneratorResolver + */ + private $generatorResolver; + /** * @param CollectionFactory $productAttributeCollectionFactory + * @param GeneratorResolver $generatorResolver */ - public function __construct(CollectionFactory $productAttributeCollectionFactory) - { + public function __construct( + CollectionFactory $productAttributeCollectionFactory, + GeneratorResolver $generatorResolver = null + ) { $this->productAttributeCollectionFactory = $productAttributeCollectionFactory; + $this->generatorResolver = $generatorResolver + ?: ObjectManager::getInstance()->get(GeneratorResolver::class); } /** - * Generate dynamic fields requests + * Generate dynamic fields requests. * * @return array */ @@ -46,11 +64,12 @@ public function generate() $requests['quick_search_container'] = $this->generateRequest(EavAttributeInterface::IS_FILTERABLE_IN_SEARCH, 'quick_search_container', true); $requests['advanced_search_container'] = $this->generateAdvancedSearchRequest(); + return $requests; } /** - * Generate search request + * Generate search request. * * @param string $attributeType * @param string $container @@ -62,7 +81,7 @@ private function generateRequest($attributeType, $container, $useFulltext) $request = []; foreach ($this->getSearchableAttributes() as $attribute) { if ($attribute->getData($attributeType)) { - if (!in_array($attribute->getAttributeCode(), ['price', 'category_ids'])) { + if (!in_array($attribute->getAttributeCode(), ['price', 'category_ids'], true)) { $queryName = $attribute->getAttributeCode() . '_query'; $request['queries'][$container]['queryReference'][] = [ @@ -76,58 +95,33 @@ private function generateRequest($attributeType, $container, $useFulltext) 'filterReference' => [['ref' => $filterName]], ]; $bucketName = $attribute->getAttributeCode() . self::BUCKET_SUFFIX; - if ($attribute->getBackendType() == 'decimal') { - $request['filters'][$filterName] = [ - 'type' => FilterInterface::TYPE_RANGE, - 'name' => $filterName, - 'field' => $attribute->getAttributeCode(), - 'from' => '$' . $attribute->getAttributeCode() . '.from$', - 'to' => '$' . $attribute->getAttributeCode() . '.to$', - ]; - $request['aggregations'][$bucketName] = [ - 'type' => BucketInterface::TYPE_DYNAMIC, - 'name' => $bucketName, - 'field' => $attribute->getAttributeCode(), - 'method' => 'manual', - 'metric' => [["type" => "count"]], - ]; - } else { - $request['filters'][$filterName] = [ - 'type' => FilterInterface::TYPE_TERM, - 'name' => $filterName, - 'field' => $attribute->getAttributeCode(), - 'value' => '$' . $attribute->getAttributeCode() . '$', - ]; - $request['aggregations'][$bucketName] = [ - 'type' => BucketInterface::TYPE_TERM, - 'name' => $bucketName, - 'field' => $attribute->getAttributeCode(), - 'metric' => [["type" => "count"]], - ]; - } + $generator = $this->generatorResolver->getGeneratorForType($attribute->getBackendType()); + $request['filters'][$filterName] = $generator->getFilterData($attribute, $filterName); + $request['aggregations'][$bucketName] = $generator->getAggregationData($attribute, $bucketName); } } /** @var $attribute Attribute */ - if (in_array($attribute->getAttributeCode(), ['price', 'sku']) - || !$attribute->getIsSearchable() - ) { - //same fields have special semantics + if (!$attribute->getIsSearchable() || in_array($attribute->getAttributeCode(), ['price', 'sku'], true)) { + // Some fields have their own specific handlers continue; } - if ($useFulltext) { + + // Match search by custom price attribute isn't supported + if ($useFulltext && $attribute->getFrontendInput() !== 'price') { $request['queries']['search']['match'][] = [ 'field' => $attribute->getAttributeCode(), 'boost' => $attribute->getSearchWeight() ?: 1, ]; } } + return $request; } /** - * Retrieve searchable attributes + * Retrieve searchable attributes. * - * @return \Magento\Catalog\Model\Entity\Attribute[] + * @return \Magento\Catalog\Model\ResourceModel\Product\Attribute\Collection */ protected function getSearchableAttributes() { @@ -142,7 +136,7 @@ protected function getSearchableAttributes() } /** - * Generate advanced search request + * Generate advanced search request. * * @return array * @SuppressWarnings(PHPMD.CyclomaticComplexity) @@ -231,6 +225,7 @@ private function generateAdvancedSearchRequest() ]; } } + return $request; } } diff --git a/app/code/Magento/CatalogSearch/Model/Search/RequestGenerator/Decimal.php b/app/code/Magento/CatalogSearch/Model/Search/RequestGenerator/Decimal.php new file mode 100644 index 0000000000000..44842be8ca7da --- /dev/null +++ b/app/code/Magento/CatalogSearch/Model/Search/RequestGenerator/Decimal.php @@ -0,0 +1,45 @@ + FilterInterface::TYPE_RANGE, + 'name' => $filterName, + 'field' => $attribute->getAttributeCode(), + 'from' => '$' . $attribute->getAttributeCode() . '.from$', + 'to' => '$' . $attribute->getAttributeCode() . '.to$', + ]; + } + + /** + * {@inheritdoc} + */ + public function getAggregationData(Attribute $attribute, $bucketName) + { + return [ + 'type' => BucketInterface::TYPE_DYNAMIC, + 'name' => $bucketName, + 'field' => $attribute->getAttributeCode(), + 'method' => 'manual', + 'metric' => [['type' => 'count']], + ]; + } +} diff --git a/app/code/Magento/CatalogSearch/Model/Search/RequestGenerator/General.php b/app/code/Magento/CatalogSearch/Model/Search/RequestGenerator/General.php new file mode 100644 index 0000000000000..5f4efb683fb23 --- /dev/null +++ b/app/code/Magento/CatalogSearch/Model/Search/RequestGenerator/General.php @@ -0,0 +1,43 @@ + FilterInterface::TYPE_TERM, + 'name' => $filterName, + 'field' => $attribute->getAttributeCode(), + 'value' => '$' . $attribute->getAttributeCode() . '$', + ]; + } + + /** + * {@inheritdoc} + */ + public function getAggregationData(Attribute $attribute, $bucketName) + { + return [ + 'type' => BucketInterface::TYPE_TERM, + 'name' => $bucketName, + 'field' => $attribute->getAttributeCode(), + 'metric' => [['type' => 'count']], + ]; + } +} diff --git a/app/code/Magento/CatalogSearch/Model/Search/RequestGenerator/GeneratorInterface.php b/app/code/Magento/CatalogSearch/Model/Search/RequestGenerator/GeneratorInterface.php new file mode 100644 index 0000000000000..d4099966befbc --- /dev/null +++ b/app/code/Magento/CatalogSearch/Model/Search/RequestGenerator/GeneratorInterface.php @@ -0,0 +1,33 @@ +defaultGenerator = $defaultGenerator; + $this->generators = $generators; + } + + /** + * Return search generator for specified attribute backend type. + * + * @param string $type + * @return GeneratorInterface + * @throws \InvalidArgumentException + */ + public function getGeneratorForType($type) + { + $generator = isset($this->generators[$type]) ? $this->generators[$type] : $this->defaultGenerator; + if (!($generator instanceof GeneratorInterface)) { + throw new \InvalidArgumentException( + 'Generator must implement ' . GeneratorInterface::class + ); + } + + return $generator; + } +} diff --git a/app/code/Magento/CatalogSearch/Model/Search/TableMapper.php b/app/code/Magento/CatalogSearch/Model/Search/TableMapper.php index fc6c98b22a1c7..2b8b79127c962 100644 --- a/app/code/Magento/CatalogSearch/Model/Search/TableMapper.php +++ b/app/code/Magento/CatalogSearch/Model/Search/TableMapper.php @@ -7,7 +7,10 @@ namespace Magento\CatalogSearch\Model\Search; use Magento\Catalog\Model\ResourceModel\Product\Attribute\CollectionFactory; -use Magento\Eav\Model\Entity\Attribute\AbstractAttribute; +use Magento\CatalogSearch\Model\Adapter\Mysql\Filter\AliasResolver; +use Magento\CatalogSearch\Model\Search\FilterMapper\FilterStrategyInterface; +use Magento\Eav\Model\Config as EavConfig; +use Magento\Framework\App\Config\ScopeConfigInterface; use Magento\Framework\App\ResourceConnection as AppResource; use Magento\Framework\DB\Select; use Magento\Framework\Search\Request\FilterInterface; @@ -16,14 +19,22 @@ use Magento\Framework\Search\RequestInterface; use Magento\Framework\Search\Request\QueryInterface as RequestQueryInterface; use Magento\Store\Model\StoreManagerInterface; +use Magento\Framework\App\ObjectManager; /** + * 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) + * @SuppressWarnings(PHPMD.NPathComplexity) */ class TableMapper { /** - * @var Resource + * Resource connection. + * + * @var AppResource */ private $resource; @@ -33,121 +44,128 @@ class TableMapper private $storeManager; /** + * @deprecated + * * @var \Magento\Catalog\Model\ResourceModel\Product\Attribute\Collection */ private $attributeCollection; /** + * Eav attribute config. + * + * @var EavConfig + */ + private $eavConfig; + + /** + * Scope config. + * + * @var ScopeConfigInterface + */ + private $scopeConfig; + + /** + * FilterStrategyInterface provides the interface to work with strategies. + * + * @var FilterStrategyInterface + */ + private $filterStrategy; + + /** + * Table alias resolver for Search Request filter. + * + * @var AliasResolver + */ + private $aliasResolver; + + /** + * TableMapper constructor. * @param AppResource $resource * @param StoreManagerInterface $storeManager * @param CollectionFactory $attributeCollectionFactory + * @param EavConfig|null $eavConfig + * @param ScopeConfigInterface|null $scopeConfig + * @param FilterStrategyInterface|null $filterStrategy + * @param AliasResolver|null $aliasResolver */ public function __construct( AppResource $resource, StoreManagerInterface $storeManager, - CollectionFactory $attributeCollectionFactory + CollectionFactory $attributeCollectionFactory, + EavConfig $eavConfig = null, + ScopeConfigInterface $scopeConfig = null, + FilterStrategyInterface $filterStrategy = null, + AliasResolver $aliasResolver = null ) { $this->resource = $resource; $this->storeManager = $storeManager; $this->attributeCollection = $attributeCollectionFactory->create(); + $this->eavConfig = $eavConfig ?: ObjectManager::getInstance()->get(EavConfig::class); + $this->scopeConfig = $scopeConfig ?: ObjectManager::getInstance()->get(ScopeConfigInterface::class); + $this->filterStrategy = $filterStrategy ?: ObjectManager::getInstance()->get(FilterStrategyInterface::class); + $this->aliasResolver = $aliasResolver ?: ObjectManager::getInstance()->get(AliasResolver::class); } /** + * Adds tables to select. + * * @param Select $select * @param RequestInterface $request + * * @return Select */ 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) = $this->getMappingData($filter); - if (!array_key_exists($alias, $mappedTables)) { - $select->joinLeft( - [$alias => $table], - $mapOn, - $mappedFields - ); - $mappedTables[$alias] = $table; + $alias = $this->aliasResolver->getAlias($filter); + + if (!array_key_exists($alias, $appliedFilters)) { + $isApplied = $this->filterStrategy->apply($filter, $select); + if ($isApplied) { + $appliedFilters[$alias] = true; + } } } + 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'] - * ] - * @param FilterInterface $filter - * @return array - */ - private function getMappingData(FilterInterface $filter) - { - $alias = null; - $table = null; - $mapOn = null; - $mappedFields = null; - $field = $filter->getField(); - $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) - ) { - $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]; + 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: @@ -196,56 +214,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 - */ - private function getAttributeByCode($field) - { - $attribute = $this->attributeCollection->getItemByColumnValue('attribute_code', $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..cb7b12fc698ef --- /dev/null +++ b/app/code/Magento/CatalogSearch/Test/Unit/Model/Adapter/Mysql/Filter/AliasResolverTest.php @@ -0,0 +1,70 @@ +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 357e6d3669b62..c8dd43c74a10d 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 @@ -11,6 +11,7 @@ use Magento\Framework\Search\Request\FilterInterface; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; use PHPUnit_Framework_MockObject_MockObject as MockObject; +use Magento\CatalogSearch\Model\Adapter\Mysql\Filter\AliasResolver; /** * @SuppressWarnings(PHPMD.CouplingBetweenObjects) @@ -77,42 +78,47 @@ class PreprocessorTest extends \PHPUnit_Framework_TestCase */ private $metadataPoolMock; + /** + * @var AliasResolver|\PHPUnit_Framework_MockObject_MockObject + */ + private $aliasResolver; + protected function setUp() { $objectManagerHelper = new ObjectManagerHelper($this); - $this->conditionManager = $this->getMockBuilder('\Magento\Framework\Search\Adapter\Mysql\ConditionManager') + $this->conditionManager = $this->getMockBuilder(\Magento\Framework\Search\Adapter\Mysql\ConditionManager::class) ->disableOriginalConstructor() ->setMethods(['wrapBrackets']) ->getMock(); - $this->scopeResolver = $this->getMockBuilder('\Magento\Framework\App\ScopeResolverInterface') + $this->scopeResolver = $this->getMockBuilder(\Magento\Framework\App\ScopeResolverInterface::class) ->disableOriginalConstructor() ->setMethods(['getScope']) ->getMockForAbstractClass(); - $this->scope = $this->getMockBuilder('\Magento\Framework\App\ScopeInterface') + $this->scope = $this->getMockBuilder(\Magento\Framework\App\ScopeInterface::class) ->disableOriginalConstructor() ->setMethods(['getId']) ->getMockForAbstractClass(); $this->scopeResolver->expects($this->any()) ->method('getScope') ->will($this->returnValue($this->scope)); - $this->config = $this->getMockBuilder('\Magento\Eav\Model\Config') + $this->config = $this->getMockBuilder(\Magento\Eav\Model\Config::class) ->disableOriginalConstructor() ->setMethods(['getAttribute']) ->getMock(); - $this->attribute = $this->getMockBuilder('\Magento\Eav\Model\Entity\Attribute\AbstractAttribute') + $this->attribute = $this->getMockBuilder(\Magento\Eav\Model\Entity\Attribute\AbstractAttribute::class) ->disableOriginalConstructor() ->setMethods(['getBackendTable', 'isStatic', 'getAttributeId', 'getAttributeCode', 'getFrontendInput']) ->getMockForAbstractClass(); - $this->resource = $resource = $this->getMockBuilder('\Magento\Framework\App\ResourceConnection') + $this->resource = $resource = $this->getMockBuilder(\Magento\Framework\App\ResourceConnection::class) ->disableOriginalConstructor() ->setMethods(['getConnection', 'getTableName']) ->getMock(); - $this->connection = $this->getMockBuilder('\Magento\Framework\DB\Adapter\AdapterInterface') + $this->connection = $this->getMockBuilder(\Magento\Framework\DB\Adapter\AdapterInterface::class) ->disableOriginalConstructor() ->setMethods(['select', 'getIfNullSql', 'quote']) ->getMockForAbstractClass(); - $this->select = $this->getMockBuilder('\Magento\Framework\DB\Select') + $this->select = $this->getMockBuilder(\Magento\Framework\DB\Select::class) ->disableOriginalConstructor() ->setMethods(['from', 'join', 'where', '__toString', 'joinLeft', 'columns', 'having']) ->getMock(); @@ -125,7 +131,7 @@ protected function setUp() $resource->expects($this->atLeastOnce()) ->method('getConnection') ->will($this->returnValue($this->connection)); - $this->filter = $this->getMockBuilder('\Magento\Framework\Search\Request\FilterInterface') + $this->filter = $this->getMockBuilder(\Magento\Framework\Search\Request\FilterInterface::class) ->disableOriginalConstructor() ->setMethods(['getField', 'getValue', 'getType']) ->getMockForAbstractClass(); @@ -141,7 +147,10 @@ function ($select) { ) ); - $this->tableMapper = $this->getMockBuilder('\Magento\CatalogSearch\Model\Search\TableMapper') + $this->tableMapper = $this->getMockBuilder(\Magento\CatalogSearch\Model\Search\TableMapper::class) + ->disableOriginalConstructor() + ->getMock(); + $this->aliasResolver = $this->getMockBuilder(AliasResolver::class) ->disableOriginalConstructor() ->getMock(); $this->metadataPoolMock = $this->getMockBuilder(\Magento\Framework\EntityManager\MetadataPool::class) @@ -156,7 +165,7 @@ function ($select) { $metadata->expects($this->any())->method('getLinkField')->willReturn('entity_id'); $this->target = $objectManagerHelper->getObject( - 'Magento\CatalogSearch\Model\Adapter\Mysql\Filter\Preprocessor', + \Magento\CatalogSearch\Model\Adapter\Mysql\Filter\Preprocessor::class, [ 'conditionManager' => $this->conditionManager, 'scopeResolver' => $this->scopeResolver, @@ -165,6 +174,7 @@ function ($select) { 'attributePrefix' => 'attr_', 'metadataPool' => $this->metadataPoolMock, 'tableMapper' => $this->tableMapper, + 'aliasResolver' => $this->aliasResolver, ] ); } @@ -234,7 +244,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 +282,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..2522a14f1be79 --- /dev/null +++ b/app/code/Magento/CatalogSearch/Test/Unit/Model/Search/FilterMapper/FilterContextTest.php @@ -0,0 +1,269 @@ +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, + ] + ); + } + + /** + * Tests apply() method on exclusion filter. + */ + 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)); + } + + /** + * Tests apply() method without attribute. + */ + 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)); + } + + /** + * Tests apply() method on term filter by select attribute. + */ + 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)); + } + + /** + * Tests apply() method on term filter by multiselect attribute. + */ + 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)); + } + + /** + * Tests apply() method on term filter by static attribute. + */ + 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)); + } + + /** + * Tests apply() method on term filter by unknown attribute type. + */ + 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)); + } + + /** + * Creates filter mock. + * + * @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; + } + + /** + * Creaates attribute mock. + * + * @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/RequestGenerator/DecimalTest.php b/app/code/Magento/CatalogSearch/Test/Unit/Model/Search/RequestGenerator/DecimalTest.php new file mode 100644 index 0000000000000..f699257358dd4 --- /dev/null +++ b/app/code/Magento/CatalogSearch/Test/Unit/Model/Search/RequestGenerator/DecimalTest.php @@ -0,0 +1,83 @@ +attribute = $this->getMockBuilder(Attribute::class) + ->disableOriginalConstructor() + ->setMethods(['getAttributeCode']) + ->getMockForAbstractClass(); + $objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); + $this->decimal = $objectManager->getObject(Decimal::class); + } + + /** + * Tests retrieving filter data by search request generator. + * + * @return void + */ + public function testGetFilterData() + { + $filterName = 'test_filter_name'; + $attributeCode = 'test_attribute_code'; + $expected = [ + 'type' => FilterInterface::TYPE_RANGE, + 'name' => $filterName, + 'field' => $attributeCode, + 'from' => '$' . $attributeCode . '.from$', + 'to' => '$' . $attributeCode . '.to$', + ]; + $this->attribute->expects($this->atLeastOnce()) + ->method('getAttributeCode') + ->willReturn($attributeCode); + $actual = $this->decimal->getFilterData($this->attribute, $filterName); + $this->assertEquals($expected, $actual); + } + + /** + * Test retrieving aggregation data by search request generator. + * + * @return void + */ + public function testGetAggregationData() + { + $bucketName = 'test_bucket_name'; + $attributeCode = 'test_attribute_code'; + $expected = [ + 'type' => BucketInterface::TYPE_DYNAMIC, + 'name' => $bucketName, + 'field' => $attributeCode, + 'method' => 'manual', + 'metric' => [['type' => 'count']], + ]; + $this->attribute->expects($this->atLeastOnce()) + ->method('getAttributeCode') + ->willReturn($attributeCode); + $actual = $this->decimal->getAggregationData($this->attribute, $bucketName); + $this->assertEquals($expected, $actual); + } +} diff --git a/app/code/Magento/CatalogSearch/Test/Unit/Model/Search/RequestGenerator/GeneralTest.php b/app/code/Magento/CatalogSearch/Test/Unit/Model/Search/RequestGenerator/GeneralTest.php new file mode 100644 index 0000000000000..1c111374fe5fa --- /dev/null +++ b/app/code/Magento/CatalogSearch/Test/Unit/Model/Search/RequestGenerator/GeneralTest.php @@ -0,0 +1,81 @@ +attribute = $this->getMockBuilder(Attribute::class) + ->disableOriginalConstructor() + ->setMethods(['getAttributeCode']) + ->getMockForAbstractClass(); + $objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); + $this->general = $objectManager->getObject(General::class); + } + + /** + * Tests retrieving filter data by search request generator. + * + * @return void + */ + public function testGetFilterData() + { + $filterName = 'test_general_filter_name'; + $attributeCode = 'test_general_attribute_code'; + $expected = [ + 'type' => FilterInterface::TYPE_TERM, + 'name' => $filterName, + 'field' => $attributeCode, + 'value' => '$' . $attributeCode . '$', + ]; + $this->attribute->expects($this->atLeastOnce()) + ->method('getAttributeCode') + ->willReturn($attributeCode); + $actual = $this->general->getFilterData($this->attribute, $filterName); + $this->assertEquals($expected, $actual); + } + + /** + * Test retrieving aggregation data by search request generator. + * + * @return void + */ + public function testGetAggregationData() + { + $bucketName = 'test_bucket_name'; + $attributeCode = 'test_attribute_code'; + $expected = [ + 'type' => BucketInterface::TYPE_TERM, + 'name' => $bucketName, + 'field' => $attributeCode, + 'metric' => [['type' => 'count']], + ]; + $this->attribute->expects($this->atLeastOnce()) + ->method('getAttributeCode') + ->willReturn($attributeCode); + $actual = $this->general->getAggregationData($this->attribute, $bucketName); + $this->assertEquals($expected, $actual); + } +} diff --git a/app/code/Magento/CatalogSearch/Test/Unit/Model/Search/RequestGenerator/GeneratorResolverTest.php b/app/code/Magento/CatalogSearch/Test/Unit/Model/Search/RequestGenerator/GeneratorResolverTest.php new file mode 100644 index 0000000000000..97f7a5567fb88 --- /dev/null +++ b/app/code/Magento/CatalogSearch/Test/Unit/Model/Search/RequestGenerator/GeneratorResolverTest.php @@ -0,0 +1,94 @@ +defaultGenerator = $this->getMockBuilder(GeneratorInterface::class) + ->setMethods([]) + ->getMockForAbstractClass(); + + $this->datetimeGenerator = $this->getMockBuilder(GeneratorInterface::class) + ->setMethods([]) + ->getMockForAbstractClass(); + + $this->rangeGenerator = $this->getMockBuilder(GeneratorInterface::class) + ->setMethods([]) + ->getMockForAbstractClass(); + + $invalidTypeGenerator = $this->getMockBuilder(\stdClass::class) + ->setMethods([]); + + $objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); + $this->resolver = $objectManager->getObject( + GeneratorResolver::class, + [ + 'defaultGenerator' => $this->defaultGenerator, + 'generators' => [ + 'datetime' => $this->datetimeGenerator, + 'range' => $this->datetimeGenerator, + 'invalid_type' => $invalidTypeGenerator, + ], + ] + ); + } + + /** + * Tests resolving type specific search generator. + * + * @return void + */ + public function testGetSpecificGenerator() + { + $this->assertEquals($this->rangeGenerator, $this->resolver->getGeneratorForType('range')); + $this->assertEquals($this->datetimeGenerator, $this->resolver->getGeneratorForType('datetime')); + } + + /** + * Tests resolving fallback search generator. + * + * @return void + */ + public function testGetFallbackGenerator() + { + $this->assertEquals($this->defaultGenerator, $this->resolver->getGeneratorForType('unknown_type')); + } + + /** + * Tests resolving search generator with invalid type. + * + * @expectedException InvalidArgumentException + * @return void + */ + public function testGetInvalidGeneratorType() + { + $this->resolver->getGeneratorForType('invalid_type'); + } +} diff --git a/app/code/Magento/CatalogSearch/Test/Unit/Model/Search/RequestGeneratorTest.php b/app/code/Magento/CatalogSearch/Test/Unit/Model/Search/RequestGeneratorTest.php index 6d9d46905db6a..0d3069163946c 100644 --- a/app/code/Magento/CatalogSearch/Test/Unit/Model/Search/RequestGeneratorTest.php +++ b/app/code/Magento/CatalogSearch/Test/Unit/Model/Search/RequestGeneratorTest.php @@ -6,7 +6,12 @@ namespace Magento\CatalogSearch\Test\Unit\Model\Search; use Magento\Catalog\Model\ResourceModel\Product\Attribute\CollectionFactory; +use Magento\CatalogSearch\Model\Search\RequestGenerator\GeneratorResolver; +use Magento\CatalogSearch\Model\Search\RequestGenerator\GeneratorInterface; +/** + * Test for Magento\CatalogSearch\Model\Search\RequestGenerator. + */ class RequestGeneratorTest extends \PHPUnit_Framework_TestCase { /** @var \Magento\Framework\TestFramework\Unit\Helper\ObjectManager */ @@ -18,18 +23,39 @@ class RequestGeneratorTest extends \PHPUnit_Framework_TestCase /** @var CollectionFactory | \PHPUnit_Framework_MockObject_MockObject */ protected $productAttributeCollectionFactory; + /** + * {@inheritdoc} + */ protected function setUp() { $this->productAttributeCollectionFactory = - $this->getMockBuilder('Magento\Catalog\Model\ResourceModel\Product\Attribute\CollectionFactory') + $this->getMockBuilder(\Magento\Catalog\Model\ResourceModel\Product\Attribute\CollectionFactory::class) ->setMethods(['create']) ->disableOriginalConstructor() ->getMock(); + $generatorResolver = $this->getMockBuilder(GeneratorResolver::class) + ->disableOriginalConstructor() + ->setMethods(['getGeneratorForType']) + ->getMock(); + $generator = $this->getMockBuilder(GeneratorInterface::class) + ->setMethods(['getFilterData', 'getAggregationData']) + ->getMockForAbstractClass(); + $generator->expects($this->any()) + ->method('getFilterData') + ->willReturn(['some filter data goes here']); + $generator->expects($this->any()) + ->method('getAggregationData') + ->willReturn(['some aggregation data goes here']); + $generatorResolver->method('getGeneratorForType') + ->willReturn($generator); $this->objectManagerHelper = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); $this->object = $this->objectManagerHelper->getObject( - 'Magento\\CatalogSearch\\Model\\Search\\RequestGenerator', - ['productAttributeCollectionFactory' => $this->productAttributeCollectionFactory] + \Magento\CatalogSearch\Model\Search\RequestGenerator::class, + [ + 'productAttributeCollectionFactory' => $this->productAttributeCollectionFactory, + 'generatorResolver' => $generatorResolver + ] ); } @@ -87,17 +113,28 @@ public function attributesProvider() ], ['attr_int', 'int', 0, 1, 0] ], + [ + [ + 'quick_search_container' => ['queries' => 2, 'filters' => 1, 'aggregations' => 1], + 'advanced_search_container' => ['queries' => 0, 'filters' => 0, 'aggregations' => 0], + 'catalog_view_container' => ['queries' => 0, 'filters' => 0, 'aggregations' => 0], + ], + ['custom_price_attr', 'price', 0, 1, 0], + ], ]; } /** + * Tests generate dynamic fields requests. + * * @param array $countResult * @param $attributeOptions + * @return void * @dataProvider attributesProvider */ public function testGenerate($countResult, $attributeOptions) { - $collection = $this->getMockBuilder('Magento\Catalog\Model\ResourceModel\Product\Attribute\Collection') + $collection = $this->getMockBuilder(\Magento\Catalog\Model\ResourceModel\Product\Attribute\Collection::class) ->disableOriginalConstructor() ->getMock(); $collection->expects($this->any()) @@ -124,31 +161,36 @@ public function testGenerate($countResult, $attributeOptions) $this->assertEquals( $countResult['quick_search_container']['queries'], - $this->countVal($result['quick_search_container']['queries']) + $this->countVal($result['quick_search_container']['queries']), + 'Queries count for "quick_search_container" doesn\'t match' ); $this->assertEquals( $countResult['advanced_search_container']['queries'], - $this->countVal($result['advanced_search_container']['queries']) + $this->countVal($result['advanced_search_container']['queries']), + 'Queries count for "advanced_search_container" doesn\'t match' ); $this->assertEquals( $countResult['advanced_search_container']['filters'], - $this->countVal($result['advanced_search_container']['filters']) + $this->countVal($result['advanced_search_container']['filters']), + 'Filters count for "advanced_search_container" doesn\'t match' ); $this->assertEquals( $countResult['catalog_view_container']['queries'], - $this->countVal($result['catalog_view_container']['queries']) + $this->countVal($result['catalog_view_container']['queries']), + 'Queries count for "catalog_view_container" doesn\'t match' ); } /** - * Create attribute mock + * Create attribute mock. * * @param $attributeOptions - * @return \PHPUnit_Framework_MockObject_MockObject + * @return \Magento\Catalog\Model\Entity\Attribute|\PHPUnit_Framework_MockObject_MockObject */ private function createAttributeMock($attributeOptions) { - $attribute = $this->getMockBuilder('Magento\Catalog\Model\ResourceModel\Product\Attribute') + /** @var \Magento\Catalog\Model\Entity\Attribute|\PHPUnit_Framework_MockObject_MockObject $attribute */ + $attribute = $this->getMockBuilder(\Magento\Catalog\Model\ResourceModel\Eav\Attribute::class) ->disableOriginalConstructor() ->setMethods( [ @@ -184,8 +226,8 @@ private function createAttributeMock($attributeOptions) ->method('getData') ->willReturnMap( [ - ['is_filterable', $attributeOptions[2]], - ['is_filterable_in_search', $attributeOptions[3]] + ['is_filterable', null, $attributeOptions[2]], + ['is_filterable_in_search', null, $attributeOptions[3]], ] ); @@ -196,6 +238,12 @@ private function createAttributeMock($attributeOptions) return $attribute; } + /** + * Return count. + * + * @param array|\Countable $value + * @return int + */ private function countVal(&$value) { return !empty($value) ? count($value) : 0; 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 f0200c115a7a0..19cca14d38046 100644 --- a/app/code/Magento/CatalogSearch/Test/Unit/Model/Search/TableMapperTest.php +++ b/app/code/Magento/CatalogSearch/Test/Unit/Model/Search/TableMapperTest.php @@ -8,7 +8,10 @@ use Magento\Framework\Search\Request\FilterInterface; use Magento\Framework\Search\Request\QueryInterface; -use \Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\Catalog\Model\ResourceModel\Product\Attribute\Collection; +use Magento\Catalog\Model\ResourceModel\Product\Attribute\CollectionFactory; +use Magento\CatalogSearch\Model\Adapter\Mysql\Filter\AliasResolver; /** * Test for \Magento\CatalogSearch\Model\Search\TableMapper @@ -16,9 +19,6 @@ */ class TableMapperTest extends \PHPUnit_Framework_TestCase { - const WEBSITE_ID = 4512; - const STORE_ID = 2514; - /** * @var \Magento\Catalog\Model\ResourceModel\Product\Attribute\Collection|\PHPUnit_Framework_MockObject_MockObject */ @@ -55,91 +55,86 @@ class TableMapperTest extends \PHPUnit_Framework_TestCase private $resource; /** - * @var \Magento\Store\Api\Data\StoreInterface|\PHPUnit_Framework_MockObject_MockObject + * @var \Magento\CatalogSearch\Model\Search\TableMapper */ - private $store; + private $target; /** - * @var \Magento\CatalogSearch\Model\Search\TableMapper + * @var AliasResolver|\PHPUnit_Framework_MockObject_MockObject */ - private $target; + private $aliasResolver; + + /** + * @var \Magento\Eav\Model\Config|\PHPUnit_Framework_MockObject_MockObject + */ + private $eavConfig; protected function setUp() { $objectManager = new ObjectManager($this); - $this->connection = $this->getMockBuilder('\Magento\Framework\DB\Adapter\AdapterInterface') + $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->resource = $this->getMockBuilder('\Magento\Framework\App\ResourceConnection') + $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->website = $this->getMockBuilder('\Magento\Store\Api\Data\WebsiteInterface') - ->disableOriginalConstructor() - ->getMockForAbstractClass(); - $this->website->expects($this->any()) - ->method('getId') - ->willReturn(self::WEBSITE_ID); - $this->store = $this->getMockBuilder('\Magento\Store\Api\Data\StoreInterface') + + $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->store->expects($this->any()) - ->method('getId') - ->willReturn(self::STORE_ID); - $this->storeManager = $this->getMockBuilder('\Magento\Store\Model\StoreManagerInterface') + $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' - ) + $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' - ) + $attributeCollectionFactory = $this->getMockBuilder(CollectionFactory::class) ->setMethods(['create']) ->disableOriginalConstructor() ->getMock(); $attributeCollectionFactory->expects($this->once()) ->method('create') ->willReturn($this->attributeCollection); + + $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', + \Magento\CatalogSearch\Model\Search\TableMapper::class, [ 'resource' => $this->resource, 'storeManager' => $this->storeManager, - 'attributeCollectionFactory' => $attributeCollectionFactory + 'attributeCollectionFactory' => $attributeCollectionFactory, + 'eavConfig' => $this->eavConfig, + 'aliasResolver' => $this->aliasResolver, ] ); - $this->select = $this->getMockBuilder('\Magento\Framework\DB\Select') + $this->select = $this->getMockBuilder(\Magento\Framework\DB\Select::class) ->disableOriginalConstructor() ->getMock(); - $this->request = $this->getMockBuilder('\Magento\Framework\Search\RequestInterface') + $this->request = $this->getMockBuilder(\Magento\Framework\Search\RequestInterface::class) ->disableOriginalConstructor() ->getMock(); } @@ -151,14 +146,7 @@ public function testAddPriceFilter() $this->request->expects($this->once()) ->method('getQuery') ->willReturn($query); - $this->select->expects($this->once()) - ->method('joinLeft') - ->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'); } @@ -167,18 +155,11 @@ 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('joinLeft') - ->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'); } @@ -190,46 +171,25 @@ public function testAddCategoryIds() $this->request->expects($this->once()) ->method('getQuery') ->willReturn($query); - $this->select->expects($this->once()) - ->method('joinLeft') - ->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')), @@ -244,45 +204,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')), @@ -298,53 +226,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('joinLeft') - ->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( [ @@ -361,45 +249,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( [ @@ -416,47 +272,19 @@ 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'); } /** * @param $filter + * * @return \Magento\Framework\Search\Request\Query\Filter|\PHPUnit_Framework_MockObject_MockObject */ private function createFilterQuery($filter) { - $query = $this->getMockBuilder('\Magento\Framework\Search\Request\Query\Filter') + $query = $this->getMockBuilder(\Magento\Framework\Search\Request\Query\Filter::class) ->disableOriginalConstructor() ->getMock(); $query->method('getType') @@ -470,12 +298,14 @@ private function createFilterQuery($filter) * @param array $must * @param array $should * @param array $mustNot + * * @return \Magento\Framework\Search\Request\Query\BoolExpression|\PHPUnit_Framework_MockObject_MockObject + * * @internal param $filter */ private function createBoolQuery(array $must, array $should, array $mustNot) { - $query = $this->getMockBuilder('\Magento\Framework\Search\Request\Query\BoolExpression') + $query = $this->getMockBuilder(\Magento\Framework\Search\Request\Query\BoolExpression::class) ->disableOriginalConstructor() ->getMock(); $query->method('getType') @@ -493,12 +323,14 @@ private function createBoolQuery(array $must, array $should, array $mustNot) * @param array $must * @param array $should * @param array $mustNot + * * @return \Magento\Framework\Search\Request\Filter\BoolExpression|\PHPUnit_Framework_MockObject_MockObject + * * @internal param $filter */ private function createBoolFilter(array $must, array $should, array $mustNot) { - $query = $this->getMockBuilder('\Magento\Framework\Search\Request\Filter\BoolExpression') + $query = $this->getMockBuilder(\Magento\Framework\Search\Request\Filter\BoolExpression::class) ->disableOriginalConstructor() ->getMock(); $query->method('getType') @@ -514,12 +346,13 @@ private function createBoolFilter(array $must, array $should, array $mustNot) /** * @param string $field + * * @return \Magento\Framework\Search\Request\Filter\Range|\PHPUnit_Framework_MockObject_MockObject */ private function createRangeFilter($field) { $filter = $this->createFilterMock( - '\Magento\Framework\Search\Request\Filter\Range', + \Magento\Framework\Search\Request\Filter\Range::class, FilterInterface::TYPE_RANGE, $field ); @@ -528,12 +361,13 @@ private function createRangeFilter($field) /** * @param string $field + * * @return \Magento\Framework\Search\Request\Filter\Term|\PHPUnit_Framework_MockObject_MockObject */ private function createTermFilter($field) { $filter = $this->createFilterMock( - '\Magento\Framework\Search\Request\Filter\Term', + \Magento\Framework\Search\Request\Filter\Term::class, FilterInterface::TYPE_TERM, $field ); @@ -544,6 +378,7 @@ private function createTermFilter($field) * @param string $class * @param string $type * @param string $field + * * @return \PHPUnit_Framework_MockObject_MockObject */ private function createFilterMock($class, $type, $field) @@ -555,40 +390,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') - ->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->attributeCollection->expects($this->at($positionInCollection)) - ->method('getItemByColumnValue') - ->with('attribute_code', $code) - ->willReturn($attribute); + return $filter; } } diff --git a/app/code/Magento/CatalogSearch/composer.json b/app/code/Magento/CatalogSearch/composer.json index 3cf93028d177f..91c8b30f28287 100644 --- a/app/code/Magento/CatalogSearch/composer.json +++ b/app/code/Magento/CatalogSearch/composer.json @@ -15,7 +15,7 @@ "magento/framework": "100.1.*" }, "type": "magento2-module", - "version": "100.1.5", + "version": "100.1.6", "license": [ "OSL-3.0", "AFL-3.0" diff --git a/app/code/Magento/CatalogSearch/etc/di.xml b/app/code/Magento/CatalogSearch/etc/di.xml index fcea9f1563898..25993adf3530c 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 @@ -237,4 +238,12 @@ + + + \Magento\CatalogSearch\Model\Search\RequestGenerator\General + + Magento\CatalogSearch\Model\Search\RequestGenerator\Decimal + + + diff --git a/app/code/Magento/CatalogUrlRewrite/Model/Category/ChildrenUrlRewriteGenerator.php b/app/code/Magento/CatalogUrlRewrite/Model/Category/ChildrenUrlRewriteGenerator.php index 349a4772633ed..4824e31965270 100644 --- a/app/code/Magento/CatalogUrlRewrite/Model/Category/ChildrenUrlRewriteGenerator.php +++ b/app/code/Magento/CatalogUrlRewrite/Model/Category/ChildrenUrlRewriteGenerator.php @@ -7,6 +7,9 @@ use Magento\Catalog\Model\Category; use Magento\CatalogUrlRewrite\Model\CategoryUrlRewriteGeneratorFactory; +use Magento\CatalogUrlRewrite\Model\CategoryUrlRewriteGenerator; +use Magento\UrlRewrite\Model\MergeDataProviderFactory; +use Magento\Framework\App\ObjectManager; class ChildrenUrlRewriteGenerator { @@ -16,16 +19,29 @@ class ChildrenUrlRewriteGenerator /** @var \Magento\CatalogUrlRewrite\Model\CategoryUrlRewriteGeneratorFactory */ protected $categoryUrlRewriteGeneratorFactory; + /** + * Container for new generated url rewrites. + * + * @var \Magento\UrlRewrite\Model\MergeDataProvider + */ + private $mergeDataProviderPrototype; + /** * @param \Magento\CatalogUrlRewrite\Model\Category\ChildrenCategoriesProvider $childrenCategoriesProvider * @param \Magento\CatalogUrlRewrite\Model\CategoryUrlRewriteGeneratorFactory $categoryUrlRewriteGeneratorFactory + * @param \Magento\UrlRewrite\Model\MergeDataProviderFactory|null $mergeDataProviderFactory */ public function __construct( ChildrenCategoriesProvider $childrenCategoriesProvider, - CategoryUrlRewriteGeneratorFactory $categoryUrlRewriteGeneratorFactory + CategoryUrlRewriteGeneratorFactory $categoryUrlRewriteGeneratorFactory, + MergeDataProviderFactory $mergeDataProviderFactory = null ) { $this->childrenCategoriesProvider = $childrenCategoriesProvider; $this->categoryUrlRewriteGeneratorFactory = $categoryUrlRewriteGeneratorFactory; + if (!isset($mergeDataProviderFactory)) { + $mergeDataProviderFactory = ObjectManager::getInstance()->get(MergeDataProviderFactory::class); + } + $this->mergeDataProviderPrototype = $mergeDataProviderFactory->create(); } /** @@ -33,19 +49,21 @@ public function __construct( * * @param int $storeId * @param \Magento\Catalog\Model\Category $category + * @param int|null $rootCategoryId * @return \Magento\UrlRewrite\Service\V1\Data\UrlRewrite[] */ - public function generate($storeId, Category $category) + public function generate($storeId, Category $category, $rootCategoryId = null) { - $urls = []; - foreach ($this->childrenCategoriesProvider->getChildren($category) as $childCategory) { + $mergeDataProvider = clone $this->mergeDataProviderPrototype; + foreach ($this->childrenCategoriesProvider->getChildren($category, true) as $childCategory) { $childCategory->setStoreId($storeId); $childCategory->setData('save_rewrites_history', $category->getData('save_rewrites_history')); - $urls = array_merge( - $urls, - $this->categoryUrlRewriteGeneratorFactory->create()->generate($childCategory) + /** @var CategoryUrlRewriteGenerator $categoryUrlRewriteGenerator */ + $categoryUrlRewriteGenerator = $this->categoryUrlRewriteGeneratorFactory->create(); + $mergeDataProvider->merge( + $categoryUrlRewriteGenerator->generate($childCategory, false, $rootCategoryId) ); } - return $urls; + return $mergeDataProvider->getData(); } } diff --git a/app/code/Magento/CatalogUrlRewrite/Model/Category/CurrentUrlRewritesRegenerator.php b/app/code/Magento/CatalogUrlRewrite/Model/Category/CurrentUrlRewritesRegenerator.php index d4581744bcb07..4189520806668 100644 --- a/app/code/Magento/CatalogUrlRewrite/Model/Category/CurrentUrlRewritesRegenerator.php +++ b/app/code/Magento/CatalogUrlRewrite/Model/Category/CurrentUrlRewritesRegenerator.php @@ -5,13 +5,8 @@ */ namespace Magento\CatalogUrlRewrite\Model\Category; -use Magento\Catalog\Model\Category; -use Magento\CatalogUrlRewrite\Model\CategoryUrlPathGenerator; use Magento\CatalogUrlRewrite\Model\CategoryUrlRewriteGenerator; use Magento\UrlRewrite\Model\OptionProvider; -use Magento\UrlRewrite\Model\UrlFinderInterface; -use Magento\UrlRewrite\Service\V1\Data\UrlRewrite; -use Magento\UrlRewrite\Service\V1\Data\UrlRewriteFactory; class CurrentUrlRewritesRegenerator { @@ -21,25 +16,64 @@ class CurrentUrlRewritesRegenerator /** @var \Magento\UrlRewrite\Service\V1\Data\UrlRewriteFactory */ protected $urlRewriteFactory; - /** @var UrlFinderInterface */ + /** + * @var \Magento\UrlRewrite\Model\UrlFinderInterface + * @deprecated + */ protected $urlFinder; - /** @var \Magento\Catalog\Model\Category */ + /** + * @var \Magento\Catalog\Model\Category + * @deprecated + */ protected $category; + /** + * Data class for url storage. + * + * @var \Magento\UrlRewrite\Service\V1\Data\UrlRewrite + */ + private $urlRewritePrototype; + + /** + * Finds specific queried url rewrites identified by specific fields. + * + * @var \Magento\CatalogUrlRewrite\Model\Map\UrlRewriteFinder + */ + private $urlRewriteFinder; + + /** + * Container for new generated url rewrites. + * @var \Magento\UrlRewrite\Model\MergeDataProvider + */ + private $mergeDataProviderPrototype; + /** * @param \Magento\CatalogUrlRewrite\Model\CategoryUrlPathGenerator $categoryUrlPathGenerator * @param \Magento\UrlRewrite\Service\V1\Data\UrlRewriteFactory $urlRewriteFactory - * @param UrlFinderInterface $urlFinder + * @param \Magento\UrlRewrite\Model\UrlFinderInterface $urlFinder + * @param \Magento\CatalogUrlRewrite\Model\Map\UrlRewriteFinder|null $urlRewriteFinder + * @param \Magento\UrlRewrite\Model\MergeDataProviderFactory|null $mergeDataProviderFactory */ public function __construct( - CategoryUrlPathGenerator $categoryUrlPathGenerator, - UrlRewriteFactory $urlRewriteFactory, - UrlFinderInterface $urlFinder + \Magento\CatalogUrlRewrite\Model\CategoryUrlPathGenerator $categoryUrlPathGenerator, + \Magento\UrlRewrite\Service\V1\Data\UrlRewriteFactory $urlRewriteFactory, + \Magento\UrlRewrite\Model\UrlFinderInterface $urlFinder, + \Magento\CatalogUrlRewrite\Model\Map\UrlRewriteFinder $urlRewriteFinder = null, + \Magento\UrlRewrite\Model\MergeDataProviderFactory $mergeDataProviderFactory = null ) { $this->categoryUrlPathGenerator = $categoryUrlPathGenerator; $this->urlRewriteFactory = $urlRewriteFactory; + $this->urlRewritePrototype = $urlRewriteFactory->create(); $this->urlFinder = $urlFinder; + $this->urlRewriteFinder = $urlRewriteFinder ?: \Magento\Framework\App\ObjectManager::getInstance() + ->get(\Magento\CatalogUrlRewrite\Model\Map\UrlRewriteFinder::class); + if (!isset($mergeDataProviderFactory)) { + $mergeDataProviderFactory = \Magento\Framework\App\ObjectManager::getInstance()->get( + \Magento\UrlRewrite\Model\MergeDataProviderFactory::class + ); + } + $this->mergeDataProviderPrototype = $mergeDataProviderFactory->create(); } /** @@ -47,70 +81,70 @@ public function __construct( * * @param int $storeId * @param \Magento\Catalog\Model\Category $category + * @param int|null $rootCategoryId * @return \Magento\UrlRewrite\Service\V1\Data\UrlRewrite[] */ - public function generate($storeId, Category $category) + public function generate($storeId, \Magento\Catalog\Model\Category $category, $rootCategoryId = null) { - $this->category = $category; - - $currentUrlRewrites = $this->urlFinder->findAllByData( - [ - UrlRewrite::STORE_ID => $storeId, - UrlRewrite::ENTITY_ID => $category->getId(), - UrlRewrite::ENTITY_TYPE => CategoryUrlRewriteGenerator::ENTITY_TYPE, - ] + $mergeDataProvider = clone $this->mergeDataProviderPrototype; + $currentUrlRewrites = $this->urlRewriteFinder->findAllByData( + $category->getEntityId(), + $storeId, + CategoryUrlRewriteGenerator::ENTITY_TYPE, + $rootCategoryId ); - $urlRewrites = []; foreach ($currentUrlRewrites as $rewrite) { - if ($rewrite->getIsAutogenerated()) { - $urlRewrites = array_merge($urlRewrites, $this->generateForAutogenerated($rewrite, $storeId)); - } else { - $urlRewrites = array_merge($urlRewrites, $this->generateForCustom($rewrite, $storeId)); - } + $generated = $rewrite->getIsAutogenerated() + ? $this->generateForAutogenerated($rewrite, $storeId, $category) + : $this->generateForCustom($rewrite, $storeId, $category); + $mergeDataProvider->merge($generated); } - return $urlRewrites; + + return $mergeDataProvider->getData(); } /** * @param \Magento\UrlRewrite\Service\V1\Data\UrlRewrite $url * @param int $storeId + * @param \Magento\Catalog\Model\Category|null $category * @return array */ - protected function generateForAutogenerated($url, $storeId) + protected function generateForAutogenerated($url, $storeId, \Magento\Catalog\Model\Category $category = null) { - $urls = []; - if ($this->category->getData('save_rewrites_history')) { - $targetPath = $this->categoryUrlPathGenerator->getUrlPathWithSuffix($this->category, $storeId); + if ($category->getData('save_rewrites_history')) { + $targetPath = $this->categoryUrlPathGenerator->getUrlPathWithSuffix($category, $storeId); if ($url->getRequestPath() !== $targetPath) { - $urls[$url->getRequestPath() . '_' . $storeId] = $this->urlRewriteFactory->create() - ->setEntityType(CategoryUrlRewriteGenerator::ENTITY_TYPE) - ->setEntityId($this->category->getId()) + $generatedUrl = clone $this->urlRewritePrototype; + $generatedUrl->setEntityType(CategoryUrlRewriteGenerator::ENTITY_TYPE) + ->setEntityId($category->getEntityId()) ->setRequestPath($url->getRequestPath()) ->setTargetPath($targetPath) ->setRedirectType(OptionProvider::PERMANENT) ->setStoreId($storeId) ->setIsAutogenerated(0); + + return [$generatedUrl]; } } - return $urls; + return []; } /** * @param \Magento\UrlRewrite\Service\V1\Data\UrlRewrite $url * @param int $storeId - * @return array + * @param \Magento\Catalog\Model\Category|null $category + * @return \Magento\UrlRewrite\Service\V1\Data\UrlRewrite[] */ - protected function generateForCustom($url, $storeId) + protected function generateForCustom($url, $storeId, \Magento\Catalog\Model\Category $category = null) { - $urls = []; $targetPath = !$url->getRedirectType() ? $url->getTargetPath() - : $this->categoryUrlPathGenerator->getUrlPathWithSuffix($this->category, $storeId); + : $this->categoryUrlPathGenerator->getUrlPathWithSuffix($category, $storeId); if ($url->getRequestPath() !== $targetPath) { - $urls[$url->getRequestPath() . '_' . $storeId] = $this->urlRewriteFactory->create() - ->setEntityType(CategoryUrlRewriteGenerator::ENTITY_TYPE) - ->setEntityId($this->category->getId()) + $generatedUrl = clone $this->urlRewritePrototype; + $generatedUrl->setEntityType(CategoryUrlRewriteGenerator::ENTITY_TYPE) + ->setEntityId($category->getEntityId()) ->setRequestPath($url->getRequestPath()) ->setTargetPath($targetPath) ->setRedirectType($url->getRedirectType()) @@ -118,7 +152,10 @@ protected function generateForCustom($url, $storeId) ->setDescription($url->getDescription()) ->setIsAutogenerated(0) ->setMetadata($url->getMetadata()); + + return [$generatedUrl]; } - return $urls; + + return []; } } diff --git a/app/code/Magento/CatalogUrlRewrite/Model/Category/Plugin/Storage.php b/app/code/Magento/CatalogUrlRewrite/Model/Category/Plugin/Storage.php index b02e994d66a2a..6da36ba733319 100644 --- a/app/code/Magento/CatalogUrlRewrite/Model/Category/Plugin/Storage.php +++ b/app/code/Magento/CatalogUrlRewrite/Model/Category/Plugin/Storage.php @@ -5,30 +5,38 @@ */ namespace Magento\CatalogUrlRewrite\Model\Category\Plugin; -use Magento\CatalogUrlRewrite\Model\Category\ProductFactory; use Magento\CatalogUrlRewrite\Model\ProductUrlRewriteGenerator; use Magento\UrlRewrite\Model\StorageInterface; use Magento\UrlRewrite\Model\UrlFinderInterface; use Magento\UrlRewrite\Service\V1\Data\UrlRewrite; +use Magento\CatalogUrlRewrite\Model\ResourceModel\Category\Product; class Storage { - /** @var UrlFinderInterface */ - protected $urlFinder; + /** + * Url Finder Interface. + * + * @var UrlFinderInterface + */ + private $urlFinder; - /** @var ProductFactory */ - protected $productFactory; + /** + * Product resource model. + * + * @var Product + */ + private $productResource; /** * @param UrlFinderInterface $urlFinder - * @param ProductFactory $productFactory + * @param Product $productResource */ public function __construct( UrlFinderInterface $urlFinder, - ProductFactory $productFactory + Product $productResource ) { $this->urlFinder = $urlFinder; - $this->productFactory = $productFactory; + $this->productResource = $productResource; } /** @@ -51,7 +59,7 @@ public function aroundReplace(StorageInterface $object, \Closure $proceed, array ]; } if ($toSave) { - $this->productFactory->create()->getResource()->saveMultiple($toSave); + $this->productResource->saveMultiple($toSave); } } @@ -63,14 +71,7 @@ public function aroundReplace(StorageInterface $object, \Closure $proceed, array */ public function beforeDeleteByData(StorageInterface $object, array $data) { - $toRemove = []; - $records = $this->urlFinder->findAllByData($data); - foreach ($records as $record) { - $toRemove[] = $record->getUrlRewriteId(); - } - if ($toRemove) { - $this->productFactory->create()->getResource()->removeMultiple($toRemove); - } + $this->productResource->removeMultipleByProductCategory($data); } /** diff --git a/app/code/Magento/CatalogUrlRewrite/Model/CategoryBasedProductRewriteGenerator.php b/app/code/Magento/CatalogUrlRewrite/Model/CategoryBasedProductRewriteGenerator.php new file mode 100644 index 0000000000000..5f52c7a158ad3 --- /dev/null +++ b/app/code/Magento/CatalogUrlRewrite/Model/CategoryBasedProductRewriteGenerator.php @@ -0,0 +1,61 @@ +productScopeRewriteGenerator = $productScopeRewriteGenerator; + } + + /** + * Generates product url rewrites based on category + * + * @param Product $product + * @param Category $category + * @param int|null $rootCategoryId + * @return \Magento\UrlRewrite\Service\V1\Data\UrlRewrite[] + */ + public function generate(Product $product, Category $category, $rootCategoryId = null) + { + if ($product->getVisibility() == Visibility::VISIBILITY_NOT_VISIBLE) { + return []; + } + + $storeId = $product->getStoreId(); + + $urls = $this->productScopeRewriteGenerator->isGlobalScope($storeId) + ? $this->productScopeRewriteGenerator->generateForGlobalScope([$category], $product, $rootCategoryId) + : $this->productScopeRewriteGenerator->generateForSpecificStoreView( + $storeId, + [$category], + $product, + $rootCategoryId + ); + + return $urls; + } +} diff --git a/app/code/Magento/CatalogUrlRewrite/Model/CategoryUrlRewriteGenerator.php b/app/code/Magento/CatalogUrlRewrite/Model/CategoryUrlRewriteGenerator.php index 5748b69ea3ce4..34063c54b02ea 100644 --- a/app/code/Magento/CatalogUrlRewrite/Model/CategoryUrlRewriteGenerator.php +++ b/app/code/Magento/CatalogUrlRewrite/Model/CategoryUrlRewriteGenerator.php @@ -12,6 +12,8 @@ use Magento\CatalogUrlRewrite\Service\V1\StoreViewService; use Magento\Store\Model\Store; use Magento\Catalog\Api\CategoryRepositoryInterface; +use Magento\Framework\App\ObjectManager; +use Magento\UrlRewrite\Model\MergeDataProviderFactory; class CategoryUrlRewriteGenerator { @@ -21,7 +23,10 @@ class CategoryUrlRewriteGenerator /** @var StoreViewService */ protected $storeViewService; - /** @var \Magento\Catalog\Model\Category */ + /** + * @var \Magento\Catalog\Model\Category + * @deprecated + */ protected $category; /** @var \Magento\CatalogUrlRewrite\Model\Category\CanonicalUrlRewriteGenerator */ @@ -38,72 +43,97 @@ class CategoryUrlRewriteGenerator */ protected $overrideStoreUrls; + /** + * Container for new generated url rewrites. + * + * @var \Magento\UrlRewrite\Model\MergeDataProvider + */ + private $mergeDataProviderPrototype; + /** * @param \Magento\CatalogUrlRewrite\Model\Category\CanonicalUrlRewriteGenerator $canonicalUrlRewriteGenerator * @param \Magento\CatalogUrlRewrite\Model\Category\CurrentUrlRewritesRegenerator $currentUrlRewritesRegenerator * @param \Magento\CatalogUrlRewrite\Model\Category\ChildrenUrlRewriteGenerator $childrenUrlRewriteGenerator * @param \Magento\CatalogUrlRewrite\Service\V1\StoreViewService $storeViewService * @param \Magento\Catalog\Api\CategoryRepositoryInterface $categoryRepository + * @param \Magento\UrlRewrite\Model\MergeDataProviderFactory|null $mergeDataProviderFactory */ public function __construct( CanonicalUrlRewriteGenerator $canonicalUrlRewriteGenerator, CurrentUrlRewritesRegenerator $currentUrlRewritesRegenerator, ChildrenUrlRewriteGenerator $childrenUrlRewriteGenerator, StoreViewService $storeViewService, - CategoryRepositoryInterface $categoryRepository + CategoryRepositoryInterface $categoryRepository, + MergeDataProviderFactory $mergeDataProviderFactory = null ) { $this->storeViewService = $storeViewService; $this->canonicalUrlRewriteGenerator = $canonicalUrlRewriteGenerator; $this->childrenUrlRewriteGenerator = $childrenUrlRewriteGenerator; $this->currentUrlRewritesRegenerator = $currentUrlRewritesRegenerator; $this->categoryRepository = $categoryRepository; + if (!isset($mergeDataProviderFactory)) { + $mergeDataProviderFactory = ObjectManager::getInstance()->get(MergeDataProviderFactory::class); + } + $this->mergeDataProviderPrototype = $mergeDataProviderFactory->create(); } /** - * {@inheritdoc} + * @param \Magento\Catalog\Model\Category $category + * @param bool $overrideStoreUrls + * @param int|null $rootCategoryId + * @return \Magento\UrlRewrite\Service\V1\Data\UrlRewrite[] */ - public function generate($category, $overrideStoreUrls = false) + public function generate($category, $overrideStoreUrls = false, $rootCategoryId = null) { - $this->category = $category; - $this->overrideStoreUrls = $overrideStoreUrls; + if ($rootCategoryId === null) { + $rootCategoryId = $category->getEntityId(); + } - $storeId = $this->category->getStoreId(); + $storeId = $category->getStoreId(); $urls = $this->isGlobalScope($storeId) - ? $this->generateForGlobalScope() - : $this->generateForSpecificStoreView($storeId); + ? $this->generateForGlobalScope($category, $overrideStoreUrls, $rootCategoryId) + : $this->generateForSpecificStoreView($storeId, $category, $rootCategoryId); - $this->category = null; return $urls; } /** - * Generate list of urls for global scope + * Generate list of urls for global scope. * + * @param \Magento\Catalog\Model\Category|null $category + * @param bool $overrideStoreUrls + * @param int|null $rootCategoryId * @return \Magento\UrlRewrite\Service\V1\Data\UrlRewrite[] */ - protected function generateForGlobalScope() - { - $urls = []; - $categoryId = $this->category->getId(); - foreach ($this->category->getStoreIds() as $storeId) { + protected function generateForGlobalScope( + Category $category = null, + $overrideStoreUrls = false, + $rootCategoryId = null + ) { + $mergeDataProvider = clone $this->mergeDataProviderPrototype; + $categoryId = $category->getId(); + foreach ($category->getStoreIds() as $storeId) { if (!$this->isGlobalScope($storeId) - && $this->isOverrideUrlsForStore($storeId, $categoryId) + && $this->isOverrideUrlsForStore($storeId, $categoryId, $overrideStoreUrls) ) { - $this->updateCategoryUrlForStore($storeId); - $urls = array_merge($urls, $this->generateForSpecificStoreView($storeId)); + $this->updateCategoryUrlForStore($storeId, $category); + $mergeDataProvider->merge($this->generateForSpecificStoreView($storeId, $category, $rootCategoryId)); } } - return $urls; + $result = $mergeDataProvider->getData(); + + return $result; } /** * @param int $storeId * @param int $categoryId + * @param bool $overrideStoreUrls * @return bool */ - protected function isOverrideUrlsForStore($storeId, $categoryId) + protected function isOverrideUrlsForStore($storeId, $categoryId, $overrideStoreUrls = false) { - return $this->overrideStoreUrls || !$this->storeViewService->doesEntityHaveOverriddenUrlKeyForStore( + return $overrideStoreUrls || !$this->storeViewService->doesEntityHaveOverriddenUrlKeyForStore( $storeId, $categoryId, Category::ENTITY @@ -114,12 +144,18 @@ protected function isOverrideUrlsForStore($storeId, $categoryId) * Override url key and url path for category in specific Store * * @param int $storeId + * @param \Magento\Catalog\Model\Category|null $category * @return void */ - protected function updateCategoryUrlForStore($storeId) + protected function updateCategoryUrlForStore($storeId, Category $category = null) { - $category = $this->categoryRepository->get($this->category->getId(), $storeId); - $this->category->addData(['url_key' => $category->getUrlKey(), 'url_path' => $category->getUrlPath()]); + $categoryFromRepository = $this->categoryRepository->get($category->getId(), $storeId); + $category->addData( + [ + 'url_key' => $categoryFromRepository->getUrlKey(), + 'url_path' => $categoryFromRepository->getUrlPath() + ] + ); } /** @@ -137,15 +173,23 @@ protected function isGlobalScope($storeId) * Generate list of urls per store * * @param string $storeId + * @param \Magento\Catalog\Model\Category|null $category + * @param int|null $rootCategoryId * @return \Magento\UrlRewrite\Service\V1\Data\UrlRewrite[] */ - protected function generateForSpecificStoreView($storeId) + protected function generateForSpecificStoreView($storeId, Category $category = null, $rootCategoryId = null) { - $urls = array_merge( - $this->canonicalUrlRewriteGenerator->generate($storeId, $this->category), - $this->childrenUrlRewriteGenerator->generate($storeId, $this->category), - $this->currentUrlRewritesRegenerator->generate($storeId, $this->category) + $mergeDataProvider = clone $this->mergeDataProviderPrototype; + $mergeDataProvider->merge( + $this->canonicalUrlRewriteGenerator->generate($storeId, $category) ); - return $urls; + $mergeDataProvider->merge( + $this->childrenUrlRewriteGenerator->generate($storeId, $category, $rootCategoryId) + ); + $mergeDataProvider->merge( + $this->currentUrlRewritesRegenerator->generate($storeId, $category, $rootCategoryId) + ); + + return $mergeDataProvider->getData(); } } diff --git a/app/code/Magento/CatalogUrlRewrite/Model/Map/DataCategoryHashMap.php b/app/code/Magento/CatalogUrlRewrite/Model/Map/DataCategoryHashMap.php new file mode 100644 index 0000000000000..5a05414e084c9 --- /dev/null +++ b/app/code/Magento/CatalogUrlRewrite/Model/Map/DataCategoryHashMap.php @@ -0,0 +1,104 @@ +categoryRepository = $categoryRepository; + $this->categoryResourceFactory = $categoryResourceFactory; + } + + /** + * Returns an array of categories ids that includes category identified by $categoryId and all its subcategories. + * + * @param int $categoryId + * @return array + */ + public function getAllData($categoryId) + { + if (!isset($this->hashMap[$categoryId])) { + $category = $this->categoryRepository->get($categoryId); + $this->hashMap[$categoryId] = $this->getAllCategoryChildrenIds($category); + } + + return $this->hashMap[$categoryId]; + } + + /** + * {@inheritdoc} + */ + public function getData($categoryId, $key) + { + $categorySpecificData = $this->getAllData($categoryId); + if (isset($categorySpecificData[$key])) { + return $categorySpecificData[$key]; + } + + return []; + } + + /** + * Queries the database for sub-categories ids from a category. + * + * @param CategoryInterface $category + * @return int[] + */ + private function getAllCategoryChildrenIds(CategoryInterface $category) + { + $categoryResource = $this->categoryResourceFactory->create(); + $connection = $categoryResource->getConnection(); + $select = $connection->select() + ->from($categoryResource->getEntityTable(), 'entity_id') + ->where($connection->quoteIdentifier('path') . ' LIKE :c_path'); + $bind = ['c_path' => $category->getPath() . '%']; + + return $connection->fetchCol($select, $bind); + } + + /** + * {@inheritdoc} + */ + public function resetData($categoryId) + { + unset($this->hashMap[$categoryId]); + } +} diff --git a/app/code/Magento/CatalogUrlRewrite/Model/Map/DataCategoryUrlRewriteDatabaseMap.php b/app/code/Magento/CatalogUrlRewrite/Model/Map/DataCategoryUrlRewriteDatabaseMap.php new file mode 100644 index 0000000000000..861696e23de02 --- /dev/null +++ b/app/code/Magento/CatalogUrlRewrite/Model/Map/DataCategoryUrlRewriteDatabaseMap.php @@ -0,0 +1,156 @@ +connection = $connection; + $this->hashMapPool = $hashMapPool; + $this->temporaryTableService = $temporaryTableService; + } + + /** + * Generates data from categoryId and stores it into a temporary table. + * + * @param int $categoryId + * @return void + */ + private function generateTableAdapter($categoryId) + { + if (!isset($this->createdTableAdapters[$categoryId])) { + $this->createdTableAdapters[$categoryId] = $this->generateData($categoryId); + } + } + + /** + * Queries the database for all category url rewrites that are affected by the category identified by $categoryId. + * It returns the name of the temporary table where the resulting data is stored. + * + * @param int $categoryId + * @return string + */ + private function generateData($categoryId) + { + $urlRewritesConnection = $this->connection->getConnection(); + $select = $urlRewritesConnection->select() + ->from( + ['e' => $this->connection->getTableName('url_rewrite')], + ['e.*', 'hash_key' => new \Zend_Db_Expr( + "CONCAT(e.store_id,'" . MergeDataProvider::SEPARATOR . "', e.entity_id)" + ) + ] + ) + ->where('entity_type = ?', $this->entityType) + ->where( + $urlRewritesConnection->prepareSqlCondition( + 'entity_id', + [ + 'in' => array_merge( + $this->hashMapPool->getDataMap(DataCategoryUsedInProductsHashMap::class, $categoryId) + ->getAllData($categoryId), + $this->hashMapPool->getDataMap(DataCategoryHashMap::class, $categoryId) + ->getAllData($categoryId) + ) + ] + ) + ); + $mapName = $this->temporaryTableService->createFromSelect( + $select, + $this->connection->getConnection(), + [ + 'PRIMARY' => ['url_rewrite_id'], + 'HASHKEY_ENTITY_STORE' => ['hash_key'], + 'ENTITY_STORE' => ['entity_id', 'store_id'] + ] + ); + + return $mapName; + } + + /** + * {@inheritdoc} + */ + public function destroyTableAdapter($categoryId) + { + $this->hashMapPool->resetMap(DataCategoryUsedInProductsHashMap::class, $categoryId); + $this->hashMapPool->resetMap(DataCategoryHashMap::class, $categoryId); + if (isset($this->createdTableAdapters[$categoryId])) { + $this->temporaryTableService->dropTable($this->createdTableAdapters[$categoryId]); + unset($this->createdTableAdapters[$categoryId]); + } + } + + /** + * Gets data by criteria from a map identified by a category Id. + * + * @param int $categoryId + * @param string $key + * @return array + */ + public function getData($categoryId, $key) + { + $this->generateTableAdapter($categoryId); + $urlRewritesConnection = $this->connection->getConnection(); + $select = $urlRewritesConnection->select()->from(['e' => $this->createdTableAdapters[$categoryId]]); + if (strlen($key) > 0) { + $select->where('hash_key = ?', $key); + } + + return $urlRewritesConnection->fetchAll($select); + } +} diff --git a/app/code/Magento/CatalogUrlRewrite/Model/Map/DataCategoryUsedInProductsHashMap.php b/app/code/Magento/CatalogUrlRewrite/Model/Map/DataCategoryUsedInProductsHashMap.php new file mode 100644 index 0000000000000..4e2a0c4bfc386 --- /dev/null +++ b/app/code/Magento/CatalogUrlRewrite/Model/Map/DataCategoryUsedInProductsHashMap.php @@ -0,0 +1,112 @@ +connection = $connection; + $this->hashMapPool = $hashMapPool; + } + + /** + * Returns an array of product ids for all DataProductHashMap list, + * that occur in other categories not part of DataCategoryHashMap list. + * + * @param int $categoryId + * @return array + */ + public function getAllData($categoryId) + { + if (!isset($this->hashMap[$categoryId])) { + $productsLinkConnection = $this->connection->getConnection(); + $select = $productsLinkConnection->select() + ->from($this->connection->getTableName('catalog_category_product'), ['category_id']) + ->where( + $productsLinkConnection->prepareSqlCondition( + 'product_id', + [ + 'in' => $this->hashMapPool->getDataMap( + DataProductHashMap::class, + $categoryId + )->getAllData($categoryId) + ] + ) + ) + ->where( + $productsLinkConnection->prepareSqlCondition( + 'category_id', + [ + 'nin' => $this->hashMapPool->getDataMap( + DataCategoryHashMap::class, + $categoryId + )->getAllData($categoryId) + ] + ) + )->group('category_id'); + + $this->hashMap[$categoryId] = $productsLinkConnection->fetchCol($select); + } + + return $this->hashMap[$categoryId]; + } + + /** + * {@inheritdoc} + */ + public function getData($categoryId, $key) + { + $categorySpecificData = $this->getAllData($categoryId); + if (isset($categorySpecificData[$key])) { + return $categorySpecificData[$key]; + } + + return []; + } + + /** + * {@inheritdoc} + */ + public function resetData($categoryId) + { + $this->hashMapPool->resetMap(DataProductHashMap::class, $categoryId); + $this->hashMapPool->resetMap(DataCategoryHashMap::class, $categoryId); + unset($this->hashMap[$categoryId]); + } +} diff --git a/app/code/Magento/CatalogUrlRewrite/Model/Map/DataProductHashMap.php b/app/code/Magento/CatalogUrlRewrite/Model/Map/DataProductHashMap.php new file mode 100644 index 0000000000000..67712ff23b3cb --- /dev/null +++ b/app/code/Magento/CatalogUrlRewrite/Model/Map/DataProductHashMap.php @@ -0,0 +1,113 @@ +collectionFactory = $collectionFactory; + $this->hashMapPool = $hashMapPool; + $this->connection = $connection; + } + + /** + * Returns an array of ids of all visible products and assigned to a category and all its subcategories. + * + * @param int $categoryId + * @return array + */ + public function getAllData($categoryId) + { + if (!isset($this->hashMap[$categoryId])) { + $productsCollection = $this->collectionFactory->create(); + $productsCollection->getSelect() + ->joinInner( + ['cp' => $this->connection->getTableName('catalog_category_product')], + 'cp.product_id = e.entity_id', + [] + ) + ->where( + $productsCollection->getConnection()->prepareSqlCondition( + 'cp.category_id', + [ + 'in' => $this->hashMapPool->getDataMap( + DataCategoryHashMap::class, + $categoryId + )->getAllData($categoryId) + ] + ) + )->group('e.entity_id'); + $this->hashMap[$categoryId] = $productsCollection->getAllIds(); + } + + return $this->hashMap[$categoryId]; + } + + /** + * {@inheritdoc} + */ + public function getData($categoryId, $key) + { + $categorySpecificData = $this->getAllData($categoryId); + if (isset($categorySpecificData[$key])) { + return $categorySpecificData[$key]; + } + + return []; + } + + /** + * {@inheritdoc} + */ + public function resetData($categoryId) + { + $this->hashMapPool->resetMap(DataCategoryHashMap::class, $categoryId); + unset($this->hashMap[$categoryId]); + } +} diff --git a/app/code/Magento/CatalogUrlRewrite/Model/Map/DataProductUrlRewriteDatabaseMap.php b/app/code/Magento/CatalogUrlRewrite/Model/Map/DataProductUrlRewriteDatabaseMap.php new file mode 100644 index 0000000000000..1ae302c6873b1 --- /dev/null +++ b/app/code/Magento/CatalogUrlRewrite/Model/Map/DataProductUrlRewriteDatabaseMap.php @@ -0,0 +1,146 @@ +connection = $connection; + $this->hashMapPool = $hashMapPool; + $this->temporaryTableService = $temporaryTableService; + } + + /** + * Generates data from categoryId and stores it into a temporary table. + * + * @param int $categoryId + * @return void + */ + private function generateTableAdapter($categoryId) + { + if (!isset($this->createdTableAdapters[$categoryId])) { + $this->createdTableAdapters[$categoryId] = $this->generateData($categoryId); + } + } + + /** + * {@inheritdoc} + */ + public function getData($categoryId, $key) + { + $this->generateTableAdapter($categoryId); + $urlRewritesConnection = $this->connection->getConnection(); + $select = $urlRewritesConnection->select() + ->from(['e' => $this->createdTableAdapters[$categoryId]]) + ->where('hash_key = ?', $key); + + return $urlRewritesConnection->fetchAll($select); + } + + /** + * Queries the database for all category url rewrites that are affected by the category identified by $categoryId. + * It returns the name of the temporary table where the resulting data is stored. + * + * @param int $categoryId + * @return string + */ + private function generateData($categoryId) + { + $urlRewritesConnection = $this->connection->getConnection(); + $select = $urlRewritesConnection->select() + ->from( + ['e' => $this->connection->getTableName('url_rewrite')], + ['e.*', 'hash_key' => new \Zend_Db_Expr( + "CONCAT(e.store_id,'" . MergeDataProvider::SEPARATOR . "', e.entity_id)" + ) + ] + ) + ->where('entity_type = ?', $this->entityType) + ->where( + $urlRewritesConnection->prepareSqlCondition( + 'entity_id', + [ + 'in' => $this->hashMapPool->getDataMap(DataProductHashMap::class, $categoryId) + ->getAllData($categoryId) + ] + ) + ); + $mapName = $this->temporaryTableService->createFromSelect( + $select, + $this->connection->getConnection(), + [ + 'PRIMARY' => ['url_rewrite_id'], + 'HASHKEY_ENTITY_STORE' => ['hash_key'], + 'ENTITY_STORE' => ['entity_id', 'store_id'] + ] + ); + + return $mapName; + } + + /** + * {@inheritdoc} + */ + public function destroyTableAdapter($categoryId) + { + $this->hashMapPool->resetMap(DataProductHashMap::class, $categoryId); + if (isset($this->createdTableAdapters[$categoryId])) { + $this->temporaryTableService->dropTable($this->createdTableAdapters[$categoryId]); + unset($this->createdTableAdapters[$categoryId]); + } + } +} diff --git a/app/code/Magento/CatalogUrlRewrite/Model/Map/DatabaseMapInterface.php b/app/code/Magento/CatalogUrlRewrite/Model/Map/DatabaseMapInterface.php new file mode 100644 index 0000000000000..1365c81435398 --- /dev/null +++ b/app/code/Magento/CatalogUrlRewrite/Model/Map/DatabaseMapInterface.php @@ -0,0 +1,38 @@ +objectManager = $objectManager; + } + + /** + * Gets a map by instance and category Id. + * + * @param string $instanceName + * @param int $categoryId + * @return DatabaseMapInterface + */ + public function getDataMap($instanceName, $categoryId) + { + $key = $instanceName . '-' . $categoryId; + if (!isset($this->dataArray[$key])) { + $instance = $this->objectManager->create( + $instanceName, + [ + 'category' => $categoryId + ] + ); + if (!$instance instanceof DatabaseMapInterface) { + throw new \InvalidArgumentException( + $instanceName . ' does not implement interface ' . DatabaseMapInterface::class + ); + } + $this->dataArray[$key] = $instance; + } + + return $this->dataArray[$key]; + } + + /** + * Resets a database map by instance and category Id. + * + * @param string $instanceName + * @param int $categoryId + * @return void + */ + public function resetMap($instanceName, $categoryId) + { + $key = $instanceName . '-' . $categoryId; + if (isset($this->dataArray[$key])) { + $this->dataArray[$key]->destroyTableAdapter($categoryId); + unset($this->dataArray[$key]); + } + } +} diff --git a/app/code/Magento/CatalogUrlRewrite/Model/Map/HashMapInterface.php b/app/code/Magento/CatalogUrlRewrite/Model/Map/HashMapInterface.php new file mode 100644 index 0000000000000..bda0abfba60ef --- /dev/null +++ b/app/code/Magento/CatalogUrlRewrite/Model/Map/HashMapInterface.php @@ -0,0 +1,43 @@ +objectManager = $objectManager; + } + + /** + * Gets a map by instance and category Id. + * + * @param string $instanceName + * @param int $categoryId + * @return HashMapInterface + * @throws \Exception + */ + public function getDataMap($instanceName, $categoryId) + { + $key = $instanceName . '-' . $categoryId; + if (!isset($this->dataArray[$key])) { + $instance = $this->objectManager->create( + $instanceName, + [ + 'category' => $categoryId + ] + ); + if (!$instance instanceof HashMapInterface) { + throw new \InvalidArgumentException( + $instanceName . ' does not implement interface ' . HashMapInterface::class + ); + } + $this->dataArray[$key] = $instance; + } + + return $this->dataArray[$key]; + } + + /** + * Resets data in a hash map by instance name and category Id. + * + * @param string $instanceName + * @param int $categoryId + * @return void + */ + public function resetMap($instanceName, $categoryId) + { + $key = $instanceName . '-' . $categoryId; + if (isset($this->dataArray[$key])) { + $this->dataArray[$key]->resetData($categoryId); + unset($this->dataArray[$key]); + } + } +} diff --git a/app/code/Magento/CatalogUrlRewrite/Model/Map/UrlRewriteFinder.php b/app/code/Magento/CatalogUrlRewrite/Model/Map/UrlRewriteFinder.php new file mode 100644 index 0000000000000..61670c15ed23e --- /dev/null +++ b/app/code/Magento/CatalogUrlRewrite/Model/Map/UrlRewriteFinder.php @@ -0,0 +1,146 @@ +databaseMapPool = $databaseMapPool; + $this->urlFinder = $urlFinder; + $this->urlRewriteClassNames = $urlRewriteClassNames; + $this->urlRewritePrototype = $urlRewriteFactory->create(); + } + + /** + * Retrieves existing url rewrites filtered by identifiers from prebuild database maps. + * This method will fall-back to by using UrlFinderInterface when map type is not found in configured list. + * + * @param int $entityId + * @param int $storeId + * @param string $entityType + * @param int|null $rootCategoryId + * @return \Magento\UrlRewrite\Service\V1\Data\UrlRewrite[] + */ + public function findAllByData($entityId, $storeId, $entityType, $rootCategoryId = null) + { + if ($rootCategoryId + && is_numeric($entityId) + && is_numeric($storeId) + && is_string($entityType) + && isset($this->urlRewriteClassNames[$entityType]) + ) { + $map = $this->databaseMapPool->getDataMap($this->urlRewriteClassNames[$entityType], $rootCategoryId); + if ($map) { + $key = $storeId . '_' . $entityId; + + return $this->arrayToUrlRewriteObject($map->getData($rootCategoryId, $key)); + } + } + + return $this->urlFinder->findAllByData( + [ + UrlRewrite::STORE_ID => $storeId, + UrlRewrite::ENTITY_ID => $entityId, + UrlRewrite::ENTITY_TYPE => $entityType + ] + ); + } + + /** + * Transfers an array values to url rewrite object values. + * + * @param array $data + * @return UrlRewrite[] + */ + private function arrayToUrlRewriteObject(array $data) + { + foreach ($data as $key => $array) { + $data[$key] = $this->createUrlRewrite($array); + } + + return $data; + } + + /** + * Creates url rewrite object and sets $data to its properties by key->value. + * + * @param array $data + * @return UrlRewrite + */ + private function createUrlRewrite(array $data) + { + $dataObject = clone $this->urlRewritePrototype; + $dataObject->setUrlRewriteId($data['url_rewrite_id']); + $dataObject->setEntityType($data['entity_type']); + $dataObject->setEntityId($data['entity_id']); + $dataObject->setRequestPath($data['request_path']); + $dataObject->setTargetPath($data['target_path']); + $dataObject->setRedirectType($data['redirect_type']); + $dataObject->setStoreId($data['store_id']); + $dataObject->setDescription($data['description']); + $dataObject->setIsAutogenerated($data['is_autogenerated']); + $dataObject->setMetadata($data['metadata']); + + return $dataObject; + } +} diff --git a/app/code/Magento/CatalogUrlRewrite/Model/Product/CurrentUrlRewritesRegenerator.php b/app/code/Magento/CatalogUrlRewrite/Model/Product/CurrentUrlRewritesRegenerator.php index 5f1f01f7b08a8..72b03df45f70f 100644 --- a/app/code/Magento/CatalogUrlRewrite/Model/Product/CurrentUrlRewritesRegenerator.php +++ b/app/code/Magento/CatalogUrlRewrite/Model/Product/CurrentUrlRewritesRegenerator.php @@ -14,20 +14,31 @@ use Magento\CatalogUrlRewrite\Model\ProductUrlRewriteGenerator; use Magento\CatalogUrlRewrite\Model\ProductUrlPathGenerator; use Magento\UrlRewrite\Service\V1\Data\UrlRewriteFactory; -use Magento\Store\Model\StoreManagerInterface; +use Magento\CatalogUrlRewrite\Model\Map\UrlRewriteFinder; +use Magento\Framework\App\ObjectManager; +use Magento\UrlRewrite\Model\MergeDataProviderFactory; /** * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class CurrentUrlRewritesRegenerator { - /** @var Product */ + /** + * @var Product + * @deprecated + */ protected $product; - /** @var ObjectRegistry */ + /** + * @var ObjectRegistry + * @deprecated + */ protected $productCategories; - /** @var UrlFinderInterface */ + /** + * @var UrlFinderInterface + * @deprecated + */ protected $urlFinder; /** @var ProductUrlPathGenerator */ @@ -36,19 +47,49 @@ class CurrentUrlRewritesRegenerator /** @var UrlRewriteFactory */ protected $urlRewriteFactory; + /** + * Data class for url storage. + * + * @var UrlRewrite + */ + private $urlRewritePrototype; + + /** + * Finds specific queried url rewrites identified by specific fields. + * @var UrlRewriteFinder + */ + private $urlRewriteFinder; + + /** + * Container for new generated url rewrites. + * + * @var \Magento\UrlRewrite\Model\MergeDataProvider + */ + private $mergeDataProviderPrototype; + /** * @param UrlFinderInterface $urlFinder * @param ProductUrlPathGenerator $productUrlPathGenerator * @param UrlRewriteFactory $urlRewriteFactory + * @param UrlRewriteFinder|null $urlRewriteFinder + * @param \Magento\UrlRewrite\Model\MergeDataProviderFactory|null $mergeDataProviderFactory */ public function __construct( UrlFinderInterface $urlFinder, ProductUrlPathGenerator $productUrlPathGenerator, - UrlRewriteFactory $urlRewriteFactory + UrlRewriteFactory $urlRewriteFactory, + UrlRewriteFinder $urlRewriteFinder = null, + MergeDataProviderFactory $mergeDataProviderFactory = null ) { $this->urlFinder = $urlFinder; $this->productUrlPathGenerator = $productUrlPathGenerator; $this->urlRewriteFactory = $urlRewriteFactory; + $this->urlRewritePrototype = $urlRewriteFactory->create(); + $this->urlRewriteFinder = $urlRewriteFinder ?: ObjectManager::getInstance()->get(UrlRewriteFinder::class); + if (!isset($mergeDataProviderFactory)) { + $mergeDataProviderFactory = ObjectManager::getInstance()->get(MergeDataProviderFactory::class); + } + $this->mergeDataProviderPrototype = $mergeDataProviderFactory->create(); } /** @@ -57,106 +98,107 @@ public function __construct( * @param int $storeId * @param Product $product * @param ObjectRegistry $productCategories + * @param int|null $rootCategoryId * @return UrlRewrite[] */ - public function generate($storeId, Product $product, ObjectRegistry $productCategories) + public function generate($storeId, Product $product, ObjectRegistry $productCategories, $rootCategoryId = null) { - $this->product = $product; - $this->productCategories = $productCategories; - - $currentUrlRewrites = $this->urlFinder->findAllByData( - [ - UrlRewrite::STORE_ID => $storeId, - UrlRewrite::ENTITY_ID => $this->product->getId(), - UrlRewrite::ENTITY_TYPE => ProductUrlRewriteGenerator::ENTITY_TYPE, - ] + $mergeDataProvider = clone $this->mergeDataProviderPrototype; + $currentUrlRewrites = $this->urlRewriteFinder->findAllByData( + $product->getEntityId(), + $storeId, + ProductUrlRewriteGenerator::ENTITY_TYPE, + $rootCategoryId ); - $urlRewrites = []; foreach ($currentUrlRewrites as $currentUrlRewrite) { - $category = $this->retrieveCategoryFromMetadata($currentUrlRewrite); + $category = $this->retrieveCategoryFromMetadata($currentUrlRewrite, $productCategories); if ($category === false) { continue; } - $url = $currentUrlRewrite->getIsAutogenerated() - ? $this->generateForAutogenerated($currentUrlRewrite, $storeId, $category) - : $this->generateForCustom($currentUrlRewrite, $storeId, $category); - $urlRewrites = array_merge($urlRewrites, $url); + $generated = $currentUrlRewrite->getIsAutogenerated() + ? $this->generateForAutogenerated($currentUrlRewrite, $storeId, $category, $product) + : $this->generateForCustom($currentUrlRewrite, $storeId, $category, $product); + + $mergeDataProvider->merge($generated); } - $this->product = null; - $this->productCategories = null; - return $urlRewrites; + return $mergeDataProvider->getData(); } /** * @param UrlRewrite $url * @param int $storeId * @param Category|null $category + * @param Product|null $product * @return array */ - protected function generateForAutogenerated($url, $storeId, $category) + protected function generateForAutogenerated($url, $storeId, $category, $product = null) { - if (!$this->product->getData('save_rewrites_history')) { - return []; - } - $targetPath = $this->productUrlPathGenerator->getUrlPathWithSuffix($this->product, $storeId, $category); - if ($url->getRequestPath() === $targetPath) { - return []; + if ($product->getData('save_rewrites_history')) { + $targetPath = $this->productUrlPathGenerator->getUrlPathWithSuffix($product, $storeId, $category); + if ($url->getRequestPath() !== $targetPath) { + $generatedUrl = clone $this->urlRewritePrototype; + $generatedUrl->setEntityType(ProductUrlRewriteGenerator::ENTITY_TYPE) + ->setEntityId($product->getEntityId()) + ->setRequestPath($url->getRequestPath()) + ->setTargetPath($targetPath) + ->setRedirectType(OptionProvider::PERMANENT) + ->setStoreId($storeId) + ->setDescription($url->getDescription()) + ->setIsAutogenerated(0) + ->setMetadata($url->getMetadata()); + + return [$generatedUrl]; + } } - return [ - $this->urlRewriteFactory->create() - ->setEntityType(ProductUrlRewriteGenerator::ENTITY_TYPE) - ->setEntityId($this->product->getId()) - ->setRequestPath($url->getRequestPath()) - ->setTargetPath($targetPath) - ->setRedirectType(OptionProvider::PERMANENT) - ->setStoreId($storeId) - ->setDescription($url->getDescription()) - ->setIsAutogenerated(0) - ->setMetadata($url->getMetadata()) - ]; + + return []; } /** * @param UrlRewrite $url * @param int $storeId * @param Category|null $category - * @return array + * @param Product|null $product + * @return UrlRewrite[] */ - protected function generateForCustom($url, $storeId, $category) + protected function generateForCustom($url, $storeId, $category, $product = null) { $targetPath = $url->getRedirectType() - ? $this->productUrlPathGenerator->getUrlPathWithSuffix($this->product, $storeId, $category) + ? $this->productUrlPathGenerator->getUrlPathWithSuffix($product, $storeId, $category) : $url->getTargetPath(); - if ($url->getRequestPath() === $targetPath) { - return []; - } - return [ - $this->urlRewriteFactory->create() - ->setEntityType(ProductUrlRewriteGenerator::ENTITY_TYPE) - ->setEntityId($this->product->getId()) + if ($url->getRequestPath() !== $targetPath) { + $generatedUrl = clone $this->urlRewritePrototype; + $generatedUrl->setEntityType(ProductUrlRewriteGenerator::ENTITY_TYPE) + ->setEntityId($product->getEntityId()) ->setRequestPath($url->getRequestPath()) ->setTargetPath($targetPath) ->setRedirectType($url->getRedirectType()) ->setStoreId($storeId) ->setDescription($url->getDescription()) ->setIsAutogenerated(0) - ->setMetadata($url->getMetadata()) - ]; + ->setMetadata($url->getMetadata()); + + return [$generatedUrl]; + } + + return []; } /** * @param UrlRewrite $url + * @param ObjectRegistry|null $productCategories * @return Category|null|bool */ - protected function retrieveCategoryFromMetadata($url) + protected function retrieveCategoryFromMetadata($url, ObjectRegistry $productCategories = null) { $metadata = $url->getMetadata(); if (isset($metadata['category_id'])) { - $category = $this->productCategories->get($metadata['category_id']); + $category = $productCategories->get($metadata['category_id']); return $category === null ? false : $category; } + return null; } } diff --git a/app/code/Magento/CatalogUrlRewrite/Model/ProductScopeRewriteGenerator.php b/app/code/Magento/CatalogUrlRewrite/Model/ProductScopeRewriteGenerator.php new file mode 100644 index 0000000000000..40753a16f69a3 --- /dev/null +++ b/app/code/Magento/CatalogUrlRewrite/Model/ProductScopeRewriteGenerator.php @@ -0,0 +1,213 @@ +storeViewService = $storeViewService; + $this->storeManager = $storeManager; + $this->objectRegistryFactory = $objectRegistryFactory; + $this->canonicalUrlRewriteGenerator = $canonicalUrlRewriteGenerator; + $this->categoriesUrlRewriteGenerator = $categoriesUrlRewriteGenerator; + $this->currentUrlRewritesRegenerator = $currentUrlRewritesRegenerator; + $this->anchorUrlRewriteGenerator = $anchorUrlRewriteGenerator; + if (!isset($mergeDataProviderFactory)) { + $mergeDataProviderFactory = ObjectManager::getInstance()->get(MergeDataProviderFactory::class); + } + $this->mergeDataProviderPrototype = $mergeDataProviderFactory->create(); + } + + /** + * Check is global scope. + * + * @param int|null $storeId + * @return bool + */ + public function isGlobalScope($storeId) + { + return null === $storeId || $storeId == Store::DEFAULT_STORE_ID; + } + + /** + * Generate url rewrites for global scope. + * + * @param \Magento\Framework\Data\Collection|\Magento\Catalog\Model\Category[] $productCategories + * @param Product $product + * @param int|null $rootCategoryId + * @return array + */ + public function generateForGlobalScope($productCategories, Product $product, $rootCategoryId = null) + { + $productId = $product->getEntityId(); + $mergeDataProvider = clone $this->mergeDataProviderPrototype; + + foreach ($product->getStoreIds() as $id) { + if (!$this->isGlobalScope($id) && + !$this->storeViewService->doesEntityHaveOverriddenUrlKeyForStore( + $id, + $productId, + Product::ENTITY + )) { + $mergeDataProvider->merge( + $this->generateForSpecificStoreView($id, $productCategories, $product, $rootCategoryId) + ); + } + } + + return $mergeDataProvider->getData(); + } + + /** + * Generate list of urls for specific store view. + * + * @param int $storeId + * @param \Magento\Framework\Data\Collection|Category[] $productCategories + * @param \Magento\Catalog\Model\Product $product + * @param int|null $rootCategoryId + * @return \Magento\UrlRewrite\Service\V1\Data\UrlRewrite[] + */ + public function generateForSpecificStoreView($storeId, $productCategories, Product $product, $rootCategoryId = null) + { + $mergeDataProvider = clone $this->mergeDataProviderPrototype; + $categories = []; + foreach ($productCategories as $category) { + if ($this->isCategoryProperForGenerating($category, $storeId)) { + $categories[] = $category; + } + } + $productCategories = $this->objectRegistryFactory->create(['entities' => $categories]); + + $mergeDataProvider->merge( + $this->canonicalUrlRewriteGenerator->generate($storeId, $product) + ); + $mergeDataProvider->merge( + $this->categoriesUrlRewriteGenerator->generate($storeId, $product, $productCategories) + ); + $mergeDataProvider->merge( + $this->currentUrlRewritesRegenerator->generate( + $storeId, + $product, + $productCategories, + $rootCategoryId + ) + ); + $mergeDataProvider->merge( + $this->anchorUrlRewriteGenerator->generate($storeId, $product, $productCategories) + ); + + return $mergeDataProvider->getData(); + } + + /** + * Check possibility for url rewrite generation. + * + * @param \Magento\Catalog\Model\Category $category + * @param int $storeId + * @return bool + */ + public function isCategoryProperForGenerating(Category $category, $storeId) + { + if ($category->getParentId() != \Magento\Catalog\Model\Category::TREE_ROOT_ID) { + list(, $rootCategoryId) = $category->getParentIds(); + return $rootCategoryId == $this->storeManager->getStore($storeId)->getRootCategoryId(); + } + + return false; + } +} diff --git a/app/code/Magento/CatalogUrlRewrite/Model/ProductUrlRewriteGenerator.php b/app/code/Magento/CatalogUrlRewrite/Model/ProductUrlRewriteGenerator.php index 8519492d76346..8522c854a0a91 100644 --- a/app/code/Magento/CatalogUrlRewrite/Model/ProductUrlRewriteGenerator.php +++ b/app/code/Magento/CatalogUrlRewrite/Model/ProductUrlRewriteGenerator.php @@ -9,9 +9,8 @@ use Magento\CatalogUrlRewrite\Model\Product\CanonicalUrlRewriteGenerator; use Magento\CatalogUrlRewrite\Model\Product\CategoriesUrlRewriteGenerator; use Magento\CatalogUrlRewrite\Model\Product\CurrentUrlRewritesRegenerator; -use Magento\CatalogUrlRewrite\Model\Product\AnchorUrlRewriteGenerator; use Magento\CatalogUrlRewrite\Service\V1\StoreViewService; -use Magento\Store\Model\Store; +use Magento\Framework\App\ObjectManager; use Magento\Catalog\Model\Product\Visibility; /** @@ -26,32 +25,60 @@ class ProductUrlRewriteGenerator */ const ENTITY_TYPE = 'product'; - /** @var \Magento\CatalogUrlRewrite\Service\V1\StoreViewService */ + /** + * @var \Magento\CatalogUrlRewrite\Service\V1\StoreViewService + * @deprecated + */ protected $storeViewService; - /** @var \Magento\Catalog\Model\Product */ + /** + * @var \Magento\Catalog\Model\Product + * @deprecated + */ protected $product; - /** @var \Magento\CatalogUrlRewrite\Model\Product\CurrentUrlRewritesRegenerator */ + /** + * @var \Magento\CatalogUrlRewrite\Model\Product\CurrentUrlRewritesRegenerator + * @deprecated + */ protected $currentUrlRewritesRegenerator; - /** @var \Magento\CatalogUrlRewrite\Model\Product\CategoriesUrlRewriteGenerator */ + /** + * @var \Magento\CatalogUrlRewrite\Model\Product\CategoriesUrlRewriteGenerator + * @deprecated + */ protected $categoriesUrlRewriteGenerator; - /** @var \Magento\CatalogUrlRewrite\Model\Product\CanonicalUrlRewriteGenerator */ + /** + * @var \Magento\CatalogUrlRewrite\Model\Product\CanonicalUrlRewriteGenerator + * @deprecated + */ protected $canonicalUrlRewriteGenerator; - /** @var \Magento\CatalogUrlRewrite\Model\ObjectRegistryFactory */ + /** + * @var \Magento\CatalogUrlRewrite\Model\ObjectRegistryFactory + * @deprecated + */ protected $objectRegistryFactory; - /** @var \Magento\CatalogUrlRewrite\Model\ObjectRegistry */ + /** + * @var \Magento\CatalogUrlRewrite\Model\ObjectRegistry + * @deprecated + */ protected $productCategories; - /** @var \Magento\Store\Model\StoreManagerInterface */ + /** + * @var \Magento\Store\Model\StoreManagerInterface + * @deprecated + */ protected $storeManager; - /** @var AnchorUrlRewriteGenerator */ - private $anchorUrlRewriteGenerator; + /** + * Generates url rewrites for different scopes. + * + * @var ProductScopeRewriteGenerator + */ + private $productScopeRewriteGenerator; /** * @param \Magento\CatalogUrlRewrite\Model\Product\CanonicalUrlRewriteGenerator $canonicalUrlRewriteGenerator @@ -78,123 +105,105 @@ public function __construct( } /** - * @return AnchorUrlRewriteGenerator + * Retrieve Delegator for generation rewrites in different scopes. * * @deprecated + * @return ProductScopeRewriteGenerator|mixed */ - private function getAnchorUrlRewriteGenerator() + private function getProductScopeRewriteGenerator() { - if ($this->anchorUrlRewriteGenerator === null) { - $this->anchorUrlRewriteGenerator = \Magento\Framework\App\ObjectManager::getInstance() - ->get('Magento\CatalogUrlRewrite\Model\Product\AnchorUrlRewriteGenerator'); + if (!$this->productScopeRewriteGenerator) { + $this->productScopeRewriteGenerator = ObjectManager::getInstance() + ->get(ProductScopeRewriteGenerator::class); } - return $this->anchorUrlRewriteGenerator; + + return $this->productScopeRewriteGenerator; } /** - * Generate product url rewrites + * Generate product url rewrites. * - * @param \Magento\Catalog\Model\Product $product + * @param Product $product + * @param int|null $rootCategoryId * @return \Magento\UrlRewrite\Service\V1\Data\UrlRewrite[] */ - public function generate(Product $product) + public function generate(Product $product, $rootCategoryId = null) { if ($product->getVisibility() == Visibility::VISIBILITY_NOT_VISIBLE) { return []; } - $this->product = $product; - $storeId = $this->product->getStoreId(); + $storeId = $product->getStoreId(); $productCategories = $product->getCategoryCollection() ->addAttributeToSelect('url_key') ->addAttributeToSelect('url_path'); $urls = $this->isGlobalScope($storeId) - ? $this->generateForGlobalScope($productCategories) - : $this->generateForSpecificStoreView($storeId, $productCategories); + ? $this->generateForGlobalScope($productCategories, $product, $rootCategoryId) + : $this->generateForSpecificStoreView($storeId, $productCategories, $product, $rootCategoryId); - $this->product = null; return $urls; } /** - * Check is global scope + * Check is global scope. * + * @deprecated * @param int|null $storeId * @return bool */ protected function isGlobalScope($storeId) { - return null === $storeId || $storeId == Store::DEFAULT_STORE_ID; + return $this->getProductScopeRewriteGenerator()->isGlobalScope($storeId); } /** - * Generate list of urls for global scope + * Generate list of urls for global scope. * + * @deprecated * @param \Magento\Framework\Data\Collection $productCategories + * @param Product|null $product + * @param int|null $rootCategoryId * @return \Magento\UrlRewrite\Service\V1\Data\UrlRewrite[] */ - protected function generateForGlobalScope($productCategories) + protected function generateForGlobalScope($productCategories, $product = null, $rootCategoryId = null) { - $urls = []; - $productId = $this->product->getEntityId(); - foreach ($this->product->getStoreIds() as $id) { - if (!$this->isGlobalScope($id) - && !$this->storeViewService->doesEntityHaveOverriddenUrlKeyForStore($id, $productId, Product::ENTITY) - ) { - $urls = array_merge($urls, $this->generateForSpecificStoreView($id, $productCategories)); - } - } - return $urls; + return $this->getProductScopeRewriteGenerator()->generateForGlobalScope( + $productCategories, + $product, + $rootCategoryId + ); } /** - * Generate list of urls for specific store view + * Generate list of urls for specific store view. * + * @deprecated * @param int $storeId * @param \Magento\Framework\Data\Collection $productCategories + * @param Product|null $product + * @param int|null $rootCategoryId * @return \Magento\UrlRewrite\Service\V1\Data\UrlRewrite[] */ - protected function generateForSpecificStoreView($storeId, $productCategories) - { - $categories = []; - foreach ($productCategories as $category) { - if ($this->isCategoryProperForGenerating($category, $storeId)) { - $categories[] = $category; - } - } - $this->productCategories = $this->objectRegistryFactory->create(['entities' => $categories]); - /** - * @var $urls \Magento\UrlRewrite\Service\V1\Data\UrlRewrite[] - */ - $urls = array_merge( - $this->canonicalUrlRewriteGenerator->generate($storeId, $this->product), - $this->categoriesUrlRewriteGenerator->generate($storeId, $this->product, $this->productCategories), - $this->currentUrlRewritesRegenerator->generate($storeId, $this->product, $this->productCategories), - $this->getAnchorUrlRewriteGenerator()->generate($storeId, $this->product, $this->productCategories) - ); - - /* Reduce duplicates. Last wins */ - $result = []; - foreach ($urls as $url) { - $result[$url->getTargetPath() . '-' . $url->getStoreId()] = $url; - } - $this->productCategories = null; - return $result; + protected function generateForSpecificStoreView( + $storeId, + $productCategories, + $product = null, + $rootCategoryId = null + ) { + return $this->getProductScopeRewriteGenerator() + ->generateForSpecificStoreView($storeId, $productCategories, $product, $rootCategoryId); } /** + * @deprecated * @param \Magento\Catalog\Model\Category $category * @param int $storeId * @return bool */ protected function isCategoryProperForGenerating($category, $storeId) { - if ($category->getParentId() != \Magento\Catalog\Model\Category::TREE_ROOT_ID) { - list(, $rootCategoryId) = $category->getParentIds(); - return $rootCategoryId == $this->storeManager->getStore($storeId)->getRootCategoryId(); - } - return false; + return $this->getProductScopeRewriteGenerator()->isCategoryProperForGenerating($category, $storeId); } } diff --git a/app/code/Magento/CatalogUrlRewrite/Model/ResourceModel/Category/Product.php b/app/code/Magento/CatalogUrlRewrite/Model/ResourceModel/Category/Product.php index f2043d305c4e8..efe75e57a3f1d 100644 --- a/app/code/Magento/CatalogUrlRewrite/Model/ResourceModel/Category/Product.php +++ b/app/code/Magento/CatalogUrlRewrite/Model/ResourceModel/Category/Product.php @@ -6,6 +6,7 @@ namespace Magento\CatalogUrlRewrite\Model\ResourceModel\Category; use Magento\Framework\Model\ResourceModel\Db\AbstractDb; +use Magento\UrlRewrite\Model\Storage\DbStorage; class Product extends AbstractDb { @@ -51,10 +52,13 @@ public function saveMultiple(array $insertData) foreach ($data as $insertData) { $totalCount += $connection->insertMultiple($this->getTable(self::TABLE_NAME), $insertData); } + return $totalCount; } /** + * Removes data by primary key. + * * @param array $removeData * @return int */ @@ -65,4 +69,37 @@ public function removeMultiple(array $removeData) ['url_rewrite_id in (?)' => $removeData] ); } + + /** + * Removes multiple entities from url_rewrite table using entities from catalog_url_rewrite_product_category. + * Example: $filter = ['category_id' => [1, 2, 3], 'product_id' => [1, 2, 3]] + * + * @param array $filter + * @return int + */ + public function removeMultipleByProductCategory(array $filter) + { + return $this->getConnection()->delete( + $this->getTable(self::TABLE_NAME), + ['url_rewrite_id in (?)' => $this->prepareSelect($filter)] + ); + } + + /** + * Prepare select statement for specific filter. + * + * @param array $data + * @return \Magento\Framework\DB\Select + */ + private function prepareSelect($data) + { + $select = $this->getConnection()->select(); + $select->from($this->getTable(DbStorage::TABLE_NAME), 'url_rewrite_id'); + + foreach ($data as $column => $value) { + $select->where($this->getConnection()->quoteIdentifier($column) . ' IN (?)', $value); + } + + return $select; + } } diff --git a/app/code/Magento/CatalogUrlRewrite/Model/UrlRewriteBunchReplacer.php b/app/code/Magento/CatalogUrlRewrite/Model/UrlRewriteBunchReplacer.php new file mode 100644 index 0000000000000..48d51f6d8a9a5 --- /dev/null +++ b/app/code/Magento/CatalogUrlRewrite/Model/UrlRewriteBunchReplacer.php @@ -0,0 +1,43 @@ +urlPersist = $urlPersist; + } + + /** + * Do Bunch Replace, with default bunch value = 10000. + * + * @param array $urls + * @param int $bunchSize + * @return void + */ + public function doBunchReplace(array $urls, $bunchSize = 10000) + { + foreach (array_chunk($urls, $bunchSize) as $urlsBunch) { + $this->urlPersist->replace($urlsBunch); + } + } +} diff --git a/app/code/Magento/CatalogUrlRewrite/Observer/AfterImportDataObserver.php b/app/code/Magento/CatalogUrlRewrite/Observer/AfterImportDataObserver.php index a4847fa11b1c4..6a9567aac72d0 100644 --- a/app/code/Magento/CatalogUrlRewrite/Observer/AfterImportDataObserver.php +++ b/app/code/Magento/CatalogUrlRewrite/Observer/AfterImportDataObserver.php @@ -6,11 +6,9 @@ namespace Magento\CatalogUrlRewrite\Observer; use Magento\Catalog\Model\Category; -use Magento\Catalog\Model\Product; use Magento\CatalogImportExport\Model\Import\Product as ImportProduct; use Magento\CatalogUrlRewrite\Model\ProductUrlRewriteGenerator; use Magento\Framework\Event\Observer; -use Magento\Framework\App\ResourceConnection; use Magento\ImportExport\Model\Import as ImportExport; use Magento\Store\Model\Store; use Magento\UrlRewrite\Model\UrlPersistInterface; @@ -20,6 +18,9 @@ use Magento\UrlRewrite\Model\UrlFinderInterface; use Magento\Framework\Event\ObserverInterface; use Magento\Catalog\Model\Product\Visibility; +use Magento\Framework\App\ObjectManager; +use Magento\UrlRewrite\Model\MergeDataProviderFactory; + /** * Class AfterImportDataObserver * @@ -96,6 +97,13 @@ class AfterImportDataObserver implements ObserverInterface 'visibility', ]; + /** + * Container for new generated url rewrites. + * + * @var \Magento\UrlRewrite\Model\MergeDataProvider + */ + private $mergeDataProviderPrototype; + /** * @param \Magento\Catalog\Model\ProductFactory $catalogProductFactory * @param \Magento\CatalogUrlRewrite\Model\ObjectRegistryFactory $objectRegistryFactory @@ -105,6 +113,7 @@ class AfterImportDataObserver implements ObserverInterface * @param UrlPersistInterface $urlPersist * @param UrlRewriteFactory $urlRewriteFactory * @param UrlFinderInterface $urlFinder + * @param \Magento\UrlRewrite\Model\MergeDataProviderFactory|null $mergeDataProviderFactory * @throws \InvalidArgumentException * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ @@ -116,7 +125,8 @@ public function __construct( \Magento\Store\Model\StoreManagerInterface $storeManager, UrlPersistInterface $urlPersist, UrlRewriteFactory $urlRewriteFactory, - UrlFinderInterface $urlFinder + UrlFinderInterface $urlFinder, + MergeDataProviderFactory $mergeDataProviderFactory = null ) { $this->urlPersist = $urlPersist; $this->catalogProductFactory = $catalogProductFactory; @@ -126,6 +136,10 @@ public function __construct( $this->storeManager = $storeManager; $this->urlRewriteFactory = $urlRewriteFactory; $this->urlFinder = $urlFinder; + if (!isset($mergeDataProviderFactory)) { + $mergeDataProviderFactory = ObjectManager::getInstance()->get(MergeDataProviderFactory::class); + } + $this->mergeDataProviderPrototype = $mergeDataProviderFactory->create(); } /** @@ -258,24 +272,15 @@ protected function populateGlobalProduct($product) */ protected function generateUrls() { - /** - * @var $urls \Magento\UrlRewrite\Service\V1\Data\UrlRewrite[] - */ - $urls = array_merge( - $this->canonicalUrlRewriteGenerate(), - $this->categoriesUrlRewriteGenerate(), - $this->currentUrlRewritesRegenerate() - ); - - /* Reduce duplicates. Last wins */ - $result = []; - foreach ($urls as $url) { - $result[$url->getTargetPath() . '-' . $url->getStoreId()] = $url; - } + $mergeDataProvider = clone $this->mergeDataProviderPrototype; + $mergeDataProvider->merge($this->canonicalUrlRewriteGenerate()); + $mergeDataProvider->merge($this->categoriesUrlRewriteGenerate()); + $mergeDataProvider->merge($this->currentUrlRewritesRegenerate()); $this->productCategories = null; - + unset($this->products); $this->products = []; - return $result; + + return $mergeDataProvider->getData(); } /** diff --git a/app/code/Magento/CatalogUrlRewrite/Observer/CategoryProcessUrlRewriteMovingObserver.php b/app/code/Magento/CatalogUrlRewrite/Observer/CategoryProcessUrlRewriteMovingObserver.php index 469b4ff78d08c..0a50ae74895ae 100644 --- a/app/code/Magento/CatalogUrlRewrite/Observer/CategoryProcessUrlRewriteMovingObserver.php +++ b/app/code/Magento/CatalogUrlRewrite/Observer/CategoryProcessUrlRewriteMovingObserver.php @@ -8,7 +8,9 @@ use Magento\Catalog\Model\Category; use Magento\CatalogUrlRewrite\Block\UrlKeyRenderer; use Magento\CatalogUrlRewrite\Model\CategoryUrlRewriteGenerator; +use Magento\CatalogUrlRewrite\Model\UrlRewriteBunchReplacer; use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\App\ObjectManager; use Magento\Store\Model\ScopeInterface; use Magento\UrlRewrite\Model\UrlPersistInterface; use Magento\Framework\Event\ObserverInterface; @@ -27,6 +29,13 @@ class CategoryProcessUrlRewriteMovingObserver implements ObserverInterface /** @var UrlRewriteHandler */ protected $urlRewriteHandler; + /** + * Url Rewrite Replacer based on bunches. + * + * @var UrlRewriteBunchReplacer + */ + private $urlRewriteBunchReplacer; + /** * @param CategoryUrlRewriteGenerator $categoryUrlRewriteGenerator * @param UrlPersistInterface $urlPersist @@ -45,6 +54,21 @@ public function __construct( $this->urlRewriteHandler = $urlRewriteHandler; } + /** + * Retrieve Url Rewrite Replacer based on bunches. + * + * @deprecated + * @return UrlRewriteBunchReplacer + */ + private function getUrlRewriteBunchReplacer() + { + if (!$this->urlRewriteBunchReplacer) { + $this->urlRewriteBunchReplacer = ObjectManager::getInstance()->get(UrlRewriteBunchReplacer::class); + } + + return $this->urlRewriteBunchReplacer; + } + /** * @param \Magento\Framework\Event\Observer $observer * @return void @@ -65,7 +89,7 @@ public function execute(\Magento\Framework\Event\Observer $observer) $this->urlRewriteHandler->generateProductUrlRewrites($category) ); $this->urlRewriteHandler->deleteCategoryRewritesForChildren($category); - $this->urlPersist->replace($urlRewrites); + $this->getUrlRewriteBunchReplacer()->doBunchReplace($urlRewrites); } } } diff --git a/app/code/Magento/CatalogUrlRewrite/Observer/CategoryProcessUrlRewriteSavingObserver.php b/app/code/Magento/CatalogUrlRewrite/Observer/CategoryProcessUrlRewriteSavingObserver.php index b7c6109ae6ada..65400883779f3 100644 --- a/app/code/Magento/CatalogUrlRewrite/Observer/CategoryProcessUrlRewriteSavingObserver.php +++ b/app/code/Magento/CatalogUrlRewrite/Observer/CategoryProcessUrlRewriteSavingObserver.php @@ -3,37 +3,77 @@ * Copyright © 2013-2017 Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +// @codingStandardsIgnoreFile + namespace Magento\CatalogUrlRewrite\Observer; use Magento\Catalog\Model\Category; use Magento\CatalogUrlRewrite\Model\CategoryUrlRewriteGenerator; -use Magento\UrlRewrite\Model\UrlPersistInterface; +use Magento\CatalogUrlRewrite\Model\UrlRewriteBunchReplacer; use Magento\Framework\Event\ObserverInterface; +use Magento\CatalogUrlRewrite\Model\Map\DatabaseMapPool; +use Magento\CatalogUrlRewrite\Model\Map\DataCategoryUrlRewriteDatabaseMap; +use Magento\CatalogUrlRewrite\Model\Map\DataProductUrlRewriteDatabaseMap; class CategoryProcessUrlRewriteSavingObserver implements ObserverInterface { - /** @var CategoryUrlRewriteGenerator */ - protected $categoryUrlRewriteGenerator; + /** + * Category UrlRewrite generator. + * + * @var CategoryUrlRewriteGenerator + */ + private $categoryUrlRewriteGenerator; - /** @var UrlPersistInterface */ - protected $urlPersist; + /** + * Url Rewrite Replacer based on bunches. + * + * @var UrlRewriteBunchReplacer + */ + private $urlRewriteBunchReplacer; - /** @var UrlRewriteHandler */ - protected $urlRewriteHandler; + /** + * UrlRewrite handler. + * + * @var UrlRewriteHandler + */ + private $urlRewriteHandler; + + /** + * Pool for database maps. + * @var DatabaseMapPool + */ + private $databaseMapPool; + + /** + * Class names of maps that hold data for url rewrites entities. + * + * @var string[] + */ + private $dataUrlRewriteClassNames; /** * @param CategoryUrlRewriteGenerator $categoryUrlRewriteGenerator - * @param UrlPersistInterface $urlPersist * @param UrlRewriteHandler $urlRewriteHandler + * @param UrlRewriteBunchReplacer $urlRewriteBunchReplacer + * @param DatabaseMapPool $databaseMapPool + * @param string[] $dataUrlRewriteClassNames */ public function __construct( CategoryUrlRewriteGenerator $categoryUrlRewriteGenerator, - UrlPersistInterface $urlPersist, - UrlRewriteHandler $urlRewriteHandler + UrlRewriteHandler $urlRewriteHandler, + UrlRewriteBunchReplacer $urlRewriteBunchReplacer, + DatabaseMapPool $databaseMapPool, + $dataUrlRewriteClassNames = [ + DataCategoryUrlRewriteDatabaseMap::class, + DataProductUrlRewriteDatabaseMap::class + ] ) { $this->categoryUrlRewriteGenerator = $categoryUrlRewriteGenerator; - $this->urlPersist = $urlPersist; $this->urlRewriteHandler = $urlRewriteHandler; + $this->urlRewriteBunchReplacer = $urlRewriteBunchReplacer; + $this->databaseMapPool = $databaseMapPool; + $this->dataUrlRewriteClassNames = $dataUrlRewriteClassNames; } /** @@ -45,7 +85,7 @@ public function __construct( public function execute(\Magento\Framework\Event\Observer $observer) { /** @var Category $category */ - $category = $observer->getEvent()->getCategory(); + $category = $observer->getEvent()->getData('category'); if ($category->getParentId() == Category::TREE_ROOT_ID) { return; } @@ -53,11 +93,28 @@ public function execute(\Magento\Framework\Event\Observer $observer) || $category->dataHasChangedFor('is_anchor') || $category->getIsChangedProductList() ) { - $urlRewrites = array_merge( - $this->categoryUrlRewriteGenerator->generate($category), - $this->urlRewriteHandler->generateProductUrlRewrites($category) - ); - $this->urlPersist->replace($urlRewrites); + $categoryUrlRewriteResult = $this->categoryUrlRewriteGenerator->generate($category); + $this->urlRewriteBunchReplacer->doBunchReplace($categoryUrlRewriteResult); + + $productUrlRewriteResult = $this->urlRewriteHandler->generateProductUrlRewrites($category); + $this->urlRewriteBunchReplacer->doBunchReplace($productUrlRewriteResult); + + //frees memory for maps that are self-initialized in multiple classes that were called by the generators + $this->resetUrlRewritesDataMaps($category); + } + } + + /** + * Resets used data maps to free up memory and temporary tables. + * + * @param Category $category + * @return void + */ + private function resetUrlRewritesDataMaps($category) + { + foreach ($this->dataUrlRewriteClassNames as $className) { + $this->databaseMapPool->resetMap($className, $category->getEntityId()); } + } } diff --git a/app/code/Magento/CatalogUrlRewrite/Observer/CategoryUrlPathAutogeneratorObserver.php b/app/code/Magento/CatalogUrlRewrite/Observer/CategoryUrlPathAutogeneratorObserver.php index 206ad612900a9..93892eb8156d8 100644 --- a/app/code/Magento/CatalogUrlRewrite/Observer/CategoryUrlPathAutogeneratorObserver.php +++ b/app/code/Magento/CatalogUrlRewrite/Observer/CategoryUrlPathAutogeneratorObserver.php @@ -47,7 +47,8 @@ public function execute(\Magento\Framework\Event\Observer $observer) { /** @var Category $category */ $category = $observer->getEvent()->getCategory(); - if ($category->getUrlKey() !== false) { + $useDefaultAttribute = !$category->isObjectNew() && !empty($category->getData('use_default')['url_key']); + if ($category->getUrlKey() !== false && !$useDefaultAttribute) { $category->setUrlKey($this->categoryUrlPathGenerator->getUrlKey($category)) ->setUrlPath($this->categoryUrlPathGenerator->getUrlPath($category)); if (!$category->isObjectNew()) { diff --git a/app/code/Magento/CatalogUrlRewrite/Observer/UrlRewriteHandler.php b/app/code/Magento/CatalogUrlRewrite/Observer/UrlRewriteHandler.php index f0f0ef208bf76..cc8d3cc4aecdd 100644 --- a/app/code/Magento/CatalogUrlRewrite/Observer/UrlRewriteHandler.php +++ b/app/code/Magento/CatalogUrlRewrite/Observer/UrlRewriteHandler.php @@ -6,11 +6,14 @@ namespace Magento\CatalogUrlRewrite\Observer; use Magento\Catalog\Model\Category; +use Magento\CatalogUrlRewrite\Model\CategoryBasedProductRewriteGenerator; use Magento\CatalogUrlRewrite\Model\CategoryUrlRewriteGenerator; use Magento\CatalogUrlRewrite\Model\ProductUrlRewriteGenerator; +use Magento\Framework\App\ObjectManager; use Magento\Framework\Event\Observer as EventObserver; use Magento\UrlRewrite\Model\UrlPersistInterface; use Magento\UrlRewrite\Service\V1\Data\UrlRewrite; +use Magento\UrlRewrite\Model\MergeDataProviderFactory; class UrlRewriteHandler { @@ -32,25 +35,45 @@ class UrlRewriteHandler /** @var \Magento\Catalog\Model\ResourceModel\Product\CollectionFactory */ protected $productCollectionFactory; + /** + * Generates product url rewrites based on category. + * + * @var CategoryBasedProductRewriteGenerator + */ + private $categoryBasedProductRewriteGenerator; + + /** + * Container for new generated url rewrites. + * + * @var \Magento\UrlRewrite\Model\MergeDataProvider + */ + private $mergeDataProviderPrototype; + /** * @param \Magento\CatalogUrlRewrite\Model\Category\ChildrenCategoriesProvider $childrenCategoriesProvider * @param CategoryUrlRewriteGenerator $categoryUrlRewriteGenerator * @param ProductUrlRewriteGenerator $productUrlRewriteGenerator * @param UrlPersistInterface $urlPersist * @param \Magento\Catalog\Model\ResourceModel\Product\CollectionFactory $productCollectionFactory + * @param \Magento\UrlRewrite\Model\MergeDataProviderFactory|null $mergeDataProviderFactory */ public function __construct( \Magento\CatalogUrlRewrite\Model\Category\ChildrenCategoriesProvider $childrenCategoriesProvider, CategoryUrlRewriteGenerator $categoryUrlRewriteGenerator, ProductUrlRewriteGenerator $productUrlRewriteGenerator, UrlPersistInterface $urlPersist, - \Magento\Catalog\Model\ResourceModel\Product\CollectionFactory $productCollectionFactory + \Magento\Catalog\Model\ResourceModel\Product\CollectionFactory $productCollectionFactory, + MergeDataProviderFactory $mergeDataProviderFactory = null ) { $this->childrenCategoriesProvider = $childrenCategoriesProvider; $this->categoryUrlRewriteGenerator = $categoryUrlRewriteGenerator; $this->productUrlRewriteGenerator = $productUrlRewriteGenerator; $this->urlPersist = $urlPersist; $this->productCollectionFactory = $productCollectionFactory; + if (!isset($mergeDataProviderFactory)) { + $mergeDataProviderFactory = ObjectManager::getInstance()->get(MergeDataProviderFactory::class); + } + $this->mergeDataProviderPrototype = $mergeDataProviderFactory->create(); } /** @@ -61,10 +84,10 @@ public function __construct( */ public function generateProductUrlRewrites(Category $category) { + $mergeDataProvider = clone $this->mergeDataProviderPrototype; $this->isSkippedProduct = []; $saveRewriteHistory = $category->getData('save_rewrites_history'); $storeId = $category->getStoreId(); - $productUrls = []; if ($category->getAffectedProductIds()) { $this->isSkippedProduct = $category->getAffectedProductIds(); $collection = $this->productCollectionFactory->create() @@ -77,38 +100,54 @@ public function generateProductUrlRewrites(Category $category) foreach ($collection as $product) { $product->setStoreId($storeId); $product->setData('save_rewrites_history', $saveRewriteHistory); - $productUrls = array_merge($productUrls, $this->productUrlRewriteGenerator->generate($product)); + $mergeDataProvider->merge( + $this->productUrlRewriteGenerator->generate($product, $category->getEntityId()) + ); } } else { - $productUrls = array_merge( - $productUrls, - $this->getCategoryProductsUrlRewrites($category, $storeId, $saveRewriteHistory) + $mergeDataProvider->merge( + $this->getCategoryProductsUrlRewrites( + $category, + $storeId, + $saveRewriteHistory, + $category->getEntityId() + ) ); } foreach ($this->childrenCategoriesProvider->getChildren($category, true) as $childCategory) { - $productUrls = array_merge( - $productUrls, - $this->getCategoryProductsUrlRewrites($childCategory, $storeId, $saveRewriteHistory) + $mergeDataProvider->merge( + $this->getCategoryProductsUrlRewrites( + $childCategory, + $storeId, + $saveRewriteHistory, + $category->getEntityId() + ) ); } - return $productUrls; + + return $mergeDataProvider->getData(); } /** * @param Category $category * @param int $storeId * @param bool $saveRewriteHistory + * @param int|null $rootCategoryId * @return UrlRewrite[] */ - public function getCategoryProductsUrlRewrites(Category $category, $storeId, $saveRewriteHistory) - { + public function getCategoryProductsUrlRewrites( + Category $category, + $storeId, + $saveRewriteHistory, + $rootCategoryId = null + ) { + $mergeDataProvider = clone $this->mergeDataProviderPrototype; /** @var \Magento\Catalog\Model\ResourceModel\Product\Collection $productCollection */ $productCollection = $category->getProductCollection() ->addAttributeToSelect('name') ->addAttributeToSelect('visibility') ->addAttributeToSelect('url_key') ->addAttributeToSelect('url_path'); - $productUrls = []; foreach ($productCollection as $product) { if (in_array($product->getId(), $this->isSkippedProduct)) { continue; @@ -116,9 +155,28 @@ public function getCategoryProductsUrlRewrites(Category $category, $storeId, $sa $this->isSkippedProduct[] = $product->getId(); $product->setStoreId($storeId); $product->setData('save_rewrites_history', $saveRewriteHistory); - $productUrls = array_merge($productUrls, $this->productUrlRewriteGenerator->generate($product)); + $mergeDataProvider->merge( + $this->getCategoryBasedProductRewriteGenerator()->generate($product, $category, $rootCategoryId) + ); } - return $productUrls; + + return $mergeDataProvider->getData(); + } + + /** + * Retrieve generator, which use single category for different products. + * + * @deprecated + * @return CategoryBasedProductRewriteGenerator|mixed + */ + private function getCategoryBasedProductRewriteGenerator() + { + if (!$this->categoryBasedProductRewriteGenerator) { + $this->categoryBasedProductRewriteGenerator = ObjectManager::getInstance() + ->get(CategoryBasedProductRewriteGenerator::class); + } + + return $this->categoryBasedProductRewriteGenerator; } /** diff --git a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/Category/ChildrenUrlRewriteGeneratorTest.php b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/Category/ChildrenUrlRewriteGeneratorTest.php index da3b0435ebd37..f41d35f7688f3 100644 --- a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/Category/ChildrenUrlRewriteGeneratorTest.php +++ b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/Category/ChildrenUrlRewriteGeneratorTest.php @@ -10,45 +10,59 @@ class ChildrenUrlRewriteGeneratorTest extends \PHPUnit_Framework_TestCase { /** @var \Magento\CatalogUrlRewrite\Model\Category\ChildrenUrlRewriteGenerator */ - protected $childrenUrlRewriteGenerator; + private $childrenUrlRewriteGenerator; /** @var \PHPUnit_Framework_MockObject_MockObject */ - protected $category; + private $category; /** @var \PHPUnit_Framework_MockObject_MockObject */ - protected $childrenCategoriesProvider; + private $childrenCategoriesProvider; /** @var \PHPUnit_Framework_MockObject_MockObject */ - protected $categoryUrlRewriteGeneratorFactory; + private $categoryUrlRewriteGeneratorFactory; /** @var \PHPUnit_Framework_MockObject_MockObject */ - protected $categoryUrlRewriteGenerator; + private $categoryUrlRewriteGenerator; + + /** @var \PHPUnit_Framework_MockObject_MockObject */ + private $mergeDataProvider; protected function setUp() { $this->childrenCategoriesProvider = $this->getMockBuilder( - 'Magento\CatalogUrlRewrite\Model\Category\ChildrenCategoriesProvider' + \Magento\CatalogUrlRewrite\Model\Category\ChildrenCategoriesProvider::class )->disableOriginalConstructor()->getMock(); - $this->category = $this->getMockBuilder('Magento\Catalog\Model\Category') + $this->category = $this->getMockBuilder(\Magento\Catalog\Model\Category::class) ->disableOriginalConstructor()->getMock(); $this->categoryUrlRewriteGeneratorFactory = $this->getMockBuilder( - 'Magento\CatalogUrlRewrite\Model\CategoryUrlRewriteGeneratorFactory' + \Magento\CatalogUrlRewrite\Model\CategoryUrlRewriteGeneratorFactory::class )->disableOriginalConstructor()->setMethods(['create'])->getMock(); $this->categoryUrlRewriteGenerator = $this->getMockBuilder( - 'Magento\CatalogUrlRewrite\Model\CategoryUrlRewriteGenerator' + \Magento\CatalogUrlRewrite\Model\CategoryUrlRewriteGenerator::class )->disableOriginalConstructor()->getMock(); + $mergeDataProviderFactory = $this->getMock( + \Magento\UrlRewrite\Model\MergeDataProviderFactory::class, + ['create'], + [], + '', + false + ); + $this->mergeDataProvider = new \Magento\UrlRewrite\Model\MergeDataProvider; + $mergeDataProviderFactory->expects($this->once())->method('create')->willReturn($this->mergeDataProvider); + $this->childrenUrlRewriteGenerator = (new ObjectManager($this))->getObject( - 'Magento\CatalogUrlRewrite\Model\Category\ChildrenUrlRewriteGenerator', + \Magento\CatalogUrlRewrite\Model\Category\ChildrenUrlRewriteGenerator::class, [ 'childrenCategoriesProvider' => $this->childrenCategoriesProvider, - 'categoryUrlRewriteGeneratorFactory' => $this->categoryUrlRewriteGeneratorFactory + 'categoryUrlRewriteGeneratorFactory' => $this->categoryUrlRewriteGeneratorFactory, + 'mergeDataProviderFactory' => $mergeDataProviderFactory ] ); } public function testNoChildrenCategories() { - $this->childrenCategoriesProvider->expects($this->once())->method('getChildren')->with($this->category, false) + $this->childrenCategoriesProvider->expects($this->once())->method('getChildren')->with($this->category, true) ->will($this->returnValue([])); $this->assertEquals([], $this->childrenUrlRewriteGenerator->generate('store_id', $this->category)); @@ -59,23 +73,33 @@ public function testGenerate() $storeId = 'store_id'; $saveRewritesHistory = 'flag'; - $childCategory = $this->getMockBuilder('Magento\Catalog\Model\Category') + $childCategory = $this->getMockBuilder(\Magento\Catalog\Model\Category::class) ->disableOriginalConstructor()->getMock(); $childCategory->expects($this->once())->method('setStoreId')->with($storeId); $childCategory->expects($this->once())->method('setData') ->with('save_rewrites_history', $saveRewritesHistory); - $this->childrenCategoriesProvider->expects($this->once())->method('getChildren')->with($this->category, false) + $this->childrenCategoriesProvider->expects($this->once())->method('getChildren')->with($this->category, true) ->will($this->returnValue([$childCategory])); $this->category->expects($this->any())->method('getData')->with('save_rewrites_history') ->will($this->returnValue($saveRewritesHistory)); $this->categoryUrlRewriteGeneratorFactory->expects($this->once())->method('create') ->will($this->returnValue($this->categoryUrlRewriteGenerator)); - $this->categoryUrlRewriteGenerator->expects($this->once())->method('generate')->with($childCategory) - ->will($this->returnValue([['url-1', 'url-2']])); + $url1 = new \Magento\UrlRewrite\Service\V1\Data\UrlRewrite(); + $url1->setRequestPath('category-1') + ->setStoreId(1); + $url2 = new \Magento\UrlRewrite\Service\V1\Data\UrlRewrite(); + $url2->setRequestPath('category-2') + ->setStoreId(2); + $url3 = new \Magento\UrlRewrite\Service\V1\Data\UrlRewrite(); + $url3->setRequestPath('category-1') + ->setStoreId(1); + $this->categoryUrlRewriteGenerator->expects($this->once())->method('generate') + ->with($childCategory, false, 1) + ->will($this->returnValue([$url1, $url2, $url3])); $this->assertEquals( - [['url-1', 'url-2']], - $this->childrenUrlRewriteGenerator->generate($storeId, $this->category) + ['category-1_1' => $url1, 'category-2_2' => $url2], + $this->childrenUrlRewriteGenerator->generate($storeId, $this->category, 1) ); } } diff --git a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/Category/CurrentUrlRewritesRegeneratorTest.php b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/Category/CurrentUrlRewritesRegeneratorTest.php index d97a14ae56b68..10d78f5c92df4 100644 --- a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/Category/CurrentUrlRewritesRegeneratorTest.php +++ b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/Category/CurrentUrlRewritesRegeneratorTest.php @@ -3,7 +3,6 @@ * Copyright © 2013-2017 Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile namespace Magento\CatalogUrlRewrite\Test\Unit\Model\Category; use Magento\CatalogUrlRewrite\Model\CategoryUrlRewriteGenerator; @@ -14,70 +13,79 @@ class CurrentUrlRewritesRegeneratorTest extends \PHPUnit_Framework_TestCase { /** @var \Magento\CatalogUrlRewrite\Model\Category\CurrentUrlRewritesRegenerator */ - protected $currentUrlRewritesRegenerator; - - /** @var \PHPUnit_Framework_MockObject_MockObject */ - protected $filter; - - /** @var \Magento\UrlRewrite\Model\UrlFinderInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $urlFinder; + private $currentUrlRewritesRegenerator; /** @var \Magento\CatalogUrlRewrite\Model\CategoryUrlPathGenerator|\PHPUnit_Framework_MockObject_MockObject */ - protected $categoryUrlPathGenerator; + private $categoryUrlPathGenerator; /** @var \Magento\Catalog\Model\Category|\PHPUnit_Framework_MockObject_MockObject */ - protected $category; + private $category; /** @var \Magento\UrlRewrite\Service\V1\Data\UrlRewriteFactory|\PHPUnit_Framework_MockObject_MockObject */ - protected $urlRewriteFactory; + private $urlRewriteFactory; /** @var \Magento\UrlRewrite\Service\V1\Data\UrlRewrite|\PHPUnit_Framework_MockObject_MockObject */ - protected $urlRewrite; + private $urlRewrite; + + /** @var \PHPUnit_Framework_MockObject_MockObject */ + private $mergeDataProvider; + + /** @var \PHPUnit_Framework_MockObject_MockObject */ + private $urlRewriteFinder; protected function setUp() { - $this->urlRewriteFactory = $this->getMockBuilder('Magento\UrlRewrite\Service\V1\Data\UrlRewriteFactory') + $this->urlRewriteFactory = $this->getMockBuilder(\Magento\UrlRewrite\Service\V1\Data\UrlRewriteFactory::class) ->setMethods(['create']) ->disableOriginalConstructor()->getMock(); - $this->urlRewrite = $this->getMockBuilder('Magento\UrlRewrite\Service\V1\Data\UrlRewrite') - ->disableOriginalConstructor()->getMock(); - $this->category = $this->getMockBuilder('Magento\Catalog\Model\Category') + $this->urlRewrite = $this->getMockBuilder(\Magento\UrlRewrite\Service\V1\Data\UrlRewrite::class) ->disableOriginalConstructor()->getMock(); - $this->filter = $this->getMockBuilder('Magento\UrlRewrite\Service\V1\Data\Filter') - ->disableOriginalConstructor()->getMock(); - $this->filter->expects($this->any())->method('setStoreId')->will($this->returnSelf()); - $this->filter->expects($this->any())->method('setEntityId')->will($this->returnSelf()); - $this->urlFinder = $this->getMockBuilder('Magento\UrlRewrite\Model\UrlFinderInterface') + $this->category = $this->getMockBuilder(\Magento\Catalog\Model\Category::class) ->disableOriginalConstructor()->getMock(); $this->categoryUrlPathGenerator = $this->getMockBuilder( - 'Magento\CatalogUrlRewrite\Model\CategoryUrlPathGenerator' + \Magento\CatalogUrlRewrite\Model\CategoryUrlPathGenerator::class )->disableOriginalConstructor()->getMock(); + $this->urlRewriteFinder = $this->getMockBuilder(\Magento\CatalogUrlRewrite\Model\Map\UrlRewriteFinder::class) + ->disableOriginalConstructor()->getMock(); + $this->urlRewriteFactory->expects($this->once())->method('create') + ->willReturn($this->urlRewrite); + $mergeDataProviderFactory = $this->getMock( + \Magento\UrlRewrite\Model\MergeDataProviderFactory::class, + ['create'], + [], + '', + false + ); + $this->mergeDataProvider = new \Magento\UrlRewrite\Model\MergeDataProvider; + $mergeDataProviderFactory->expects($this->once())->method('create')->willReturn($this->mergeDataProvider); + $this->currentUrlRewritesRegenerator = (new ObjectManager($this))->getObject( - 'Magento\CatalogUrlRewrite\Model\Category\CurrentUrlRewritesRegenerator', + \Magento\CatalogUrlRewrite\Model\Category\CurrentUrlRewritesRegenerator::class, [ - 'urlFinder' => $this->urlFinder, 'categoryUrlPathGenerator' => $this->categoryUrlPathGenerator, - 'urlRewriteFactory' => $this->urlRewriteFactory + 'urlRewriteFactory' => $this->urlRewriteFactory, + 'mergeDataProviderFactory' => $mergeDataProviderFactory, + 'urlRewriteFinder' => $this->urlRewriteFinder ] ); } public function testIsAutogeneratedWithoutSaveRewriteHistory() { - $this->urlFinder->expects($this->once())->method('findAllByData') + $this->urlRewriteFinder->expects($this->once())->method('findAllByData') ->will($this->returnValue($this->getCurrentRewritesMocks([[UrlRewrite::IS_AUTOGENERATED => 1]]))); $this->category->expects($this->once())->method('getData')->with('save_rewrites_history') ->will($this->returnValue(false)); $this->assertEquals( [], - $this->currentUrlRewritesRegenerator->generate('store_id', $this->category) + $this->currentUrlRewritesRegenerator->generate('store_id', $this->category, $this->category) ); } public function testSkipGenerationForAutogenerated() { - $this->urlFinder->expects($this->once())->method('findAllByData') + $this->urlRewriteFinder->expects($this->once())->method('findAllByData') ->will( $this->returnValue( $this->getCurrentRewritesMocks( @@ -94,7 +102,7 @@ public function testSkipGenerationForAutogenerated() $this->assertEquals( [], - $this->currentUrlRewritesRegenerator->generate('store_id', $this->category) + $this->currentUrlRewritesRegenerator->generate('store_id', $this->category, $this->category) ); } @@ -104,7 +112,7 @@ public function testIsAutogenerated() $targetPath = 'some-path.html'; $storeId = 2; $categoryId = 12; - $this->urlFinder->expects($this->once())->method('findAllByData') + $this->urlRewriteFinder->expects($this->once())->method('findAllByData') ->will( $this->returnValue( $this->getCurrentRewritesMocks( @@ -120,23 +128,23 @@ public function testIsAutogenerated() ) ) ); - $this->category->expects($this->any())->method('getId')->will($this->returnValue($categoryId)); + $this->category->expects($this->any())->method('getEntityId')->will($this->returnValue($categoryId)); $this->category->expects($this->once())->method('getData')->with('save_rewrites_history') ->will($this->returnValue(true)); $this->categoryUrlPathGenerator->expects($this->once())->method('getUrlPathWithSuffix') ->will($this->returnValue($targetPath)); - $this->prepareUrlRewriteMock($storeId, $categoryId, $requestPath, $targetPath, OptionProvider::PERMANENT); + $this->prepareUrlRewriteMock($storeId, $categoryId, $requestPath, $targetPath, OptionProvider::PERMANENT, 0); $this->assertEquals( ['autogenerated.html_2' => $this->urlRewrite], - $this->currentUrlRewritesRegenerator->generate($storeId, $this->category) + $this->currentUrlRewritesRegenerator->generate($storeId, $this->category, $this->category) ); } public function testSkipGenerationForCustom() { - $this->urlFinder->expects($this->once())->method('findAllByData') + $this->urlRewriteFinder->expects($this->once())->method('findAllByData') ->will( $this->returnValue( $this->getCurrentRewritesMocks( @@ -155,7 +163,7 @@ public function testSkipGenerationForCustom() $this->assertEquals( [], - $this->currentUrlRewritesRegenerator->generate('store_id', $this->category) + $this->currentUrlRewritesRegenerator->generate('store_id', $this->category, $this->category) ); } @@ -166,7 +174,7 @@ public function testGenerationForCustomWithoutTargetPathGeneration() $requestPath = 'generate-for-custom-without-redirect-type.html'; $targetPath = 'custom-target-path.html'; $description = 'description'; - $this->urlFinder->expects($this->once())->method('findAllByData') + $this->urlRewriteFinder->expects($this->once())->method('findAllByData') ->will( $this->returnValue( $this->getCurrentRewritesMocks( @@ -184,16 +192,14 @@ public function testGenerationForCustomWithoutTargetPathGeneration() ) ); $this->categoryUrlPathGenerator->expects($this->never())->method('getUrlPathWithSuffix'); - $this->category->expects($this->any())->method('getId')->will($this->returnValue($categoryId)); + $this->category->expects($this->any())->method('getEntityId')->will($this->returnValue($categoryId)); $this->urlRewrite->expects($this->once())->method('setDescription')->with($description) ->will($this->returnSelf()); - $this->urlRewriteFactory->expects($this->once())->method('create') - ->willReturn($this->urlRewrite); - $this->prepareUrlRewriteMock($storeId, $categoryId, $requestPath, $targetPath, 0); + $this->prepareUrlRewriteMock($storeId, $categoryId, $requestPath, $targetPath, 0, 0); $this->assertEquals( ['generate-for-custom-without-redirect-type.html_12' => $this->urlRewrite], - $this->currentUrlRewritesRegenerator->generate($storeId, $this->category) + $this->currentUrlRewritesRegenerator->generate($storeId, $this->category, $this->category) ); } @@ -204,7 +210,7 @@ public function testGenerationForCustomWithTargetPathGeneration() $requestPath = 'generate-for-custom-without-redirect-type.html'; $targetPath = 'generated-target-path.html'; $description = 'description'; - $this->urlFinder->expects($this->once())->method('findAllByData') + $this->urlRewriteFinder->expects($this->once())->method('findAllByData') ->will( $this->returnValue( $this->getCurrentRewritesMocks( @@ -223,16 +229,14 @@ public function testGenerationForCustomWithTargetPathGeneration() ); $this->categoryUrlPathGenerator->expects($this->any())->method('getUrlPathWithSuffix') ->will($this->returnValue($targetPath)); - $this->category->expects($this->any())->method('getId')->will($this->returnValue($categoryId)); + $this->category->expects($this->any())->method('getEntityId')->will($this->returnValue($categoryId)); $this->urlRewrite->expects($this->once())->method('setDescription')->with($description) ->will($this->returnSelf()); - $this->urlRewriteFactory->expects($this->once())->method('create') - ->willReturn($this->urlRewrite); - $this->prepareUrlRewriteMock($storeId, $categoryId, $requestPath, $targetPath, 'code'); + $this->prepareUrlRewriteMock($storeId, $categoryId, $requestPath, $targetPath, 'code', 0); $this->assertEquals( ['generate-for-custom-without-redirect-type.html_12' => $this->urlRewrite], - $this->currentUrlRewritesRegenerator->generate($storeId, $this->category) + $this->currentUrlRewritesRegenerator->generate($storeId, $this->category, $this->category) ); } @@ -245,7 +249,7 @@ protected function getCurrentRewritesMocks($currentRewrites) $rewrites = []; foreach ($currentRewrites as $urlRewrite) { /** @var \PHPUnit_Framework_MockObject_MockObject */ - $url = $this->getMockBuilder('Magento\UrlRewrite\Service\V1\Data\UrlRewrite') + $url = $this->getMockBuilder(\Magento\UrlRewrite\Service\V1\Data\UrlRewrite::class) ->disableOriginalConstructor()->getMock(); foreach ($urlRewrite as $key => $value) { $url->expects($this->any()) @@ -263,9 +267,16 @@ protected function getCurrentRewritesMocks($currentRewrites) * @param mixed $requestPath * @param mixed $targetPath * @param mixed $redirectType + * @param int $isAutoGenerated */ - protected function prepareUrlRewriteMock($storeId, $categoryId, $requestPath, $targetPath, $redirectType) - { + protected function prepareUrlRewriteMock( + $storeId, + $categoryId, + $requestPath, + $targetPath, + $redirectType, + $isAutoGenerated + ) { $this->urlRewrite->expects($this->any())->method('setStoreId')->with($storeId) ->will($this->returnSelf()); $this->urlRewrite->expects($this->any())->method('setEntityId')->with($categoryId) @@ -276,11 +287,14 @@ protected function prepareUrlRewriteMock($storeId, $categoryId, $requestPath, $t ->will($this->returnSelf()); $this->urlRewrite->expects($this->any())->method('setTargetPath')->with($targetPath) ->will($this->returnSelf()); - $this->urlRewrite->expects($this->any())->method('setIsAutogenerated')->with(0) + $this->urlRewrite->expects($this->any())->method('setIsAutogenerated')->with($isAutoGenerated) ->will($this->returnSelf()); $this->urlRewrite->expects($this->any())->method('setRedirectType')->with($redirectType) ->will($this->returnSelf()); $this->urlRewrite->expects($this->any())->method('setMetadata')->with([])->will($this->returnSelf()); + $this->urlRewrite->expects($this->any())->method('getTargetPath')->willReturn($targetPath); + $this->urlRewrite->expects($this->any())->method('getRequestPath')->willReturn($requestPath); + $this->urlRewrite->expects($this->any())->method('getStoreId')->willReturn($storeId); $this->urlRewriteFactory->expects($this->any())->method('create')->will($this->returnValue($this->urlRewrite)); } } diff --git a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/Category/Plugin/StorageTest.php b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/Category/Plugin/StorageTest.php new file mode 100644 index 0000000000000..82f7863592410 --- /dev/null +++ b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/Category/Plugin/StorageTest.php @@ -0,0 +1,122 @@ +storage = $this->getMockBuilder(StorageInterface::class) + ->getMockForAbstractClass(); + $this->urlFinder = $this->getMockBuilder(UrlFinderInterface::class) + ->getMockForAbstractClass(); + $this->product = $this->getMockBuilder(Product::class) + ->disableOriginalConstructor() + ->getMock(); + $this->productResourceModel = $this->getMockBuilder(ProductResourceModel::class) + ->disableOriginalConstructor() + ->getMock(); + $this->urlRewrite = $this->getMockBuilder(UrlRewrite::class) + ->disableOriginalConstructor() + ->setMethods(['getMetadata', 'getEntityType', 'getIsAutogenerated', 'getUrlRewriteId', 'getEntityId']) + ->getMock(); + + $this->plugin = (new ObjectManager($this))->getObject( + CategoryStoragePlugin::class, + [ + 'urlFinder' => $this->urlFinder, + 'productResource' => $this->productResourceModel + ] + ); + } + + /** + * Covers afterReplace() method. + * + * @return void + */ + public function testAroundReplace() + { + $this->urlRewrite->expects(static::any())->method('getMetadata')->willReturn(['category_id' => '5']); + $this->urlRewrite->expects(static::once())->method('getEntityTYpe')->willReturn('product'); + $this->urlRewrite->expects(static::once())->method('getIsAutogenerated')->willReturn(1); + $this->urlRewrite->expects(static::once())->method('getUrlRewriteId')->willReturn('4'); + $this->urlRewrite->expects(static::once())->method('getEntityId')->willReturn('2'); + $this->urlRewrite->setData('request_path', 'test'); + $this->urlRewrite->setData('store_id', '1'); + $productUrls = ['targetPath' => $this->urlRewrite]; + + $this->urlFinder->expects(static::once())->method('findAllByData')->willReturn([$this->urlRewrite]); + + $this->productResourceModel->expects(static::once())->method('saveMultiple')->willReturnSelf(); + + $className = 'Magento\UrlRewrite\Model\StorageInterface'; + /** @var \Magento\SalesRule\Model\Rule|\PHPUnit_Framework_MockObject_MockObject $subject */ + $subject = $this->getMock($className, [], [], '', false); + $closureMock = function () use ($subject) { + return $subject; + }; + + $this->plugin->aroundReplace($this->storage, $closureMock, $productUrls); + } + + /** + * Covers beforeDeleteByData() method. + * + * @return void + */ + public function testBeforeDeleteByData() + { + $data = [1, 2, 3]; + $this->productResourceModel->expects(static::once()) + ->method('removeMultipleByProductCategory') + ->with($data)->willReturnSelf(); + $this->plugin->beforeDeleteByData($this->storage, $data); + } +} diff --git a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/CategoryBasedProductRewriteGeneratorTest.php b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/CategoryBasedProductRewriteGeneratorTest.php new file mode 100644 index 0000000000000..1b4029b112268 --- /dev/null +++ b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/CategoryBasedProductRewriteGeneratorTest.php @@ -0,0 +1,114 @@ +productScopeRewriteGeneratorMock = $this->getMockBuilder(ProductScopeRewriteGenerator::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->generator = new CategoryBasedProductRewriteGenerator( + $this->productScopeRewriteGeneratorMock + ); + } + + /** + * Covers generate() with global scope. + * + * @return void + */ + public function testGenerationWithGlobalScope() + { + $categoryMock = $this->getMockBuilder(Category::class) + ->disableOriginalConstructor() + ->getMock(); + $productMock = $this->getMockBuilder(Product::class) + ->disableOriginalConstructor() + ->getMock(); + $storeId = 1; + $categoryId = 1; + $urls = ['dummy-url.html']; + + $productMock->expects($this->once()) + ->method('getVisibility') + ->willReturn(2); + $productMock->expects($this->once()) + ->method('getStoreId') + ->willReturn($storeId); + $this->productScopeRewriteGeneratorMock->expects($this->once()) + ->method('isGlobalScope') + ->with($storeId) + ->willReturn(true); + $this->productScopeRewriteGeneratorMock->expects($this->once()) + ->method('generateForGlobalScope') + ->with([$categoryMock], $productMock, $categoryId) + ->willReturn($urls); + + $this->assertEquals($urls, $this->generator->generate($productMock, $categoryMock, $categoryId)); + } + + /** + * Covers generate() with specific store. + * + * @return void + */ + public function testGenerationWithSpecificStore() + { + $categoryMock = $this->getMockBuilder(Category::class) + ->disableOriginalConstructor() + ->getMock(); + $productMock = $this->getMockBuilder(Product::class) + ->disableOriginalConstructor() + ->getMock(); + $storeId = 1; + $categoryId = 1; + $urls = ['dummy-url.html']; + + $productMock->expects($this->once()) + ->method('getVisibility') + ->willReturn(2); + $productMock->expects($this->once()) + ->method('getStoreId') + ->willReturn($storeId); + $this->productScopeRewriteGeneratorMock->expects($this->once()) + ->method('isGlobalScope') + ->with($storeId) + ->willReturn(false); + $this->productScopeRewriteGeneratorMock->expects($this->once()) + ->method('generateForSpecificStoreView') + ->with($storeId, [$categoryMock], $productMock, $categoryId) + ->willReturn($urls); + + $this->assertEquals($urls, $this->generator->generate($productMock, $categoryMock, $categoryId)); + } +} diff --git a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/CategoryUrlRewriteGeneratorTest.php b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/CategoryUrlRewriteGeneratorTest.php index d273947629f8b..c3a2ebb47b156 100644 --- a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/CategoryUrlRewriteGeneratorTest.php +++ b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/CategoryUrlRewriteGeneratorTest.php @@ -14,25 +14,28 @@ class CategoryUrlRewriteGeneratorTest extends \PHPUnit_Framework_TestCase { /** @var \PHPUnit_Framework_MockObject_MockObject */ - protected $canonicalUrlRewriteGenerator; + private $canonicalUrlRewriteGenerator; /** @var \PHPUnit_Framework_MockObject_MockObject */ - protected $currentUrlRewritesRegenerator; + private $currentUrlRewritesRegenerator; /** @var \PHPUnit_Framework_MockObject_MockObject */ - protected $childrenUrlRewriteGenerator; + private $childrenUrlRewriteGenerator; /** @var \Magento\CatalogUrlRewrite\Model\CategoryUrlRewriteGenerator */ - protected $categoryUrlRewriteGenerator; + private $categoryUrlRewriteGenerator; /** @var \Magento\CatalogUrlRewrite\Service\V1\StoreViewService|\PHPUnit_Framework_MockObject_MockObject */ - protected $storeViewService; + private $storeViewService; /** @var \Magento\Catalog\Model\Category|\PHPUnit_Framework_MockObject_MockObject */ - protected $category; + private $category; /** @var \Magento\Catalog\Api\CategoryRepositoryInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $categoryRepository; + private $categoryRepository; + + /** @var \PHPUnit_Framework_MockObject_MockObject */ + private $mergeDataProvider; /** * Test method @@ -40,27 +43,37 @@ class CategoryUrlRewriteGeneratorTest extends \PHPUnit_Framework_TestCase protected function setUp() { $this->currentUrlRewritesRegenerator = $this->getMockBuilder( - 'Magento\CatalogUrlRewrite\Model\Category\CurrentUrlRewritesRegenerator' + \Magento\CatalogUrlRewrite\Model\Category\CurrentUrlRewritesRegenerator::class )->disableOriginalConstructor()->getMock(); $this->canonicalUrlRewriteGenerator = $this->getMockBuilder( - 'Magento\CatalogUrlRewrite\Model\Category\CanonicalUrlRewriteGenerator' + \Magento\CatalogUrlRewrite\Model\Category\CanonicalUrlRewriteGenerator::class )->disableOriginalConstructor()->getMock(); $this->childrenUrlRewriteGenerator = $this->getMockBuilder( - 'Magento\CatalogUrlRewrite\Model\Category\ChildrenUrlRewriteGenerator' + \Magento\CatalogUrlRewrite\Model\Category\ChildrenUrlRewriteGenerator::class )->disableOriginalConstructor()->getMock(); - $this->storeViewService = $this->getMockBuilder('Magento\CatalogUrlRewrite\Service\V1\StoreViewService') + $this->storeViewService = $this->getMockBuilder(\Magento\CatalogUrlRewrite\Service\V1\StoreViewService::class) ->disableOriginalConstructor()->getMock(); - $this->category = $this->getMock('Magento\Catalog\Model\Category', [], [], '', false); - $this->categoryRepository = $this->getMock('Magento\Catalog\Api\CategoryRepositoryInterface'); + $this->category = $this->getMock(\Magento\Catalog\Model\Category::class, [], [], '', false); + $this->categoryRepository = $this->getMock(\Magento\Catalog\Api\CategoryRepositoryInterface::class); + $mergeDataProviderFactory = $this->getMock( + \Magento\UrlRewrite\Model\MergeDataProviderFactory::class, + ['create'], + [], + '', + false + ); + $this->mergeDataProvider = new \Magento\UrlRewrite\Model\MergeDataProvider; + $mergeDataProviderFactory->expects($this->once())->method('create')->willReturn($this->mergeDataProvider); $this->categoryUrlRewriteGenerator = (new ObjectManager($this))->getObject( - 'Magento\CatalogUrlRewrite\Model\CategoryUrlRewriteGenerator', + \Magento\CatalogUrlRewrite\Model\CategoryUrlRewriteGenerator::class, [ 'canonicalUrlRewriteGenerator' => $this->canonicalUrlRewriteGenerator, 'childrenUrlRewriteGenerator' => $this->childrenUrlRewriteGenerator, 'currentUrlRewritesRegenerator' => $this->currentUrlRewritesRegenerator, 'storeViewService' => $this->storeViewService, 'categoryRepository' => $this->categoryRepository, + 'mergeDataProviderFactory' => $mergeDataProviderFactory ] ); } @@ -70,27 +83,33 @@ protected function setUp() */ public function testGenerationForGlobalScope() { + $categoryId = 1; $this->category->expects($this->any())->method('getStoreId')->will($this->returnValue(null)); $this->category->expects($this->any())->method('getStoreIds')->will($this->returnValue([1])); $this->storeViewService->expects($this->once())->method('doesEntityHaveOverriddenUrlKeyForStore') ->will($this->returnValue(false)); $canonical = new \Magento\UrlRewrite\Service\V1\Data\UrlRewrite(); - $canonical->setTargetPath('category-1') + $canonical->setRequestPath('category-1') ->setStoreId(1); $this->canonicalUrlRewriteGenerator->expects($this->any())->method('generate') - ->will($this->returnValue([$canonical])); - $children = new \Magento\UrlRewrite\Service\V1\Data\UrlRewrite(); - $children->setTargetPath('category-2') + ->will($this->returnValue(['category-1' => $canonical])); + $children1 = new \Magento\UrlRewrite\Service\V1\Data\UrlRewrite(); + $children1->setRequestPath('category-2') + ->setStoreId(2); + $children2 = new \Magento\UrlRewrite\Service\V1\Data\UrlRewrite(); + $children2->setRequestPath('category-22') ->setStoreId(2); $this->childrenUrlRewriteGenerator->expects($this->any())->method('generate') - ->will($this->returnValue([$children])); + ->with(1, $this->category, $categoryId) + ->will($this->returnValue(['category-2' => $children1, 'category-1' => $children2])); $current = new \Magento\UrlRewrite\Service\V1\Data\UrlRewrite(); - $current->setTargetPath('category-3') + $current->setRequestPath('category-3') ->setStoreId(3); $this->currentUrlRewritesRegenerator->expects($this->any())->method('generate') - ->will($this->returnValue([$current])); + ->with(1, $this->category, $categoryId) + ->will($this->returnValue(['category-3' => $current])); $categoryForSpecificStore = $this->getMock( - 'Magento\Catalog\Model\Category', + \Magento\Catalog\Model\Category::class, ['getUrlKey', 'getUrlPath'], [], '', @@ -99,8 +118,13 @@ public function testGenerationForGlobalScope() $this->categoryRepository->expects($this->once())->method('get')->willReturn($categoryForSpecificStore); $this->assertEquals( - [$canonical, $children, $current], - $this->categoryUrlRewriteGenerator->generate($this->category) + [ + 'category-1_1' => $canonical, + 'category-2_2' => $children1, + 'category-22_2' => $children2, + 'category-3_3' => $current + ], + $this->categoryUrlRewriteGenerator->generate($this->category, false, $categoryId) ); } @@ -112,7 +136,7 @@ public function testGenerationForSpecificStore() $this->category->expects($this->any())->method('getStoreId')->will($this->returnValue(1)); $this->category->expects($this->never())->method('getStoreIds'); $canonical = new \Magento\UrlRewrite\Service\V1\Data\UrlRewrite(); - $canonical->setTargetPath('category-1') + $canonical->setRequestPath('category-1') ->setStoreId(1); $this->canonicalUrlRewriteGenerator->expects($this->any())->method('generate') ->will($this->returnValue([$canonical])); @@ -121,7 +145,10 @@ public function testGenerationForSpecificStore() $this->currentUrlRewritesRegenerator->expects($this->any())->method('generate') ->will($this->returnValue([])); - $this->assertEquals([$canonical], $this->categoryUrlRewriteGenerator->generate($this->category)); + $this->assertEquals( + ['category-1_1' => $canonical], + $this->categoryUrlRewriteGenerator->generate($this->category, 1) + ); } /** @@ -135,4 +162,18 @@ public function testSkipGenerationForGlobalScope() $this->assertEquals([], $this->categoryUrlRewriteGenerator->generate($this->category)); } + + /** + * Test method + */ + public function testSkipGenerationForGlobalScopeWithCategory() + { + $this->category->expects($this->any())->method('getStoreIds')->will($this->returnValue([1, 2])); + $this->category->expects($this->any())->method('getEntityId')->will($this->returnValue(1)); + $this->category->expects($this->any())->method('getStoreId')->will($this->returnValue(false)); + $this->storeViewService->expects($this->exactly(2))->method('doesEntityHaveOverriddenUrlKeyForStore') + ->will($this->returnValue(true)); + + $this->assertEquals([], $this->categoryUrlRewriteGenerator->generate($this->category, false, 1)); + } } diff --git a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/Map/DataCategoryHashMapTest.php b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/Map/DataCategoryHashMapTest.php new file mode 100644 index 0000000000000..aaec181834e07 --- /dev/null +++ b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/Map/DataCategoryHashMapTest.php @@ -0,0 +1,104 @@ +categoryRepository = $this->getMock(CategoryRepository::class, [], [], '', false); + $this->categoryResourceFactory = $this->getMock(CategoryFactory::class, ['create'], [], '', false); + $this->categoryResource = $this->getMock( + Category::class, + ['getConnection', 'getEntityTable'], + [], + '', + false + ); + + $this->categoryResourceFactory->expects($this->any()) + ->method('create') + ->willReturn($this->categoryResource); + + $this->model = (new ObjectManager($this))->getObject( + DataCategoryHashMap::class, + [ + 'categoryRepository' => $this->categoryRepository, + 'categoryResourceFactory' => $this->categoryResourceFactory + ] + ); + } + + /** + * Tests getAllData, getData and resetData functionality. + */ + public function testGetAllData() + { + $categoryIds = ['1' => [1, 2, 3], '2' => [2, 3], '3' => 3]; + $categoryIdsOther = ['2' => [2, 3, 4]]; + + $categoryMock = $this->getMock(CategoryInterface::class, [], [], '', false); + $connectionAdapterMock = $this->getMock(AdapterInterface::class); + $selectMock = $this->getMock(Select::class, [], [], '', false); + + $this->categoryRepository->expects($this->any()) + ->method('get') + ->willReturn($categoryMock); + $categoryMock->expects($this->any()) + ->method('getResource') + ->willReturn($this->categoryResource); + $this->categoryResource->expects($this->any()) + ->method('getConnection') + ->willReturn($connectionAdapterMock); + $this->categoryResource->expects($this->any()) + ->method('getEntityTable') + ->willReturn('category_entity'); + $connectionAdapterMock->expects($this->any()) + ->method('select') + ->willReturn($selectMock); + $selectMock->expects($this->any()) + ->method('from') + ->willReturnSelf(); + $selectMock->expects($this->any()) + ->method('where') + ->willReturnSelf(); + $connectionAdapterMock->expects($this->any()) + ->method('fetchCol') + ->willReturnOnConsecutiveCalls($categoryIds, $categoryIdsOther, $categoryIds); + + $this->assertEquals($categoryIds, $this->model->getAllData(1)); + $this->assertEquals($categoryIds[2], $this->model->getData(1, 2)); + $this->assertEquals($categoryIdsOther, $this->model->getAllData(2)); + $this->assertEquals($categoryIdsOther[2], $this->model->getData(2, 2)); + $this->model->resetData(1); + $this->assertEquals($categoryIds[2], $this->model->getData(1, 2)); + $this->assertEquals($categoryIds, $this->model->getAllData(1)); + } +} diff --git a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/Map/DataCategoryUrlRewriteDatabaseMapTest.php b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/Map/DataCategoryUrlRewriteDatabaseMapTest.php new file mode 100644 index 0000000000000..549a2db774208 --- /dev/null +++ b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/Map/DataCategoryUrlRewriteDatabaseMapTest.php @@ -0,0 +1,125 @@ +hashMapPoolMock = $this->getMock(HashMapPool::class, [], [], '', false); + $this->dataCategoryMapMock = $this->getMock(DataProductHashMap::class, [], [], '', false); + $this->dataCategoryUsedInProductsMapMock = $this->getMock( + DataCategoryUsedInProductsHashMap::class, + [], + [], + '', + false + ); + $this->temporaryTableServiceMock = $this->getMock(TemporaryTableService::class, [], [], '', false); + $this->connectionMock = $this->getMock(ResourceConnection::class, [], [], '', false); + + $this->hashMapPoolMock->expects($this->any()) + ->method('getDataMap') + ->willReturnOnConsecutiveCalls($this->dataCategoryUsedInProductsMapMock, $this->dataCategoryMapMock); + + $this->model = (new ObjectManager($this))->getObject( + DataCategoryUrlRewriteDatabaseMap::class, + [ + 'connection' => $this->connectionMock, + 'hashMapPool' => $this->hashMapPoolMock, + 'temporaryTableService' => $this->temporaryTableServiceMock + ] + ); + } + + /** + * Tests getAllData, getData and resetData functionality + */ + public function testGetAllData() + { + $productStoreIds = [ + '1' => ['store_id' => 1, 'category_id' => 1], + '2' => ['store_id' => 2, 'category_id' => 1], + '3' => ['store_id' => 3, 'category_id' => 1], + '4' => ['store_id' => 1, 'category_id' => 2], + '5' => ['store_id' => 2, 'category_id' => 2], + ]; + + $connectionMock = $this->getMock(AdapterInterface::class); + $selectMock = $this->getMock(Select::class, [], [], '', false); + + $this->connectionMock->expects($this->any()) + ->method('getConnection') + ->willReturn($connectionMock); + $connectionMock->expects($this->any()) + ->method('select') + ->willReturn($selectMock); + $connectionMock->expects($this->any()) + ->method('fetchAll') + ->willReturn($productStoreIds[3]); + $selectMock->expects($this->any()) + ->method('from') + ->willReturnSelf(); + $selectMock->expects($this->any()) + ->method('joinInner') + ->willReturnSelf(); + $selectMock->expects($this->any()) + ->method('where') + ->willReturnSelf(); + $this->dataCategoryMapMock->expects($this->once()) + ->method('getAllData') + ->willReturn([]); + $this->dataCategoryUsedInProductsMapMock->expects($this->once()) + ->method('getAllData') + ->willReturn([]); + $this->temporaryTableServiceMock->expects($this->any()) + ->method('createFromSelect') + ->withConsecutive( + $selectMock, + $connectionMock, + [ + 'PRIMARY' => ['url_rewrite_id'], + 'HASHKEY_ENTITY_STORE' => ['hash_key'], + 'ENTITY_STORE' => ['entity_id', 'store_id'] + ] + ) + ->willReturn('tempTableName'); + + $this->assertEquals($productStoreIds[3], $this->model->getData(1, '3_1')); + } +} diff --git a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/Map/DataCategoryUsedInProductsHashMapTest.php b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/Map/DataCategoryUsedInProductsHashMapTest.php new file mode 100644 index 0000000000000..e04aef2e97545 --- /dev/null +++ b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/Map/DataCategoryUsedInProductsHashMapTest.php @@ -0,0 +1,108 @@ +hashMapPoolMock = $this->getMock(HashMapPool::class, [], [], '', false); + $this->dataCategoryMapMock = $this->getMock(DataCategoryHashMap::class, [], [], '', false); + $this->dataProductMapMock = $this->getMock(DataProductHashMap::class, [], [], '', false); + $this->connectionMock = $this->getMock(ResourceConnection::class, [], [], '', false); + + $this->hashMapPoolMock->expects($this->any()) + ->method('getDataMap') + ->willReturnOnConsecutiveCalls( + $this->dataProductMapMock, + $this->dataCategoryMapMock, + $this->dataProductMapMock, + $this->dataCategoryMapMock, + $this->dataProductMapMock, + $this->dataCategoryMapMock + ); + + $this->model = (new ObjectManager($this))->getObject( + DataCategoryUsedInProductsHashMap::class, + [ + 'connection' => $this->connectionMock, + 'hashMapPool' => $this->hashMapPoolMock + ] + ); + } + + /** + * Tests getAllData, getData and resetData functionality + */ + public function testGetAllData() + { + $categoryIds = ['1' => [1, 2, 3], '2' => [2, 3], '3' => 3]; + $categoryIdsOther = ['2' => [2, 3, 4]]; + + $connectionMock = $this->getMock(AdapterInterface::class); + $selectMock = $this->getMock(Select::class, [], [], '', false); + + $this->connectionMock->expects($this->any()) + ->method('getConnection') + ->willReturn($connectionMock); + $connectionMock->expects($this->any()) + ->method('select') + ->willReturn($selectMock); + $connectionMock->expects($this->any()) + ->method('fetchCol') + ->willReturnOnConsecutiveCalls($categoryIds, $categoryIdsOther, $categoryIds); + $selectMock->expects($this->any()) + ->method('from') + ->willReturnSelf(); + $selectMock->expects($this->any()) + ->method('joinInner') + ->willReturnSelf(); + $selectMock->expects($this->any()) + ->method('where') + ->willReturnSelf(); + $this->hashMapPoolMock->expects($this->at(4)) + ->method('resetMap') + ->with(DataProductHashMap::class, 1); + $this->hashMapPoolMock->expects($this->at(5)) + ->method('resetMap') + ->with(DataCategoryHashMap::class, 1); + + $this->assertEquals($categoryIds, $this->model->getAllData(1)); + $this->assertEquals($categoryIds[2], $this->model->getData(1, 2)); + $this->assertEquals($categoryIdsOther, $this->model->getAllData(2)); + $this->assertEquals($categoryIdsOther[2], $this->model->getData(2, 2)); + $this->model->resetData(1); + $this->assertEquals($categoryIds[2], $this->model->getData(1, 2)); + $this->assertEquals($categoryIds, $this->model->getAllData(1)); + } +} diff --git a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/Map/DataProductHashMapTest.php b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/Map/DataProductHashMapTest.php new file mode 100644 index 0000000000000..7b4e06902a1df --- /dev/null +++ b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/Map/DataProductHashMapTest.php @@ -0,0 +1,121 @@ +hashMapPoolMock = $this->getMock(HashMapPool::class, [], [], '', false); + $this->dataCategoryMapMock = $this->getMock(DataCategoryHashMap::class, [], [], '', false); + $this->collectionFactoryMock = $this->getMock(CollectionFactory::class, ['create'], [], '', false); + $this->productCollectionMock = $this->getMock( + ProductCollection::class, + ['getSelect', 'getConnection', 'getAllIds'], + [], + '', + false + ); + + $this->collectionFactoryMock->expects($this->any()) + ->method('create') + ->willReturn($this->productCollectionMock); + + $this->hashMapPoolMock->expects($this->any()) + ->method('getDataMap') + ->willReturn($this->dataCategoryMapMock); + + $this->model = (new ObjectManager($this))->getObject( + DataProductHashMap::class, + [ + 'collectionFactory' => $this->collectionFactoryMock, + 'hashMapPool' => $this->hashMapPoolMock + ] + ); + } + + /** + * Tests getAllData, getData and resetData functionality + */ + public function testGetAllData() + { + $productIds = ['1' => [1, 2, 3], '2' => [2, 3], '3' => 3]; + $productIdsOther = ['2' => [2, 3, 4]]; + + $connectionMock = $this->getMock(AdapterInterface::class); + $selectMock = $this->getMock(Select::class, [], [], '', false); + + $this->productCollectionMock->expects($this->exactly(3)) + ->method('getAllIds') + ->willReturnOnConsecutiveCalls($productIds, $productIdsOther, $productIds); + $this->productCollectionMock->expects($this->any()) + ->method('getConnection') + ->willReturn($connectionMock); + $connectionMock->expects($this->any()) + ->method('getTableName') + ->willReturn($this->returnValue($this->returnArgument(0))); + $this->productCollectionMock->expects($this->any()) + ->method('getSelect') + ->willReturn($selectMock); + $selectMock->expects($this->any()) + ->method('from') + ->willReturnSelf(); + $selectMock->expects($this->any()) + ->method('joinInner') + ->willReturnSelf(); + $selectMock->expects($this->any()) + ->method('where') + ->willReturnSelf(); + $this->dataCategoryMapMock->expects($this->any()) + ->method('getAllData') + ->willReturn([]); + $this->hashMapPoolMock->expects($this->any()) + ->method('resetMap') + ->with(DataCategoryHashMap::class, 1); + $this->assertEquals($productIds, $this->model->getAllData(1)); + $this->assertEquals($productIds[2], $this->model->getData(1, 2)); + $this->assertEquals($productIdsOther, $this->model->getAllData(2)); + $this->assertEquals($productIdsOther[2], $this->model->getData(2, 2)); + $this->model->resetData(1); + $this->assertEquals($productIds[2], $this->model->getData(1, 2)); + $this->assertEquals($productIds, $this->model->getAllData(1)); + } +} diff --git a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/Map/DataProductUrlRewriteDatabaseMapTest.php b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/Map/DataProductUrlRewriteDatabaseMapTest.php new file mode 100644 index 0000000000000..eaabc9ca99318 --- /dev/null +++ b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/Map/DataProductUrlRewriteDatabaseMapTest.php @@ -0,0 +1,112 @@ +hashMapPoolMock = $this->getMock(HashMapPool::class, [], [], '', false); + $this->dataProductMapMock = $this->getMock(DataProductHashMap::class, [], [], '', false); + $this->temporaryTableServiceMock = $this->getMock(TemporaryTableService::class, [], [], '', false); + $this->connectionMock = $this->getMock(ResourceConnection::class, [], [], '', false); + + $this->hashMapPoolMock->expects($this->any()) + ->method('getDataMap') + ->willReturn($this->dataProductMapMock); + + $this->model = (new ObjectManager($this))->getObject( + DataProductUrlRewriteDatabaseMap::class, + [ + 'connection' => $this->connectionMock, + 'hashMapPool' => $this->hashMapPoolMock, + 'temporaryTableService' => $this->temporaryTableServiceMock + ] + ); + } + + /** + * Tests getAllData, getData and resetData functionality + */ + public function testGetAllData() + { + $productStoreIds = [ + '1' => ['store_id' => 1, 'product_id' => 1], + '2' => ['store_id' => 2, 'product_id' => 1], + '3' => ['store_id' => 3, 'product_id' => 1], + '4' => ['store_id' => 1, 'product_id' => 2], + '5' => ['store_id' => 2, 'product_id' => 2], + ]; + + $connectionMock = $this->getMock(AdapterInterface::class); + $selectMock = $this->getMock(Select::class, [], [], '', false); + + $this->connectionMock->expects($this->any()) + ->method('getConnection') + ->willReturn($connectionMock); + $connectionMock->expects($this->any()) + ->method('select') + ->willReturn($selectMock); + $connectionMock->expects($this->any()) + ->method('fetchAll') + ->willReturn($productStoreIds[3]); + $selectMock->expects($this->any()) + ->method('from') + ->willReturnSelf(); + $selectMock->expects($this->any()) + ->method('joinInner') + ->willReturnSelf(); + $selectMock->expects($this->any()) + ->method('where') + ->willReturnSelf(); + + $this->dataProductMapMock->expects($this->any()) + ->method('getAllData') + ->willReturn([]); + + $this->temporaryTableServiceMock->expects($this->any()) + ->method('createFromSelect') + ->withConsecutive( + $selectMock, + $connectionMock, + [ + 'PRIMARY' => ['url_rewrite_id'], + 'HASHKEY_ENTITY_STORE' => ['hash_key'], + 'ENTITY_STORE' => ['entity_id', 'store_id'] + ] + ) + ->willReturn('tempTableName'); + + $this->assertEquals($productStoreIds[3], $this->model->getData(1, '3_1')); + } +} diff --git a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/Map/DatabaseMapPoolTest.php b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/Map/DatabaseMapPoolTest.php new file mode 100644 index 0000000000000..86ad2f53f647c --- /dev/null +++ b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/Map/DatabaseMapPoolTest.php @@ -0,0 +1,78 @@ +objectManagerMock = $this->getMock(ObjectManagerInterface::class); + + $this->model = (new ObjectManager($this))->getObject( + DatabaseMapPool::class, + [ + 'objectManager' => $this->objectManagerMock, + ] + ); + } + + /** + * Tests getDataMap(). + */ + public function testGetDataMap() + { + $dataCategoryMapMock = $this->getMock(DataCategoryUrlRewriteDatabaseMap::class, [], [], '', false); + $dataProductMapMock = $this->getMock(DataProductUrlRewriteDatabaseMap::class, [], [], '', false); + + $this->objectManagerMock->expects($this->any()) + ->method('create') + ->willReturnMap( + [ + [ + DataCategoryUrlRewriteDatabaseMap::class, + ['category' => 1], + $dataCategoryMapMock + ], + [ + DataProductUrlRewriteDatabaseMap::class, + ['category' => 1], + $dataProductMapMock + ] + ] + ); + $this->assertSame($dataCategoryMapMock, $this->model->getDataMap(DataCategoryUrlRewriteDatabaseMap::class, 1)); + $this->assertSame($dataProductMapMock, $this->model->getDataMap(DataProductUrlRewriteDatabaseMap::class, 1)); + } + + /** + * Tests getDataMap() with exception. + */ + public function testGetDataMapException() + { + $nonInterface = $this->getMock(DatabaseMapPool::class, [], [], '', false); + + $this->objectManagerMock->expects($this->any()) + ->method('create') + ->willReturn($nonInterface); + $this->setExpectedException(\InvalidArgumentException::class); + $this->model->getDataMap(DatabaseMapPool::class, 1); + } +} diff --git a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/Map/HashMapPoolTest.php b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/Map/HashMapPoolTest.php new file mode 100644 index 0000000000000..f6540d74a94b4 --- /dev/null +++ b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/Map/HashMapPoolTest.php @@ -0,0 +1,89 @@ +objectManagerMock = $this->getMock(ObjectManagerInterface::class); + + $this->model = (new ObjectManager($this))->getObject( + HashMapPool::class, + [ + 'objectManager' => $this->objectManagerMock, + ] + ); + } + + /** + * Tests getDataMap(). + */ + public function testGetDataMap() + { + $dataCategoryMapMock = $this->getMock(DataCategoryHashMap::class, [], [], '', false); + $dataProductMapMock = $this->getMock(DataProductHashMap::class, [], [], '', false); + $dataProductMapMockOtherCategory = $this->getMock(DataCategoryUsedInProductsHashMap::class, [], [], '', false); + + $this->objectManagerMock->expects($this->any()) + ->method('create') + ->willReturnMap( + [ + [ + DataCategoryHashMap::class, + ['category' => 1], + $dataCategoryMapMock + ], + [ + DataProductHashMap::class, + ['category' => 1], + $dataProductMapMock + ], + [ + DataCategoryUsedInProductsHashMap::class, + ['category' => 2], + $dataProductMapMockOtherCategory + ] + ] + ); + $this->assertSame($dataCategoryMapMock, $this->model->getDataMap(DataCategoryHashMap::class, 1)); + $this->assertSame($dataProductMapMock, $this->model->getDataMap(DataProductHashMap::class, 1)); + $this->assertSame( + $dataProductMapMockOtherCategory, + $this->model->getDataMap(DataCategoryUsedInProductsHashMap::class, 2) + ); + } + + /** + * Tests getDataMap() with exception. + */ + public function testGetDataMapException() + { + $nonInterface = $this->getMock(HashMapPool::class, [], [], '', false); + + $this->objectManagerMock->expects($this->any()) + ->method('create') + ->willReturn($nonInterface); + $this->setExpectedException(\InvalidArgumentException::class); + $this->model->getDataMap(HashMapPool::class, 1); + } +} diff --git a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/Map/UrlRewriteFinderTest.php b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/Map/UrlRewriteFinderTest.php new file mode 100644 index 0000000000000..47647048dc828 --- /dev/null +++ b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/Map/UrlRewriteFinderTest.php @@ -0,0 +1,167 @@ +databaseMapPoolMock = $this->getMock(DatabaseMapPool::class, [], [], '', false); + $this->urlFinderMock = $this->getMock(UrlFinderInterface::class); + $this->urlRewriteFactoryMock = $this->getMock(UrlRewriteFactory::class, ['create'], [], '', false); + $this->urlRewritePrototypeMock = new UrlRewrite(); + + $this->urlRewriteFactoryMock->expects($this->any()) + ->method('create') + ->willReturn($this->urlRewritePrototypeMock); + + $urlRewriteClassesNamesArray = [ + UrlRewriteFinder::ENTITY_TYPE_PRODUCT => DataProductUrlRewriteDatabaseMap::class, + UrlRewriteFinder::ENTITY_TYPE_CATEGORY => DataCategoryUrlRewriteDatabaseMap::class + ]; + + $this->model = (new ObjectManager($this))->getObject( + UrlRewriteFinder::class, + [ + 'databaseMapPool' => $this->databaseMapPoolMock, + 'urlFinder' => $this->urlFinderMock, + 'urlRewriteFactory' => $this->urlRewriteFactoryMock, + 'urlRewriteClassNames' => $urlRewriteClassesNamesArray + ] + ); + } + + /** + * Covers findAllByData() using urlFinder. + * + * @return void + */ + public function testGetByIdentifiersFallback() + { + $expected = [1, 2, 3]; + $this->databaseMapPoolMock->expects($this->never()) + ->method('getDataMap'); + + $this->urlFinderMock->expects($this->exactly(7)) + ->method('findAllByData') + ->willReturn($expected); + + $this->assertEquals($expected, $this->model->findAllByData(1, 1, UrlRewriteFinder::ENTITY_TYPE_CATEGORY)); + $this->assertEquals($expected, $this->model->findAllByData(1, 1, UrlRewriteFinder::ENTITY_TYPE_PRODUCT)); + $this->assertEquals($expected, $this->model->findAllByData('a', 1, UrlRewriteFinder::ENTITY_TYPE_PRODUCT), 1); + $this->assertEquals($expected, $this->model->findAllByData('a', 'a', UrlRewriteFinder::ENTITY_TYPE_PRODUCT), 1); + $this->assertEquals($expected, $this->model->findAllByData(1, 'a', UrlRewriteFinder::ENTITY_TYPE_PRODUCT), 1); + $this->assertEquals($expected, $this->model->findAllByData(1, 1, 'cms', 1)); + $this->assertEquals($expected, $this->model->findAllByData(1, 1, 'cms')); + } + + /** + * Covers findAllByData() Product URL rewrites. + * + * @return void + */ + public function testGetByIdentifiersProduct() + { + $data =[ + [ + 'url_rewrite_id' => '1', + 'entity_type' => 'product', + 'entity_id' => '3', + 'request_path' => 'request_path', + 'target_path' => 'target_path', + 'redirect_type' => 'redirect_type', + 'store_id' => '4', + 'description' => 'description', + 'is_autogenerated' => '1', + 'metadata' => '{}' + ] + ]; + + $dataProductMapMock = $this->getMock(DataProductUrlRewriteDatabaseMap::class, [], [], '', false); + $this->databaseMapPoolMock->expects($this->once()) + ->method('getDataMap') + ->with(DataProductUrlRewriteDatabaseMap::class, 1) + ->willReturn($dataProductMapMock); + + $this->urlFinderMock->expects($this->never()) + ->method('findAllByData') + ->willReturn([]); + + $dataProductMapMock->expects($this->once()) + ->method('getData') + ->willReturn($data); + + $urlRewriteResultArray = $this->model->findAllByData(1, 1, UrlRewriteFinder::ENTITY_TYPE_PRODUCT, 1); + $this->assertEquals($data[0], $urlRewriteResultArray[0]->toArray()); + } + + /** + * Covers findAllByData() Category URL rewrites. + * + * @return void + */ + public function testGetByIdentifiersCategory() + { + $data =[ + [ + 'url_rewrite_id' => '1', + 'entity_type' => 'category', + 'entity_id' => '3', + 'request_path' => 'request_path', + 'target_path' => 'target_path', + 'redirect_type' => 'redirect_type', + 'store_id' => '4', + 'description' => 'description', + 'is_autogenerated' => '1', + 'metadata' => '{}' + ] + ]; + + $dataCategoryMapMock = $this->getMock(DataCategoryUrlRewriteDatabaseMap::class, [], [], '', false); + $this->databaseMapPoolMock->expects($this->once()) + ->method('getDataMap') + ->with(DataCategoryUrlRewriteDatabaseMap::class, 1) + ->willReturn($dataCategoryMapMock); + + $this->urlFinderMock->expects($this->never()) + ->method('findAllByData') + ->willReturn([]); + + $dataCategoryMapMock->expects($this->once()) + ->method('getData') + ->willReturn($data); + + $urlRewriteResultArray = $this->model->findAllByData(1, 1, UrlRewriteFinder::ENTITY_TYPE_CATEGORY, 1); + $this->assertEquals($data[0], $urlRewriteResultArray[0]->toArray()); + } +} diff --git a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/Product/CurrentUrlRewritesRegeneratorTest.php b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/Product/CurrentUrlRewritesRegeneratorTest.php index 9daaa8dc44851..b5515c2c25459 100644 --- a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/Product/CurrentUrlRewritesRegeneratorTest.php +++ b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/Product/CurrentUrlRewritesRegeneratorTest.php @@ -10,70 +10,81 @@ use Magento\UrlRewrite\Model\OptionProvider; use Magento\UrlRewrite\Service\V1\Data\UrlRewrite; +/** + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ class CurrentUrlRewritesRegeneratorTest extends \PHPUnit_Framework_TestCase { /** @var \Magento\CatalogUrlRewrite\Model\Product\CurrentUrlRewritesRegenerator */ - protected $currentUrlRewritesRegenerator; - - /** @var \PHPUnit_Framework_MockObject_MockObject */ - protected $filter; - - /** @var \Magento\UrlRewrite\Model\UrlFinderInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $urlFinder; + private $currentUrlRewritesRegenerator; /** @var \Magento\CatalogUrlRewrite\Model\ProductUrlPathGenerator|\PHPUnit_Framework_MockObject_MockObject */ - protected $productUrlPathGenerator; + private $productUrlPathGenerator; /** @var \Magento\Catalog\Model\Product|\PHPUnit_Framework_MockObject_MockObject */ - protected $product; + private $product; /** @var \Magento\Catalog\Model\Category|\PHPUnit_Framework_MockObject_MockObject */ - protected $category; + private $category; /** @var \Magento\CatalogUrlRewrite\Model\ObjectRegistry|\PHPUnit_Framework_MockObject_MockObject */ - protected $objectRegistry; + private $objectRegistry; /** @var \Magento\UrlRewrite\Service\V1\Data\UrlRewriteFactory|\PHPUnit_Framework_MockObject_MockObject */ - protected $urlRewriteFactory; + private $urlRewriteFactory; /** @var \Magento\UrlRewrite\Service\V1\Data\UrlRewrite|\PHPUnit_Framework_MockObject_MockObject */ - protected $urlRewrite; + private $urlRewrite; + + /** @var \PHPUnit_Framework_MockObject_MockObject */ + private $mergeDataProvider; + + /** @var \PHPUnit_Framework_MockObject_MockObject */ + private $urlRewriteFinder; protected function setUp() { - $this->urlRewriteFactory = $this->getMockBuilder('Magento\UrlRewrite\Service\V1\Data\UrlRewriteFactory') + $this->urlRewriteFactory = $this->getMockBuilder(\Magento\UrlRewrite\Service\V1\Data\UrlRewriteFactory::class) ->setMethods(['create']) ->disableOriginalConstructor()->getMock(); - $this->urlRewrite = $this->getMockBuilder('Magento\UrlRewrite\Service\V1\Data\UrlRewrite') - ->disableOriginalConstructor()->getMock(); - $this->product = $this->getMockBuilder('Magento\Catalog\Model\Product') + $this->urlRewrite = $this->getMockBuilder(\Magento\UrlRewrite\Service\V1\Data\UrlRewrite::class) ->disableOriginalConstructor()->getMock(); - $this->category = $this->getMockBuilder('Magento\Catalog\Model\Category') + $this->product = $this->getMockBuilder(\Magento\Catalog\Model\Product::class) ->disableOriginalConstructor()->getMock(); - $this->objectRegistry = $this->getMockBuilder('\Magento\CatalogUrlRewrite\Model\ObjectRegistry') + $this->category = $this->getMockBuilder(\Magento\Catalog\Model\Category::class) ->disableOriginalConstructor()->getMock(); - $this->filter = $this->getMockBuilder('Magento\UrlRewrite\Service\V1\Data\Filter') + $this->objectRegistry = $this->getMockBuilder(\Magento\CatalogUrlRewrite\Model\ObjectRegistry::class) ->disableOriginalConstructor()->getMock(); - $this->filter->expects($this->any())->method('setStoreId')->will($this->returnSelf()); - $this->filter->expects($this->any())->method('setEntityId')->will($this->returnSelf()); - $this->urlFinder = $this->getMockBuilder('Magento\UrlRewrite\Model\UrlFinderInterface') + $this->urlRewriteFinder = $this->getMockBuilder(\Magento\CatalogUrlRewrite\Model\Map\UrlRewriteFinder::class) ->disableOriginalConstructor()->getMock(); + $this->urlRewriteFactory->expects($this->once())->method('create') + ->willReturn($this->urlRewrite); $this->productUrlPathGenerator = $this->getMockBuilder( - 'Magento\CatalogUrlRewrite\Model\ProductUrlPathGenerator' + \Magento\CatalogUrlRewrite\Model\ProductUrlPathGenerator::class )->disableOriginalConstructor()->getMock(); + $mergeDataProviderFactory = $this->getMock( + \Magento\UrlRewrite\Model\MergeDataProviderFactory::class, + ['create'], + [], + '', + false + ); + $this->mergeDataProvider = new \Magento\UrlRewrite\Model\MergeDataProvider; + $mergeDataProviderFactory->expects($this->once())->method('create')->willReturn($this->mergeDataProvider); $this->currentUrlRewritesRegenerator = (new ObjectManager($this))->getObject( - 'Magento\CatalogUrlRewrite\Model\Product\CurrentUrlRewritesRegenerator', + \Magento\CatalogUrlRewrite\Model\Product\CurrentUrlRewritesRegenerator::class, [ - 'urlFinder' => $this->urlFinder, 'productUrlPathGenerator' => $this->productUrlPathGenerator, - 'urlRewriteFactory' => $this->urlRewriteFactory + 'urlRewriteFactory' => $this->urlRewriteFactory, + 'mergeDataProviderFactory' => $mergeDataProviderFactory, + 'urlRewriteFinder' => $this->urlRewriteFinder ] ); } public function testIsAutogeneratedWithoutSaveRewriteHistory() { - $this->urlFinder->expects($this->once())->method('findAllByData') + $this->urlRewriteFinder->expects($this->once())->method('findAllByData') ->will($this->returnValue($this->getCurrentRewritesMocks([[UrlRewrite::IS_AUTOGENERATED => 1]]))); $this->product->expects($this->once())->method('getData')->with('save_rewrites_history') ->will($this->returnValue(false)); @@ -86,7 +97,7 @@ public function testIsAutogeneratedWithoutSaveRewriteHistory() public function testSkipGenerationForAutogenerated() { - $this->urlFinder->expects($this->once())->method('findAllByData') + $this->urlRewriteFinder->expects($this->once())->method('findAllByData') ->will($this->returnValue($this->getCurrentRewritesMocks([ [UrlRewrite::IS_AUTOGENERATED => 1, UrlRewrite::REQUEST_PATH => 'same-path'], ]))); @@ -97,7 +108,7 @@ public function testSkipGenerationForAutogenerated() $this->assertEquals( [], - $this->currentUrlRewritesRegenerator->generate('store_id', $this->product, $this->objectRegistry) + $this->currentUrlRewritesRegenerator->generate('store_id', $this->product, $this->objectRegistry, 1) ); } @@ -105,21 +116,22 @@ public function testIsAutogeneratedWithoutCategory() { $requestPath = 'autogenerated.html'; $targetPath = 'some-path.html'; + $autoGenerated = 1; $storeId = 2; $productId = 12; $description = 'description'; - $this->urlFinder->expects($this->once())->method('findAllByData') + $this->urlRewriteFinder->expects($this->once())->method('findAllByData') ->will($this->returnValue($this->getCurrentRewritesMocks([ [ UrlRewrite::REQUEST_PATH => $requestPath, UrlRewrite::TARGET_PATH => 'custom-target-path', UrlRewrite::STORE_ID => $storeId, - UrlRewrite::IS_AUTOGENERATED => 1, + UrlRewrite::IS_AUTOGENERATED => $autoGenerated, UrlRewrite::METADATA => [], UrlRewrite::DESCRIPTION => $description, ], ]))); - $this->product->expects($this->any())->method('getId')->will($this->returnValue($productId)); + $this->product->expects($this->any())->method('getEntityId')->will($this->returnValue($productId)); $this->product->expects($this->once())->method('getData')->with('save_rewrites_history') ->will($this->returnValue(true)); $this->productUrlPathGenerator->expects($this->once())->method('getUrlPathWithSuffix') @@ -130,6 +142,7 @@ public function testIsAutogeneratedWithoutCategory() $productId, $requestPath, $targetPath, + 0, OptionProvider::PERMANENT, [], $description @@ -137,7 +150,7 @@ public function testIsAutogeneratedWithoutCategory() $this->assertEquals( [$this->urlRewrite], - $this->currentUrlRewritesRegenerator->generate($storeId, $this->product, $this->objectRegistry) + $this->currentUrlRewritesRegenerator->generate($storeId, $this->product, $this->objectRegistry, 1) ); } @@ -146,21 +159,22 @@ public function testIsAutogeneratedWithCategory() $productId = 12; $requestPath = 'autogenerated.html'; $targetPath = 'simple-product.html'; + $autoGenerated = 1; $storeId = 2; $metadata = ['category_id' => 2, 'some_another_data' => 1]; $description = 'description'; - $this->urlFinder->expects($this->once())->method('findAllByData') + $this->urlRewriteFinder->expects($this->once())->method('findAllByData') ->will($this->returnValue($this->getCurrentRewritesMocks([ [ UrlRewrite::REQUEST_PATH => $requestPath, UrlRewrite::TARGET_PATH => 'some-path.html', UrlRewrite::STORE_ID => $storeId, - UrlRewrite::IS_AUTOGENERATED => 1, + UrlRewrite::IS_AUTOGENERATED => $autoGenerated, UrlRewrite::METADATA => $metadata, UrlRewrite::DESCRIPTION => $description, ], ]))); - $this->product->expects($this->any())->method('getId')->will($this->returnValue($productId)); + $this->product->expects($this->any())->method('getEntityId')->will($this->returnValue($productId)); $this->product->expects($this->once())->method('getData')->with('save_rewrites_history') ->will($this->returnValue(true)); $this->productUrlPathGenerator->expects($this->once())->method('getUrlPathWithSuffix') @@ -171,6 +185,7 @@ public function testIsAutogeneratedWithCategory() $productId, $requestPath, $targetPath, + 0, OptionProvider::PERMANENT, $metadata, $description @@ -178,13 +193,13 @@ public function testIsAutogeneratedWithCategory() $this->assertEquals( [$this->urlRewrite], - $this->currentUrlRewritesRegenerator->generate($storeId, $this->product, $this->objectRegistry) + $this->currentUrlRewritesRegenerator->generate($storeId, $this->product, $this->objectRegistry, 2) ); } public function testSkipGenerationForCustom() { - $this->urlFinder->expects($this->once())->method('findAllByData') + $this->urlRewriteFinder->expects($this->once())->method('findAllByData') ->will($this->returnValue($this->getCurrentRewritesMocks([ [ UrlRewrite::IS_AUTOGENERATED => 0, @@ -207,21 +222,31 @@ public function testGenerationForCustomWithoutTargetPathGeneration() $productId = 123; $requestPath = 'generate-for-custom-without-redirect-type.html'; $targetPath = 'custom-target-path.html'; + $autoGenerated = 0; $description = 'description'; - $this->urlFinder->expects($this->once())->method('findAllByData') + $this->urlRewriteFinder->expects($this->once())->method('findAllByData') ->will($this->returnValue($this->getCurrentRewritesMocks([ [ UrlRewrite::REQUEST_PATH => $requestPath, UrlRewrite::TARGET_PATH => $targetPath, UrlRewrite::REDIRECT_TYPE => 0, - UrlRewrite::IS_AUTOGENERATED => 0, + UrlRewrite::IS_AUTOGENERATED => $autoGenerated, UrlRewrite::DESCRIPTION => $description, UrlRewrite::METADATA => [], ], ]))); $this->productUrlPathGenerator->expects($this->never())->method('getUrlPathWithSuffix'); - $this->product->expects($this->any())->method('getId')->will($this->returnValue($productId)); - $this->prepareUrlRewriteMock($storeId, $productId, $requestPath, $targetPath, 0, [], $description); + $this->product->expects($this->any())->method('getEntityId')->will($this->returnValue($productId)); + $this->prepareUrlRewriteMock( + $storeId, + $productId, + $requestPath, + $targetPath, + $autoGenerated, + 0, + [], + $description + ); $this->assertEquals( [$this->urlRewrite], @@ -235,22 +260,23 @@ public function testGenerationForCustomWithTargetPathGeneration() $productId = 123; $requestPath = 'generate-for-custom-without-redirect-type.html'; $targetPath = 'generated-target-path.html'; + $autoGenerated = 0; $description = 'description'; - $this->urlFinder->expects($this->once())->method('findAllByData') + $this->urlRewriteFinder->expects($this->once())->method('findAllByData') ->will($this->returnValue($this->getCurrentRewritesMocks([ [ UrlRewrite::REQUEST_PATH => $requestPath, UrlRewrite::TARGET_PATH => 'custom-target-path.html', UrlRewrite::REDIRECT_TYPE => 'code', - UrlRewrite::IS_AUTOGENERATED => 0, + UrlRewrite::IS_AUTOGENERATED => $autoGenerated, UrlRewrite::DESCRIPTION => $description, UrlRewrite::METADATA => [], ], ]))); $this->productUrlPathGenerator->expects($this->any())->method('getUrlPathWithSuffix') ->will($this->returnValue($targetPath)); - $this->product->expects($this->any())->method('getId')->will($this->returnValue($productId)); - $this->prepareUrlRewriteMock($storeId, $productId, $requestPath, $targetPath, 'code', [], $description); + $this->product->expects($this->any())->method('getEntityId')->will($this->returnValue($productId)); + $this->prepareUrlRewriteMock($storeId, $productId, $requestPath, $targetPath, 0, 'code', [], $description); $this->assertEquals( [$this->urlRewrite], @@ -267,7 +293,7 @@ protected function getCurrentRewritesMocks($currentRewrites) $rewrites = []; foreach ($currentRewrites as $urlRewrite) { /** @var \PHPUnit_Framework_MockObject_MockObject */ - $url = $this->getMockBuilder('Magento\UrlRewrite\Service\V1\Data\UrlRewrite') + $url = $this->getMockBuilder(\Magento\UrlRewrite\Service\V1\Data\UrlRewrite::class) ->disableOriginalConstructor()->getMock(); foreach ($urlRewrite as $key => $value) { $url->expects($this->any()) @@ -284,6 +310,7 @@ protected function getCurrentRewritesMocks($currentRewrites) * @param mixed $productId * @param mixed $requestPath * @param mixed $targetPath + * @param mixed $autoGenerated * @param mixed $redirectType * @param mixed $metadata * @param mixed $description @@ -293,6 +320,7 @@ protected function prepareUrlRewriteMock( $productId, $requestPath, $targetPath, + $autoGenerated, $redirectType, $metadata, $description @@ -307,7 +335,7 @@ protected function prepareUrlRewriteMock( ->will($this->returnSelf()); $this->urlRewrite->expects($this->any())->method('setTargetPath')->with($targetPath) ->will($this->returnSelf()); - $this->urlRewrite->expects($this->any())->method('setIsAutogenerated')->with(0) + $this->urlRewrite->expects($this->any())->method('setIsAutogenerated')->with($autoGenerated) ->will($this->returnSelf()); $this->urlRewrite->expects($this->any())->method('setRedirectType')->with($redirectType) ->will($this->returnSelf()); diff --git a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/ProductScopeRewriteGeneratorTest.php b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/ProductScopeRewriteGeneratorTest.php new file mode 100644 index 0000000000000..da117a0d4a37a --- /dev/null +++ b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/ProductScopeRewriteGeneratorTest.php @@ -0,0 +1,194 @@ +currentUrlRewritesRegenerator = $this->getMockBuilder( + \Magento\CatalogUrlRewrite\Model\Product\CurrentUrlRewritesRegenerator::class + )->disableOriginalConstructor()->getMock(); + $this->canonicalUrlRewriteGenerator = $this->getMockBuilder( + \Magento\CatalogUrlRewrite\Model\Product\CanonicalUrlRewriteGenerator::class + )->disableOriginalConstructor()->getMock(); + $this->categoriesUrlRewriteGenerator = $this->getMockBuilder( + \Magento\CatalogUrlRewrite\Model\Product\CategoriesUrlRewriteGenerator::class + )->disableOriginalConstructor()->getMock(); + $this->anchorUrlRewriteGenerator = $this->getMockBuilder( + \Magento\CatalogUrlRewrite\Model\Product\AnchorUrlRewriteGenerator::class + )->disableOriginalConstructor()->getMock(); + $this->objectRegistryFactory = $this->getMockBuilder( + \Magento\CatalogUrlRewrite\Model\ObjectRegistryFactory::class + )->disableOriginalConstructor()->setMethods(['create'])->getMock(); + $this->storeViewService = $this->getMockBuilder(\Magento\CatalogUrlRewrite\Service\V1\StoreViewService::class) + ->disableOriginalConstructor()->getMock(); + $this->storeManager = $this->getMock(StoreManagerInterface::class); + $mergeDataProviderFactory = $this->getMock( + \Magento\UrlRewrite\Model\MergeDataProviderFactory::class, + ['create'], + [], + '', + false + ); + $this->mergeDataProvider = new \Magento\UrlRewrite\Model\MergeDataProvider; + $mergeDataProviderFactory->expects($this->once())->method('create')->willReturn($this->mergeDataProvider); + + $this->productScopeGenerator = (new ObjectManager($this))->getObject( + \Magento\CatalogUrlRewrite\Model\ProductScopeRewriteGenerator::class, + [ + 'canonicalUrlRewriteGenerator' => $this->canonicalUrlRewriteGenerator, + 'categoriesUrlRewriteGenerator' => $this->categoriesUrlRewriteGenerator, + 'currentUrlRewritesRegenerator' => $this->currentUrlRewritesRegenerator, + 'anchorUrlRewriteGenerator' => $this->anchorUrlRewriteGenerator, + 'objectRegistryFactory' => $this->objectRegistryFactory, + 'storeViewService' => $this->storeViewService, + 'storeManager' => $this->storeManager, + 'mergeDataProviderFactory' => $mergeDataProviderFactory + ] + ); + } + + public function testGenerationForGlobalScope() + { + $product = $this->getMock(\Magento\Catalog\Model\Product::class, [], [], '', false); + $product->expects($this->any())->method('getStoreId')->will($this->returnValue(null)); + $product->expects($this->any())->method('getStoreIds')->will($this->returnValue([1])); + $this->storeViewService->expects($this->once())->method('doesEntityHaveOverriddenUrlKeyForStore') + ->will($this->returnValue(false)); + $categoryMock = $this->getMockBuilder(Category::class) + ->disableOriginalConstructor() + ->getMock(); + $categoryMock->expects($this->once()) + ->method('getParentId') + ->willReturn(1); + $this->initObjectRegistryFactory([]); + $canonical = new \Magento\UrlRewrite\Service\V1\Data\UrlRewrite(); + $canonical->setRequestPath('category-1') + ->setStoreId(1); + $this->canonicalUrlRewriteGenerator->expects($this->any())->method('generate') + ->will($this->returnValue([$canonical])); + $categories = new \Magento\UrlRewrite\Service\V1\Data\UrlRewrite(); + $categories->setRequestPath('category-2') + ->setStoreId(2); + $this->categoriesUrlRewriteGenerator->expects($this->any())->method('generate') + ->will($this->returnValue([$categories])); + $current = new \Magento\UrlRewrite\Service\V1\Data\UrlRewrite(); + $current->setRequestPath('category-3') + ->setStoreId(3); + $this->currentUrlRewritesRegenerator->expects($this->any())->method('generate') + ->will($this->returnValue([$current])); + $anchorCategories = new \Magento\UrlRewrite\Service\V1\Data\UrlRewrite(); + $anchorCategories->setRequestPath('category-4') + ->setStoreId(4); + $this->anchorUrlRewriteGenerator->expects($this->any())->method('generate') + ->will($this->returnValue([$anchorCategories])); + + $this->assertEquals( + [ + 'category-1_1' => $canonical, + 'category-2_2' => $categories, + 'category-3_3' => $current, + 'category-4_4' => $anchorCategories + ], + $this->productScopeGenerator->generateForGlobalScope([$categoryMock], $product, 1) + ); + } + + public function testGenerationForSpecificStore() + { + $product = $this->getMock(\Magento\Catalog\Model\Product::class, [], [], '', false); + $product->expects($this->any())->method('getStoreId')->will($this->returnValue(1)); + $product->expects($this->never())->method('getStoreIds'); + $storeRootCategoryId = 'root-for-store-id'; + $category = $this->getMock(\Magento\Catalog\Model\Category::class, [], [], '', false); + $category->expects($this->any())->method('getParentIds') + ->will($this->returnValue(['root-id', $storeRootCategoryId])); + $category->expects($this->any())->method('getParentId')->will($this->returnValue('parent_id')); + $category->expects($this->any())->method('getId')->will($this->returnValue('category_id')); + $store = $this->getMockBuilder(\Magento\Store\Model\Store::class)->disableOriginalConstructor()->getMock(); + $store->expects($this->any())->method('getRootCategoryId')->will($this->returnValue($storeRootCategoryId)); + $this->storeManager->expects($this->any())->method('getStore')->will($this->returnValue($store)); + $this->initObjectRegistryFactory([$category]); + $canonical = new \Magento\UrlRewrite\Service\V1\Data\UrlRewrite(); + $canonical->setRequestPath('category-1') + ->setStoreId(1); + $this->canonicalUrlRewriteGenerator->expects($this->any())->method('generate') + ->will($this->returnValue([$canonical])); + $this->categoriesUrlRewriteGenerator->expects($this->any())->method('generate') + ->will($this->returnValue([])); + $this->currentUrlRewritesRegenerator->expects($this->any())->method('generate') + ->will($this->returnValue([])); + $this->anchorUrlRewriteGenerator->expects($this->any())->method('generate') + ->will($this->returnValue([])); + + $this->assertEquals( + ['category-1_1' => $canonical], + $this->productScopeGenerator->generateForSpecificStoreView(1, [$category], $product, 1) + ); + } + + /** + * Test method + */ + public function testSkipGenerationForGlobalScope() + { + $product = $this->getMock(\Magento\Catalog\Model\Product::class, [], [], '', false); + $product->expects($this->any())->method('getStoreIds')->will($this->returnValue([1, 2])); + $this->storeViewService->expects($this->exactly(2))->method('doesEntityHaveOverriddenUrlKeyForStore') + ->will($this->returnValue(true)); + + $this->assertEquals([], $this->productScopeGenerator->generateForGlobalScope([], $product, 1)); + } + + /** + * @param array $entities + */ + protected function initObjectRegistryFactory($entities) + { + $objectRegistry = $this->getMockBuilder(\Magento\CatalogUrlRewrite\Model\ObjectRegistry::class) + ->disableOriginalConstructor()->getMock(); + $this->objectRegistryFactory->expects($this->any())->method('create') + ->with(['entities' => $entities]) + ->will($this->returnValue($objectRegistry)); + } +} diff --git a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/ProductUrlRewriteGeneratorTest.php b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/ProductUrlRewriteGeneratorTest.php index 3798b5d602153..985ad15e1b6b1 100644 --- a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/ProductUrlRewriteGeneratorTest.php +++ b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/ProductUrlRewriteGeneratorTest.php @@ -4,13 +4,17 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\CatalogUrlRewrite\Test\Unit\Model; -use Magento\Catalog\Model\Category; +use Magento\Catalog\Model\Product; +use Magento\CatalogUrlRewrite\Model\ProductScopeRewriteGenerator; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +/** + * Tests ProductUrlRewriteGenerator class. + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ class ProductUrlRewriteGeneratorTest extends \PHPUnit_Framework_TestCase { /** @var \PHPUnit_Framework_MockObject_MockObject */ @@ -43,39 +47,66 @@ class ProductUrlRewriteGeneratorTest extends \PHPUnit_Framework_TestCase /** @var \Magento\Catalog\Model\ResourceModel\Category\Collection|\PHPUnit_Framework_MockObject_MockObject */ protected $categoriesCollection; + /** @var \PHPUnit_Framework_MockObject_MockObject */ + private $productScopeRewriteGenerator; + /** * Test method */ protected function setUp() { - $this->product = $this->getMock('Magento\Catalog\Model\Product', [], [], '', false); - $this->categoriesCollection = $this->getMockBuilder('Magento\Catalog\Model\ResourceModel\Category\Collection') - ->disableOriginalConstructor()->getMock(); - $this->product->expects($this->any())->method('getCategoryCollection') + $this->product = $this->getMock(\Magento\Catalog\Model\Product::class, [], [], '', false); + $this->categoriesCollection = $this->getMockBuilder( + \Magento\Catalog\Model\ResourceModel\Category\Collection::class + ) + ->disableOriginalConstructor() + ->getMock(); + $this->product->expects($this->any()) + ->method('getCategoryCollection') ->will($this->returnValue($this->categoriesCollection)); - $this->storeManager = $this->getMockBuilder('Magento\Store\Model\StoreManagerInterface') - ->disableOriginalConstructor()->getMock(); - $this->categoriesCollection->expects($this->exactly(2))->method('addAttributeToSelect') - ->will($this->returnSelf()); + $this->storeManager = $this->getMockBuilder( + \Magento\Store\Model\StoreManagerInterface::class + ) + ->disableOriginalConstructor() + ->getMock(); $this->currentUrlRewritesRegenerator = $this->getMockBuilder( - 'Magento\CatalogUrlRewrite\Model\Product\CurrentUrlRewritesRegenerator' - )->disableOriginalConstructor()->getMock(); + \Magento\CatalogUrlRewrite\Model\Product\CurrentUrlRewritesRegenerator::class + ) + ->disableOriginalConstructor() + ->getMock(); $this->canonicalUrlRewriteGenerator = $this->getMockBuilder( - 'Magento\CatalogUrlRewrite\Model\Product\CanonicalUrlRewriteGenerator' - )->disableOriginalConstructor()->getMock(); + \Magento\CatalogUrlRewrite\Model\Product\CanonicalUrlRewriteGenerator::class + ) + ->disableOriginalConstructor() + ->getMock(); $this->categoriesUrlRewriteGenerator = $this->getMockBuilder( - 'Magento\CatalogUrlRewrite\Model\Product\CategoriesUrlRewriteGenerator' - )->disableOriginalConstructor()->getMock(); + \Magento\CatalogUrlRewrite\Model\Product\CategoriesUrlRewriteGenerator::class + ) + ->disableOriginalConstructor() + ->getMock(); $this->anchorUrlRewriteGenerator = $this->getMockBuilder( - 'Magento\CatalogUrlRewrite\Model\Product\AnchorUrlRewriteGenerator' - )->disableOriginalConstructor()->getMock(); - $this->objectRegistryFactory = $this->getMockBuilder('Magento\CatalogUrlRewrite\Model\ObjectRegistryFactory') - ->disableOriginalConstructor()->setMethods(['create'])->getMock(); - $this->storeViewService = $this->getMockBuilder('Magento\CatalogUrlRewrite\Service\V1\StoreViewService') - ->disableOriginalConstructor()->getMock(); - + \Magento\CatalogUrlRewrite\Model\Product\AnchorUrlRewriteGenerator::class + ) + ->disableOriginalConstructor() + ->getMock(); + $this->objectRegistryFactory = $this->getMockBuilder( + \Magento\CatalogUrlRewrite\Model\ObjectRegistryFactory::class + ) + ->disableOriginalConstructor() + ->setMethods(['create']) + ->getMock(); + $this->storeViewService = $this->getMockBuilder( + \Magento\CatalogUrlRewrite\Service\V1\StoreViewService::class + ) + ->disableOriginalConstructor() + ->getMock(); + $this->productScopeRewriteGenerator = $this->getMockBuilder( + ProductScopeRewriteGenerator::class + ) + ->disableOriginalConstructor() + ->getMock(); $this->productUrlRewriteGenerator = (new ObjectManager($this))->getObject( - 'Magento\CatalogUrlRewrite\Model\ProductUrlRewriteGenerator', + \Magento\CatalogUrlRewrite\Model\ProductUrlRewriteGenerator::class, [ 'canonicalUrlRewriteGenerator' => $this->canonicalUrlRewriteGenerator, 'categoriesUrlRewriteGenerator' => $this->categoriesUrlRewriteGenerator, @@ -87,172 +118,43 @@ protected function setUp() ); $reflection = new \ReflectionClass(get_class($this->productUrlRewriteGenerator)); - $reflectionProperty = $reflection->getProperty('anchorUrlRewriteGenerator'); + $reflectionProperty = $reflection->getProperty('productScopeRewriteGenerator'); $reflectionProperty->setAccessible(true); - $reflectionProperty->setValue($this->productUrlRewriteGenerator, $this->anchorUrlRewriteGenerator); - } - - /** - * Test method - */ - public function testGenerationForGlobalScope() - { - $this->product->expects($this->any())->method('getStoreId')->will($this->returnValue(null)); - $this->product->expects($this->any())->method('getStoreIds')->will($this->returnValue([1])); - $this->storeViewService->expects($this->once())->method('doesEntityHaveOverriddenUrlKeyForStore') - ->will($this->returnValue(false)); - $this->categoriesCollection->expects($this->any())->method('getIterator') - ->willReturn(new \ArrayIterator([])); - $this->initObjectRegistryFactory([]); - $canonical = new \Magento\UrlRewrite\Service\V1\Data\UrlRewrite(); - $canonical->setTargetPath('category-1') - ->setStoreId(1); - $this->canonicalUrlRewriteGenerator->expects($this->any())->method('generate') - ->will($this->returnValue([$canonical])); - $categories = new \Magento\UrlRewrite\Service\V1\Data\UrlRewrite(); - $categories->setTargetPath('category-2') - ->setStoreId(2); - $this->categoriesUrlRewriteGenerator->expects($this->any())->method('generate') - ->will($this->returnValue([$categories])); - $current = new \Magento\UrlRewrite\Service\V1\Data\UrlRewrite(); - $current->setTargetPath('category-3') - ->setStoreId(3); - $this->currentUrlRewritesRegenerator->expects($this->any())->method('generate') - ->will($this->returnValue([$current])); - $anchorCategories = new \Magento\UrlRewrite\Service\V1\Data\UrlRewrite(); - $anchorCategories->setTargetPath('category-4') - ->setStoreId(4); - $this->anchorUrlRewriteGenerator->expects($this->any())->method('generate') - ->will($this->returnValue([$anchorCategories])); - - $this->assertEquals( - [ - 'category-1-1' => $canonical, - 'category-2-2' => $categories, - 'category-3-3' => $current, - 'category-4-4' => $anchorCategories - ], - $this->productUrlRewriteGenerator->generate($this->product) - ); - } - - /** - * Test method - */ - public function testGenerationForSpecificStore() - { - $this->product->expects($this->any())->method('getStoreId')->will($this->returnValue(1)); - $this->product->expects($this->never())->method('getStoreIds'); - $storeRootCategoryId = 'root-for-store-id'; - $category = $this->getMock('Magento\Catalog\Model\Category', [], [], '', false); - $category->expects($this->any())->method('getParentIds') - ->will($this->returnValue(['root-id', $storeRootCategoryId])); - $category->expects($this->any())->method('getParentId')->will($this->returnValue('parent_id')); - $category->expects($this->any())->method('getId')->will($this->returnValue('category_id')); - $store = $this->getMockBuilder('Magento\Store\Model\Store')->disableOriginalConstructor()->getMock(); - $store->expects($this->any())->method('getRootCategoryId')->will($this->returnValue($storeRootCategoryId)); - $this->storeManager->expects($this->any())->method('getStore')->will($this->returnValue($store)); - $this->categoriesCollection->expects($this->any())->method('getIterator') - ->willReturn(new \ArrayIterator([$category])); - $this->initObjectRegistryFactory([$category]); - $canonical = new \Magento\UrlRewrite\Service\V1\Data\UrlRewrite(); - $canonical->setTargetPath('category-1') - ->setStoreId(1); - $this->canonicalUrlRewriteGenerator->expects($this->any())->method('generate') - ->will($this->returnValue([$canonical])); - $this->categoriesUrlRewriteGenerator->expects($this->any())->method('generate') - ->will($this->returnValue([])); - $this->currentUrlRewritesRegenerator->expects($this->any())->method('generate') - ->will($this->returnValue([])); - $this->anchorUrlRewriteGenerator->expects($this->any())->method('generate') - ->will($this->returnValue([])); - - $this->assertEquals(['category-1-1' => $canonical], $this->productUrlRewriteGenerator->generate($this->product)); - } - - /** - * Test method - */ - public function testSkipRootCategoryForCategoriesGenerator() - { - $this->product->expects($this->any())->method('getStoreId')->will($this->returnValue(1)); - $this->product->expects($this->never())->method('getStoreIds'); - $store = $this->getMockBuilder('Magento\Store\Model\Store')->disableOriginalConstructor()->getMock(); - $store->expects($this->any())->method('getRootCategoryId')->will($this->returnValue('root-for-store-id')); - $this->storeManager->expects($this->any())->method('getStore')->will($this->returnValue($store)); - $rootCategory = $this->getMock('Magento\Catalog\Model\Category', [], [], '', false); - $rootCategory->expects($this->any())->method('getParentIds')->will($this->returnValue([1, 2])); - $rootCategory->expects($this->any())->method('getParentId')->will($this->returnValue(Category::TREE_ROOT_ID)); - $this->categoriesCollection->expects($this->any())->method('getIterator') - ->willReturn(new \ArrayIterator([$rootCategory])); - $this->initObjectRegistryFactory([]); - $canonical = new \Magento\UrlRewrite\Service\V1\Data\UrlRewrite(); - $canonical->setTargetPath('category-1') - ->setStoreId(1); - $this->canonicalUrlRewriteGenerator->expects($this->any())->method('generate') - ->will($this->returnValue([$canonical])); - $this->categoriesUrlRewriteGenerator->expects($this->any())->method('generate') - ->will($this->returnValue([])); - $this->currentUrlRewritesRegenerator->expects($this->any())->method('generate') - ->will($this->returnValue([])); - $this->anchorUrlRewriteGenerator->expects($this->any())->method('generate') - ->will($this->returnValue([])); - - $this->assertEquals(['category-1-1' => $canonical], $this->productUrlRewriteGenerator->generate($this->product)); - } - - /** - * Test method - */ - public function testSkipGenerationForNotStoreRootCategory() - { - $this->product->expects($this->any())->method('getStoreId')->will($this->returnValue(1)); - $this->product->expects($this->never())->method('getStoreIds'); - $category = $this->getMock('Magento\Catalog\Model\Category', [], [], '', false); - $category->expects($this->any())->method('getParentIds') - ->will($this->returnValue(['root-id', 'root-for-store-id'])); - $store = $this->getMockBuilder('Magento\Store\Model\Store')->disableOriginalConstructor()->getMock(); - $store->expects($this->any())->method('getRootCategoryId')->will($this->returnValue('not-root-id')); - $this->storeManager->expects($this->any())->method('getStore')->will($this->returnValue($store)); - $this->categoriesCollection->expects($this->any())->method('getIterator') - ->willReturn(new \ArrayIterator([$category])); - $this->initObjectRegistryFactory([]); - $canonical = new \Magento\UrlRewrite\Service\V1\Data\UrlRewrite(); - $canonical->setTargetPath('category-1') - ->setStoreId(1); - $this->canonicalUrlRewriteGenerator->expects($this->any())->method('generate') - ->will($this->returnValue([$canonical])); - $this->categoriesUrlRewriteGenerator->expects($this->any())->method('generate') - ->will($this->returnValue([])); - $this->currentUrlRewritesRegenerator->expects($this->any())->method('generate') - ->will($this->returnValue([])); - $this->anchorUrlRewriteGenerator->expects($this->any())->method('generate') - ->will($this->returnValue([])); - - $this->assertEquals(['category-1-1' => $canonical], $this->productUrlRewriteGenerator->generate($this->product)); - } - - /** - * Test method - */ - public function testSkipGenerationForGlobalScope() - { - $this->product->expects($this->any())->method('getStoreIds')->will($this->returnValue([1, 2])); - $this->storeViewService->expects($this->exactly(2))->method('doesEntityHaveOverriddenUrlKeyForStore') - ->will($this->returnValue(true)); - - $this->assertEquals([], $this->productUrlRewriteGenerator->generate($this->product)); + $reflectionProperty->setValue($this->productUrlRewriteGenerator, $this->productScopeRewriteGenerator); } /** - * @param array $entities + * Covers generate(). + * + * @return void */ - protected function initObjectRegistryFactory($entities) + public function testGenerate() { - $objectRegistry = $this->getMockBuilder('Magento\CatalogUrlRewrite\Model\ObjectRegistry') - ->disableOriginalConstructor()->getMock(); - $this->objectRegistryFactory->expects($this->any())->method('create') - ->with(['entities' => $entities]) - ->will($this->returnValue($objectRegistry)); + $productMock = $this->getMockBuilder(Product::class) + ->disableOriginalConstructor() + ->getMock(); + $storeId = 1; + $urls = ['dummy-url.html']; + + $productMock->expects($this->once()) + ->method('getVisibility') + ->willReturn(2); + $productMock->expects($this->once()) + ->method('getStoreId') + ->willReturn($storeId); + $productCategoriesMock = $this->getMockBuilder(\Magento\Catalog\Model\ResourceModel\Category\Collection::class) + ->disableOriginalConstructor() + ->getMock(); + $productCategoriesMock->expects($this->exactly(2)) + ->method('addAttributeToSelect') + ->withConsecutive(['url_key'], ['url_path']) + ->willReturnSelf(); + $productMock->expects($this->once()) + ->method('getCategoryCollection') + ->willReturn($productCategoriesMock); + $this->productScopeRewriteGenerator->expects($this->once()) + ->method('generateForSpecificStoreView') + ->willReturn($urls); + $this->assertEquals($urls, $this->productUrlRewriteGenerator->generate($productMock, 1)); } } diff --git a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/UrlRewriteBunchReplacerTest.php b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/UrlRewriteBunchReplacerTest.php new file mode 100644 index 0000000000000..2e4057f30d258 --- /dev/null +++ b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/UrlRewriteBunchReplacerTest.php @@ -0,0 +1,44 @@ +urlPersistMock = $this->getMock(UrlPersistInterface::class); + $this->urlRewriteBunchReplacer = new UrlRewriteBunchReplacer( + $this->urlPersistMock + ); + } + + /** + * Covers doBunchReplace() method. + * + * @return void + */ + public function testDoBunchReplace() + { + $urls = [[1], [2]]; + $this->urlPersistMock->expects($this->exactly(2)) + ->method('replace') + ->withConsecutive([[[1]]], [[[2]]]); + $this->urlRewriteBunchReplacer->doBunchReplace($urls, 1); + } +} diff --git a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Observer/AfterImportDataObserverTest.php b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Observer/AfterImportDataObserverTest.php index acb02d4a644a4..d9f9edd23387c 100644 --- a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Observer/AfterImportDataObserverTest.php +++ b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Observer/AfterImportDataObserverTest.php @@ -23,12 +23,12 @@ class AfterImportDataObserverTest extends \PHPUnit_Framework_TestCase /** * @var string */ - protected $categoryId = 10; + private $categoryId = 10; /** * @var \Magento\UrlRewrite\Model\UrlPersistInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $urlPersist; + private $urlPersist; /** * @var \Magento\UrlRewrite\Model\UrlFinderInterface|\PHPUnit_Framework_MockObject_MockObject @@ -38,12 +38,12 @@ class AfterImportDataObserverTest extends \PHPUnit_Framework_TestCase /** * @var \Magento\CatalogUrlRewrite\Model\ProductUrlRewriteGenerator|\PHPUnit_Framework_MockObject_MockObject */ - protected $productUrlRewriteGenerator; + private $productUrlRewriteGenerator; /** * @var \Magento\Catalog\Api\ProductRepositoryInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $productRepository; + private $productRepository; /** * @var \Magento\CatalogImportExport\Model\Import\Product|\PHPUnit_Framework_MockObject_MockObject @@ -53,67 +53,69 @@ class AfterImportDataObserverTest extends \PHPUnit_Framework_TestCase /** * @var \Magento\Framework\Event\Observer|\PHPUnit_Framework_MockObject_MockObject */ - protected $observer; + private $observer; /** * @var \Magento\Framework\Event|\PHPUnit_Framework_MockObject_MockObject */ - protected $event; + private $event; /** * @var \Magento\Catalog\Model\ProductFactory|\PHPUnit_Framework_MockObject_MockObject */ - protected $catalogProductFactory; + private $catalogProductFactory; /** * @var \Magento\Store\Model\StoreManagerInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $storeManager; + private $storeManager; /** * @var \Magento\CatalogUrlRewrite\Model\ObjectRegistryFactory|\PHPUnit_Framework_MockObject_MockObject */ - protected $objectRegistryFactory; + private $objectRegistryFactory; /** * @var \Magento\CatalogUrlRewrite\Model\ProductUrlPathGenerator|\PHPUnit_Framework_MockObject_MockObject */ - protected $productUrlPathGenerator; + private $productUrlPathGenerator; /** * @var \Magento\CatalogUrlRewrite\Service\V1\StoreViewService|\PHPUnit_Framework_MockObject_MockObject */ - protected $storeViewService; + private $storeViewService; /** * @var \Magento\UrlRewrite\Service\V1\Data\UrlRewriteFactory|\PHPUnit_Framework_MockObject_MockObject */ - protected $urlRewriteFactory; + private $urlRewriteFactory; /** * @var \Magento\UrlRewrite\Service\V1\Data\UrlRewrite|\PHPUnit_Framework_MockObject_MockObject */ - protected $urlRewrite; + private $urlRewrite; /** * @var \Magento\CatalogUrlRewrite\Model\ObjectRegistry|\PHPUnit_Framework_MockObject_MockObject */ - protected $objectRegistry; + private $objectRegistry; /** - * @var \Magento\CatalogUrlRewrite\Observer\AfterImportDataObserver|\PHPUnit_Framework_MockObject_MockObject + * @var \Magento\CatalogUrlRewrite\Observer\AfterImportDataObserver */ - protected $importMock; + private $import; /** - * @var \Magento\CatalogUrlRewrite\Observer\AfterImportDataObserver + * @var \Magento\Catalog\Model\Product|\PHPUnit_Framework_MockObject_MockObject */ - protected $import; + private $product; /** - * @var \Magento\Catalog\Model\Product|\PHPUnit_Framework_MockObject_MockObject + * Container for new generated url rewrites. + * + * @var \Magento\UrlRewrite\Model\MergeDataProvider|\PHPUnit_Framework_MockObject_MockObject */ - protected $product; + private $mergeDataProvider; /** * Test products returned by getBunch method of event object. @@ -147,7 +149,7 @@ class AfterImportDataObserverTest extends \PHPUnit_Framework_TestCase protected function setUp() { $this->importProduct = $this->getMock( - '\Magento\CatalogImportExport\Model\Import\Product', + \Magento\CatalogImportExport\Model\Import\Product::class, [ 'getNewSku', 'getProductCategories', @@ -160,7 +162,7 @@ protected function setUp() false ); $this->catalogProductFactory = $this->getMock( - '\Magento\Catalog\Model\ProductFactory', + \Magento\Catalog\Model\ProductFactory::class, [ 'create', ], @@ -170,53 +172,53 @@ protected function setUp() ); $this->storeManager = $this ->getMockBuilder( - '\Magento\Store\Model\StoreManagerInterface' + \Magento\Store\Model\StoreManagerInterface::class ) ->disableOriginalConstructor() ->setMethods([ 'getWebsite', ]) ->getMockForAbstractClass(); - $this->event = $this->getMock('\Magento\Framework\Event', ['getAdapter', 'getBunch'], [], '', false); + $this->event = $this->getMock(\Magento\Framework\Event::class, ['getAdapter', 'getBunch'], [], '', false); $this->event->expects($this->any())->method('getAdapter')->willReturn($this->importProduct); $this->event->expects($this->any())->method('getBunch')->willReturn($this->products); - $this->observer = $this->getMock('\Magento\Framework\Event\Observer', ['getEvent'], [], '', false); + $this->observer = $this->getMock(\Magento\Framework\Event\Observer::class, ['getEvent'], [], '', false); $this->observer->expects($this->any())->method('getEvent')->willReturn($this->event); - $this->urlPersist = $this->getMockBuilder('\Magento\UrlRewrite\Model\UrlPersistInterface') + $this->urlPersist = $this->getMockBuilder(\Magento\UrlRewrite\Model\UrlPersistInterface::class) ->disableOriginalConstructor() ->getMock(); $this->productUrlRewriteGenerator = - $this->getMockBuilder('\Magento\CatalogUrlRewrite\Model\ProductUrlRewriteGenerator') + $this->getMockBuilder(\Magento\CatalogUrlRewrite\Model\ProductUrlRewriteGenerator::class) ->disableOriginalConstructor() ->setMethods(['generate']) ->getMock(); - $this->productRepository = $this->getMockBuilder('\Magento\Catalog\Api\ProductRepositoryInterface') + $this->productRepository = $this->getMockBuilder(\Magento\Catalog\Api\ProductRepositoryInterface::class) ->disableOriginalConstructor() ->getMock(); $this->objectRegistryFactory = $this->getMock( - '\Magento\CatalogUrlRewrite\Model\ObjectRegistryFactory', + \Magento\CatalogUrlRewrite\Model\ObjectRegistryFactory::class, [], [], '', false ); $this->productUrlPathGenerator = $this->getMock( - '\Magento\CatalogUrlRewrite\Model\ProductUrlPathGenerator', + \Magento\CatalogUrlRewrite\Model\ProductUrlPathGenerator::class, [], [], '', false ); $this->storeViewService = $this->getMock( - '\Magento\CatalogUrlRewrite\Service\V1\StoreViewService', + \Magento\CatalogUrlRewrite\Service\V1\StoreViewService::class, [], [], '', false ); $this->urlRewriteFactory = $this->getMock( - '\Magento\UrlRewrite\Service\V1\Data\UrlRewriteFactory', + \Magento\UrlRewrite\Service\V1\Data\UrlRewriteFactory::class, [ 'create', ], @@ -225,7 +227,7 @@ protected function setUp() false ); $this->urlFinder = $this - ->getMockBuilder('\Magento\UrlRewrite\Model\UrlFinderInterface') + ->getMockBuilder(\Magento\UrlRewrite\Model\UrlFinderInterface::class) ->setMethods([ 'findAllByData', ]) @@ -233,22 +235,22 @@ protected function setUp() ->getMockForAbstractClass(); $this->urlRewrite = $this - ->getMockBuilder('Magento\UrlRewrite\Service\V1\Data\UrlRewrite') + ->getMockBuilder(\Magento\UrlRewrite\Service\V1\Data\UrlRewrite::class) ->disableOriginalConstructor() ->getMock(); $this->product = $this - ->getMockBuilder('Magento\Catalog\Model\Product') + ->getMockBuilder(\Magento\Catalog\Model\Product::class) ->disableOriginalConstructor() ->getMock(); $this->objectRegistry = $this - ->getMockBuilder('\Magento\CatalogUrlRewrite\Model\ObjectRegistry') + ->getMockBuilder(\Magento\CatalogUrlRewrite\Model\ObjectRegistry::class) ->disableOriginalConstructor() ->getMock(); $categoryProcessor = $this->getMock( - '\Magento\CatalogImportExport\Model\Import\Product\CategoryProcessor', + \Magento\CatalogImportExport\Model\Import\Product\CategoryProcessor::class, [ 'getCategoryById', ], @@ -257,7 +259,7 @@ protected function setUp() false ); $category = $this->getMock( - 'Magento\Catalog\Model\Category', + \Magento\Catalog\Model\Category::class, [ 'getId', ], @@ -278,10 +280,19 @@ protected function setUp() ->expects($this->any()) ->method('getCategoryProcessor') ->willReturn($categoryProcessor); + $mergeDataProviderFactory = $this->getMock( + \Magento\UrlRewrite\Model\MergeDataProviderFactory::class, + ['create'], + [], + '', + false + ); + $this->mergeDataProvider = new \Magento\UrlRewrite\Model\MergeDataProvider; + $mergeDataProviderFactory->expects($this->once())->method('create')->willReturn($this->mergeDataProvider); $this->objectManager = new ObjectManager($this); $this->import = $this->objectManager->getObject( - '\Magento\CatalogUrlRewrite\Observer\AfterImportDataObserver', + \Magento\CatalogUrlRewrite\Observer\AfterImportDataObserver::class, [ 'catalogProductFactory' => $this->catalogProductFactory, 'objectRegistryFactory' => $this->objectRegistryFactory, @@ -291,13 +302,14 @@ protected function setUp() 'urlPersist' => $this->urlPersist, 'urlRewriteFactory' => $this->urlRewriteFactory, 'urlFinder' => $this->urlFinder, + 'mergeDataProviderFactory' => $mergeDataProviderFactory ] ); } /** - * Test for afterImportData() - * Covers afterImportData() + protected methods used inside + * Test for afterImportData(). + * Covers afterImportData() + protected methods used inside. * * @covers \Magento\CatalogUrlRewrite\Observer\AfterImportDataObserver::_populateForUrlGeneration * @covers \Magento\CatalogUrlRewrite\Observer\AfterImportDataObserver::isGlobalScope @@ -312,7 +324,7 @@ public function testAfterImportData() $websiteId = 'websiteId value'; $productsCount = count($this->products); $websiteMock = $this->getMock( - '\Magento\Store\Model\Website', + \Magento\Store\Model\Website::class, [ 'getStoreIds', ], @@ -364,7 +376,7 @@ public function testAfterImportData() ->method('getStoreIdByCode') ->will($this->returnValueMap($map)); $product = $this->getMock( - '\Magento\Catalog\Model\Product', + \Magento\Catalog\Model\Product::class, [ 'getId', 'setId', @@ -433,14 +445,15 @@ public function testAfterImportData() $this->urlRewrite->expects($this->any())->method('setRequestPath')->willReturnSelf(); $this->urlRewrite->expects($this->any())->method('setTargetPath')->willReturnSelf(); $this->urlRewrite->expects($this->any())->method('getTargetPath')->willReturn('targetPath'); + $this->urlRewrite->expects($this->any())->method('getRequestPath')->willReturn('requestPath'); $this->urlRewrite->expects($this->any())->method('getStoreId') ->willReturnOnConsecutiveCalls(0, 'not global'); $this->urlRewriteFactory->expects($this->any())->method('create')->willReturn($this->urlRewrite); $productUrls = [ - 'targetPath-0' => $this->urlRewrite, - 'targetPath-not global' => $this->urlRewrite + 'requestPath_0' => $this->urlRewrite, + 'requestPath_not global' => $this->urlRewrite ]; $this->urlPersist @@ -452,7 +465,7 @@ public function testAfterImportData() } /** - * Cover canonicalUrlRewriteGenerate(). + * Covers canonicalUrlRewriteGenerate(). */ public function testCanonicalUrlRewriteGenerateWithUrlPath() { @@ -460,7 +473,7 @@ public function testCanonicalUrlRewriteGenerateWithUrlPath() $requestPath = 'simple-product.html'; $storeId = 10; $product = $this - ->getMockBuilder('Magento\Catalog\Model\Product') + ->getMockBuilder(\Magento\Catalog\Model\Product::class) ->disableOriginalConstructor() ->getMock(); $productsByStores = [$storeId => $product]; @@ -523,14 +536,14 @@ public function testCanonicalUrlRewriteGenerateWithUrlPath() } /** - * Cover canonicalUrlRewriteGenerate(). + * Covers canonicalUrlRewriteGenerate(). */ public function testCanonicalUrlRewriteGenerateWithEmptyUrlPath() { $productId = 'product_id'; $storeId = 10; $product = $this - ->getMockBuilder('Magento\Catalog\Model\Product') + ->getMockBuilder(\Magento\Catalog\Model\Product::class) ->disableOriginalConstructor() ->getMock(); $productsByStores = [$storeId => $product]; @@ -553,7 +566,7 @@ public function testCanonicalUrlRewriteGenerateWithEmptyUrlPath() } /** - * Cover categoriesUrlRewriteGenerate(). + * Covers categoriesUrlRewriteGenerate(). */ public function testCategoriesUrlRewriteGenerate() { @@ -562,7 +575,7 @@ public function testCategoriesUrlRewriteGenerate() $productId = 'product_id'; $canonicalUrlPathWithCategory = 'canonical-path-with-category'; $product = $this - ->getMockBuilder('Magento\Catalog\Model\Product') + ->getMockBuilder(\Magento\Catalog\Model\Product::class) ->disableOriginalConstructor() ->getMock(); $productsByStores = [ @@ -587,7 +600,7 @@ public function testCategoriesUrlRewriteGenerate() ->expects($this->any()) ->method('getCanonicalUrlPath') ->will($this->returnValue($canonicalUrlPathWithCategory)); - $category = $this->getMock('Magento\Catalog\Model\Category', [], [], '', false); + $category = $this->getMock(\Magento\Catalog\Model\Category::class, [], [], '', false); $category ->expects($this->any()) ->method('getId') @@ -715,7 +728,7 @@ protected function currentUrlRewritesRegeneratorGetCurrentRewritesMocks($current /** * @var \PHPUnit_Framework_MockObject_MockObject */ - $url = $this->getMockBuilder('Magento\UrlRewrite\Service\V1\Data\UrlRewrite') + $url = $this->getMockBuilder(\Magento\UrlRewrite\Service\V1\Data\UrlRewrite::class) ->disableOriginalConstructor()->getMock(); foreach ($urlRewrite as $key => $value) { $url->expects($this->any()) diff --git a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Observer/CategoryProcessUrlRewriteMovingObserverTest.php b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Observer/CategoryProcessUrlRewriteMovingObserverTest.php new file mode 100644 index 0000000000000..b90a67bcda2b3 --- /dev/null +++ b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Observer/CategoryProcessUrlRewriteMovingObserverTest.php @@ -0,0 +1,321 @@ +categoryUrlRewriteGeneratorMock = $this->getMock( + CategoryUrlRewriteGenerator::class, + [ + 'generate' + ], + [], + '', + false + ); + $this->urlPersist = $this->getMock(UrlPersistInterface::class, [], [], '', false); + $this->urlRewriteHandler = $this->getMock( + UrlRewriteHandler::class, + [ + 'generateProductUrlRewrites', + 'deleteCategoryRewritesForChildren', + ], + [], + '', + false + ); + + $this->scopeConfig = $this->getMockBuilder(ScopeConfigInterface::class) + ->setMethods(['isSetFlag']) + ->getMockForAbstractClass(); + + $this->urlRewriteBunchReplacerMock = $this->getMock( + UrlRewriteBunchReplacer::class, + ['doBunchReplace'], + [], + '', + false + ); + + $this->objectManager = new ObjectManager($this); + $this->observerModel = $this->objectManager->getObject( + CategoryProcessUrlRewriteMovingObserver::class, + [ + 'categoryUrlRewriteGenerator' => $this->categoryUrlRewriteGeneratorMock, + 'urlPersist' => $this->urlPersist, + 'scopeConfig' => $this->scopeConfig, + 'urlRewriteHandler' => $this->urlRewriteHandler + ] + ); + $this->objectManager->setBackwardCompatibleProperty( + $this->observerModel, + 'urlRewriteBunchReplacer', + $this->urlRewriteBunchReplacerMock + ); + } + + /** + * Covers execite() method. + * + * @dataProvider testExecuteDataProvider + * @return void + */ + public function testExecute($changedParent, $saveRewritesHistory) + { + $storeId = 1; + $category = $this->getMock( + \Magento\Catalog\Model\Category::class, + [ + 'getStoreId', + 'dataHasChangedFor' + ], + [], + '', + false + ); + $category->expects($this->once())->method('dataHasChangedFor')->with('parent_id')->willReturn($changedParent); + + if ($changedParent) { + $this->getMockData($saveRewritesHistory, $category, $storeId); + } + + $event = $this->getMock(\Magento\Framework\Event::class, ['getCategory'], [], '', false); + $event->expects($this->any())->method('getCategory')->willReturn($category); + + $observer = $this->getMock( + \Magento\Framework\Event\Observer::class, + ['getEvent'], + [], + '', + false + ); + $observer->expects($this->any())->method('getEvent')->willReturn($event); + $this->observerModel->execute($observer); + } + + /** + * Data provider for testExecute() + * + * @return array + */ + public function testExecuteDataProvider() + { + return [ + 1 => [ + 'changedParent' => true, + 'saveRewritesHistory' => true + ], + 2 => [ + 'changedParent' =>false, + 'saveRewritesHistory' => true + ], + ]; + } + + /** + * Returns Mock data for test. + * + * @param $saveRewritesHistory + * @param $category + * @param $storeId + */ + private function getMockData($saveRewritesHistory, $category, $storeId) + { + $category->expects($this->once())->method('getStoreId')->willReturn($storeId); + $this->scopeConfig->expects($this->once()) + ->method('isSetFlag') + ->with( + UrlKeyRenderer::XML_PATH_SEO_SAVE_HISTORY, + \Magento\Store\Model\ScopeInterface::SCOPE_STORE, + $storeId + )->willReturn($saveRewritesHistory); + + $this->categoryUrlRewriteGeneratorMock + ->expects($this->once()) + ->method('generate') + ->with($category, true) + ->willReturn($this->getCategoryUrlRewritesGenerated()); + + $this->urlRewriteHandler + ->expects($this->once()) + ->method('generateProductUrlRewrites') + ->with($category) + ->willReturn($this->getProductUrlRewritesGenerated()); + $this->urlRewriteHandler + ->expects($this->once()) + ->method('deleteCategoryRewritesForChildren') + ->with($category) + ->willReturn(null); + + $this->urlRewriteBunchReplacerMock->expects($this->once()) + ->method('doBunchReplace'); + } + + /** + * Returns an array of generated UrlRewrites for category. + * + * @return array + */ + private function getCategoryUrlRewritesGenerated() + { + $categoryUrlRewriteGenerated1 = new \Magento\UrlRewrite\Service\V1\Data\UrlRewrite(); + $categoryUrlRewriteGenerated1->setRequestPath('category1/category2.html') + ->setStoreId(1) + ->setEntityType('category') + ->setEntityId(6) + ->setTargetPath('catalog/category/view/id/6'); + $categoryUrlRewriteGenerated2 = new \Magento\UrlRewrite\Service\V1\Data\UrlRewrite(); + $categoryUrlRewriteGenerated2->setRequestPath('category2.html') + ->setStoreId(1) + ->setEntityType('category') + ->setEntityId(6) + ->setTargetPath('category1/category2.html') + ->setRedirectType(301) + ->setIsAutogenerated(0); + + $categoryUrlRewriteGenerated = [ + 'category1/category2.html_1' => $categoryUrlRewriteGenerated1, + 'category2.html_1' => $categoryUrlRewriteGenerated2 + ]; + + return $categoryUrlRewriteGenerated; + } + + /** + * Returns an array of generated UrlRewrites for products. + * + * @return array + */ + private function getProductUrlRewritesGenerated() + { + $productUrlRewriteGenerated1 = new \Magento\UrlRewrite\Service\V1\Data\UrlRewrite(); + $productUrlRewriteGenerated1->setEntityType('product') + ->setEntityId(1) + ->setRequestPath('simple1.html') + ->setTargetPath('catalog/product/view/id/1') + ->setStoreId(1); + $productUrlRewriteGenerated2 = new \Magento\UrlRewrite\Service\V1\Data\UrlRewrite(); + $productUrlRewriteGenerated2->setEntityType('product') + ->setEntityId(1) + ->setRequestPath('category1/category2/simple1.html') + ->setTargetPath('catalog/product/view/id/1/category/6') + ->setStoreId(1) + ->setMetadata('a:1:{s:11:"category_id";s:1:"6";}'); + $productUrlRewriteGenerated3 = new \Magento\UrlRewrite\Service\V1\Data\UrlRewrite(); + $productUrlRewriteGenerated3->setEntityType('product') + ->setEntityId(1) + ->setRequestPath('category2/simple1.html') + ->setTargetPath('category1/category2/simple1.html') + ->setRedirectType(301) + ->setStoreId(1) + ->setDescription(null) + ->setIsAutogenerated(0) + ->setMetadata('a:1:{s:11:"category_id";s:1:"6";}'); + $productUrlRewriteGenerated4 = new \Magento\UrlRewrite\Service\V1\Data\UrlRewrite(); + $productUrlRewriteGenerated4->setEntityType('product') + ->setEntityId(1) + ->setRequestPath('category1/simple1.html') + ->setTargetPath('catalog/product/view/id/1/category/5') + ->setStoreId(1) + ->setMetadata('a:1:{s:11:"category_id";s:1:"5";}'); + $productUrlRewriteGenerated5 = new \Magento\UrlRewrite\Service\V1\Data\UrlRewrite(); + $productUrlRewriteGenerated5->setEntityType('product') + ->setEntityId(2) + ->setRequestPath('simple2.html') + ->setTargetPath('catalog/product/view/id/2') + ->setStoreId(1); + $productUrlRewriteGenerated6 = new \Magento\UrlRewrite\Service\V1\Data\UrlRewrite(); + $productUrlRewriteGenerated6->setEntityType('product') + ->setEntityId(2) + ->setRequestPath('category1/category2/simple2.html') + ->setTargetPath('catalog/product/view/id/2/category/6') + ->setStoreId(1) + ->setMetadata('a:1:{s:11:"category_id";s:1:"6";}'); + $productUrlRewriteGenerated7 = new \Magento\UrlRewrite\Service\V1\Data\UrlRewrite(); + $productUrlRewriteGenerated7->setEntityType('product') + ->setEntityId(2) + ->setRequestPath('category2/simple2.html') + ->setTargetPath('category1/category2/simple2.html') + ->setRedirectType(301) + ->setStoreId(1) + ->setDescription(null) + ->setIsAutogenerated(0) + ->setMetadata('a:1:{s:11:"category_id";s:1:"6";}'); + + $productUrlRewriteGenerated8 = new \Magento\UrlRewrite\Service\V1\Data\UrlRewrite(); + $productUrlRewriteGenerated8->setEntityType('product') + ->setEntityId(2) + ->setRequestPath('category1/simple2.html') + ->setTargetPath('catalog/product/view/id/2/category/5') + ->setStoreId(1) + ->setMetadata('a:1:{s:11:"category_id";s:1:"6";}'); + + $productUrlRewriteGenerated = [ + 'simple1.html_1' => $productUrlRewriteGenerated1, + 'category1/category2/simple1.html_1' => $productUrlRewriteGenerated2, + 'category2/simple1.html_1' => $productUrlRewriteGenerated3, + 'category1/simple1.html_1' => $productUrlRewriteGenerated4, + 'simple2.html_1' => $productUrlRewriteGenerated5, + 'category1/category2/simple2.html_1' => $productUrlRewriteGenerated6, + 'category2/simple2.html_1' => $productUrlRewriteGenerated7, + 'category1/simple2.html_1' => $productUrlRewriteGenerated8 + ]; + + return $productUrlRewriteGenerated; + } +} diff --git a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Observer/CategoryProcessUrlRewriteSavingObserverTest.php b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Observer/CategoryProcessUrlRewriteSavingObserverTest.php new file mode 100644 index 0000000000000..f22417c1be1e8 --- /dev/null +++ b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Observer/CategoryProcessUrlRewriteSavingObserverTest.php @@ -0,0 +1,261 @@ +categoryUrlRewriteGeneratorMock = $this->getMock( + CategoryUrlRewriteGenerator::class, + ['generate'], + [], + '', + false + ); + $this->urlRewriteHandlerMock = $this->getMock( + UrlRewriteHandler::class, + ['generateProductUrlRewrites'], + [], + '', + false + ); + $this->urlRewriteBunchReplacerMock = $this->getMock( + UrlRewriteBunchReplacer::class, + ['doBunchReplace'], + [], + '', + false + ); + $this->databaseMapPoolMock = $this->getMock(DatabaseMapPool::class, ['resetMap'], [], '', false); + + $this->objectManager = new ObjectManager($this); + $this->observerModel = $this->objectManager->getObject( + CategoryProcessUrlRewriteSavingObserver::class, + [ + 'categoryUrlRewriteGenerator' => $this->categoryUrlRewriteGeneratorMock, + 'urlRewriteHandler' => $this->urlRewriteHandlerMock, + 'urlRewriteBunchReplacer' => $this->urlRewriteBunchReplacerMock, + 'databaseMapPool' => $this->databaseMapPoolMock, + 'dataUrlRewriteClassNames' => $this->dataUrlRewriteClassNames + ] + ); + } + + /** + * Covers execite() method. + * + * @dataProvider executeDataProvider + * @return void + */ + public function testExecute( + $parentId, + $isChangedUrlKey, + $isChangedIsAnchor, + $isChangedProductList + ) { + $categoryId = 6; + /** @var \PHPUnit_Framework_MockObject_MockObject $category */ + $category = $this->getMock( + \Magento\Catalog\Model\Category::class, + [ + 'getEntityId', + 'getParentId', + 'dataHasChangedFor', + 'getIsChangedProductList' + ], + [], + '', + false + ); + $category->expects($this->any())->method('getEntityId')->willReturn($categoryId); + $category->expects($this->once())->method('getParentId')->willReturn($parentId); + $category->expects($this->any())->method('dataHasChangedFor') + ->willReturnMap( + [ + ['url_key', $isChangedUrlKey], + ['is_anchor', $isChangedIsAnchor] + ] + ); + $category->expects($this->any())->method('getIsChangedProductList')->willReturn($isChangedProductList); + + $categoryUrlRewriteResult = $this->getCategoryUrlRewriteResult(); + $this->categoryUrlRewriteGeneratorMock + ->expects($this->any()) + ->method('generate') + ->with($category) + ->willReturn($categoryUrlRewriteResult); + $productUrlRewriteResult = $this->getProductUrlRewriteResult(); + $this->urlRewriteHandlerMock + ->expects($this->any()) + ->method('generateProductUrlRewrites') + ->with($category) + ->willReturn($productUrlRewriteResult); + $this->urlRewriteBunchReplacerMock + ->expects($this->any()) + ->method('doBunchReplace'); + $this->databaseMapPoolMock + ->expects($this->any()) + ->method('resetMap'); + + $event = $this->getMock(\Magento\Framework\Event::class, ['getData'], [], '', false); + $event->expects($this->any())->method('getData')->with('category')->willReturn($category); + $observer = $this->getMock( + \Magento\Framework\Event\Observer::class, + ['getEvent'], + [], + '', + false + ); + $observer->expects($this->any())->method('getEvent')->willReturn($event); + + $this->observerModel->execute($observer); + } + + /** + * Data provider for testExecute(). + * + * @return array + */ + public function executeDataProvider() + { + return [ + 'tree_root_category_parent' => [ + 'parentId' => \Magento\Catalog\Model\Category::TREE_ROOT_ID, + 'isChangedUrlKey' => true, + 'isChangedIsAnchor' => true, + 'isChangedProductList' => true, + ], + 'true_category_data' => [ + 'parentId' => 3, + 'isChangedUrlKey' => true, + 'isChangedIsAnchor' => true, + 'isChangedProductList' => true, + ], + 'false_category_data' => [ + 'parentId' => 3, + 'isChangedUrlKey' => true, + 'isChangedIsAnchor' => true, + 'isChangedProductList' => true, + ], + 'true_isChangedUrlKey' => [ + 'parentId' => 3, + 'isChangedUrlKey' => true, + 'isChangedIsAnchor' => false, + 'isChangedProductList' => false, + ], + 'true_isChangedIsAnchor' => [ + 'parentId' => 3, + 'isChangedUrlKey' => false, + 'isChangedIsAnchor' => true, + 'isChangedProductList' => false, + ], + 'true_isChangedProductList' => [ + 'parentId' => 3, + 'isChangedUrlKey' => false, + 'isChangedIsAnchor' => false, + 'isChangedProductList' => true, + ], + ]; + } + + /** + * Returns category urlRewrite result. + * + * @return array + */ + private function getCategoryUrlRewriteResult() + { + $categoryUrlRewriteResult1 = new \Magento\UrlRewrite\Service\V1\Data\UrlRewrite(); + $categoryUrlRewriteResult1->setRequestPath('category2.html') + ->setStoreId(1) + ->setEntityType('category') + ->setEntityId(6) + ->setTargetPath('catalog/category/view/id/6'); + $categoryUrlRewriteResult = [ + 'category2.html_1' => $categoryUrlRewriteResult1, + ]; + + return $categoryUrlRewriteResult; + } + + /** + * Returns products urlRewrite result. + * + * @return array + */ + private function getProductUrlRewriteResult() + { + $productUrlRewriteResult1 = new \Magento\UrlRewrite\Service\V1\Data\UrlRewrite(); + $productUrlRewriteResult1->setEntityType('product') + ->setEntityId(1) + ->setRequestPath('simple1.html') + ->setTargetPath('catalog/product/view/id/1') + ->setStoreId(1); + $productUrlRewriteResult2 = new \Magento\UrlRewrite\Service\V1\Data\UrlRewrite(); + $productUrlRewriteResult2->setEntityType('product') + ->setEntityId(1) + ->setRequestPath('category2/simple1.html') + ->setTargetPath('catalog/product/view/id/1/category/6') + ->setStoreId(1) + ->setMetadata('a:1:{s:11:"category_id";s:1:"6";}'); + + $productUrlRewriteResult = [ + 'simple1.html_1' => $productUrlRewriteResult1, + 'category2/simple1.html_1' => $productUrlRewriteResult2, + ]; + + return $productUrlRewriteResult; + } +} diff --git a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Observer/CategoryUrlPathAutogeneratorObserverTest.php b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Observer/CategoryUrlPathAutogeneratorObserverTest.php index a6fdc41cd11ee..db153f6cb0a4a 100644 --- a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Observer/CategoryUrlPathAutogeneratorObserverTest.php +++ b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Observer/CategoryUrlPathAutogeneratorObserverTest.php @@ -37,16 +37,34 @@ class CategoryUrlPathAutogeneratorObserverTest extends \PHPUnit_Framework_TestCa protected function setUp() { $this->observer = $this->getMock( - 'Magento\Framework\Event\Observer', - ['getEvent', 'getCategory'], + \Magento\Framework\Event\Observer::class, + [ + 'getEvent', + 'getCategory' + ], + [], + '', + false + ); + $this->categoryResource = $this->getMock( + \Magento\Catalog\Model\ResourceModel\Category::class, + [], [], '', false ); - $this->categoryResource = $this->getMock('Magento\Catalog\Model\ResourceModel\Category', [], [], '', false); $this->category = $this->getMock( - 'Magento\Catalog\Model\Category', - ['setUrlKey', 'setUrlPath', 'dataHasChangedFor', 'isObjectNew', 'getResource', 'getUrlKey', 'getStoreId'], + \Magento\Catalog\Model\Category::class, + [ + 'setUrlKey', + 'setUrlPath', + 'dataHasChangedFor', + 'isObjectNew', + 'getResource', + 'getUrlKey', + 'getStoreId', + 'getData' + ], [], '', false @@ -55,18 +73,18 @@ protected function setUp() $this->observer->expects($this->any())->method('getEvent')->willReturnSelf(); $this->observer->expects($this->any())->method('getCategory')->willReturn($this->category); $this->categoryUrlPathGenerator = $this->getMock( - 'Magento\CatalogUrlRewrite\Model\CategoryUrlPathGenerator', + \Magento\CatalogUrlRewrite\Model\CategoryUrlPathGenerator::class, [], [], '', false ); $this->childrenCategoriesProvider = $this->getMock( - 'Magento\CatalogUrlRewrite\Model\Category\ChildrenCategoriesProvider' + \Magento\CatalogUrlRewrite\Model\Category\ChildrenCategoriesProvider::class ); $this->storeViewService = $this->getMock( - 'Magento\CatalogUrlRewrite\Service\V1\StoreViewService', + \Magento\CatalogUrlRewrite\Service\V1\StoreViewService::class, [], [], '', @@ -74,7 +92,7 @@ protected function setUp() ); $this->categoryUrlPathAutogeneratorObserver = (new ObjectManagerHelper($this))->getObject( - 'Magento\CatalogUrlRewrite\Observer\CategoryUrlPathAutogeneratorObserver', + \Magento\CatalogUrlRewrite\Observer\CategoryUrlPathAutogeneratorObserver::class, [ 'categoryUrlPathGenerator' => $this->categoryUrlPathGenerator, 'childrenCategoriesProvider' => $this->childrenCategoriesProvider, @@ -90,7 +108,7 @@ public function testSetCategoryUrlAndCategoryPath() $this->category->expects($this->once())->method('setUrlKey')->with('urk_key')->willReturnSelf(); $this->categoryUrlPathGenerator->expects($this->once())->method('getUrlPath')->willReturn('url_path'); $this->category->expects($this->once())->method('setUrlPath')->with('url_path')->willReturnSelf(); - $this->category->expects($this->once())->method('isObjectNew')->willReturn(true); + $this->category->expects($this->exactly(2))->method('isObjectNew')->willReturn(true); $this->categoryUrlPathAutogeneratorObserver->execute($this->observer); } @@ -114,7 +132,7 @@ public function testUrlKeyAndUrlPathUpdating() $this->category->expects($this->once())->method('setUrlKey')->with('url_key')->willReturnSelf(); $this->category->expects($this->once())->method('setUrlPath')->with('url_path')->willReturnSelf(); // break code execution - $this->category->expects($this->once())->method('isObjectNew')->willReturn(true); + $this->category->expects($this->exactly(2))->method('isObjectNew')->willReturn(true); $this->categoryUrlPathAutogeneratorObserver->execute($this->observer); } @@ -128,7 +146,7 @@ public function testUrlPathAttributeNoUpdatingIfCategoryIsNew() $this->category->expects($this->any())->method('setUrlKey')->willReturnSelf(); $this->category->expects($this->any())->method('setUrlPath')->willReturnSelf(); - $this->category->expects($this->once())->method('isObjectNew')->willReturn(true); + $this->category->expects($this->exactly(2))->method('isObjectNew')->willReturn(true); $this->categoryResource->expects($this->never())->method('saveAttribute'); $this->categoryUrlPathAutogeneratorObserver->execute($this->observer); @@ -142,7 +160,7 @@ public function testUrlPathAttributeUpdating() $this->category->expects($this->any())->method('getUrlKey')->willReturn('not_formatted_url_key'); $this->category->expects($this->any())->method('setUrlKey')->willReturnSelf(); $this->category->expects($this->any())->method('setUrlPath')->willReturnSelf(); - $this->category->expects($this->once())->method('isObjectNew')->willReturn(false); + $this->category->expects($this->exactly(2))->method('isObjectNew')->willReturn(false); $this->categoryResource->expects($this->once())->method('saveAttribute')->with($this->category, 'url_path'); @@ -162,7 +180,7 @@ public function testChildrenUrlPathAttributeNoUpdatingIfParentUrlPathIsNotChange $this->category->expects($this->any())->method('getUrlKey')->willReturn('not_formatted_url_key'); $this->category->expects($this->any())->method('setUrlKey')->willReturnSelf(); $this->category->expects($this->any())->method('setUrlPath')->willReturnSelf(); - $this->category->expects($this->once())->method('isObjectNew')->willReturn(false); + $this->category->expects($this->exactly(2))->method('isObjectNew')->willReturn(false); // break code execution $this->category->expects($this->once())->method('dataHasChangedFor')->with('url_path')->willReturn(false); @@ -177,7 +195,7 @@ public function testChildrenUrlPathAttributeUpdatingForSpecificStore() $this->category->expects($this->any())->method('getUrlKey')->willReturn('not_formatted_url_key'); $this->category->expects($this->any())->method('setUrlKey')->willReturnSelf(); $this->category->expects($this->any())->method('setUrlPath')->willReturnSelf(); - $this->category->expects($this->any())->method('isObjectNew')->willReturn(false); + $this->category->expects($this->exactly(2))->method('isObjectNew')->willReturn(false); $this->category->expects($this->any())->method('dataHasChangedFor')->willReturn(true); // only for specific store $this->category->expects($this->atLeastOnce())->method('getStoreId')->willReturn(1); diff --git a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Observer/UrlRewriteHandlerTest.php b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Observer/UrlRewriteHandlerTest.php new file mode 100644 index 0000000000000..c54a1a8bf9f2b --- /dev/null +++ b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Observer/UrlRewriteHandlerTest.php @@ -0,0 +1,394 @@ +categoryMock = $this->getMockBuilder(\Magento\Catalog\Model\Category::class) + ->setMethods( + [ + 'getAffectedProductIds', + 'getData', + 'getStoreId', + 'getEntityId', + 'getId', + 'getProductCollection' + ] + ) + ->disableOriginalConstructor() + ->getMock(); + $this->childrenCategoriesProviderMock = $this->getMockBuilder(ChildrenCategoriesProvider::class) + ->getMock(); + $this->categoryUrlRewriteGeneratorMock = $this->getMockBuilder(CategoryUrlRewriteGenerator::class) + ->disableOriginalConstructor() + ->getMock(); + $this->productUrlRewriteGeneratorMock = $this->getMockBuilder(ProductUrlRewriteGenerator::class) + ->setMethods(['generate']) + ->disableOriginalConstructor() + ->getMock(); + $this->urlPersistMock = $this->getMockBuilder(UrlPersistInterface::class) + ->getMock(); + $this->collectionFactoryMock = $this->getMockBuilder(CollectionFactory::class) + ->setMethods(['create']) + ->disableOriginalConstructor() + ->getMock(); + $this->mergeDataProviderFactoryMock = $this->getMockBuilder(MergeDataProviderFactory::class) + ->setMethods(['create']) + ->disableOriginalConstructor() + ->getMock(); + $this->mergeDataProviderMock = $this->getMockBuilder(MergeDataProvider::class) + ->setMethods(['getData']) + ->disableOriginalConstructor() + ->getMock(); + $this->mergeDataProviderFactoryMock->expects($this->any()) + ->method('create') + ->willReturn($this->mergeDataProviderMock); + + $this->urlRewriteHandler = new UrlRewriteHandler( + $this->childrenCategoriesProviderMock, + $this->categoryUrlRewriteGeneratorMock, + $this->productUrlRewriteGeneratorMock, + $this->urlPersistMock, + $this->collectionFactoryMock, + $this->mergeDataProviderFactoryMock + ); + + $this->categoryBasedProductRewriteGeneratorMock = $this->getMockBuilder( + CategoryBasedProductRewriteGenerator::class + ) + ->disableOriginalConstructor() + ->getMock(); + + $this->objectManager = new ObjectManager($this); + $this->objectManager->setBackwardCompatibleProperty( + $this->urlRewriteHandler, + 'categoryBasedProductRewriteGenerator', + $this->categoryBasedProductRewriteGeneratorMock + ); + + $this->productItem = $this->getMock( + \Magento\Catalog\Model\Product::class, + [ + 'getId', + 'setStoreId', + 'setData' + ], + [], + '', + false + ); + } + + public function testDeleteCategoryRewritesForChildren() + { + $this->categoryMock->expects($this->once()) + ->method('getId') + ->willReturn(2); + + $this->childrenCategoriesProviderMock->expects($this->once()) + ->method('getChildrenIds') + ->with($this->categoryMock, true) + ->willReturn([3, 4]); + + $this->urlRewriteHandler->deleteCategoryRewritesForChildren($this->categoryMock); + } + + /** + * Covers generateProductUrlRewrites(), getCategoryProductsUrlRewrites() methods. + * + * @dataProvider generateProductUrlRewritesDataProvider + * @return void + */ + public function testGenerateProductUrlRewrites($affectedProductIds) + { + $storeId = 0; + $saveRewritesHistory = true; + $categoryId = 6; + $this->categoryMock->expects($this->once()) + ->method('getData') + ->with('save_rewrites_history') + ->willReturn($saveRewritesHistory); + $this->categoryMock->expects($this->once()) + ->method('getStoreId') + ->willReturn($storeId); + $this->categoryMock->expects($this->any()) + ->method('getAffectedProductIds') + ->willReturn($affectedProductIds); + $this->categoryMock->expects($this->once()) + ->method('getEntityId') + ->willReturn($categoryId); + + if ($affectedProductIds) { + $this->callIfAffectedProductsIsset($saveRewritesHistory, $storeId, $categoryId, $affectedProductIds); + } else { + $this->getCategoryProductsUrlRewrites($saveRewritesHistory, $storeId, $categoryId, $affectedProductIds); + } + + $this->childrenCategoriesProviderMock->expects($this->once()) + ->method('getChildren') + ->with($this->categoryMock, true) + ->willReturn([]); + $generatedUrlRewrites = $this->getProductUrlRewriteResult($affectedProductIds); + $this->mergeDataProviderMock->expects($this->any()) + ->method('getData') + ->willReturn($generatedUrlRewrites); + + $this->assertEquals( + $generatedUrlRewrites, + $this->urlRewriteHandler->generateProductUrlRewrites($this->categoryMock) + ); + } + + /** + * Calls when $affectedProductIds is not empty. + * + * @param $saveRewritesHistory + * @param $storeId + * @param $categoryId + * @param $affectedProductIds + * @return void + */ + private function callIfAffectedProductsIsset($saveRewritesHistory, $storeId, $categoryId, $affectedProductIds) + { + $productCollectionMock = $this->getMockBuilder(\Magento\Catalog\Model\ResourceModel\Product\Collection::class) + ->setMethods( + [ + 'getData', + 'setStoreId', + 'addIdFilter', + 'addAttributeToSelect' + ] + ) + ->disableOriginalConstructor() + ->getMock(); + $productCollectionMock->expects($this->once()) + ->method('setStoreId') + ->with($storeId) + ->willReturn($productCollectionMock); + $productCollectionMock->expects($this->once()) + ->method('addIdFilter') + ->with($affectedProductIds) + ->willReturn($productCollectionMock); + $productCollectionMock = $this->setAdditionalMocks($productCollectionMock, $storeId, $saveRewritesHistory); + $this->collectionFactoryMock->expects($this->any()) + ->method('create') + ->willReturn($productCollectionMock); + $this->productUrlRewriteGeneratorMock->expects($this->once()) + ->method('generate') + ->with($this->productItem, $categoryId) + ->willReturn($this->getProductUrlRewriteResult($affectedProductIds)); + } + + /** + * Calls when $affectedProductIds is empty. + * + * @param $saveRewritesHistory + * @param $storeId + * @param $categoryId + * @param $affectedProductIds + * @return void + */ + private function getCategoryProductsUrlRewrites($saveRewritesHistory, $storeId, $categoryId, $affectedProductIds) + { + $productCollection = $this->getMock( + \Magento\Catalog\Model\ResourceModel\Product\Collection::class, + ['addAttributeToSelect'], + [], + '', + false + ); + $productCollection = $this->setAdditionalMocks($productCollection, $storeId, $saveRewritesHistory); + $this->categoryMock->expects($this->once()) + ->method('getProductCollection') + ->willReturn($productCollection); + $categoryBasedProductRewriteGenerated = $this->getProductUrlRewriteResult($affectedProductIds); + $this->categoryBasedProductRewriteGeneratorMock->expects($this->once()) + ->method('generate') + ->with($this->productItem, $this->categoryMock, $categoryId) + ->willReturn($categoryBasedProductRewriteGenerated); + $this->productItem->expects($this->exactly(2)) + ->method('getId') + ->willReturn(1); + } + + /** + * DataProvider for testGenerateProductUrlRewrites(). + * + * @return array + */ + public function generateProductUrlRewritesDataProvider() + { + return [ + 1 => [ + 'affectedProductIds' => null + ], + 2 => [ + 'affectedProductIds' => [0 => 2] + ] + ]; + } + + /** + * Returns products urlRewrite result. + * + * @return array + */ + private function getProductUrlRewriteResult($affectedProductIds) + { + if ($affectedProductIds) { + $productUrlRewriteResult1 = new \Magento\UrlRewrite\Service\V1\Data\UrlRewrite(); + $productUrlRewriteResult1->setEntityType('product') + ->setEntityId(2) + ->setRequestPath('simple2.html') + ->setTargetPath('catalog/product/view/id/2') + ->setStoreId(1); + $productUrlRewriteResult = [ + 'simple1.html_1' => $productUrlRewriteResult1, + ]; + } else { + $productUrlRewriteResult1 = new \Magento\UrlRewrite\Service\V1\Data\UrlRewrite(); + $productUrlRewriteResult1->setEntityType('product') + ->setEntityId(1) + ->setRequestPath('simple1.html') + ->setTargetPath('catalog/product/view/id/1') + ->setStoreId(1); + $productUrlRewriteResult2 = new \Magento\UrlRewrite\Service\V1\Data\UrlRewrite(); + $productUrlRewriteResult2->setEntityType('product') + ->setEntityId(1) + ->setRequestPath('category2/simple1.html') + ->setTargetPath('catalog/product/view/id/1/category/6') + ->setStoreId(1) + ->setMetadata('a:1:{s:11:"category_id";s:1:"6";}'); + + $productUrlRewriteResult = [ + 'simple1.html_1' => $productUrlRewriteResult1, + 'category2/simple1.html_1' => $productUrlRewriteResult2, + ]; + } + + return $productUrlRewriteResult; + } + + /** + * Sets additional data to the product Collection Mock. + * + * @param $productCollectionMock + * @return $productCollectionMock + */ + private function setAdditionalMocks($productCollectionMock, $storeId, $saveRewritesHistory) + { + $productCollectionMock->expects($this->any())->method('addAttributeToSelect') + ->willReturnMap( + [ + ['visibility', false, $productCollectionMock], + ['name', false, $productCollectionMock], + ['url_key', false, $productCollectionMock], + ['url_path', false, $productCollectionMock] + ] + ); + $this->productItem->expects($this->once()) + ->method('setStoreId') + ->with($storeId) + ->willReturn($this->productItem); + $this->productItem->expects($this->once()) + ->method('setData') + ->with('save_rewrites_history', $saveRewritesHistory) + ->willReturn($this->productItem); + $this->objectManager->setBackwardCompatibleProperty( + $productCollectionMock, + '_items', + [$this->productItem] + ); + $this->objectManager->setBackwardCompatibleProperty( + $productCollectionMock, + '_isCollectionLoaded', + true + ); + + return $productCollectionMock; + } +} diff --git a/app/code/Magento/CatalogUrlRewrite/composer.json b/app/code/Magento/CatalogUrlRewrite/composer.json index a17ec77187f0e..e9fae0372d6eb 100644 --- a/app/code/Magento/CatalogUrlRewrite/composer.json +++ b/app/code/Magento/CatalogUrlRewrite/composer.json @@ -14,7 +14,7 @@ "magento/module-ui": "100.1.*" }, "type": "magento2-module", - "version": "100.1.3", + "version": "100.1.4", "license": [ "OSL-3.0", "AFL-3.0" diff --git a/app/code/Magento/CatalogUrlRewrite/etc/di.xml b/app/code/Magento/CatalogUrlRewrite/etc/di.xml index 5a8974fc52da4..7f0fc718083ec 100644 --- a/app/code/Magento/CatalogUrlRewrite/etc/di.xml +++ b/app/code/Magento/CatalogUrlRewrite/etc/di.xml @@ -23,4 +23,12 @@ + + + + Magento\CatalogUrlRewrite\Model\Map\DataProductUrlRewriteDatabaseMap + Magento\CatalogUrlRewrite\Model\Map\DataCategoryUrlRewriteDatabaseMap + + + diff --git a/app/code/Magento/CatalogWidget/Block/Product/Widget/Conditions.php b/app/code/Magento/CatalogWidget/Block/Product/Widget/Conditions.php index f1c075fd54924..2a852a708fa88 100644 --- a/app/code/Magento/CatalogWidget/Block/Product/Widget/Conditions.php +++ b/app/code/Magento/CatalogWidget/Block/Product/Widget/Conditions.php @@ -73,6 +73,7 @@ public function __construct( $this->conditions = $conditions; $this->rule = $rule; $this->registry = $registry; + parent::__construct($context, $data); } @@ -81,12 +82,17 @@ public function __construct( */ protected function _construct() { + $widgetParameters = []; $widget = $this->registry->registry('current_widget_instance'); + if ($widget) { $widgetParameters = $widget->getWidgetParameters(); - if (isset($widgetParameters['conditions'])) { - $this->rule->loadPost($widgetParameters); - } + } elseif (($widgetOptions = $this->getLayout()->getBlock('wysiwyg_widget.options')) != false) { + $widgetParameters = $widgetOptions->getWidgetValues(); + } + + if (isset($widgetParameters['conditions'])) { + $this->rule->loadPost($widgetParameters); } } diff --git a/app/code/Magento/CatalogWidget/Test/Unit/Block/Product/Widget/ConditionsTest.php b/app/code/Magento/CatalogWidget/Test/Unit/Block/Product/Widget/ConditionsTest.php new file mode 100644 index 0000000000000..79a25d2f397bd --- /dev/null +++ b/app/code/Magento/CatalogWidget/Test/Unit/Block/Product/Widget/ConditionsTest.php @@ -0,0 +1,174 @@ +objectManagerHelper = new ObjectManagerHelper($this); + $this->ruleMock = $this->getMockBuilder(Rule::class) + ->disableOriginalConstructor() + ->getMock(); + $this->registryMock = $this->getMockBuilder(Registry::class) + ->disableOriginalConstructor() + ->getMock(); + $this->layoutMock = $this->getMockForAbstractClass(LayoutInterface::class); + $this->blockMock = $this->getMockBuilder(BlockInterface::class) + ->setMethods(['getWidgetValues']) + ->getMockForAbstractClass(); + $this->contextMock = $this->getMockBuilder(Context::class) + ->disableOriginalConstructor() + ->getMock(); + $this->contextMock->expects($this->once()) + ->method('getLayout') + ->willReturn($this->layoutMock); + } + + /** + * @return void + */ + public function testConstructWithEmptyData() + { + $this->registryMock->expects($this->once()) + ->method('registry') + ->with('current_widget_instance') + ->willReturn(null); + $this->layoutMock->expects($this->once()) + ->method('getBlock') + ->with('wysiwyg_widget.options') + ->willReturn(null); + $this->blockMock->expects($this->never()) + ->method('getWidgetValues'); + $this->ruleMock->expects($this->never()) + ->method('loadPost'); + + $this->objectManagerHelper->getObject( + Conditions::class, + [ + 'context' => $this->contextMock, + 'registry' => $this->registryMock, + 'rule' => $this->ruleMock, + ] + ); + } + + /** + * @return void + */ + public function testConstructWithWidgetInstance() + { + $widgetParams = ['conditions' => 'some conditions']; + + /** @var \Magento\Widget\Model\Widget\Instance|\PHPUnit_Framework_MockObject_MockObject $widgetMock */ + $widgetMock = $this->getMockBuilder(\Magento\Widget\Model\Widget\Instance::class) + ->disableOriginalConstructor() + ->getMock(); + $widgetMock->expects($this->once()) + ->method('getWidgetParameters') + ->willReturn($widgetParams); + + $this->layoutMock->expects($this->never()) + ->method('getBlock'); + $this->blockMock->expects($this->never()) + ->method('getWidgetValues'); + $this->registryMock->expects($this->once()) + ->method('registry') + ->with('current_widget_instance') + ->willReturn($widgetMock); + $this->ruleMock->expects($this->once()) + ->method('loadPost') + ->with($widgetParams) + ->willReturnSelf(); + + $this->objectManagerHelper->getObject( + Conditions::class, + [ + 'context' => $this->contextMock, + 'registry' => $this->registryMock, + 'rule' => $this->ruleMock, + ] + ); + } + + /** + * @return void + */ + public function testConstructWithParamsFromBlock() + { + $widgetParams = ['conditions' => 'some conditions']; + + $this->registryMock->expects($this->once()) + ->method('registry') + ->with('current_widget_instance') + ->willReturn(null); + $this->layoutMock->expects($this->once()) + ->method('getBlock') + ->with('wysiwyg_widget.options') + ->willReturn($this->blockMock); + $this->blockMock->expects($this->once()) + ->method('getWidgetValues') + ->willReturn($widgetParams); + $this->ruleMock->expects($this->once()) + ->method('loadPost') + ->with($widgetParams) + ->willReturnSelf(); + + $this->objectManagerHelper->getObject( + Conditions::class, + [ + 'context' => $this->contextMock, + 'registry' => $this->registryMock, + 'rule' => $this->ruleMock, + ] + ); + } +} diff --git a/app/code/Magento/CatalogWidget/composer.json b/app/code/Magento/CatalogWidget/composer.json index f9dc7163f4337..52b8740804dd4 100644 --- a/app/code/Magento/CatalogWidget/composer.json +++ b/app/code/Magento/CatalogWidget/composer.json @@ -14,7 +14,7 @@ "magento/framework": "100.1.*" }, "type": "magento2-module", - "version": "100.1.2", + "version": "100.1.3", "license": [ "OSL-3.0", "AFL-3.0" diff --git a/app/code/Magento/Checkout/Block/Cart.php b/app/code/Magento/Checkout/Block/Cart.php index 0b80561d4573e..04159a5a7566a 100644 --- a/app/code/Magento/Checkout/Block/Cart.php +++ b/app/code/Magento/Checkout/Block/Cart.php @@ -232,4 +232,14 @@ public function getItemsCount() { return $this->getQuote()->getItemsCount(); } + + /** + * Render pagination HTML. + * + * @return string + */ + public function getPagerHtml() + { + return $this->getChildHtml('pager'); + } } diff --git a/app/code/Magento/Checkout/Block/Cart/Grid.php b/app/code/Magento/Checkout/Block/Cart/Grid.php new file mode 100644 index 0000000000000..34f8e91edfd3f --- /dev/null +++ b/app/code/Magento/Checkout/Block/Cart/Grid.php @@ -0,0 +1,176 @@ + than number from + * Store->Configuration->Sales->Checkout->Shopping Cart->Number of items to display pager and + * custom_items weren't set to cart block. + */ +class Grid extends \Magento\Checkout\Block\Cart +{ + /** + * Config settings path to determine when pager on checkout/cart/index will be visible. + */ + const XPATH_CONFIG_NUMBER_ITEMS_TO_DISPLAY_PAGER = 'checkout/cart/number_items_to_display_pager'; + + /** + * Quote item resource collection. + * + * @var \Magento\Quote\Model\ResourceModel\Quote\Item\Collection + */ + private $itemsCollection; + + /** + * Quote item resource collection factory. + * + * @var \Magento\Quote\Model\ResourceModel\Quote\Item\CollectionFactory + * + */ + private $itemCollectionFactory; + + /** + * Join extension attributes processor. + * + * @var \Magento\Framework\Api\ExtensionAttribute\JoinProcessorInterface + */ + private $joinAttributeProcessor; + + /** + * Is pager displayed on shopping cart page. + * + * @var bool + */ + private $isPagerDisplayed; + + /** + * @param \Magento\Framework\View\Element\Template\Context $context + * @param \Magento\Customer\Model\Session $customerSession + * @param \Magento\Checkout\Model\Session $checkoutSession + * @param \Magento\Catalog\Model\ResourceModel\Url $catalogUrlBuilder + * @param \Magento\Checkout\Helper\Cart $cartHelper + * @param \Magento\Framework\App\Http\Context $httpContext + * @param \Magento\Quote\Model\ResourceModel\Quote\Item\CollectionFactory $itemCollectionFactory + * @param \Magento\Framework\Api\ExtensionAttribute\JoinProcessorInterface $joinProcessor + * @param array $data + */ + public function __construct( + \Magento\Framework\View\Element\Template\Context $context, + \Magento\Customer\Model\Session $customerSession, + \Magento\Checkout\Model\Session $checkoutSession, + \Magento\Catalog\Model\ResourceModel\Url $catalogUrlBuilder, + \Magento\Checkout\Helper\Cart $cartHelper, + \Magento\Framework\App\Http\Context $httpContext, + \Magento\Quote\Model\ResourceModel\Quote\Item\CollectionFactory $itemCollectionFactory, + \Magento\Framework\Api\ExtensionAttribute\JoinProcessorInterface $joinProcessor, + array $data = [] + ) { + $this->itemCollectionFactory = $itemCollectionFactory; + $this->joinAttributeProcessor = $joinProcessor; + parent::__construct( + $context, + $customerSession, + $checkoutSession, + $catalogUrlBuilder, + $cartHelper, + $httpContext, + $data + ); + } + + /** + * Prepare Quote Item Product URLs. + * When we don't have custom_items, items URLs will be collected for Collection limited by pager. + * Pager limit on checkout/cart/index is determined by configuration. + * Configuration path is Store->Configuration->Sales->Checkout->Shopping Cart->Number of items to display pager. + * + * @return void + */ + protected function _construct() + { + if (!$this->isPagerDisplayedOnPage()) { + parent::_construct(); + } + if ($this->hasData('template')) { + $this->setTemplate($this->getData('template')); + } + } + + /** + * {@inheritdoc} + */ + protected function _prepareLayout() + { + parent::_prepareLayout(); + if ($this->isPagerDisplayedOnPage()) { + $availableLimit = (int)$this->_scopeConfig->getValue( + self::XPATH_CONFIG_NUMBER_ITEMS_TO_DISPLAY_PAGER, + \Magento\Store\Model\ScopeInterface::SCOPE_STORE + ); + $itemsCollection = $this->getItemsForGrid(); + /** @var \Magento\Theme\Block\Html\Pager $pager */ + $pager = $this->getLayout()->createBlock(\Magento\Theme\Block\Html\Pager::class); + $pager->setAvailableLimit([$availableLimit => $availableLimit])->setCollection($itemsCollection); + $this->setChild('pager', $pager); + $itemsCollection->load(); + $this->prepareItemUrls(); + } + + return $this; + } + + /** + * Prepare quote items collection for pager. + * + * @return \Magento\Quote\Model\ResourceModel\Quote\Item\Collection + */ + public function getItemsForGrid() + { + if (!$this->itemsCollection) { + /** @var \Magento\Quote\Model\ResourceModel\Quote\Item\Collection $itemCollection */ + $itemCollection = $this->itemCollectionFactory->create(); + $itemCollection->setQuote($this->getQuote()); + $itemCollection->addFieldToFilter('parent_item_id', ['null' => true]); + $this->joinAttributeProcessor->process($itemCollection); + $this->itemsCollection = $itemCollection; + } + + return $this->itemsCollection; + } + + /** + * {@inheritdoc} + */ + public function getItems() + { + if (!$this->isPagerDisplayedOnPage()) { + return parent::getItems(); + } + + return $this->getItemsForGrid()->getItems(); + } + + /** + * Verify pager visibility. + * If cart block has custom_items and items qty in the shopping cart < limit from stores configuration. + * + * @return bool + */ + private function isPagerDisplayedOnPage() + { + if (!$this->isPagerDisplayed) { + $availableLimit = (int)$this->_scopeConfig->getValue( + self::XPATH_CONFIG_NUMBER_ITEMS_TO_DISPLAY_PAGER, + \Magento\Store\Model\ScopeInterface::SCOPE_STORE + ); + $this->isPagerDisplayed = !$this->getCustomItems() && $availableLimit < $this->getItemsCount(); + } + + return $this->isPagerDisplayed; + } +} diff --git a/app/code/Magento/Checkout/Block/Cart/Sidebar.php b/app/code/Magento/Checkout/Block/Cart/Sidebar.php index 03c705d8a4751..4d51c613c08e8 100644 --- a/app/code/Magento/Checkout/Block/Cart/Sidebar.php +++ b/app/code/Magento/Checkout/Block/Cart/Sidebar.php @@ -8,22 +8,22 @@ use Magento\Store\Model\ScopeInterface; /** - * Cart sidebar block + * Cart sidebar block. */ class Sidebar extends AbstractCart { /** - * Xml pah to checkout sidebar display value + * Xml pah to checkout sidebar display value. */ const XML_PATH_CHECKOUT_SIDEBAR_DISPLAY = 'checkout/sidebar/display'; /** - * Xml pah to checkout sidebar count value + * Xml pah to checkout sidebar count value. */ const XML_PATH_CHECKOUT_SIDEBAR_COUNT = 'checkout/sidebar/count'; /** - * @var \Magento\Catalog\Helper\Image + * @var \Magento\Catalog\Helper\Image. */ protected $imageHelper; @@ -56,7 +56,7 @@ public function __construct( } /** - * Returns minicart config + * Returns minicart config. * * @return array */ @@ -70,7 +70,8 @@ public function getConfig() 'imageTemplate' => $this->getImageHtmlTemplate(), 'baseUrl' => $this->getBaseUrl(), 'minicartMaxItemsVisible' => $this->getMiniCartMaxItemsCount(), - 'websiteId' => $this->_storeManager->getStore()->getWebsiteId() + 'websiteId' => $this->_storeManager->getStore()->getWebsiteId(), + 'maxItemsToDisplay' => $this->getMaxItemsToDisplay(), ]; } @@ -85,7 +86,7 @@ public function getImageHtmlTemplate() } /** - * Get one page checkout page url + * Get one page checkout page url. * * @codeCoverageIgnore * @return string @@ -96,7 +97,7 @@ public function getCheckoutUrl() } /** - * Get shopping cart page url + * Get shopping cart page url. * * @return string * @codeCoverageIgnore @@ -107,7 +108,7 @@ public function getShoppingCartUrl() } /** - * Get update cart item url + * Get update cart item url. * * @return string * @codeCoverageIgnore @@ -118,7 +119,7 @@ public function getUpdateItemQtyUrl() } /** - * Get remove cart item url + * Get remove cart item url. * * @return string * @codeCoverageIgnore @@ -129,7 +130,7 @@ public function getRemoveItemUrl() } /** - * Define if Mini Shopping Cart Pop-Up Menu enabled + * Define if Mini Shopping Cart Pop-Up Menu enabled. * * @return bool * @codeCoverageIgnore @@ -144,7 +145,7 @@ public function getIsNeedToDisplaySideBar() } /** - * Return totals from custom quote if needed + * Return totals from custom quote if needed. * * @return array */ @@ -158,7 +159,7 @@ public function getTotalsCache() } /** - * Retrieve subtotal block html + * Retrieve subtotal block html. * * @codeCoverageIgnore * @return string @@ -180,7 +181,7 @@ public function getBaseUrl() } /** - * Return max visible item count for minicart + * Return max visible item count for minicart. * * @return int */ @@ -188,4 +189,18 @@ private function getMiniCartMaxItemsCount() { return (int)$this->_scopeConfig->getValue('checkout/sidebar/count', ScopeInterface::SCOPE_STORE); } + + /** + * Returns maximum cart items to display. + * This setting regulates how many items will be displayed in minicart. + * + * @return int + */ + private function getMaxItemsToDisplay() + { + return (int)$this->_scopeConfig->getValue( + 'checkout/sidebar/max_items_display_count', + ScopeInterface::SCOPE_STORE + ); + } } diff --git a/app/code/Magento/Checkout/Model/Cart/CheckoutSummaryConfigProvider.php b/app/code/Magento/Checkout/Model/Cart/CheckoutSummaryConfigProvider.php new file mode 100644 index 0000000000000..475aef1cc4734 --- /dev/null +++ b/app/code/Magento/Checkout/Model/Cart/CheckoutSummaryConfigProvider.php @@ -0,0 +1,69 @@ +urlBuilder = $urlBuilder; + $this->scopeConfig = $scopeConfig; + } + + /** + * {@inheritdoc} + */ + public function getConfig() + { + return [ + 'maxCartItemsToDisplay' => $this->getMaxCartItemsToDisplay(), + 'cartUrl' => $this->urlBuilder->getUrl('checkout/cart') + ]; + } + + /** + * Returns maximum cart items to display. + * This setting regulates how many items will be displayed in checkout summary block. + * + * @return int + */ + private function getMaxCartItemsToDisplay() + { + return (int)$this->scopeConfig->getValue( + 'checkout/options/max_items_display_count', + ScopeInterface::SCOPE_STORE + ); + } +} diff --git a/app/code/Magento/Checkout/Test/Unit/Block/Cart/GridTest.php b/app/code/Magento/Checkout/Test/Unit/Block/Cart/GridTest.php new file mode 100644 index 0000000000000..f29827bf906fe --- /dev/null +++ b/app/code/Magento/Checkout/Test/Unit/Block/Cart/GridTest.php @@ -0,0 +1,247 @@ +itemCollectionFactoryMock = + $this->getMockBuilder(\Magento\Quote\Model\ResourceModel\Quote\Item\CollectionFactory::class) + ->disableOriginalConstructor() + ->setMethods(['create']) + ->getMock(); + $this->joinAttributeProcessorMock = + $this->getMockBuilder(\Magento\Framework\Api\ExtensionAttribute\JoinProcessorInterface::class) + ->getMockForAbstractClass(); + $this->scopeConfigMock = $this->getMockBuilder(\Magento\Framework\App\Config\ScopeConfigInterface::class) + ->getMockForAbstractClass(); + $this->checkoutSessionMock = $this->getMockBuilder(\Magento\Checkout\Model\Session::class) + ->disableOriginalConstructor() + ->getMock(); + $this->itemCollectionMock = $objectManagerHelper + ->getCollectionMock(\Magento\Quote\Model\ResourceModel\Quote\Item\Collection::class, []); + $this->quoteMock = $this->getMockBuilder(\Magento\Quote\Model\Quote::class) + ->disableOriginalConstructor() + ->getMock(); + $this->layoutMock = $this->getMockBuilder(\Magento\Framework\View\LayoutInterface::class) + ->getMockForAbstractClass(); + $this->pagerBlockMock = $this->getMockBuilder(\Magento\Theme\Block\Html\Pager::class) + ->disableOriginalConstructor() + ->getMock(); + $this->checkoutSessionMock->expects($this->any())->method('getQuote')->willReturn($this->quoteMock); + $this->quoteMock->expects($this->any())->method('getAllVisibleItems')->willReturn([]); + $this->scopeConfigMock->expects($this->at(0)) + ->method('getValue') + ->with( + \Magento\Checkout\Block\Cart\Grid::XPATH_CONFIG_NUMBER_ITEMS_TO_DISPLAY_PAGER, + \Magento\Store\Model\ScopeInterface::SCOPE_STORE, + null + )->willReturn(20); + $this->block = $objectManagerHelper->getObject( + \Magento\Checkout\Block\Cart\Grid::class, + [ + 'itemCollectionFactory' => $this->itemCollectionFactoryMock, + 'joinAttributeProcessor' => $this->joinAttributeProcessorMock, + 'scopeConfig' => $this->scopeConfigMock, + 'checkoutSession' => $this->checkoutSessionMock, + 'layout' => $this->layoutMock, + 'data' => ['template' => 'cart/form1.phtml'], + ] + ); + } + + /** + * @return void + */ + public function testGetTemplate() + { + $this->assertEquals('cart/form1.phtml', $this->block->getTemplate()); + } + + /** + * @covers \Magento\Checkout\Block\Cart\Grid::getItemsForGrid + * + * @return void + */ + public function testGetItemsForGrid() + { + $this->getMockItemsForGrid(); + $this->assertEquals($this->itemCollectionMock, $this->block->getItemsForGrid()); + } + + /** + * @cover \Magento\Checkout\Block\Cart\Grid::_prepareLayout + * + * @return void + */ + public function testSetLayout() + { + $itemsCount = 150; + $availableLimit = 20; + $this->getMockItemsForGrid(); + $this->quoteMock->expects($this->once())->method('getItemsCount')->willReturn($itemsCount); + $this->scopeConfigMock->expects($this->at(1)) + ->method('getValue') + ->with( + \Magento\Checkout\Block\Cart\Grid::XPATH_CONFIG_NUMBER_ITEMS_TO_DISPLAY_PAGER, + \Magento\Store\Model\ScopeInterface::SCOPE_STORE, + null + )->willReturn($availableLimit); + $this->layoutMock + ->expects($this->once()) + ->method('createBlock') + ->with(\Magento\Theme\Block\Html\Pager::class) + ->willReturn($this->pagerBlockMock); + $this->pagerBlockMock + ->expects($this->once()) + ->method('setAvailableLimit') + ->with([$availableLimit => $availableLimit]) + ->willReturnSelf(); + $this->pagerBlockMock + ->expects($this->once()) + ->method('setCollection') + ->with($this->itemCollectionMock) + ->willReturnSelf(); + $this->layoutMock->expects($this->once())->method('setChild')->with(null, null, 'pager'); + $this->itemCollectionMock->expects($this->once())->method('load')->willReturnSelf(); + $this->quoteMock->expects($this->never())->method('getAllVisibleItems'); + $this->itemCollectionMock->expects($this->once())->method('getItems')->willReturn([]); + $this->block->setLayout($this->layoutMock); + } + + /** + * @covers \Magento\Checkout\Block\Cart\Grid::getItems + * + * @return void + */ + public function testGetItems() + { + $this->getMockItemsForGrid(); + $this->quoteMock->expects($this->once())->method('getItemsCount')->willReturn(20); + $this->itemCollectionMock->expects($this->once())->method('getItems')->willReturn(['expected']); + + $this->assertEquals(['expected'], $this->block->getItems()); + } + + /** + * Prepare data for getItemsForGrid() method. + * + * @return void + */ + private function getMockItemsForGrid() + { + $this->itemCollectionFactoryMock + ->expects($this->once()) + ->method('create') + ->willReturn($this->itemCollectionMock); + $this->checkoutSessionMock->expects($this->any())->method('getQuote')->willReturn($this->quoteMock); + $this->itemCollectionMock->expects($this->once())->method('setQuote')->with($this->quoteMock)->willReturnSelf(); + $this->itemCollectionMock + ->expects($this->once()) + ->method('addFieldToFilter') + ->with('parent_item_id', ['null' => true]) + ->willReturnSelf(); + $this->joinAttributeProcessorMock->expects($this->once())->method('process')->with($this->itemCollectionMock); + } + + /** + * @cover \Magento\Checkout\Block\Cart::prepareItemUrls + * + * @return void + */ + public function testGetItemsIfCustomItemsExists() + { + $itemMock = $this->getMockBuilder(\Magento\Quote\Model\Quote\Item::class) + ->disableOriginalConstructor() + ->getMock(); + $storeManager = $this->getMockBuilder(\Magento\Store\Model\StoreManagerInterface::class) + ->getMockForAbstractClass(); + $storeMock = $this->getMockBuilder(\Magento\Store\Api\Data\StoreInterface::class) + ->getMockForAbstractClass(); + $storeManager->expects($this->once())->method('getStore')->willReturn($storeMock); + $objectManagerHelper = new ObjectManagerHelper($this); + + $this->block = $objectManagerHelper->getObject( + \Magento\Checkout\Block\Cart\Grid::class, + [ + 'itemCollectionFactory' => $this->itemCollectionFactoryMock, + 'joinAttributeProcessor' => $this->joinAttributeProcessorMock, + 'scopeConfig' => $this->scopeConfigMock, + 'checkoutSession' => $this->checkoutSessionMock, + 'layout' => $this->layoutMock, + 'data' => ['custom_items' => [$itemMock]], + 'storeManager' => $storeManager, + ] + ); + + $this->assertEquals([$itemMock], $this->block->getItems()); + } + + /** + * Test getItems() method when pager is not displayed on page. + * + * @return void + */ + public function testGetItemsWhenPagerNotVisible() + { + $this->assertEquals([], $this->block->getItems()); + } +} diff --git a/app/code/Magento/Checkout/Test/Unit/Block/Cart/SidebarTest.php b/app/code/Magento/Checkout/Test/Unit/Block/Cart/SidebarTest.php index d8b156ae159c4..af62bb00686c5 100644 --- a/app/code/Magento/Checkout/Test/Unit/Block/Cart/SidebarTest.php +++ b/app/code/Magento/Checkout/Test/Unit/Block/Cart/SidebarTest.php @@ -5,6 +5,11 @@ */ namespace Magento\Checkout\Test\Unit\Block\Cart; +/** + * Test for Magento\Checkout\Block\Cart\SideBar. + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ class SidebarTest extends \PHPUnit_Framework_TestCase { /** @var \Magento\Framework\TestFramework\Unit\Helper\ObjectManager */ @@ -54,14 +59,14 @@ protected function setUp() { $this->_objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); - $this->requestMock = $this->getMock('\Magento\Framework\App\RequestInterface'); - $this->layoutMock = $this->getMock('\Magento\Framework\View\Layout', [], [], '', false); - $this->checkoutSessionMock = $this->getMock('\Magento\Checkout\Model\Session', [], [], '', false); - $this->urlBuilderMock = $this->getMock('\Magento\Framework\UrlInterface', [], [], '', false); - $this->storeManagerMock = $this->getMock('\Magento\Store\Model\StoreManagerInterface', [], [], '', false); - $this->imageHelper = $this->getMock('Magento\Catalog\Helper\Image', [], [], '', false); + $this->requestMock = $this->getMock(\Magento\Framework\App\RequestInterface::class); + $this->layoutMock = $this->getMock(\Magento\Framework\View\Layout::class, [], [], '', false); + $this->checkoutSessionMock = $this->getMock(\Magento\Checkout\Model\Session::class, [], [], '', false); + $this->urlBuilderMock = $this->getMock(\Magento\Framework\UrlInterface::class, [], [], '', false); + $this->storeManagerMock = $this->getMock(\Magento\Store\Model\StoreManagerInterface::class, [], [], '', false); + $this->imageHelper = $this->getMock(\Magento\Catalog\Helper\Image::class, [], [], '', false); $this->scopeConfigMock = $this->getMock( - '\Magento\Framework\App\Config\ScopeConfigInterface', + \Magento\Framework\App\Config\ScopeConfigInterface::class, [], [], '', @@ -69,7 +74,7 @@ protected function setUp() ); $contextMock = $this->getMock( - '\Magento\Framework\View\Element\Template\Context', + \Magento\Framework\View\Element\Template\Context::class, ['getLayout', 'getUrlBuilder', 'getStoreManager', 'getScopeConfig', 'getRequest'], [], '', @@ -92,7 +97,7 @@ protected function setUp() ->will($this->returnValue($this->requestMock)); $this->model = $this->_objectManager->getObject( - 'Magento\Checkout\Block\Cart\Sidebar', + \Magento\Checkout\Block\Cart\Sidebar::class, [ 'context' => $contextMock, 'imageHelper' => $this->imageHelper, @@ -104,15 +109,13 @@ protected function setUp() public function testGetTotalsHtml() { $totalsHtml = "$134.36"; - $totalsBlockMock = $this->getMockBuilder('\Magento\Checkout\Block\Shipping\Price') + $totalsBlockMock = $this->getMockBuilder(\Magento\Checkout\Block\Shipping\Price::class) ->disableOriginalConstructor() ->setMethods(['toHtml']) ->getMock(); - $totalsBlockMock->expects($this->once()) ->method('toHtml') ->will($this->returnValue($totalsHtml)); - $this->layoutMock->expects($this->once()) ->method('getBlock') ->with('checkout.cart.minicart.totals') @@ -123,7 +126,7 @@ public function testGetTotalsHtml() public function testGetConfig() { - $storeMock = $this->getMock('\Magento\Store\Model\Store', [], [], '', false); + $storeMock = $this->getMock(\Magento\Store\Model\Store::class, [], [], '', false); $websiteId = 100; $shoppingCartUrl = 'http://url.com/cart'; @@ -141,7 +144,8 @@ public function testGetConfig() 'imageTemplate' => $imageTemplate, 'baseUrl' => $baseUrl, 'minicartMaxItemsVisible' => 3, - 'websiteId' => $websiteId + 'websiteId' => $websiteId, + 'maxItemsToDisplay' => 8, ]; $valueMap = [ @@ -154,21 +158,25 @@ public function testGetConfig() $this->requestMock->expects($this->any()) ->method('isSecure') ->willReturn(false); - $this->urlBuilderMock->expects($this->exactly(4)) ->method('getUrl') ->willReturnMap($valueMap); $this->storeManagerMock->expects($this->exactly(2))->method('getStore')->willReturn($storeMock); $storeMock->expects($this->once())->method('getBaseUrl')->willReturn($baseUrl); - $storeMock->expects($this->once())->method('getWebsiteId')->willReturn($websiteId); $this->imageHelper->expects($this->once())->method('getFrame')->willReturn(false); - - $this->scopeConfigMock->expects($this->once()) + $this->scopeConfigMock->expects($this->at(0)) ->method('getValue') ->with( \Magento\Checkout\Block\Cart\Sidebar::XML_PATH_CHECKOUT_SIDEBAR_COUNT, \Magento\Store\Model\ScopeInterface::SCOPE_STORE )->willReturn(3); + $this->scopeConfigMock->expects($this->at(1)) + ->method('getValue') + ->with( + 'checkout/sidebar/max_items_display_count', + \Magento\Store\Model\ScopeInterface::SCOPE_STORE + )->willReturn(8); + $storeMock->expects($this->once())->method('getWebsiteId')->willReturn($websiteId); $this->assertEquals($expectedResult, $this->model->getConfig()); } @@ -187,7 +195,7 @@ public function testGetIsNeedToDisplaySideBar() public function testGetTotalsCache() { - $quoteMock = $this->getMock('\Magento\Quote\Model\Quote', [], [], '', false); + $quoteMock = $this->getMock(\Magento\Quote\Model\Quote::class, [], [], '', false); $totalsMock = ['totals']; $this->checkoutSessionMock->expects($this->once())->method('getQuote')->willReturn($quoteMock); $quoteMock->expects($this->once())->method('getTotals')->willReturn($totalsMock); diff --git a/app/code/Magento/Checkout/Test/Unit/Model/Cart/CheckoutSummaryConfigProviderTest.php b/app/code/Magento/Checkout/Test/Unit/Model/Cart/CheckoutSummaryConfigProviderTest.php new file mode 100644 index 0000000000000..2b754ecfbcce8 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Unit/Model/Cart/CheckoutSummaryConfigProviderTest.php @@ -0,0 +1,63 @@ +urlBuilderMock = $this->getMockBuilder(UrlInterface::class)->getMock(); + $this->scopeConfigMock = $this->getMockBuilder(ScopeConfigInterface::class)->getMock(); + $this->model = new CheckoutSummaryConfigProvider($this->urlBuilderMock, $this->scopeConfigMock); + } + + /** + * @covers \Magento\Checkout\Model\Cart\CheckoutSummaryConfigProvider + * + * @return void + */ + public function testGetConfig() + { + $maxItemsCount = 10; + $cartUrl = 'url/to/cart/page'; + $expectedResult = [ + 'maxCartItemsToDisplay' => $maxItemsCount, + 'cartUrl' => $cartUrl + ]; + + $this->urlBuilderMock->expects($this->once())->method('getUrl')->with('checkout/cart')->willReturn($cartUrl); + $this->scopeConfigMock->expects($this->once()) + ->method('getValue') + ->with('checkout/options/max_items_display_count', ScopeInterface::SCOPE_STORE) + ->willReturn($maxItemsCount); + + $this->assertEquals($expectedResult, $this->model->getConfig()); + } +} diff --git a/app/code/Magento/Checkout/composer.json b/app/code/Magento/Checkout/composer.json index a0ebb3e72701a..1bc8b30c77d4e 100644 --- a/app/code/Magento/Checkout/composer.json +++ b/app/code/Magento/Checkout/composer.json @@ -27,7 +27,7 @@ "magento/module-cookie": "100.1.*" }, "type": "magento2-module", - "version": "100.1.6", + "version": "100.1.7", "license": [ "OSL-3.0", "AFL-3.0" diff --git a/app/code/Magento/Checkout/etc/adminhtml/system.xml b/app/code/Magento/Checkout/etc/adminhtml/system.xml index 0b0ec9e276f89..fec2ec4fd6f4c 100644 --- a/app/code/Magento/Checkout/etc/adminhtml/system.xml +++ b/app/code/Magento/Checkout/etc/adminhtml/system.xml @@ -25,6 +25,9 @@ \Magento\Checkout\Model\Adminhtml\BillingAddressDisplayOptions + + + @@ -35,6 +38,9 @@ Magento\Config\Model\Config\Source\Yesno + + + @@ -50,7 +56,10 @@ Magento\Config\Model\Config\Source\Yesno - + + + + diff --git a/app/code/Magento/Checkout/etc/config.xml b/app/code/Magento/Checkout/etc/config.xml index 4bac74fb0efdc..dcc63a978e0a3 100644 --- a/app/code/Magento/Checkout/etc/config.xml +++ b/app/code/Magento/Checkout/etc/config.xml @@ -11,10 +11,12 @@ 1 1 + 10 30 0 + 20 1 @@ -22,6 +24,7 @@ 1 5 + 10 general diff --git a/app/code/Magento/Checkout/etc/frontend/di.xml b/app/code/Magento/Checkout/etc/frontend/di.xml index 2ef7a2498fd6a..40385740a6130 100644 --- a/app/code/Magento/Checkout/etc/frontend/di.xml +++ b/app/code/Magento/Checkout/etc/frontend/di.xml @@ -47,6 +47,7 @@ Magento\Checkout\Model\DefaultConfigProvider + Magento\Checkout\Model\Cart\CheckoutSummaryConfigProvider diff --git a/app/code/Magento/Checkout/view/frontend/layout/checkout_cart_index.xml b/app/code/Magento/Checkout/view/frontend/layout/checkout_cart_index.xml index 0c619e89cc9f6..6e5a8fb0f1017 100644 --- a/app/code/Magento/Checkout/view/frontend/layout/checkout_cart_index.xml +++ b/app/code/Magento/Checkout/view/frontend/layout/checkout_cart_index.xml @@ -180,7 +180,7 @@ - + diff --git a/app/code/Magento/Checkout/view/frontend/templates/cart/form.phtml b/app/code/Magento/Checkout/view/frontend/templates/cart/form.phtml index 44a842f311f8c..9d170d4d73ae2 100644 --- a/app/code/Magento/Checkout/view/frontend/templates/cart/form.phtml +++ b/app/code/Magento/Checkout/view/frontend/templates/cart/form.phtml @@ -6,7 +6,7 @@ // @codingStandardsIgnoreFile -/** @var $block \Magento\Checkout\Block\Cart */ +/** @var $block \Magento\Checkout\Block\Cart\Grid */ ?> helper('Magento\Tax\Helper\Data')->displayCartBothPrices() ? 2 : 1); ?> getChildHtml('form_before') ?> @@ -17,6 +17,9 @@ class="form form-cart"> getBlockHtml('formkey'); ?>
+ getPagerHtml()): ?> +
getPagerHtml(); ?>
+ load rates from cache + if (!cartCache.isChanged('address', quote.shippingAddress()) && + !cartCache.isChanged('cartVersion', customerData.get('cart')()['data_id']) && + cartCache.get('rates') + ) { + shippingService.setShippingRates(cartCache.get('rates')); + + return; } - }); - quote.shippingMethod.subscribe(function () { - totalsDefaultProvider.estimateTotals(quote.shippingAddress()); - }); - quote.billingAddress.subscribe(function () { - var type = quote.billingAddress().getType(); - if (quote.isVirtual()) { - // update totals block when estimated address was set - totalsProcessors['default'] = totalsDefaultProvider; - totalsProcessors[type] - ? totalsProcessors[type].estimateTotals(quote.billingAddress()) - : totalsProcessors['default'].estimateTotals(quote.billingAddress()); - } - }); - } -); + + // update rates list when estimated address was set + rateProcessors['default'] = defaultProcessor; + rateProcessors[type] ? + rateProcessors[type].getRates(quote.shippingAddress()) : + rateProcessors['default'].getRates(quote.shippingAddress()); + + // save rates to cache after load + shippingService.getShippingRates().subscribe(function (rates) { + cartCache.set('rates', rates); + }); + } + }); + quote.shippingMethod.subscribe(function () { + totalsDefaultProvider.estimateTotals(quote.shippingAddress()); + }); + quote.billingAddress.subscribe(function () { + var type = quote.billingAddress().getType(); + + if (quote.isVirtual()) { + // update totals block when estimated address was set + totalsProcessors['default'] = totalsDefaultProvider; + totalsProcessors[type] ? + totalsProcessors[type].estimateTotals(quote.billingAddress()) : + totalsProcessors['default'].estimateTotals(quote.billingAddress()); + } + }); +}); diff --git a/app/code/Magento/Checkout/view/frontend/web/js/model/cart/totals-processor/default.js b/app/code/Magento/Checkout/view/frontend/web/js/model/cart/totals-processor/default.js index 2ed0f1fdc4773..1aff03ea32b49 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/model/cart/totals-processor/default.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/model/cart/totals-processor/default.js @@ -2,55 +2,103 @@ * Copyright © 2013-2017 Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -define( - [ - 'underscore', - 'Magento_Checkout/js/model/resource-url-manager', - 'Magento_Checkout/js/model/quote', - 'mage/storage', - 'Magento_Checkout/js/model/totals', - 'Magento_Checkout/js/model/error-processor' - ], - function (_, resourceUrlManager, quote, storage, totalsService, errorProcessor) { - 'use strict'; - - return { - requiredFields: ['countryId', 'region', 'regionId', 'postcode'], - - /** - * Get shipping rates for specified address. - */ - estimateTotals: function (address) { - var serviceUrl, payload; - totalsService.isLoading(true); - serviceUrl = resourceUrlManager.getUrlForTotalsEstimationForNewAddress(quote), - payload = { - addressInformation: { - address: _.pick(address, this.requiredFields) - } - }; - - if (quote.shippingMethod() && quote.shippingMethod()['method_code']) { - payload.addressInformation['shipping_method_code'] = quote.shippingMethod()['method_code']; - payload.addressInformation['shipping_carrier_code'] = quote.shippingMethod()['carrier_code']; - } - - storage.post( - serviceUrl, JSON.stringify(payload), false - ).done( - function (result) { - quote.setTotals(result); - } - ).fail( - function (response) { - errorProcessor.process(response); - } - ).always( - function () { - totalsService.isLoading(false); - } - ); + +define([ + 'underscore', + 'Magento_Checkout/js/model/resource-url-manager', + 'Magento_Checkout/js/model/quote', + 'mage/storage', + 'Magento_Checkout/js/model/totals', + 'Magento_Checkout/js/model/error-processor', + 'Magento_Checkout/js/model/cart/cache', + 'Magento_Customer/js/customer-data', +], function (_, resourceUrlManager, quote, storage, totalsService, errorProcessor, cartCache, customerData) { + 'use strict'; + + /** + * Load data from server. + * + * @param {Object} address + */ + var loadFromServer = function (address) { + var serviceUrl, + payload; + + // Start loader for totals block + totalsService.isLoading(true); + serviceUrl = resourceUrlManager.getUrlForTotalsEstimationForNewAddress(quote); + payload = { + addressInformation: { + address: _.pick(address, cartCache.requiredFields) } }; - } -); + + if (quote.shippingMethod() && quote.shippingMethod()['method_code']) { + payload.addressInformation['shipping_method_code'] = quote.shippingMethod()['method_code']; + payload.addressInformation['shipping_carrier_code'] = quote.shippingMethod()['carrier_code']; + } + + storage.post( + serviceUrl, JSON.stringify(payload), false + ).done(function (result) { + var data = { + totals: result, + address: address, + cartVersion: customerData.get('cart')()['data_id'], + shippingMethodCode: null, + shippingCarrierCode: null + }; + + if (quote.shippingMethod() && quote.shippingMethod()['method_code']) { + data.shippingMethodCode = quote.shippingMethod()['method_code']; + data.shippingCarrierCode = quote.shippingMethod()['carrier_code']; + } + + quote.setTotals(result); + cartCache.set('cart-data', data); + }).fail(function (response) { + errorProcessor.process(response); + }).always(function () { + // Stop loader for totals block + totalsService.isLoading(false); + }); + }; + + return { + /** + * Array of required address fields. + * + * @property {Array.String} requiredFields + * @deprecated Use cart cache. + */ + requiredFields: cartCache.requiredFields, + + /** + * Get shipping rates for specified address. + * + * @param {Object} address + */ + estimateTotals: function (address) { + var data = { + shippingMethodCode: null, + shippingCarrierCode: null + }; + + if (quote.shippingMethod() && quote.shippingMethod()['method_code']) { + data.shippingMethodCode = quote.shippingMethod()['method_code']; + data.shippingCarrierCode = quote.shippingMethod()['carrier_code']; + } + + if (!cartCache.isChanged('cartVersion', customerData.get('cart')()['data_id']) && + !cartCache.isChanged('shippingMethodCode', data.shippingMethodCode) && + !cartCache.isChanged('shippingCarrierCode', data.shippingCarrierCode) && + !cartCache.isChanged('address', address) && + cartCache.get('totals') + ) { + quote.setTotals(cartCache.get('totals')); + } else { + loadFromServer(address); + } + } + }; +}); 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 a8f4c8fb14491..21f97928e2a43 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 @@ -4,7 +4,11 @@ */ /*jshint browser:true jquery:true*/ /*global alert*/ -define([], function () { +define([ + 'underscore' +], function (_) { + 'use strict'; + /** * @param {Object} addressData * Returns new address object @@ -26,11 +30,11 @@ define([], function () { regionCode: (addressData.region) ? addressData.region.region_code : null, region: (addressData.region) ? addressData.region.region : null, customerId: addressData.customer_id || addressData.customerId, - street: addressData.street, + street: addressData.street ? _.compact(addressData.street) : addressData.street, 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/shipping-rates-validator.js b/app/code/Magento/Checkout/view/frontend/web/js/model/shipping-rates-validator.js index 1d6fea79fe8fc..363d85acd51b9 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 @@ -13,7 +13,8 @@ define( './postcode-validator', 'mage/translate', 'uiRegistry', - 'Magento_Checkout/js/model/quote' + 'Magento_Checkout/js/model/quote', + 'Magento_Checkout/js/model/shipping-address/form-popup-state' ], function ( $, @@ -24,7 +25,8 @@ define( postcodeValidator, $t, uiRegistry, - quote + quote, + formPopUpState ) { 'use strict'; @@ -127,12 +129,14 @@ define( }); } else { element.on('value', function () { - clearTimeout(self.validateAddressTimeout); - self.validateAddressTimeout = setTimeout(function () { - if (self.postcodeValidation()) { - self.validateFields(); - } - }, delay); + if (!formPopUpState.isVisible()) { + clearTimeout(self.validateAddressTimeout); + self.validateAddressTimeout = setTimeout(function () { + if (self.postcodeValidation()) { + self.validateFields(); + } + }, delay); + } }); observedElements.push(element); } @@ -177,7 +181,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/minicart.js b/app/code/Magento/Checkout/view/frontend/web/js/view/minicart.js index c885b51aa138e..8d54413581631 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/view/minicart.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/view/minicart.js @@ -2,13 +2,15 @@ * Copyright © 2013-2017 Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + define([ 'uiComponent', 'Magento_Customer/js/customer-data', 'jquery', 'ko', 'underscore', - 'sidebar' + 'sidebar', + 'mage/translate' ], function (Component, customerData, $, ko, _) { 'use strict'; @@ -76,6 +78,7 @@ define([ return Component.extend({ shoppingCartUrl: window.checkout.shoppingCartUrl, + maxItemsToDisplay: window.checkout.maxItemsToDisplay, cart: {}, /** @@ -144,6 +147,7 @@ define([ /** * Get cart param by name. + * * @param {String} name * @returns {*} */ @@ -155,6 +159,29 @@ define([ } return this.cart[name](); + }, + + /** + * Returns array of cart items, limited by 'maxItemsToDisplay' setting. + * + * @returns [] + */ + getCartItems: function () { + var items = this.getCartParam('items') || []; + items = items.slice(parseInt(-this.maxItemsToDisplay, 10)); + + return items; + }, + + /** + * Returns count of cart line items. + * + * @returns {Number} + */ + getCartLineItemsCount: function () { + var items = this.getCartParam('items') || []; + + return parseInt(items.length, 10); } }); }); 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 5c082e6a89df1..83217503fdcfa 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 @@ -113,7 +113,7 @@ define( if (shippingAddressData) { checkoutProvider.set( 'shippingAddress', - $.extend({}, checkoutProvider.get('shippingAddress'), shippingAddressData) + $.extend(true, {}, checkoutProvider.get('shippingAddress'), shippingAddressData) ); } checkoutProvider.on('shippingAddress', function (shippingAddressData) { @@ -150,20 +150,39 @@ define( { text: buttons.cancel.text ? buttons.cancel.text : $t('Cancel'), class: buttons.cancel.class ? buttons.cancel.class : 'action secondary action-hide-popup', - click: function () { - this.closeModal(); - } + + /** @inheritdoc */ + click: this.onClosePopUp.bind(this) } ]; this.popUpForm.options.closed = function () { self.isFormPopUpVisible(false); }; + + this.popUpForm.options.modalCloseBtnHandler = this.onClosePopUp.bind(this); + this.popUpForm.options.keyEventHandlers = { + escapeKey: this.onClosePopUp.bind(this) + }; + + /** @inheritdoc */ + this.popUpForm.options.opened = function () { + // Store temporary address for revert action in case when user click cancel action + self.temporaryAddress = $.extend(true, {}, checkoutData.getShippingAddressFromData()); + }; popUp = modal(this.popUpForm.options, $(this.popUpForm.element)); } return popUp; }, + /** + * Revert address and close modal. + */ + onClosePopUp: function () { + checkoutData.setShippingAddressFromData($.extend(true, {}, this.temporaryAddress)); + this.getPopUp().closeModal(); + }, + /** * Show address form popup */ @@ -190,7 +209,7 @@ define( newShippingAddress = createShippingAddress(addressData); selectShippingAddress(newShippingAddress); checkoutData.setSelectedShippingAddress(newShippingAddress.getKey()); - checkoutData.setNewCustomerShippingAddress(addressData); + checkoutData.setNewCustomerShippingAddress($.extend(true, {}, addressData)); this.getPopUp().closeModal(); this.isNewAddressAdded(true); } diff --git a/app/code/Magento/Checkout/view/frontend/web/js/view/summary/cart-items.js b/app/code/Magento/Checkout/view/frontend/web/js/view/summary/cart-items.js index 1c6a2ae4d1b2d..6bc822db563b5 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/view/summary/cart-items.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/view/summary/cart-items.js @@ -2,30 +2,80 @@ * Copyright © 2013-2017 Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -/*browser:true*/ -/*global define*/ -define( - [ - 'ko', - 'Magento_Checkout/js/model/totals', - 'uiComponent', - 'Magento_Checkout/js/model/step-navigator', - 'Magento_Checkout/js/model/quote' - ], - function (ko, totals, Component, stepNavigator, quote) { - 'use strict'; - return Component.extend({ - defaults: { - template: 'Magento_Checkout/summary/cart-items' - }, - totals: totals.totals(), - getItems: totals.getItems(), - getItemsQty: function() { - return parseFloat(this.totals.items_qty); - }, - isItemsBlockExpanded: function () { - return quote.isVirtual() || stepNavigator.isProcessed('shipping'); + +define([ + 'ko', + 'Magento_Checkout/js/model/totals', + 'uiComponent', + 'Magento_Checkout/js/model/step-navigator', + 'Magento_Checkout/js/model/quote' +], function (ko, totals, Component, stepNavigator, quote) { + 'use strict'; + + return Component.extend({ + defaults: { + template: 'Magento_Checkout/summary/cart-items' + }, + totals: totals.totals(), + items: ko.observable([]), + maxCartItemsToDisplay: window.checkoutConfig.maxCartItemsToDisplay, + cartUrl: window.checkoutConfig.cartUrl, + + /** + * @deprecated Please use observable property (this.items()) + */ + getItems: totals.getItems(), + + /** + * Returns cart items qty. + * + * @returns {Number} + */ + getItemsQty: function () { + return parseFloat(this.totals['items_qty']); + }, + + /** + * Returns count of cart line items. + * + * @returns {Number} + */ + getCartLineItemsCount: function () { + return parseInt(totals.getItems()().length, 10); + }, + + /** + * @inheritdoc + */ + initialize: function () { + this._super(); + // Set initial items to observable field + this.setItems(totals.getItems()()); + // Subscribe for items data changes and refresh items in view + totals.getItems().subscribe(function (items) { + this.setItems(items); + }.bind(this)); + }, + + /** + * Set items to observable field. + * + * @param {Object} items + */ + setItems: function (items) { + if (items && items.length > 0) { + items = items.slice(parseInt(-this.maxCartItemsToDisplay, 10)); } - }); - } -); + this.items(items); + }, + + /** + * Returns bool value for items block state (expanded or not). + * + * @returns {*|Boolean} + */ + isItemsBlockExpanded: function () { + return quote.isVirtual() || stepNavigator.isProcessed('shipping'); + } + }); +}); diff --git a/app/code/Magento/Checkout/view/frontend/web/template/minicart/content.html b/app/code/Magento/Checkout/view/frontend/web/template/minicart/content.html index 8cbc1fdbb29dc..487ea923a8a44 100644 --- a/app/code/Magento/Checkout/view/frontend/web/template/minicart/content.html +++ b/app/code/Magento/Checkout/view/frontend/web/template/minicart/content.html @@ -27,12 +27,19 @@
- - - + + - - + + + + + + + + + +
@@ -65,7 +72,7 @@
-
    +
      diff --git a/app/code/Magento/Checkout/view/frontend/web/template/summary/cart-items.html b/app/code/Magento/Checkout/view/frontend/web/template/summary/cart-items.html index 3ec8571a81647..60d88653924c0 100644 --- a/app/code/Magento/Checkout/view/frontend/web/template/summary/cart-items.html +++ b/app/code/Magento/Checkout/view/frontend/web/template/summary/cart-items.html @@ -4,22 +4,26 @@ * See COPYING.txt for license details. */ --> - -
      +
      - - - + + + + - - + + + + + +
        - +
      1. @@ -31,34 +35,13 @@
      -
      - - -
      -
      - - - - - - - - -
      -
      -
      -
        - -
      1. -
        - - - -
        -
      2. - -
      + +
      +
      +
      - diff --git a/app/code/Magento/Cms/Helper/Page.php b/app/code/Magento/Cms/Helper/Page.php index cebaeb5c89559..2ff8a4317e19a 100644 --- a/app/code/Magento/Cms/Helper/Page.php +++ b/app/code/Magento/Cms/Helper/Page.php @@ -156,7 +156,7 @@ public function prepareResultPage(Action $action, $pageId = null) $this->_eventManager->dispatch( 'cms_page_render', - ['page' => $this->_page, 'controller_action' => $action] + ['page' => $this->_page, 'controller_action' => $action, 'request' => $this->_getRequest()] ); if ($this->_page->getCustomLayoutUpdateXml() && $inRange) { diff --git a/app/code/Magento/Cms/Setup/UpgradeData.php b/app/code/Magento/Cms/Setup/UpgradeData.php index 7518960252e80..a960fe32b74d5 100644 --- a/app/code/Magento/Cms/Setup/UpgradeData.php +++ b/app/code/Magento/Cms/Setup/UpgradeData.php @@ -13,6 +13,9 @@ class UpgradeData implements UpgradeDataInterface { + /** + * @deprecated + */ const PRIVACY_COOKIE_PAGE_ID = 4; /** @@ -234,7 +237,10 @@ public function upgrade(ModuleDataSetupInterface $setup, ModuleContextInterface
EOD; - $privacyAndCookiePolicyPage = $this->createPage()->load(self::PRIVACY_COOKIE_PAGE_ID); + $privacyAndCookiePolicyPage = $this->createPage()->load( + 'privacy-policy-cookie-restriction-mode', + 'identifier' + ); $privacyAndCookiePolicyPageId = $privacyAndCookiePolicyPage->getId(); if ($privacyAndCookiePolicyPageId) { $privacyAndCookiePolicyPage->setContent($newPageContent); diff --git a/app/code/Magento/Cms/Test/Unit/Controller/RouterTest.php b/app/code/Magento/Cms/Test/Unit/Controller/RouterTest.php new file mode 100644 index 0000000000000..b3707c72dd98e --- /dev/null +++ b/app/code/Magento/Cms/Test/Unit/Controller/RouterTest.php @@ -0,0 +1,168 @@ +eventManagerMock = $this->getMockBuilder(\Magento\Framework\Event\ManagerInterface::class) + ->getMockForAbstractClass(); + + $this->pageFactoryMock = $this->getMockBuilder(\Magento\Cms\Model\PageFactory::class) + ->disableOriginalConstructor() + ->setMethods(['create']) + ->getMock(); + + $this->storeMock = $this->getMockBuilder(\Magento\Store\Api\Data\StoreInterface::class) + ->getMockForAbstractClass(); + + $this->storeManagerMock = $this->getMockBuilder(\Magento\Store\Model\StoreManagerInterface::class) + ->getMockForAbstractClass(); + $this->storeManagerMock->expects($this->any()) + ->method('getStore') + ->willReturn($this->storeMock); + + $this->actionFactoryMock = $this->getMockBuilder(\Magento\Framework\App\ActionFactory::class) + ->disableOriginalConstructor() + ->getMock(); + + $objectManagerHelper = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); + + $this->router = $objectManagerHelper->getObject( + \Magento\Cms\Controller\Router::class, + [ + 'eventManager' => $this->eventManagerMock, + 'pageFactory' => $this->pageFactoryMock, + 'storeManager' => $this->storeManagerMock, + 'actionFactory' => $this->actionFactoryMock, + ] + ); + } + + /** + * This test ensures that cms controller router match cms page before event params. + * + * @return void + */ + public function testMatchCmsControllerRouterMatchBeforeEventParams() + { + $identifier = '/test'; + $trimedIdentifier = 'test'; + $pageId = 1; + $storeId = 1; + + /** @var \Magento\Framework\App\RequestInterface|\PHPUnit_Framework_MockObject_MockObject $requestMock */ + $requestMock = $this->getMockBuilder(\Magento\Framework\App\RequestInterface::class) + ->setMethods([ + 'getPathInfo', + 'setModuleName', + 'setControllerName', + 'setActionName', + 'setParam', + 'setAlias', + ]) + ->getMockForAbstractClass(); + $requestMock->expects($this->once()) + ->method('getPathInfo') + ->willReturn($identifier); + $requestMock->expects($this->once()) + ->method('setModuleName') + ->with('cms') + ->willReturnSelf(); + $requestMock->expects($this->once()) + ->method('setControllerName') + ->with('page') + ->willReturnSelf(); + $requestMock->expects($this->once()) + ->method('setActionName') + ->with('view') + ->willReturnSelf(); + $requestMock->expects($this->once()) + ->method('setParam') + ->with('page_id', $pageId) + ->willReturnSelf(); + $requestMock->expects($this->once()) + ->method('setAlias') + ->with(\Magento\Framework\Url::REWRITE_REQUEST_PATH_ALIAS, $trimedIdentifier) + ->willReturnSelf(); + + $condition = new \Magento\Framework\DataObject(['identifier' => $trimedIdentifier, 'continue' => true]); + + $this->eventManagerMock->expects($this->once()) + ->method('dispatch') + ->with( + 'cms_controller_router_match_before', + [ + 'router' => $this->router, + 'condition' => $condition, + ] + ) + ->willReturnSelf(); + + $pageMock = $this->getMockBuilder(\Magento\Cms\Model\Page::class) + ->disableOriginalConstructor() + ->getMock(); + $pageMock->expects($this->once()) + ->method('checkIdentifier') + ->with($trimedIdentifier, $storeId) + ->willReturn($pageId); + + $this->pageFactoryMock->expects($this->once()) + ->method('create') + ->willReturn($pageMock); + + $this->storeMock->expects($this->once()) + ->method('getId') + ->willReturn($storeId); + + $actionMock = $this->getMockBuilder(\Magento\Framework\App\ActionInterface::class) + ->getMockForAbstractClass(); + + $this->actionFactoryMock->expects($this->once()) + ->method('create') + ->with(\Magento\Framework\App\Action\Forward::class) + ->willReturn($actionMock); + + $this->assertEquals($actionMock, $this->router->match($requestMock)); + } +} diff --git a/app/code/Magento/Cms/Test/Unit/Helper/PageTest.php b/app/code/Magento/Cms/Test/Unit/Helper/PageTest.php index ed379d9683086..77c80acacfad6 100644 --- a/app/code/Magento/Cms/Test/Unit/Helper/PageTest.php +++ b/app/code/Magento/Cms/Test/Unit/Helper/PageTest.php @@ -113,16 +113,21 @@ class PageTest extends \PHPUnit_Framework_TestCase */ protected $resultPageFactory; + /** + * @var \Magento\Framework\App\RequestInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $httpRequestMock; + protected function setUp() { - $this->actionMock = $this->getMockBuilder('Magento\Framework\App\Action\Action') + $this->actionMock = $this->getMockBuilder(\Magento\Framework\App\Action\Action::class) ->disableOriginalConstructor() ->getMock(); - $this->pageFactoryMock = $this->getMockBuilder('Magento\Cms\Model\PageFactory') + $this->pageFactoryMock = $this->getMockBuilder(\Magento\Cms\Model\PageFactory::class) ->disableOriginalConstructor() ->setMethods(['create']) ->getMock(); - $this->pageMock = $this->getMockBuilder('Magento\Cms\Model\Page') + $this->pageMock = $this->getMockBuilder(\Magento\Cms\Model\Page::class) ->disableOriginalConstructor() ->setMethods( [ @@ -141,57 +146,60 @@ protected function setUp() ] ) ->getMock(); - $this->storeManagerMock = $this->getMockBuilder('Magento\Store\Model\StoreManagerInterface') + $this->storeManagerMock = $this->getMockBuilder(\Magento\Store\Model\StoreManagerInterface::class) ->getMockForAbstractClass(); - $this->localeDateMock = $this->getMockBuilder('Magento\Framework\Stdlib\DateTime\TimezoneInterface') + $this->localeDateMock = $this->getMockBuilder(\Magento\Framework\Stdlib\DateTime\TimezoneInterface::class) ->getMockForAbstractClass(); - $this->designMock = $this->getMockBuilder('Magento\Framework\View\DesignInterface') + $this->designMock = $this->getMockBuilder(\Magento\Framework\View\DesignInterface::class) ->getMockForAbstractClass(); - $this->pageConfigMock = $this->getMockBuilder('Magento\Framework\View\Page\Config') + $this->pageConfigMock = $this->getMockBuilder(\Magento\Framework\View\Page\Config::class) ->disableOriginalConstructor() ->getMock(); - $this->escaperMock = $this->getMockBuilder('Magento\Framework\Escaper') + $this->escaperMock = $this->getMockBuilder(\Magento\Framework\Escaper::class) ->disableOriginalConstructor() ->getMock(); - $this->eventManagerMock = $this->getMockBuilder('Magento\Framework\Event\ManagerInterface') + $this->eventManagerMock = $this->getMockBuilder(\Magento\Framework\Event\ManagerInterface::class) + ->getMockForAbstractClass(); + $this->urlBuilderMock = $this->getMockBuilder(\Magento\Framework\UrlInterface::class) ->getMockForAbstractClass(); - $this->urlBuilderMock = $this->getMockBuilder('Magento\Framework\UrlInterface') + $this->httpRequestMock = $this->getMockBuilder(\Magento\Framework\App\RequestInterface::class) ->getMockForAbstractClass(); - $this->storeMock = $this->getMockBuilder('Magento\Store\Model\Store') + $this->storeMock = $this->getMockBuilder(\Magento\Store\Model\Store::class) ->disableOriginalConstructor() ->getMock(); - $this->resultPageMock = $this->getMockBuilder('Magento\Framework\View\Result\Page') + $this->resultPageMock = $this->getMockBuilder(\Magento\Framework\View\Result\Page::class) ->disableOriginalConstructor() ->getMock(); - $this->layoutMock = $this->getMockBuilder('Magento\Framework\View\LayoutInterface') + $this->layoutMock = $this->getMockBuilder(\Magento\Framework\View\LayoutInterface::class) ->getMockForAbstractClass(); - $this->layoutProcessorMock = $this->getMockBuilder('Magento\Framework\View\Layout\ProcessorInterface') + $this->layoutProcessorMock = $this->getMockBuilder(\Magento\Framework\View\Layout\ProcessorInterface::class) ->getMockForAbstractClass(); - $this->blockMock = $this->getMockBuilder('Magento\Framework\View\Element\AbstractBlock') + $this->blockMock = $this->getMockBuilder(\Magento\Framework\View\Element\AbstractBlock::class) ->disableOriginalConstructor() ->getMockForAbstractClass(); - $this->messagesBlockMock = $this->getMockBuilder('Magento\Framework\View\Element\Messages') + $this->messagesBlockMock = $this->getMockBuilder(\Magento\Framework\View\Element\Messages::class) ->disableOriginalConstructor() ->getMock(); - $this->messageManagerMock = $this->getMockBuilder('Magento\Framework\Message\ManagerInterface') + $this->messageManagerMock = $this->getMockBuilder(\Magento\Framework\Message\ManagerInterface::class) ->getMockForAbstractClass(); - $this->messageCollectionMock = $this->getMockBuilder('Magento\Framework\Message\Collection') + $this->messageCollectionMock = $this->getMockBuilder(\Magento\Framework\Message\Collection::class) ->disableOriginalConstructor() ->getMock(); $objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); $context = $objectManager->getObject( - 'Magento\Framework\App\Helper\Context', + \Magento\Framework\App\Helper\Context::class, [ 'eventManager' => $this->eventManagerMock, - 'urlBuilder' => $this->urlBuilderMock + 'urlBuilder' => $this->urlBuilderMock, + 'httpRequest' => $this->httpRequestMock, ] ); - $this->resultPageFactory = $this->getMock('Magento\Framework\View\Result\PageFactory', [], [], '', false); + $this->resultPageFactory = $this->getMock(\Magento\Framework\View\Result\PageFactory::class, [], [], '', false); $this->object = $objectManager->getObject( - 'Magento\Cms\Helper\Page', + \Magento\Cms\Helper\Page::class, [ 'context' => $context, 'pageFactory' => $this->pageFactoryMock, @@ -318,7 +326,8 @@ public function testPrepareResultPage( 'cms_page_render', [ 'page' => $this->pageMock, - 'controller_action' => $this->actionMock + 'controller_action' => $this->actionMock, + 'request' => $this->httpRequestMock, ] ); $this->pageMock->expects($this->any()) diff --git a/app/code/Magento/Cms/composer.json b/app/code/Magento/Cms/composer.json index da8d41426fca0..98545f366c186 100644 --- a/app/code/Magento/Cms/composer.json +++ b/app/code/Magento/Cms/composer.json @@ -18,7 +18,7 @@ "magento/module-cms-sample-data": "Sample Data version:100.1.*" }, "type": "magento2-module", - "version": "101.0.5", + "version": "101.0.6", "license": [ "OSL-3.0", "AFL-3.0" diff --git a/app/code/Magento/CmsUrlRewrite/etc/adminhtml/di.xml b/app/code/Magento/CmsUrlRewrite/etc/di.xml similarity index 100% rename from app/code/Magento/CmsUrlRewrite/etc/adminhtml/di.xml rename to app/code/Magento/CmsUrlRewrite/etc/di.xml diff --git a/app/code/Magento/Config/App/Config/Type/System.php b/app/code/Magento/Config/App/Config/Type/System.php index 7b20a2b68399e..6fe615f05937e 100644 --- a/app/code/Magento/Config/App/Config/Type/System.php +++ b/app/code/Magento/Config/App/Config/Type/System.php @@ -10,71 +10,106 @@ use Magento\Framework\App\Config\Spi\PostProcessorInterface; use Magento\Framework\App\Config\Spi\PreProcessorInterface; use Magento\Framework\Cache\FrontendInterface; -use Magento\Framework\DataObject; +use Magento\Framework\App\ObjectManager; +use Magento\Config\App\Config\Type\System\Reader; use Magento\Store\Model\Config\Processor\Fallback; /** - * Class process source, cache them and retrieve value by path + * System configuration type. */ class System implements ConfigTypeInterface { + /** + * Cache tag. + */ const CACHE_TAG = 'config_scopes'; + /** + * Config type. + */ const CONFIG_TYPE = 'system'; /** + * Config source. + * * @var ConfigSourceInterface */ private $source; /** - * @var DataObject[] + * Object data. + * + * @var array */ - private $data; + private $data = []; /** + * Postprocessor. + * * @var PostProcessorInterface */ private $postProcessor; /** + * Preprocessor. + * * @var PreProcessorInterface */ private $preProcessor; /** + * Cache. + * * @var FrontendInterface */ private $cache; /** + * Caching nested level. + * * @var int */ private $cachingNestedLevel; /** + * Fallback. + * * @var Fallback */ private $fallback; /** - * Key name for flag which displays whether configuration is cached or not. - * - * Once configuration is cached additional flag pushed to cache storage - * to be able check cache existence without data load. + * The type of config. * * @var string */ - private $cacheExistenceKey = self::CONFIG_TYPE . '_CACHE_EXISTS'; + private $configType; + + /** + * Reader. + * + * @var Reader + */ + private $reader; + + /** + * List of scopes that were retrieved from configuration storage. + * + * Is used to make sure that we don't try to load non-existing configuration scopes. + * + * @var array + */ + private $availableDataScopes = null; /** - * System constructor. * @param ConfigSourceInterface $source * @param PostProcessorInterface $postProcessor * @param Fallback $fallback * @param FrontendInterface $cache * @param PreProcessorInterface $preProcessor * @param int $cachingNestedLevel + * @param string $configType + * @param Reader $reader */ public function __construct( ConfigSourceInterface $source, @@ -82,7 +117,9 @@ public function __construct( Fallback $fallback, FrontendInterface $cache, PreProcessorInterface $preProcessor, - $cachingNestedLevel = 1 + $cachingNestedLevel = 1, + $configType = self::CONFIG_TYPE, + Reader $reader = null ) { $this->source = $source; $this->postProcessor = $postProcessor; @@ -90,142 +127,194 @@ public function __construct( $this->cache = $cache; $this->cachingNestedLevel = $cachingNestedLevel; $this->fallback = $fallback; + $this->configType = $configType; + $this->reader = $reader ?: ObjectManager::getInstance()->get(Reader::class); } /** + * System configuration is separated by scopes (default, websites, stores). Configuration of a scope is inherited + * from its parent scope (store inherits website). + * + * Because there can be many scopes on single instance of application, the configuration data can be pretty large, + * so it does not make sense to load all of it on every application request. That is why we cache configuration + * data by scope and only load configuration scope when a value from that scope is requested. + * + * Possible path values: + * '' - will return whole system configuration (default scope + all other scopes) + * 'default' - will return all default scope configuration values + * '{scopeType}' - will return data from all scopes of a specified {scopeType} (websites, stores) + * '{scopeType}/{scopeCode}' - will return data for all values of the scope specified by {scopeCode} and scope type + * '{scopeType}/{scopeCode}/some/config/variable' - will return value of the config variable in the specified scope + * * @inheritdoc */ public function get($path = '') { - if ($path === null) { - $path = ''; - } + if ($path === '') { + $this->data = array_replace_recursive($this->loadAllData(), $this->data); - if ($this->isConfigRead($path)) { - return $this->data->getData($path); + return $this->data; } + $pathParts = explode('/', $path); + if (count($pathParts) === 1 && $pathParts[0] !== 'default') { + if (!isset($this->data[$pathParts[0]])) { + $data = $this->reader->read(); + $this->data = array_replace_recursive($data, $this->data); + } - if (!empty($path) && $this->isCacheExists()) { - return $this->readFromCache($path); + return $this->data[$pathParts[0]]; } + $scopeType = array_shift($pathParts); + if ($scopeType === 'default') { + if (!isset($this->data[$scopeType])) { + $this->data = array_replace_recursive($this->loadDefaultScopeData($scopeType), $this->data); + } - $config = $this->loadConfig(); - $this->cacheConfig($config); - $this->data = new DataObject($config); - return $this->data->getData($path); - } + return $this->getDataByPathParts($this->data[$scopeType], $pathParts); + } + $scopeId = array_shift($pathParts); + if (!isset($this->data[$scopeType][$scopeId])) { + $this->data = array_replace_recursive($this->loadScopeData($scopeType, $scopeId), $this->data); + } - /** - * Check whether configuration is cached - * @return bool - */ - private function isCacheExists() - { - return $this->cache->load($this->cacheExistenceKey) !== false; + return isset($this->data[$scopeType][$scopeId]) + ? $this->getDataByPathParts($this->data[$scopeType][$scopeId], $pathParts) + : null; } /** - * Explode path by '/'(forward slash) separator + * Load configuration data for all scopes. * - * @param string $path * @return array */ - private function getPathParts($path) + private function loadAllData() { - $pathParts = []; - if (strpos($path, '/') !== false) { - $pathParts = explode('/', $path); + $cachedData = $this->cache->load($this->configType); + if ($cachedData === false) { + $data = $this->reader->read(); + } else { + $data = unserialize($cachedData); } - return $pathParts; + + return $data; } /** - * Check whether requested configuration data is read to memory + * Load configuration data for default scope. * - * @param string $path - * @return bool + * @param string $scopeType + * @return array */ - private function isConfigRead($path) + private function loadDefaultScopeData($scopeType) { - $pathParts = $this->getPathParts($path); - return !empty($pathParts) && isset($this->data[$pathParts[0]][$pathParts[1]]); + $cachedData = $this->cache->load($this->configType . '_' . $scopeType); + if ($cachedData === false) { + $data = $this->reader->read(); + $this->cacheData($data); + } else { + $data = [$scopeType => unserialize($cachedData)]; + } + + return $data; } /** - * Load configuration from all the sources + * Load configuration data for a specified scope. * + * @param string $scopeType + * @param string $scopeId * @return array */ - private function loadConfig() + private function loadScopeData($scopeType, $scopeId) { - $data = $this->preProcessor->process($this->source->get()); - $this->data = new DataObject($data); - $data = $this->fallback->process($data); - $this->data = new DataObject($data); + $cachedData = $this->cache->load($this->configType . '_' . $scopeType . '_' . $scopeId); + if ($cachedData === false) { + if ($this->availableDataScopes === null) { + $cachedScopeData = $this->cache->load($this->configType . '_scopes'); + if ($cachedScopeData !== false) { + $this->availableDataScopes = unserialize($cachedScopeData); + } + } + if (is_array($this->availableDataScopes) && !isset($this->availableDataScopes[$scopeType][$scopeId])) { + return [$scopeType => [$scopeId => []]]; + } + $data = $this->reader->read(); + $this->cacheData($data); + } else { + $data = [$scopeType => [$scopeId => unserialize($cachedData)]]; + } - return $this->postProcessor->process($data); + return $data; } /** - * - * Load configuration and caching it by parts. - * - * To be cached configuration is loaded first. - * Then it is cached by parts to minimize memory usage on load. - * Additional flag cached as well to give possibility check cache existence without data load. + * Cache configuration data. + * Caches data per scope to avoid reading data for all scopes on every request. * * @param array $data * @return void */ - private function cacheConfig($data) + private function cacheData(array $data) { - foreach ($data as $scope => $scopeData) { - foreach ($scopeData as $key => $config) { + $this->cache->save( + serialize($data), + $this->configType, + [self::CACHE_TAG] + ); + $this->cache->save( + serialize($data['default']), + $this->configType . '_default', + [self::CACHE_TAG] + ); + $scopes = []; + foreach (['websites', 'stores'] as $curScopeType) { + foreach ($data[$curScopeType] as $curScopeId => $curScopeData) { + $scopes[$curScopeType][$curScopeId] = 1; $this->cache->save( - serialize($config), - self::CONFIG_TYPE . '_' . $scope . $key, + serialize($curScopeData), + $this->configType . '_' . $curScopeType . '_' . $curScopeId, [self::CACHE_TAG] ); } } - $this->cache->save('1', $this->cacheExistenceKey, [self::CACHE_TAG]); + $this->cache->save( + serialize($scopes), + $this->configType . "_scopes", + [self::CACHE_TAG] + ); } /** - * Read cached configuration + * Walk nested hash map by keys from $pathParts. * - * @param string $path + * @param array $data to walk in + * @param array $pathParts keys path * @return mixed */ - private function readFromCache($path) + private function getDataByPathParts($data, $pathParts) { - if ($this->data === null) { - $this->data = new DataObject(); - } - - $result = null; - $pathParts = $this->getPathParts($path); - if (!empty($pathParts)) { - $result = $this->cache->load(self::CONFIG_TYPE . '_' . $pathParts[0] . $pathParts[1]); - } - - if ($result !== false && $result !== null) { - $readData = $this->data->getData(); - $readData[$pathParts[0]][$pathParts[1]] = unserialize($result); - $this->data = new DataObject($readData); + foreach ($pathParts as $key) { + if ((array)$data === $data && isset($data[$key])) { + $data = $data[$key]; + } elseif ($data instanceof \Magento\Framework\DataObject) { + $data = $data->getDataByKey($key); + } else { + return null; + } } - return $this->data->getData($path); + return $data; } /** - * Clean cache and global variables cache + * Clean cache and global variables cache. * * @return void */ public function clean() { - $this->data = null; + $this->data = []; $this->cache->clean(\Zend_Cache::CLEANING_MODE_MATCHING_TAG, [self::CACHE_TAG]); + $this->availableDataScopes = null; } } diff --git a/app/code/Magento/Config/App/Config/Type/System/Reader.php b/app/code/Magento/Config/App/Config/Type/System/Reader.php new file mode 100644 index 0000000000000..1dcd7ee55ffcc --- /dev/null +++ b/app/code/Magento/Config/App/Config/Type/System/Reader.php @@ -0,0 +1,78 @@ +source = $source; + $this->fallback = $fallback; + $this->preProcessor = $preProcessor; + $this->postProcessor = $postProcessor; + } + + /** + * Retrieve and process system configuration data. + * + * Processing includes configuration fallback (default, website, store) and placeholder replacement. + * + * @return array + */ + public function read() + { + return $this->postProcessor->process( + $this->fallback->process( + $this->preProcessor->process( + $this->source->get() + ) + ) + ); + } +} diff --git a/app/code/Magento/Config/Test/Unit/App/Config/Type/System/ReaderTest.php b/app/code/Magento/Config/Test/Unit/App/Config/Type/System/ReaderTest.php new file mode 100644 index 0000000000000..dfef4bde7ba21 --- /dev/null +++ b/app/code/Magento/Config/Test/Unit/App/Config/Type/System/ReaderTest.php @@ -0,0 +1,93 @@ +source = $this->getMockBuilder(ConfigSourceInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + $this->fallback = $this->getMockBuilder(Fallback::class) + ->disableOriginalConstructor() + ->getMock(); + $this->preProcessor = $this->getMockBuilder(PreProcessorInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + $this->postProcessor = $this->getMockBuilder(PostProcessorInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + + $this->model = $helper->getObject( + Reader::class, + [ + 'source' => $this->source, + 'fallback' => $this->fallback, + 'preProcessor' => $this->preProcessor, + 'postProcessor' => $this->postProcessor, + ] + ); + } + + public function testGetCachedWithLoadDefaultScopeData() + { + $data = [ + 'default' => [], + 'websites' => [], + 'stores' => [], + ]; + $this->source->expects($this->once()) + ->method('get') + ->willReturn($data); + $this->preProcessor->expects($this->once()) + ->method('process') + ->with($data) + ->willReturn($data); + $this->fallback->expects($this->once()) + ->method('process') + ->with($data) + ->willReturn($data); + $this->postProcessor->expects($this->once()) + ->method('process') + ->with($data) + ->willReturn($data); + $this->assertEquals($data, $this->model->read()); + } +} diff --git a/app/code/Magento/Config/Test/Unit/App/Config/Type/SystemTest.php b/app/code/Magento/Config/Test/Unit/App/Config/Type/SystemTest.php index 9f91d48b551d1..b519976e085fd 100644 --- a/app/code/Magento/Config/Test/Unit/App/Config/Type/SystemTest.php +++ b/app/code/Magento/Config/Test/Unit/App/Config/Type/SystemTest.php @@ -3,6 +3,7 @@ * Copyright © 2013-2017 Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Config\Test\Unit\App\Config\Type; use Magento\Config\App\Config\Type\System; @@ -10,8 +11,8 @@ use Magento\Framework\App\Config\Spi\PostProcessorInterface; use Magento\Framework\App\Config\Spi\PreProcessorInterface; use Magento\Framework\Cache\FrontendInterface; -use Magento\Framework\DataObject; use Magento\Store\Model\Config\Processor\Fallback; +use Magento\Config\App\Config\Type\System\Reader; /** * Test how Class process source, cache them and retrieve value by path @@ -49,6 +50,11 @@ class SystemTest extends \PHPUnit_Framework_TestCase */ private $configType; + /** + * @var Reader|\PHPUnit_Framework_MockObject_MockObject + */ + private $reader; + public function setUp() { $this->source = $this->getMockBuilder(ConfigSourceInterface::class) @@ -62,87 +68,85 @@ public function setUp() ->getMockForAbstractClass(); $this->preProcessor = $this->getMockBuilder(PreProcessorInterface::class) ->getMockForAbstractClass(); + $this->reader = $this->getMockBuilder(Reader::class) + ->disableOriginalConstructor() + ->getMock(); $this->configType = new System( $this->source, $this->postProcessor, $this->fallback, $this->cache, - $this->preProcessor + $this->preProcessor, + 1, + 'system', + $this->reader ); } - public function testGetCached() + public function testGetCachedWithLoadDefaultScopeData() { $path = 'default/dev/unsecure/url'; $url = 'http://magento.test/'; $data = [ - 'unsecure' => [ - 'url' => $url - ] + 'dev' => [ + 'unsecure' => [ + 'url' => $url, + ], + ], ]; - $this->cache->expects($this->any()) + $this->cache->expects($this->once()) ->method('load') - ->withConsecutive(['system_CACHE_EXISTS'], ['system_defaultdev']) - ->willReturnOnConsecutiveCalls('1', serialize($data)); - + ->willReturn(serialize($data)); $this->assertEquals($url, $this->configType->get($path)); } - public function testGetNotCached() + public function testGetCachedWithLoadAllData() { - $path = 'default/dev/unsecure/url'; $url = 'http://magento.test/'; $data = [ - 'default' => [ - 'dev' => [ - 'unsecure' => [ - 'url' => $url - ] - ] + 'dev' => [ + 'unsecure' => [ + 'url' => $url, + ], ] ]; - $dataToCache = [ - 'unsecure' => [ - 'url' => $url - ] + + $this->cache->expects($this->once()) + ->method('load') + ->willReturn(serialize($data)); + $this->assertEquals($data, $this->configType->get('')); + } + + public function testGetNotCached() + { + $path = 'stores/default/dev/unsecure/url'; + $url = 'http://magento.test/'; + + $data = [ + 'default' => [], + 'websites' => [], + 'stores' => [ + 'default' => [ + 'dev' => [ + 'unsecure' => [ + 'url' => $url, + ], + ], + ], + ], ]; $this->cache->expects($this->any()) ->method('load') - ->withConsecutive(['system_CACHE_EXISTS'], ['system_defaultdev']) ->willReturnOnConsecutiveCalls(false, false); - $this->source->expects($this->once()) - ->method('get') - ->willReturn($data); - $this->fallback->expects($this->once()) - ->method('process') - ->with($data) - ->willReturnArgument(0); - $this->preProcessor->expects($this->once()) - ->method('process') - ->with($data) - ->willReturnArgument(0); - $this->postProcessor->expects($this->once()) - ->method('process') - ->with($data) - ->willReturnArgument(0); - - $this->cache->expects($this->any()) + $this->cache->expects($this->atLeastOnce()) ->method('save') - ->withConsecutive( - [ - serialize($dataToCache), - 'system_defaultdev', - [System::CACHE_TAG] - ], - [ - '1', - 'system_CACHE_EXISTS', - [System::CACHE_TAG] - ] - ); + ->willReturnSelf(); + $this->reader->expects($this->once()) + ->method('read') + ->willReturn($data); $this->assertEquals($url, $this->configType->get($path)); } diff --git a/app/code/Magento/Config/composer.json b/app/code/Magento/Config/composer.json index 27151d1df53fa..805d67b0e40d1 100644 --- a/app/code/Magento/Config/composer.json +++ b/app/code/Magento/Config/composer.json @@ -15,7 +15,7 @@ "magento/module-deploy": "100.1.*" }, "type": "magento2-module", - "version": "100.1.4", + "version": "100.1.5", "license": [ "OSL-3.0", "AFL-3.0" diff --git a/app/code/Magento/Config/etc/di.xml b/app/code/Magento/Config/etc/di.xml index 9cb3ec4688f44..32fbfb5b331b9 100644 --- a/app/code/Magento/Config/etc/di.xml +++ b/app/code/Magento/Config/etc/di.xml @@ -82,6 +82,14 @@ systemConfigPostProcessorCompositeProxy Magento\Framework\App\Cache\Type\Config systemConfigPreProcessorComposite + Magento\Config\App\Config\Type\System\Reader\Proxy + + + + + systemConfigSourceAggregated + systemConfigPostProcessorComposite + systemConfigPreProcessorComposite diff --git a/app/code/Magento/ConfigurableImportExport/Model/Export/RowCustomizer.php b/app/code/Magento/ConfigurableImportExport/Model/Export/RowCustomizer.php index 09c2400f437f4..ec5a3f71716b2 100644 --- a/app/code/Magento/ConfigurableImportExport/Model/Export/RowCustomizer.php +++ b/app/code/Magento/ConfigurableImportExport/Model/Export/RowCustomizer.php @@ -6,7 +6,9 @@ namespace Magento\ConfigurableImportExport\Model\Export; use Magento\CatalogImportExport\Model\Export\RowCustomizerInterface; -use \Magento\CatalogImportExport\Model\Import\Product as ImportProduct; +use Magento\Catalog\Model\ResourceModel\Product\Collection as ProductCollection; +use Magento\ConfigurableProduct\Model\Product\Type\Configurable as ConfigurableProductType; +use Magento\CatalogImportExport\Model\Import\Product as ImportProduct; use Magento\ImportExport\Model\Import; class RowCustomizer implements RowCustomizerInterface @@ -19,29 +21,23 @@ class RowCustomizer implements RowCustomizerInterface /** * Prepare configurable data for export * - * @param \Magento\Catalog\Model\ResourceModel\Product\Collection $collection + * @param ProductCollection $collection * @param int[] $productIds * @return void */ public function prepareData($collection, $productIds) { $productCollection = clone $collection; - $productCollection->addAttributeToFilter( - 'entity_id', - ['in' => $productIds] - )->addAttributeToFilter( - 'type_id', - ['eq' => \Magento\ConfigurableProduct\Model\Product\Type\Configurable::TYPE_CODE] - ); + $productCollection->addAttributeToFilter('entity_id', ['in' => $productIds]) + ->addAttributeToFilter('type_id', ['eq' => ConfigurableProductType::TYPE_CODE]); while ($product = $productCollection->fetchItem()) { $productAttributesOptions = $product->getTypeInstance()->getConfigurableOptions($product); + $this->configurableData[$product->getId()] = []; + $variations = []; + $variationsLabels = []; foreach ($productAttributesOptions as $productAttributeOption) { - $this->configurableData[$product->getId()] = []; - $variations = []; - $variationsLabels = []; - foreach ($productAttributeOption as $optValues) { $variations[$optValues['sku']][] = $optValues['attribute_code'] . '=' . $optValues['option_title']; @@ -50,20 +46,21 @@ public function prepareData($collection, $productIds) $optValues['attribute_code'] . '=' . $optValues['super_attribute_label']; } } + } - foreach ($variations as $sku => $values) { - $variations[$sku] = - 'sku=' . $sku . Import::DEFAULT_GLOBAL_MULTI_VALUE_SEPARATOR - . implode(Import::DEFAULT_GLOBAL_MULTI_VALUE_SEPARATOR, $values); - } - $variations = implode(ImportProduct::PSEUDO_MULTI_LINE_SEPARATOR, $variations); - $variationsLabels = implode(Import::DEFAULT_GLOBAL_MULTI_VALUE_SEPARATOR, $variationsLabels); - - $this->configurableData[$product->getId()] = [ - 'configurable_variations' => $variations, - 'configurable_variation_labels' => $variationsLabels, - ]; + foreach ($variations as $sku => $values) { + $variations[$sku] = + 'sku=' . $sku . Import::DEFAULT_GLOBAL_MULTI_VALUE_SEPARATOR + . implode(Import::DEFAULT_GLOBAL_MULTI_VALUE_SEPARATOR, $values); } + + $this->configurableData[$product->getId()] = [ + 'configurable_variations' => implode(ImportProduct::PSEUDO_MULTI_LINE_SEPARATOR, $variations), + 'configurable_variation_labels' => implode( + Import::DEFAULT_GLOBAL_MULTI_VALUE_SEPARATOR, + $variationsLabels + ) + ]; } } diff --git a/app/code/Magento/ConfigurableImportExport/Model/Import/Product/Type/Configurable.php b/app/code/Magento/ConfigurableImportExport/Model/Import/Product/Type/Configurable.php index 4da2e903e7c1f..f5ef67ff72d7f 100644 --- a/app/code/Magento/ConfigurableImportExport/Model/Import/Product/Type/Configurable.php +++ b/app/code/Magento/ConfigurableImportExport/Model/Import/Product/Type/Configurable.php @@ -495,6 +495,7 @@ protected function _parseVariations($rowData) } if (!empty($fieldAndValuePairs['sku'])) { + $position = 0; $additionalRow['_super_products_sku'] = $fieldAndValuePairs['sku']; unset($fieldAndValuePairs['sku']); $additionalRow['display'] = isset($fieldAndValuePairs['display']) ? $fieldAndValuePairs['display'] : 1; @@ -502,8 +503,10 @@ protected function _parseVariations($rowData) foreach ($fieldAndValuePairs as $attrCode => $attrValue) { $additionalRow['_super_attribute_code'] = $attrCode; $additionalRow['_super_attribute_option'] = $attrValue; + $additionalRow['_super_attribute_position'] = $position; $additionalRows[] = $additionalRow; $additionalRow = []; + $position += 1; } } } @@ -709,7 +712,7 @@ protected function _collectSuperDataLabels($data, $productSuperAttrId, $productI $attrParams = $this->_superAttributes[$data['_super_attribute_code']]; $this->_superAttributesData['attributes'][$productId][$attrParams['id']] = [ 'product_super_attribute_id' => $productSuperAttrId, - 'position' => 0, + 'position' => $data['_super_attribute_position'], ]; $label = isset($variationLabels[$data['_super_attribute_code']]) ? $variationLabels[$data['_super_attribute_code']] diff --git a/app/code/Magento/ConfigurableImportExport/Test/Unit/Model/Export/RowCustomizerTest.php b/app/code/Magento/ConfigurableImportExport/Test/Unit/Model/Export/RowCustomizerTest.php index ab4d1fd3b5d8b..df61bb3e02a1b 100644 --- a/app/code/Magento/ConfigurableImportExport/Test/Unit/Model/Export/RowCustomizerTest.php +++ b/app/code/Magento/ConfigurableImportExport/Test/Unit/Model/Export/RowCustomizerTest.php @@ -3,51 +3,65 @@ * Copyright © 2013-2017 Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - namespace Magento\ConfigurableImportExport\Test\Unit\Model\Export; -use \Magento\CatalogImportExport\Model\Import\Product as ImportProduct; +use Magento\ConfigurableImportExport\Model\Export\RowCustomizer as ExportRowCustomizer; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; +use Magento\Catalog\Model\ResourceModel\Product\Collection as ProductCollection; +use Magento\ConfigurableProduct\Model\Product\Type\Configurable as ConfigurableProductType; +use Magento\Catalog\Model\Product; +use Magento\CatalogImportExport\Model\Import\Product as ImportProduct; use Magento\ImportExport\Model\Import; +/** + * Tests \Magento\ConfigurableImportExport\Model\Export\RowCustomizer + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ class RowCustomizerTest extends \PHPUnit_Framework_TestCase { /** - * Test existing product id. - * - * @var int + * @var ExportRowCustomizer */ - protected $initiatedProductId = 11; + private $exportRowCustomizer; /** - * @var \Magento\ConfigurableImportExport\Model\Export\RowCustomizer + * @var ObjectManagerHelper */ - protected $_model; + private $objectManagerHelper; /** - * @var \Magento\Catalog\Model\ResourceModel\Product\Collection|\PHPUnit_Framework_MockObject_MockObject + * @var ProductCollection|\PHPUnit_Framework_MockObject_MockObject */ - protected $_collectionMock; + private $productCollectionMock; + + /** + * @var ConfigurableProductType|\PHPUnit_Framework_MockObject_MockObject + */ + private $configurableProductTypeMock; + + /** + * @var int + */ + private $productId = 11; protected function setUp() { - $this->_collectionMock = $this->getMock( - 'Magento\Catalog\Model\ResourceModel\Product\Collection', - ['addAttributeToFilter', 'fetchItem', '__wakeup'], - [], - '', - false - ); - $this->_model = new \Magento\ConfigurableImportExport\Model\Export\RowCustomizer(); - } + $this->productCollectionMock = $this->getMockBuilder(ProductCollection::class) + ->disableOriginalConstructor() + ->getMock(); + $this->configurableProductTypeMock = $this->getMockBuilder(ConfigurableProductType::class) + ->disableOriginalConstructor() + ->getMock(); - public function testPrepareData() - { - $this->_initConfigurableData(); + $this->objectManagerHelper = new ObjectManagerHelper($this); + $this->exportRowCustomizer = $this->objectManagerHelper->getObject(ExportRowCustomizer::class); } public function testAddHeaderColumns() { - $this->_initConfigurableData(); + $this->initConfigurableData(); + $this->assertEquals( [ 'column_1', @@ -56,37 +70,80 @@ public function testAddHeaderColumns() 'configurable_variations', 'configurable_variation_labels', ], - $this->_model->addHeaderColumns( - ['column_1', 'column_2', 'column_3'] - ) + $this->exportRowCustomizer->addHeaderColumns(['column_1', 'column_2', 'column_3']) ); } /** * @param array $expected * @param array $data + * * @dataProvider addDataDataProvider */ public function testAddData(array $expected, array $data) { - $this->_initConfigurableData(); - $this->assertEquals( - $expected, - $this->_model->addData($data['data_row'], $data['product_id']) - ); + $this->initConfigurableData(); + + $this->assertEquals($expected, $this->exportRowCustomizer->addData($data['data_row'], $data['product_id'])); + } + + /** + * @return array + */ + public function addDataDataProvider() + { + $expectedConfigurableData = $this->getExpectedConfigurableData(); + $data = $expectedConfigurableData[$this->productId]; + + return [ + [ + '$expected' => [ + 'key_1' => 'value_1', + 'key_2' => 'value_2', + 'key_3' => 'value_3' + ], + '$data' => [ + 'data_row' => [ + 'key_1' => 'value_1', + 'key_2' => 'value_2', + 'key_3' => 'value_3' + ], + 'product_id' => 1 + ] + ], + [ + '$expected' => [ + 'key_1' => 'value_1', + 'key_2' => 'value_2', + 'key_3' => 'value_3', + 'configurable_variations' => $data['configurable_variations'], + 'configurable_variation_labels' => $data['configurable_variation_labels'] + ], + '$data' => [ + 'data_row' => [ + 'key_1' => 'value_1', + 'key_2' => 'value_2', + 'key_3' => 'value_3' + ], + 'product_id' => $this->productId + ] + ] + ]; } /** * @param array $expected * @param array $data + * * @dataProvider getAdditionalRowsCountDataProvider */ public function testGetAdditionalRowsCount(array $expected, array $data) { - $this->_initConfigurableData(); + $this->initConfigurableData(); + $this->assertEquals( $expected, - $this->_model->getAdditionalRowsCount($data['row_count'], $data['product_id']) + $this->exportRowCustomizer->getAdditionalRowsCount($data['row_count'], $data['product_id']) ); } @@ -101,7 +158,7 @@ public function getAdditionalRowsCountDataProvider() [ 'row_count' => [1, 2, 3], 'product_id' => 1 - ], + ] ], [ [1, 2, 3], @@ -120,131 +177,66 @@ public function getAdditionalRowsCountDataProvider() ]; } - /** - * @return array - */ - public function addDataDataProvider() - { - $expectedConfigurableData = $this->getExpectedConfigurableData(); - $data = $expectedConfigurableData[$this->initiatedProductId]; - - return [ - [ - '$expected' => [ - 'key_1' => 'value_1', - 'key_2' => 'value_2', - 'key_3' => 'value_3', - ], - '$data' => [ - 'data_row' => [ - 'key_1' => 'value_1', - 'key_2' => 'value_2', - 'key_3' => 'value_3', - ], - 'product_id' => 1 - ], - ], - [ - '$expected' => [ - 'key_1' => 'value_1', - 'key_2' => 'value_2', - 'key_3' => 'value_3', - 'configurable_variations' => $data['configurable_variations'], - 'configurable_variation_labels' => $data['configurable_variation_labels'], - ], - '$data' => [ - 'data_row' => [ - 'key_1' => 'value_1', - 'key_2' => 'value_2', - 'key_3' => 'value_3', - ], - 'product_id' => $this->initiatedProductId - ] - ] - ]; - } - - protected function _initConfigurableData() + private function initConfigurableData() { $productIds = [1, 2, 3]; + $expectedConfigurableData = $this->getExpectedConfigurableData(); + $productMock = $this->createProductMock(); $productAttributesOptions = [ - [//1 $productAttributeOption - [//1opt $optValue + [ + [ 'pricing_is_percent' => true, 'sku' => '_sku_', 'attribute_code' => 'code_of_attribute', 'option_title' => 'Option Title', 'pricing_value' => 112345, - 'super_attribute_label' => 'Super attribute label', + 'super_attribute_label' => 'Super attribute label' ], - [//2opt $optValue + [ 'pricing_is_percent' => false, 'sku' => '_sku_', 'attribute_code' => 'code_of_attribute', 'option_title' => 'Option Title', 'pricing_value' => 212345, - 'super_attribute_label' => '', + 'super_attribute_label' => '' ], - [//3opt $optValue + [ 'pricing_is_percent' => false, 'sku' => '_sku_2', 'attribute_code' => 'code_of_attribute_2', 'option_title' => 'Option Title 2', 'pricing_value' => 312345, - 'super_attribute_label' => 'Super attribute label 2', - ], - ], + 'super_attribute_label' => 'Super attribute label 2' + ] + ] ]; - $expectedConfigurableData = $this->getExpectedConfigurableData(); - - $productMock = $this->getMock( - 'Magento\Catalog\Model\Product', - ['getId', 'getTypeInstance', '__wakeup'], - [], - '', - false - ); - $productMock->expects($this->any()) + $productMock->expects(static::any()) ->method('getId') - ->will($this->returnValue($this->initiatedProductId)); - - $typeInstanceMock = $this->getMock( - 'Magento\ConfigurableProduct\Model\Product\Type\Configurable', - [], - [], - '', - false - ); - $typeInstanceMock->expects($this->any()) - ->method('getConfigurableOptions') - ->will($this->returnValue($productAttributesOptions)); - - $productMock->expects($this->any()) + ->willReturn($this->productId); + $productMock->expects(static::any()) ->method('getTypeInstance') - ->will($this->returnValue($typeInstanceMock)); - - $this->_collectionMock->expects($this->at(0)) - ->method('addAttributeToFilter') - ->with('entity_id', ['in' => $productIds]) - ->will($this->returnSelf()); - $this->_collectionMock->expects($this->at(1)) + ->willReturn($this->configurableProductTypeMock); + $this->configurableProductTypeMock->expects(static::any()) + ->method('getConfigurableOptions') + ->willReturn($productAttributesOptions); + $this->productCollectionMock->expects(static::atLeastOnce()) ->method('addAttributeToFilter') - ->with('type_id', ['eq' => \Magento\ConfigurableProduct\Model\Product\Type\Configurable::TYPE_CODE]) - ->will($this->returnSelf()); - $this->_collectionMock->expects($this->at(2)) - ->method('fetchItem') - ->will($this->returnValue($productMock)); - $this->_collectionMock->expects($this->at(3)) + ->willReturnMap( + [ + ['entity_id', ['in' => $productIds], 'inner', $this->productCollectionMock], + ['type_id', ['eq' => ConfigurableProductType::TYPE_CODE], 'inner', $this->productCollectionMock] + ] + ); + $this->productCollectionMock->expects(static::atLeastOnce()) ->method('fetchItem') - ->will($this->returnValue(false)); + ->willReturnOnConsecutiveCalls($productMock, false); - - $this->_model->prepareData($this->_collectionMock, $productIds); - - $configurableData = $this->getPropertyValue($this->_model, 'configurableData'); - - $this->assertEquals($expectedConfigurableData, $configurableData); + $this->exportRowCustomizer->prepareData($this->productCollectionMock, $productIds); + $this->assertEquals( + $expectedConfigurableData, + $this->getPropertyValue($this->exportRowCustomizer, 'configurableData') + ); } /** @@ -252,49 +244,56 @@ protected function _initConfigurableData() * * @return array */ - protected function getExpectedConfigurableData() + private function getExpectedConfigurableData() { return [ - $this->initiatedProductId => [ - 'configurable_variations' => implode(ImportProduct::PSEUDO_MULTI_LINE_SEPARATOR, [ - '_sku_' => 'sku=_sku_' . Import::DEFAULT_GLOBAL_MULTI_VALUE_SEPARATOR - . implode(Import::DEFAULT_GLOBAL_MULTI_VALUE_SEPARATOR, [ - 'code_of_attribute=Option Title', - 'code_of_attribute=Option Title', - ]), - '_sku_2' => 'sku=_sku_2' . Import::DEFAULT_GLOBAL_MULTI_VALUE_SEPARATOR - . implode(Import::DEFAULT_GLOBAL_MULTI_VALUE_SEPARATOR, [ - 'code_of_attribute_2=Option Title 2', - ]) - ]), - 'configurable_variation_labels' => implode(Import::DEFAULT_GLOBAL_MULTI_VALUE_SEPARATOR, [ - 'code_of_attribute' => 'code_of_attribute=Super attribute label', - 'code_of_attribute_2' => 'code_of_attribute_2=Super attribute label 2', - ]), - ], + $this->productId => [ + 'configurable_variations' => implode( + ImportProduct::PSEUDO_MULTI_LINE_SEPARATOR, + [ + '_sku_' => 'sku=_sku_' . Import::DEFAULT_GLOBAL_MULTI_VALUE_SEPARATOR + . implode( + Import::DEFAULT_GLOBAL_MULTI_VALUE_SEPARATOR, + ['code_of_attribute=Option Title', 'code_of_attribute=Option Title'] + ), + '_sku_2' => 'sku=_sku_2' . Import::DEFAULT_GLOBAL_MULTI_VALUE_SEPARATOR + . implode( + Import::DEFAULT_GLOBAL_MULTI_VALUE_SEPARATOR, + ['code_of_attribute_2=Option Title 2'] + ) + ] + ), + 'configurable_variation_labels' => implode( + Import::DEFAULT_GLOBAL_MULTI_VALUE_SEPARATOR, + [ + 'code_of_attribute' => 'code_of_attribute=Super attribute label', + 'code_of_attribute_2' => 'code_of_attribute_2=Super attribute label 2' + ] + ) + ] ]; } /** - * @param $object - * @param $property - * @param $value + * Create product mock object + * + * @return Product|\PHPUnit_Framework_MockObject_MockObject */ - protected function setPropertyValue(&$object, $property, $value) + private function createProductMock() { - $reflection = new \ReflectionClass(get_class($object)); - $reflectionProperty = $reflection->getProperty($property); - $reflectionProperty->setAccessible(true); - $reflectionProperty->setValue($object, $value); - return $object; + return $this->getMockBuilder(Product::class) + ->disableOriginalConstructor() + ->getMock(); } /** - * @param $object - * @param $property + * Get value of protected property + * + * @param object $object + * @param string $property * @return mixed */ - protected function getPropertyValue(&$object, $property) + private function getPropertyValue($object, $property) { $reflection = new \ReflectionClass(get_class($object)); $reflectionProperty = $reflection->getProperty($property); diff --git a/app/code/Magento/ConfigurableImportExport/composer.json b/app/code/Magento/ConfigurableImportExport/composer.json index 3023b308ce516..542981da181fd 100644 --- a/app/code/Magento/ConfigurableImportExport/composer.json +++ b/app/code/Magento/ConfigurableImportExport/composer.json @@ -11,7 +11,7 @@ "magento/framework": "100.1.*" }, "type": "magento2-module", - "version": "100.1.2", + "version": "100.1.3", "license": [ "OSL-3.0", "AFL-3.0" diff --git a/app/code/Magento/ConfigurableProduct/Controller/Adminhtml/Product/Initialization/Helper/Plugin/Configurable.php b/app/code/Magento/ConfigurableProduct/Controller/Adminhtml/Product/Initialization/Helper/Plugin/Configurable.php index d1017b8d6eb73..58c6dfa2809f6 100644 --- a/app/code/Magento/ConfigurableProduct/Controller/Adminhtml/Product/Initialization/Helper/Plugin/Configurable.php +++ b/app/code/Magento/ConfigurableProduct/Controller/Adminhtml/Product/Initialization/Helper/Plugin/Configurable.php @@ -95,7 +95,7 @@ public function afterInitialize(Helper $subject, ProductInterface $product) $product->setAttributeSetId($setId); } $extensionAttributes = $product->getExtensionAttributes(); - + $product->getResource()->getSortedAttributes($setId); $product->setNewVariationsAttributeSetId($setId); $configurableOptions = []; diff --git a/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable.php b/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable.php index e98f546943ff8..34efc90c95619 100644 --- a/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable.php +++ b/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable.php @@ -10,6 +10,7 @@ use Magento\Catalog\Api\ProductRepositoryInterface; use Magento\Catalog\Model\Config; use Magento\Catalog\Model\Product; +use Magento\Catalog\Model\Product\Gallery\ReadHandler as GalleryReadHandler; use Magento\CatalogInventory\Api\StockRegistryInterface; use Magento\CatalogInventory\Model\Stock\Status; use Magento\ConfigurableProduct\Model\Product\Type\Collection\SalableProcessor; @@ -18,7 +19,6 @@ use Magento\Eav\Model\Entity\Attribute\ScopedAttributeInterface; use Magento\Framework\App\ObjectManager; use Magento\Framework\EntityManager\MetadataPool; -use Magento\Catalog\Model\Product\Gallery\ReadHandler as GalleryReadHandler; /** * Configurable product type implementation @@ -203,10 +203,6 @@ class Configurable extends \Magento\Catalog\Model\Product\Type\AbstractType * @param ProductTypeConfigurable $catalogProductTypeConfigurable * @param \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig * @param \Magento\Framework\Api\ExtensionAttribute\JoinProcessorInterface $extensionAttributesJoinProcessor - * @param \Magento\Framework\Cache\FrontendInterface $cache, - * @param \Magento\Customer\Model\Session $customerSession, - * @param StockRegistryInterface $stockRegistry, - * @param ProductInterfaceFactory $productFactory * @param \Magento\Framework\Cache\FrontendInterface $cache * @param \Magento\Customer\Model\Session $customerSession * @param StockRegistryInterface $stockRegistry @@ -484,8 +480,11 @@ public function getConfigurableAttributes($product) */ protected function hasCacheData($configurableAttributes) { - $configurableAttributes = $configurableAttributes ?: unserialize($configurableAttributes); - if (is_array($configurableAttributes) && count($configurableAttributes)) { + if ($configurableAttributes) { + $configurableAttributes = unserialize($configurableAttributes); + } + $isTraversable = (is_array($configurableAttributes) || $configurableAttributes instanceof \Traversable); + if ($isTraversable && count($configurableAttributes)) { foreach ($configurableAttributes as $attribute) { /** @var \Magento\ConfigurableProduct\Model\Product\Type\Configurable\Attribute $attribute */ if ($attribute->getData('options')) { @@ -610,7 +609,7 @@ public function getUsedProducts($product, $requiredAttributeIds = null) ) ); $data = unserialize($this->getCache()->load($key)); - if (!empty($data)) { + if (is_array($data)) { $usedProducts = []; foreach ($data as $item) { $productItem = $this->productFactory->create(); @@ -941,9 +940,15 @@ public function getSelectedAttributesInfo($product) $value = $value->getSource()->getOptionText($attributeValue); } else { $value = ''; + $attributeValue = ''; } - $attributes[] = ['label' => $label, 'value' => $value]; + $attributes[] = [ + 'label' => $label, + 'value' => $value, + 'option_id' => $attributeId, + 'option_value' => $attributeValue + ]; } } } diff --git a/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Product/Indexer/Price/Configurable.php b/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Product/Indexer/Price/Configurable.php index f6fbccbab4cea..7f4f0f9e9376f 100644 --- a/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Product/Indexer/Price/Configurable.php +++ b/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Product/Indexer/Price/Configurable.php @@ -8,11 +8,11 @@ namespace Magento\ConfigurableProduct\Model\ResourceModel\Product\Indexer\Price; use Magento\Catalog\Api\Data\ProductInterface; -use Magento\Catalog\Model\Product\Attribute\Source\Status as ProductStatus; -use Magento\Catalog\Model\Product\Attribute\Source\Status; use Magento\Store\Api\StoreResolverInterface; -use Magento\Store\Model\Store; +/** + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ class Configurable extends \Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\DefaultPrice { /** @@ -29,6 +29,7 @@ class Configurable extends \Magento\Catalog\Model\ResourceModel\Product\Indexer\ * @param \Magento\Framework\Event\ManagerInterface $eventManager * @param \Magento\Framework\Module\Manager $moduleManager * @param string $connectionName + * @param StoreResolverInterface $storeResolver */ public function __construct( \Magento\Framework\Model\ResourceModel\Db\Context $context, @@ -83,63 +84,14 @@ public function reindexEntity($entityIds) protected function reindex($entityIds = null) { if ($this->hasEntity() || !empty($entityIds)) { - if (!empty($entityIds)) { - $allEntityIds = $this->getRelatedProducts($entityIds); - $this->prepareFinalPriceDataForType($allEntityIds, null); - } else { - $this->_prepareFinalPriceData($entityIds); - } + $this->prepareFinalPriceDataForType($entityIds, $this->getTypeId()); $this->_applyCustomOption(); - $this->_applyConfigurableOption($entityIds); + $this->_applyConfigurableOption(); $this->_movePriceDataToIndexTable($entityIds); } return $this; } - /** - * Get related product - * - * @param int[] $entityIds - * @return int[] - */ - private function getRelatedProducts($entityIds) - { - $metadata = $this->getMetadataPool()->getMetadata(ProductInterface::class); - $select = $this->getConnection()->select()->union( - [ - $this->getConnection()->select() - ->from( - ['e' => $this->getTable('catalog_product_entity')], - 'e.entity_id' - )->join( - ['cpsl' => $this->getTable('catalog_product_super_link')], - 'cpsl.parent_id = e.' . $metadata->getLinkField(), - [] - )->where( - 'e.entity_id IN (?)', - $entityIds - ), - $this->getConnection()->select() - ->from( - ['cpsl' => $this->getTable('catalog_product_super_link')], - 'cpsl.product_id' - )->join( - ['e' => $this->getTable('catalog_product_entity')], - 'cpsl.parent_id = e.' . $metadata->getLinkField(), - [] - )->where( - 'e.entity_id IN (?)', - $entityIds - ), - $this->getConnection()->select() - ->from($this->getTable('catalog_product_super_link'), 'product_id') - ->where('product_id in (?)', $entityIds), - ] - ); - - return array_map('intval', $this->getConnection()->fetchCol($select)); - } - /** * Retrieve table name for custom option temporary aggregation data * @@ -195,56 +147,32 @@ protected function _applyConfigurableOption() $connection = $this->getConnection(); $coaTable = $this->_getConfigurableOptionAggregateTable(); $copTable = $this->_getConfigurableOptionPriceTable(); + $linkField = $metadata->getLinkField(); $this->_prepareConfigurableOptionAggregateTable(); $this->_prepareConfigurableOptionPriceTable(); - $statusAttribute = $this->_getAttribute(ProductInterface::STATUS); - $linkField = $metadata->getLinkField(); - - $select = $connection->select()->from( - ['i' => $this->_getDefaultFinalPriceTable()], - [] - )->join( - ['e' => $this->getTable('catalog_product_entity')], - 'e.entity_id = i.entity_id', - ['parent_id' => 'e.entity_id'] - )->join( + $subSelect = $this->getSelect(); + $subSelect->join( ['l' => $this->getTable('catalog_product_super_link')], - 'l.parent_id = e.' . $linkField, - ['product_id'] - )->columns( - ['customer_group_id', 'website_id'], - 'i' + 'l.product_id = e.entity_id', + [] )->join( ['le' => $this->getTable('catalog_product_entity')], - 'le.entity_id = l.product_id', - [] - )->where( - 'le.required_options=0' - )->joinLeft( - ['status_global_attr' => $statusAttribute->getBackendTable()], - "status_global_attr.{$linkField} = le.{$linkField}" - . ' AND status_global_attr.attribute_id = ' . (int)$statusAttribute->getAttributeId() - . ' AND status_global_attr.store_id = ' . Store::DEFAULT_STORE_ID, - [] - )->joinLeft( - ['status_attr' => $statusAttribute->getBackendTable()], - "status_attr.{$linkField} = le.{$linkField}" - . ' AND status_attr.attribute_id = ' . (int)$statusAttribute->getAttributeId() - . ' AND status_attr.store_id = ' . $this->storeResolver->getCurrentStoreId(), - [] - )->where( - 'IFNULL(status_attr.value, status_global_attr.value) = ?', Status::STATUS_ENABLED - )->group( - ['e.entity_id', 'i.customer_group_id', 'i.website_id', 'l.product_id'] + 'le.' . $linkField . ' = l.parent_id', + ['parent_id' => 'entity_id'] ); - $priceColumn = $this->_addAttributeToSelect($select, 'price', 'le.' . $linkField, 0, null, true); - $tierPriceColumn = $connection->getIfNullSql('MIN(i.tier_price)', 'NULL'); - $select->columns( - ['price' => $priceColumn, 'tier_price' => $tierPriceColumn] - ); + $select = $connection->select(); + $select->from(['sub' => new \Zend_Db_Expr('(' . (string)$subSelect . ')')], '') + ->columns([ + 'sub.parent_id', + 'sub.entity_id', + 'sub.customer_group_id', + 'sub.website_id', + 'sub.price', + 'sub.tier_price', + ]); $query = $select->insertFromSelect($coaTable); $connection->query($query); diff --git a/app/code/Magento/ConfigurableProduct/Plugin/Model/Attribute/Backend/AttributeValidation.php b/app/code/Magento/ConfigurableProduct/Plugin/Model/Attribute/Backend/AttributeValidation.php new file mode 100644 index 0000000000000..d5dcf12487b41 --- /dev/null +++ b/app/code/Magento/ConfigurableProduct/Plugin/Model/Attribute/Backend/AttributeValidation.php @@ -0,0 +1,55 @@ +configurableProductType = $configurableProductType; + } + + /** + * @param \Magento\Eav\Model\Entity\Attribute\Backend\AbstractBackend $subject + * @param \Closure $proceed + * @param \Magento\Framework\DataObject $entity + * @return bool + */ + public function aroundValidate( + \Magento\Eav\Model\Entity\Attribute\Backend\AbstractBackend $subject, + \Closure $proceed, + \Magento\Framework\DataObject $entity + ) { + $attribute = $subject->getAttribute(); + if ($entity instanceof ProductInterface + && $entity->getTypeId() == Configurable::TYPE_CODE + && in_array( + $attribute->getAttributeId(), + $this->configurableProductType->getUsedProductAttributeIds($entity), + true + ) + ) { + return true; + } + return $proceed($entity); + } +} diff --git a/app/code/Magento/ConfigurableProduct/Test/Unit/Controller/Adminhtml/Product/Initialization/Helper/Plugin/ConfigurableTest.php b/app/code/Magento/ConfigurableProduct/Test/Unit/Controller/Adminhtml/Product/Initialization/Helper/Plugin/ConfigurableTest.php index fe549f600ae9c..05837399588c5 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Unit/Controller/Adminhtml/Product/Initialization/Helper/Plugin/ConfigurableTest.php +++ b/app/code/Magento/ConfigurableProduct/Test/Unit/Controller/Adminhtml/Product/Initialization/Helper/Plugin/ConfigurableTest.php @@ -7,6 +7,7 @@ use Magento\Catalog\Controller\Adminhtml\Product\Initialization\Helper; use Magento\Catalog\Model\Product; +use Magento\Catalog\Model\ResourceModel\Product as ProductResource; use Magento\ConfigurableProduct\Controller\Adminhtml\Product\Initialization\Helper\Plugin\Configurable; use Magento\ConfigurableProduct\Helper\Product\Options\Factory; use Magento\ConfigurableProduct\Model\Product\Type\Configurable as ConfigurableProduct; @@ -74,7 +75,7 @@ protected function setUp() ->disableOriginalConstructor() ->setMethods([ 'getTypeId', 'setAttributeSetId', 'getExtensionAttributes', 'setNewVariationsAttributeSetId', - 'setCanSaveConfigurableAttributes', 'setExtensionAttributes', 'hasData', 'getData' + 'setCanSaveConfigurableAttributes', 'setExtensionAttributes', 'hasData', 'getData', 'getResource' ]) ->getMock(); @@ -94,6 +95,9 @@ protected function setUp() */ public function testAfterInitializeWithAttributesAndVariations() { + $postValue = 24; + $productResourceMock = $this->getProductResource($postValue); + $attributes = [ ['attribute_id' => 90, 'values' => [ ['value_index' => 12], ['value_index' => 13] @@ -152,7 +156,7 @@ public function testAfterInitializeWithAttributesAndVariations() 'configurable-matrix' => $simpleProducts ]; $valueMap = [ - ['new-variations-attribute-set-id', null, 24], + ['new-variations-attribute-set-id', null, $postValue], ['product', [], $productData] ]; @@ -164,20 +168,24 @@ public function testAfterInitializeWithAttributesAndVariations() ->method('getTypeId') ->willReturn(ConfigurableProduct::TYPE_CODE); - $this->product->expects(static::at(4)) + $this->product->expects(static::at(5)) ->method('hasData') ->with('associated_product_ids') ->willReturn(false); - $this->product->expects(static::at(5)) + $this->product->expects(static::at(6)) ->method('hasData') ->with('configurable-matrix') ->willReturn(true); - $this->product->expects(static::at(6)) + $this->product->expects(static::at(7)) ->method('getData') ->with('configurable-matrix') ->willReturn($simpleProducts); + $this->product->expects(static::once()) + ->method('getResource') + ->willReturn($productResourceMock); + $this->request->expects(static::any()) ->method('getPost') ->willReturnMap($valueMap); @@ -225,6 +233,9 @@ public function testAfterInitializeWithAttributesAndVariations() public function testAfterInitializeWithAttributesAndWithoutVariations() { + $postValue = 24; + $productResourceMock = $this->getProductResource($postValue); + $attributes = [ ['attribute_id' => 90, 'values' => [ ['value_index' => 12], ['value_index' => 13] @@ -238,7 +249,7 @@ public function testAfterInitializeWithAttributesAndWithoutVariations() ]; $valueMap = [ - ['new-variations-attribute-set-id', null, 24], + ['new-variations-attribute-set-id', null, $postValue], ['product', [], $productData], ]; $paramValueMap = [ @@ -254,6 +265,9 @@ public function testAfterInitializeWithAttributesAndWithoutVariations() $this->product->expects(static::at(0)) ->method('getData') ->willReturn(ConfigurableProduct::TYPE_CODE); + $this->product->expects(static::once()) + ->method('getResource') + ->willReturn($productResourceMock); $this->request->expects(static::any()) ->method('getPost') @@ -331,4 +345,21 @@ public function testAfterInitializeForNotConfigurableProduct() ->method('generateSimpleProducts'); $this->plugin->afterInitialize($this->subject, $this->product); } + + /** + * generate product resource model mock + * @param $postValue + * @return \PHPUnit_Framework_MockObject_MockObject + */ + private function getProductResource($postValue) + { + $productResourceMock = $this->getMockBuilder(ProductResource::class) + ->disableOriginalConstructor() + ->getMock(); + $productResourceMock->expects(static::once()) + ->method('getSortedAttributes') + ->with($postValue); + + return $productResourceMock; + } } diff --git a/app/code/Magento/ConfigurableProduct/Test/Unit/Model/Product/Type/ConfigurableTest.php b/app/code/Magento/ConfigurableProduct/Test/Unit/Model/Product/Type/ConfigurableTest.php index 68339bdde3acc..0fadbc66db7da 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Unit/Model/Product/Type/ConfigurableTest.php +++ b/app/code/Magento/ConfigurableProduct/Test/Unit/Model/Product/Type/ConfigurableTest.php @@ -809,7 +809,14 @@ public function testGetSelectedAttributesInfo() $this->assertEquals( $this->_model->getSelectedAttributesInfo($productMock), - [['label' => 'attr_store_label', 'value' => '']] + [ + [ + 'label' => 'attr_store_label', + 'value' => '', + 'option_id' => 1, + 'option_value' => '' + ] + ] ); } diff --git a/app/code/Magento/ConfigurableProduct/Test/Unit/Pricing/Render/FinalPriceBoxTest.php b/app/code/Magento/ConfigurableProduct/Test/Unit/Pricing/Render/FinalPriceBoxTest.php index 61f423f77cee8..006fce54465ac 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Unit/Pricing/Render/FinalPriceBoxTest.php +++ b/app/code/Magento/ConfigurableProduct/Test/Unit/Pricing/Render/FinalPriceBoxTest.php @@ -9,6 +9,10 @@ use Magento\Catalog\Model\Product\Pricing\Renderer\SalableResolverInterface; use Magento\Framework\Module\Manager; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\Catalog\Pricing\Price\MinimalPriceCalculatorInterface; +use Magento\Framework\Pricing\Amount\AmountInterface; +use Magento\Framework\Pricing\Render\Amount; +use Magento\Catalog\Pricing\Price\FinalPrice; /** * Class FinalPriceBoxTest @@ -72,6 +76,11 @@ class FinalPriceBoxTest extends \PHPUnit_Framework_TestCase /** @var Manager|\PHPUnit_Framework_MockObject_MockObject */ private $moduleManager; + /** + * @var MinimalPriceCalculatorInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $minimalPriceCalculator; + /** * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ @@ -89,12 +98,12 @@ protected function setUp() ->method('getPriceInfo') ->will($this->returnValue($this->priceInfo)); - $eventManager = $this->getMock('Magento\Framework\Event\Test\Unit\ManagerStub', [], [], '', false); + $eventManager = $this->getMock(\Magento\Framework\Event\Test\Unit\ManagerStub::class, [], [], '', false); $config = $this->getMock('Magento\Store\Model\Store\Config', [], [], '', false); - $this->layout = $this->getMock('Magento\Framework\View\Layout', [], [], '', false); + $this->layout = $this->getMock(\Magento\Framework\View\Layout::class, [], [], '', false); - $this->priceBox = $this->getMock('Magento\Framework\Pricing\Render\PriceBox', [], [], '', false); - $this->logger = $this->getMock('Psr\Log\LoggerInterface'); + $this->priceBox = $this->getMock(\Magento\Framework\Pricing\Render\PriceBox::class, [], [], '', false); + $this->logger = $this->getMock(\Psr\Log\LoggerInterface::class); $this->layout->expects($this->any())->method('getBlock')->willReturn($this->priceBox); @@ -117,8 +126,8 @@ protected function setUp() ->getMockForAbstractClass(); $storeManager->expects($this->any())->method('getStore')->will($this->returnValue($store)); - $scopeConfigMock = $this->getMockForAbstractClass('Magento\Framework\App\Config\ScopeConfigInterface'); - $context = $this->getMock('Magento\Framework\View\Element\Template\Context', [], [], '', false); + $scopeConfigMock = $this->getMockForAbstractClass(\Magento\Framework\App\Config\ScopeConfigInterface::class); + $context = $this->getMock(\Magento\Framework\View\Element\Template\Context::class, [], [], '', false); $context->expects($this->any()) ->method('getEventManager') ->will($this->returnValue($eventManager)); @@ -150,11 +159,11 @@ protected function setUp() ->method('getUrlBuilder') ->will($this->returnValue($urlBuilder)); - $this->rendererPool = $this->getMockBuilder('Magento\Framework\Pricing\Render\RendererPool') + $this->rendererPool = $this->getMockBuilder(\Magento\Framework\Pricing\Render\RendererPool::class) ->disableOriginalConstructor() ->getMock(); - $this->price = $this->getMock('Magento\Framework\Pricing\Price\PriceInterface'); + $this->price = $this->getMock(\Magento\Framework\Pricing\Price\PriceInterface::class); $this->price->expects($this->any()) ->method('getPriceCode') ->will($this->returnValue(\Magento\Catalog\Pricing\Price\FinalPrice::PRICE_CODE)); @@ -165,15 +174,18 @@ protected function setUp() ->disableOriginalConstructor() ->getMockForAbstractClass(); + $this->minimalPriceCalculator = $this->getMockForAbstractClass(MinimalPriceCalculatorInterface::class); + $this->object = $this->objectManager->getObject( - 'Magento\Catalog\Pricing\Render\FinalPriceBox', + \Magento\Catalog\Pricing\Render\FinalPriceBox::class, [ 'context' => $context, 'saleableItem' => $this->product, 'rendererPool' => $this->rendererPool, 'price' => $this->price, 'data' => ['zone' => 'test_zone', 'list_category_page' => true], - 'salableResolver' => $this->salableResolverMock + 'salableResolver' => $this->salableResolverMock, + 'minimalPriceCalculator' => $this->minimalPriceCalculator ] ); @@ -191,7 +203,7 @@ protected function setUp() public function testRenderMsrpDisabled() { - $priceType = $this->getMock('Magento\Msrp\Pricing\Price\MsrpPrice', [], [], '', false); + $priceType = $this->getMock(\Magento\Msrp\Pricing\Price\MsrpPrice::class, [], [], '', false); $this->moduleManager->expects(self::once()) ->method('isEnabled') @@ -223,7 +235,7 @@ public function testRenderMsrpDisabled() public function testRenderMsrpEnabled() { - $priceType = $this->getMock('Magento\Msrp\Pricing\Price\MsrpPrice', [], [], '', false); + $priceType = $this->getMock(\Magento\Msrp\Pricing\Price\MsrpPrice::class, [], [], '', false); $this->moduleManager->expects(self::once()) ->method('isEnabled') @@ -250,7 +262,7 @@ public function testRenderMsrpEnabled() ->with($this->equalTo($this->product)) ->will($this->returnValue(true)); - $priceBoxRender = $this->getMockBuilder('Magento\Framework\Pricing\Render\PriceBox') + $priceBoxRender = $this->getMockBuilder(\Magento\Framework\Pricing\Render\PriceBox::class) ->disableOriginalConstructor() ->getMock(); $priceBoxRender->expects($this->once()) @@ -305,12 +317,19 @@ public function testRenderMsrpNotRegisteredException() public function testRenderAmountMinimal() { - $priceType = $this->getMock('Magento\Catalog\Pricing\Price\FinalPrice', [], [], '', false); - $amount = $this->getMockForAbstractClass('Magento\Framework\Pricing\Amount\AmountInterface'); $priceId = 'price_id'; $html = 'html'; + $this->object->setData('price_id', $priceId); + $this->product->expects($this->never())->method('getId'); + + $amount = $this->getMockForAbstractClass(AmountInterface::class); + + $this->minimalPriceCalculator->expects($this->once())->method('getAmount') + ->with($this->product) + ->willReturn($amount); + $arguments = [ 'zone' => 'test_zone', 'list_category_page' => true, @@ -320,24 +339,15 @@ public function testRenderAmountMinimal() 'skip_adjustments' => true, ]; - $amountRender = $this->getMock('Magento\Framework\Pricing\Render\Amount', ['toHtml'], [], '', false); + $amountRender = $this->getMock(Amount::class, ['toHtml'], [], '', false); $amountRender->expects($this->once()) ->method('toHtml') - ->will($this->returnValue($html)); - - $this->priceInfo->expects($this->once()) - ->method('getPrice') - ->with(\Magento\Catalog\Pricing\Price\FinalPrice::PRICE_CODE) - ->will($this->returnValue($priceType)); - - $priceType->expects($this->once()) - ->method('getMinimalPrice') - ->will($this->returnValue($amount)); + ->willReturn($html); $this->rendererPool->expects($this->once()) ->method('createAmountRender') ->with($amount, $this->product, $this->price, $arguments) - ->will($this->returnValue($amountRender)); + ->willReturn($amountRender); $this->assertEquals($html, $this->object->renderAmountMinimal()); } @@ -350,10 +360,10 @@ public function testRenderAmountMinimal() */ public function testHasSpecialPrice($regularPrice, $finalPrice, $expectedResult) { - $regularPriceType = $this->getMock('Magento\Catalog\Pricing\Price\RegularPrice', [], [], '', false); - $finalPriceType = $this->getMock('Magento\Catalog\Pricing\Price\FinalPrice', [], [], '', false); - $regularPriceAmount = $this->getMockForAbstractClass('Magento\Framework\Pricing\Amount\AmountInterface'); - $finalPriceAmount = $this->getMockForAbstractClass('Magento\Framework\Pricing\Amount\AmountInterface'); + $regularPriceType = $this->getMock(\Magento\Catalog\Pricing\Price\RegularPrice::class, [], [], '', false); + $finalPriceType = $this->getMock(\Magento\Catalog\Pricing\Price\FinalPrice::class, [], [], '', false); + $regularPriceAmount = $this->getMockForAbstractClass(\Magento\Framework\Pricing\Amount\AmountInterface::class); + $finalPriceAmount = $this->getMockForAbstractClass(\Magento\Framework\Pricing\Amount\AmountInterface::class); $regularPriceAmount->expects($this->once()) ->method('getValue') @@ -392,35 +402,31 @@ public function hasSpecialPriceProvider() public function testShowMinimalPrice() { - $finalPrice = 10.0; $minimalPrice = 5.0; - $displayMininmalPrice = 2.0; - - $this->object->setDisplayMinimalPrice($displayMininmalPrice); + $finalPrice = 10.0; + $displayMininmalPrice = true; - $finalPriceType = $this->getMock('Magento\Catalog\Pricing\Price\FinalPrice', [], [], '', false); + $this->minimalPriceCalculator->expects($this->once())->method('getValue')->with($this->product) + ->willReturn($minimalPrice); - $finalPriceAmount = $this->getMockForAbstractClass('Magento\Framework\Pricing\Amount\AmountInterface'); - $minimalPriceAmount = $this->getMockForAbstractClass('Magento\Framework\Pricing\Amount\AmountInterface'); + $finalPriceAmount = $this->getMockForAbstractClass(AmountInterface::class); $finalPriceAmount->expects($this->once()) ->method('getValue') ->will($this->returnValue($finalPrice)); - $minimalPriceAmount->expects($this->once()) - ->method('getValue') - ->will($this->returnValue($minimalPrice)); - $finalPriceType->expects($this->at(0)) + $finalPriceType = $this->getMock(FinalPrice::class, [], [], '', false); + + $finalPriceType->expects($this->once()) ->method('getAmount') ->will($this->returnValue($finalPriceAmount)); - $finalPriceType->expects($this->at(1)) - ->method('getMinimalPrice') - ->will($this->returnValue($minimalPriceAmount)); $this->priceInfo->expects($this->once()) ->method('getPrice') - ->with(\Magento\Catalog\Pricing\Price\FinalPrice::PRICE_CODE) - ->will($this->returnValue($finalPriceType)); + ->with(FinalPrice::PRICE_CODE) + ->willReturn($finalPriceType); + + $this->object->setDisplayMinimalPrice($displayMininmalPrice); $this->assertTrue($this->object->showMinimalPrice()); } diff --git a/app/code/Magento/ConfigurableProduct/composer.json b/app/code/Magento/ConfigurableProduct/composer.json index e92ce1399dd06..7837a300f6ac9 100644 --- a/app/code/Magento/ConfigurableProduct/composer.json +++ b/app/code/Magento/ConfigurableProduct/composer.json @@ -23,7 +23,7 @@ "magento/module-product-links-sample-data": "Sample Data version:100.1.*" }, "type": "magento2-module", - "version": "100.1.7", + "version": "100.1.8", "license": [ "OSL-3.0", "AFL-3.0" diff --git a/app/code/Magento/ConfigurableProduct/etc/di.xml b/app/code/Magento/ConfigurableProduct/etc/di.xml index f8e84bd661a6d..66d6073f75d5c 100644 --- a/app/code/Magento/ConfigurableProduct/etc/di.xml +++ b/app/code/Magento/ConfigurableProduct/etc/di.xml @@ -30,6 +30,9 @@ + + + diff --git a/app/code/Magento/Cookie/Helper/Cookie.php b/app/code/Magento/Cookie/Helper/Cookie.php index f413cd046e45d..d842489a3e2dd 100644 --- a/app/code/Magento/Cookie/Helper/Cookie.php +++ b/app/code/Magento/Cookie/Helper/Cookie.php @@ -69,11 +69,22 @@ public function __construct( public function isUserNotAllowSaveCookie() { $acceptedSaveCookiesWebsites = $this->_getAcceptedSaveCookiesWebsites(); + return $this->isCookieRestrictionModeEnabled() && + empty($acceptedSaveCookiesWebsites[$this->_website->getId()]); + } + + /** + * Check if cookie restriction mode is enabled for this store + * + * @return bool + */ + public function isCookieRestrictionModeEnabled() + { return $this->scopeConfig->getValue( self::XML_PATH_COOKIE_RESTRICTION, \Magento\Store\Model\ScopeInterface::SCOPE_STORE, $this->_currentStore - ) && empty($acceptedSaveCookiesWebsites[$this->_website->getId()]); + ); } /** diff --git a/app/code/Magento/Cookie/view/frontend/templates/html/notices.phtml b/app/code/Magento/Cookie/view/frontend/templates/html/notices.phtml index e5e4438301902..21c280a384f4a 100644 --- a/app/code/Magento/Cookie/view/frontend/templates/html/notices.phtml +++ b/app/code/Magento/Cookie/view/frontend/templates/html/notices.phtml @@ -8,7 +8,7 @@ ?> -helper('Magento\Cookie\Helper\Cookie')->isUserNotAllowSaveCookie()): ?> +helper('Magento\Cookie\Helper\Cookie')->isCookieRestrictionModeEnabled()): ?>