55 */
66namespace Magento \BundleImportExport \Model \Export ;
77
8+ use Magento \Catalog \Helper \Data as CatalogData ;
89use Magento \Catalog \Model \ResourceModel \Product \Collection ;
910use Magento \CatalogImportExport \Model \Export \RowCustomizerInterface ;
1011use Magento \CatalogImportExport \Model \Import \Product as ImportProductModel ;
1112use Magento \Bundle \Model \ResourceModel \Selection \Collection as SelectionCollection ;
13+ use Magento \Framework \App \ObjectManager ;
1214use Magento \ImportExport \Model \Import as ImportModel ;
1315use Magento \Catalog \Model \Product \Type \AbstractType ;
1416use Magento \Store \Model \StoreManagerInterface ;
1517
1618/**
17- * Class RowCustomizer
19+ * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
1820 */
1921class RowCustomizer implements RowCustomizerInterface
2022{
21- const BUNDLE_PRICE_TYPE_COL = 'bundle_price_type ' ;
23+ public const BUNDLE_PRICE_TYPE_COL = 'bundle_price_type ' ;
2224
23- const BUNDLE_SKU_TYPE_COL = 'bundle_sku_type ' ;
25+ public const BUNDLE_SKU_TYPE_COL = 'bundle_sku_type ' ;
2426
25- const BUNDLE_PRICE_VIEW_COL = 'bundle_price_view ' ;
27+ public const BUNDLE_PRICE_VIEW_COL = 'bundle_price_view ' ;
2628
27- const BUNDLE_WEIGHT_TYPE_COL = 'bundle_weight_type ' ;
29+ public const BUNDLE_WEIGHT_TYPE_COL = 'bundle_weight_type ' ;
2830
29- const BUNDLE_VALUES_COL = 'bundle_values ' ;
31+ public const BUNDLE_VALUES_COL = 'bundle_values ' ;
3032
31- const VALUE_FIXED = 'fixed ' ;
33+ public const VALUE_FIXED = 'fixed ' ;
3234
33- const VALUE_DYNAMIC = 'dynamic ' ;
35+ public const VALUE_DYNAMIC = 'dynamic ' ;
3436
35- const VALUE_PERCENT = 'percent ' ;
37+ public const VALUE_PERCENT = 'percent ' ;
3638
37- const VALUE_PRICE_RANGE = 'Price range ' ;
39+ public const VALUE_PRICE_RANGE = 'Price range ' ;
3840
39- const VALUE_AS_LOW_AS = 'As low as ' ;
41+ public const VALUE_AS_LOW_AS = 'As low as ' ;
4042
4143 /**
4244 * Mapping for bundle types
@@ -125,16 +127,26 @@ class RowCustomizer implements RowCustomizerInterface
125127 */
126128 private $ storeManager ;
127129
130+ /**
131+ * @var CatalogData
132+ */
133+ private $ catalogData ;
134+
128135 /**
129136 * @param StoreManagerInterface $storeManager
137+ * @param CatalogData|null $catalogData
130138 */
131- public function __construct (StoreManagerInterface $ storeManager )
132- {
139+ public function __construct (
140+ StoreManagerInterface $ storeManager ,
141+ ?CatalogData $ catalogData = null
142+ ) {
133143 $ this ->storeManager = $ storeManager ;
144+ $ this ->catalogData = $ catalogData ?? ObjectManager::getInstance ()->get (CatalogData::class);
134145 }
135146
136147 /**
137148 * Retrieve list of bundle specific columns
149+ *
138150 * @return array
139151 */
140152 private function getBundleColumns ()
@@ -223,6 +235,8 @@ protected function populateBundleData($collection)
223235 $ this ->bundleData [$ id ][self ::BUNDLE_PRICE_VIEW_COL ] = $ this ->getPriceViewValue ($ product ->getPriceView ());
224236 $ this ->bundleData [$ id ][self ::BUNDLE_WEIGHT_TYPE_COL ] = $ this ->getTypeValue ($ product ->getWeightType ());
225237 $ this ->bundleData [$ id ][self ::BUNDLE_VALUES_COL ] = $ this ->getFormattedBundleOptionValues ($ product );
238+ // cleanup memory
239+ unset($ this ->optionCollections [$ product ->getSku ()]);
226240 }
227241 return $ this ;
228242 }
@@ -238,13 +252,18 @@ protected function getFormattedBundleOptionValues(\Magento\Catalog\Model\Product
238252 $ optionCollections = $ this ->getProductOptionCollection ($ product );
239253 $ bundleData = '' ;
240254 $ optionTitles = $ this ->getBundleOptionTitles ($ product );
255+ $ optionsRawSelections = $ this ->getBundleOptionSelections ($ product );
241256 foreach ($ optionCollections ->getItems () as $ option ) {
242257 $ optionValues = $ this ->getFormattedOptionValues ($ option , $ optionTitles );
243- $ bundleData .= $ this ->getFormattedBundleSelections (
244- $ optionValues ,
245- $ product ->getTypeInstance ()
246- ->getSelectionsCollection ([$ option ->getId ()], $ product )
247- ->setOrder ('position ' , Collection::SORT_ORDER_ASC )
258+ $ bundleData .= implode (
259+ '' ,
260+ array_map (
261+ fn ($ selectionData ) => $ optionValues
262+ . ImportModel::DEFAULT_GLOBAL_MULTI_VALUE_SEPARATOR
263+ . $ this ->serialize ($ selectionData )
264+ . ImportProductModel::PSEUDO_MULTI_LINE_SEPARATOR ,
265+ $ optionsRawSelections [$ option ->getOptionId ()] ?? []
266+ )
248267 );
249268 }
250269
@@ -257,6 +276,7 @@ protected function getFormattedBundleOptionValues(\Magento\Catalog\Model\Product
257276 * @param string $optionValues
258277 * @param SelectionCollection $selections
259278 * @return string
279+ * @deprecared Not used anymore
260280 */
261281 protected function getFormattedBundleSelections ($ optionValues , SelectionCollection $ selections )
262282 {
@@ -273,16 +293,7 @@ protected function getFormattedBundleSelections($optionValues, SelectionCollecti
273293 ];
274294 $ bundleData .= $ optionValues
275295 . ImportModel::DEFAULT_GLOBAL_MULTI_VALUE_SEPARATOR
276- . implode (
277- ImportModel::DEFAULT_GLOBAL_MULTI_VALUE_SEPARATOR ,
278- array_map (
279- function ($ value , $ key ) {
280- return $ key . ImportProductModel::PAIR_NAME_VALUE_SEPARATOR . $ value ;
281- },
282- $ selectionData ,
283- array_keys ($ selectionData )
284- )
285- )
296+ . $ this ->serialize ($ selectionData )
286297 . ImportProductModel::PSEUDO_MULTI_LINE_SEPARATOR ;
287298 }
288299
@@ -300,18 +311,38 @@ protected function getFormattedOptionValues(
300311 \Magento \Bundle \Model \Option $ option ,
301312 array $ optionTitles = []
302313 ): string {
303- $ names = implode (ImportModel::DEFAULT_GLOBAL_MULTI_VALUE_SEPARATOR , array_map (
304- function ($ title , $ storeName ) {
305- return $ storeName . ImportProductModel::PAIR_NAME_VALUE_SEPARATOR . $ title ;
306- },
307- $ optionTitles [$ option ->getOptionId ()],
308- array_keys ($ optionTitles [$ option ->getOptionId ()])
309- ));
310- return $ names . ImportModel::DEFAULT_GLOBAL_MULTI_VALUE_SEPARATOR
311- . 'type ' . ImportProductModel::PAIR_NAME_VALUE_SEPARATOR
312- . $ option ->getType () . ImportModel::DEFAULT_GLOBAL_MULTI_VALUE_SEPARATOR
313- . 'required ' . ImportProductModel::PAIR_NAME_VALUE_SEPARATOR
314- . $ option ->getRequired ();
314+ $ data = [
315+ ...[
316+ 'name ' => $ option ->getTitle ()
317+ ],
318+ ...($ optionTitles [$ option ->getOptionId ()] ?? []),
319+ ...[
320+ 'type ' => $ option ->getType (),
321+ 'required ' => $ option ->getRequired ()
322+ ]
323+ ];
324+
325+ return $ this ->serialize ($ data );
326+ }
327+
328+ /**
329+ * Format associative array to serialized string as name1=value1,name2=value2 format
330+ *
331+ * @param array $data
332+ * @return string
333+ */
334+ private function serialize (array $ data ): string
335+ {
336+ return implode (
337+ ImportModel::DEFAULT_GLOBAL_MULTI_VALUE_SEPARATOR ,
338+ array_map (
339+ function ($ value , $ key ) {
340+ return $ key . ImportProductModel::PAIR_NAME_VALUE_SEPARATOR . $ value ;
341+ },
342+ $ data ,
343+ array_keys ($ data )
344+ )
345+ );
315346 }
316347
317348 /**
@@ -456,6 +487,72 @@ private function getBundleOptionTitles(\Magento\Catalog\Model\Product $product):
456487 return $ optionsTitles ;
457488 }
458489
490+ /**
491+ * Get bundle product options selections data
492+ *
493+ * The selection price data for the global scope is stored under the 'price' and 'price_type' keys,
494+ * while for a specific website it is stored under
495+ * public the 'price_website_<website-code>' and 'price_type_website_<website-code>' keys.
496+ *
497+ * @param \Magento\Catalog\Model\Product $product
498+ * @return array
499+ */
500+ private function getBundleOptionSelections (\Magento \Catalog \Model \Product $ product ): array
501+ {
502+ $ selections = $ this ->getBundleOptionSelectionsData ($ product );
503+
504+ if (!$ this ->catalogData ->isPriceGlobal ()) {
505+ foreach ($ product ->getWebsiteIds () as $ websiteId ) {
506+ $ websiteCode = $ this ->getWebsiteCodeById ((int ) $ websiteId );
507+ $ storeId = $ this ->getWebsiteDefaultStoreId ((int ) $ websiteId );
508+ foreach ($ this ->getProductOptionCollection ($ product , $ storeId ) as $ option ) {
509+ foreach ($ option ->getSelections () as $ selection ) {
510+ $ selectionData = $ selections [$ option ->getOptionId ()][$ selection ->getSelectionId ()] ?? [];
511+ if ($ selectionData && $ selection ->getPriceScope () == $ websiteId ) {
512+ $ selections [$ option ->getOptionId ()][$ selection ->getSelectionId ()] = [
513+ ...$ selectionData ,
514+ 'price_website_ ' . $ websiteCode => $ selection ->getSelectionPriceValue (),
515+ 'price_type_website_ ' . $ websiteCode =>
516+ $ this ->getPriceTypeValue ($ selection ->getSelectionPriceType ())
517+ ];
518+ }
519+ }
520+ }
521+ }
522+ }
523+
524+ return $ selections ;
525+ }
526+
527+ /**
528+ * Get bundle product options selections data.
529+ *
530+ * @param \Magento\Catalog\Model\Product $product
531+ * @param int $storeId
532+ * @return array
533+ */
534+ private function getBundleOptionSelectionsData (
535+ \Magento \Catalog \Model \Product $ product ,
536+ int $ storeId = \Magento \Store \Model \Store::DEFAULT_STORE_ID
537+ ): array {
538+ $ data = [];
539+ foreach ($ this ->getProductOptionCollection ($ product , $ storeId ) as $ option ) {
540+ /** @var \Magento\Bundle\Model\Option $option*/
541+ foreach ($ option ->getSelections () as $ selection ) {
542+ /** @var \Magento\Bundle\Model\Selection $selection*/
543+ $ data [$ option ->getOptionId ()][$ selection ->getSelectionId ()] = [
544+ 'sku ' => $ selection ->getSku (),
545+ 'price ' => $ selection ->getSelectionPriceValue (),
546+ 'default ' => $ selection ->getIsDefault (),
547+ 'default_qty ' => $ selection ->getSelectionQty (),
548+ 'price_type ' => $ this ->getPriceTypeValue ($ selection ->getSelectionPriceType ()),
549+ 'can_change_qty ' => $ selection ->getSelectionCanChangeQty (),
550+ ];
551+ }
552+ }
553+ return $ data ;
554+ }
555+
459556 /**
460557 * Get product options collection by provided product model.
461558 *
@@ -473,13 +570,48 @@ private function getProductOptionCollection(
473570 if (!isset ($ this ->optionCollections [$ productSku ][$ storeId ])) {
474571 $ product ->unsetData ($ this ->optionCollectionCacheKey );
475572 $ product ->setStoreId ($ storeId );
476- $ this -> optionCollections [ $ productSku ][ $ storeId ] = $ product ->getTypeInstance ()
573+ $ optionCollection = $ product ->getTypeInstance ()
477574 ->getOptionsCollection ($ product )
478575 ->setOrder ('position ' , Collection::SORT_ORDER_ASC );
576+ $ selectionCollection = $ product ->getTypeInstance ()
577+ ->getSelectionsCollection (
578+ $ product ->getTypeInstance ()->getOptionsIds ($ product ),
579+ $ product
580+ )
581+ ->setOrder ('position ' , Collection::SORT_ORDER_ASC )
582+ ->addAttributeToSort ('position ' , Collection::SORT_ORDER_ASC );
583+ $ optionCollection ->appendSelections ($ selectionCollection , true );
584+ $ this ->optionCollections [$ productSku ][$ storeId ] = $ optionCollection ;
479585 }
480586 return $ this ->optionCollections [$ productSku ][$ storeId ];
481587 }
482588
589+ /**
590+ * Retrieve default store id for website
591+ *
592+ * @param int $websiteId
593+ * @return int
594+ * @throws \Magento\Framework\Exception\LocalizedException
595+ */
596+ private function getWebsiteDefaultStoreId (int $ websiteId ): int
597+ {
598+ return (int ) $ this ->storeManager
599+ ->getGroup ($ this ->storeManager ->getWebsite ($ websiteId )->getDefaultGroupId ())
600+ ->getDefaultStoreId ();
601+ }
602+
603+ /**
604+ * Retrieve website code by its ID.
605+ *
606+ * @param int $websiteId
607+ * @return string
608+ * @throws \Magento\Framework\Exception\LocalizedException
609+ */
610+ private function getWebsiteCodeById (int $ websiteId ): string
611+ {
612+ return $ this ->storeManager ->getWebsite ($ websiteId )->getCode ();
613+ }
614+
483615 /**
484616 * Retrieve store code by it's ID.
485617 *
0 commit comments