Passed
Push — master ( b35ffc...19a63c )
by Aimeos
04:50
created

Base::loadFresh()   D

Complexity

Conditions 10
Paths 384

Size

Total Lines 49
Code Lines 22

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 22
c 1
b 0
f 0
dl 0
loc 49
rs 4.5333
cc 10
nc 384
nop 5

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
/**
4
 * @license LGPLv3, https://opensource.org/licenses/LGPL-3.0
5
 * @copyright Metaways Infosystems GmbH, 2011
6
 * @copyright Aimeos (aimeos.org), 2015-2022
7
 * @package MShop
8
 * @subpackage Order
9
 */
10
11
12
namespace Aimeos\MShop\Order\Manager;
13
14
15
/**
16
 * Basic methods and constants for order items (shopping basket).
17
 *
18
 * @package MShop
19
 * @subpackage Order
20
 */
21
abstract class Base extends \Aimeos\MShop\Common\Manager\Base
22
{
23
	/**
24
	 * Unlock basket.
25
	 * Disable the lock for the serialized basket in the session so
26
	 * modifications of the basket content are allowed again. Note that the
27
	 * locks are advisory locks that can't be enforced if code doesn't care
28
	 * about the lock.
29
	 */
30
	const LOCK_DISABLE = 0;
31
32
	/**
33
	 * Lock basket.
34
	 * Enable the lock for the serialized basket in the session so
35
	 * modifications of the basket content are not allowed any more. Note that
36
	 * the locks are advisory locks that can't be enforced if code doesn't care
37
	 * about the lock.
38
	 */
39
	const LOCK_ENABLE = 1;
40
41
42
	/**
43
	 * Returns a new and empty order item (shopping basket).
44
	 *
45
	 * @param \Aimeos\MShop\Price\Item\Iface $price Default price of the basket (usually 0.00)
46
	 * @param \Aimeos\MShop\Locale\Item\Iface $locale Locale item containing the site, language and currency
47
	 * @param array $values Associative list of key/value pairs containing, e.g. the order or user ID
48
	 * @param \Aimeos\MShop\Order\Item\Product\Iface[] $products List of ordered product items
49
	 * @param \Aimeos\MShop\Order\Item\Address\Iface[] $addresses List of order address items
50
	 * @param \Aimeos\MShop\Order\Item\Service\Iface[] $services List of order serviceitems
51
	 * @param \Aimeos\MShop\Order\Item\Product\Iface[] $coupons Associative list of coupon codes as keys and items as values
52
	 * @param \Aimeos\MShop\Customer\Item\Iface|null $custItem Customer item object if requested
53
	 * @return \Aimeos\MShop\Order\Item\Iface Order object
54
	 */
55
	abstract protected function createItemBase( \Aimeos\MShop\Price\Item\Iface $price, \Aimeos\MShop\Locale\Item\Iface $locale,
56
		array $values = [], array $products = [], array $addresses = [], array $services = [], array $coupons = [],
57
		?\Aimeos\MShop\Customer\Item\Iface $custItem = null ) : \Aimeos\MShop\Order\Item\Iface;
58
59
60
	/**
61
	 * Returns the current basket of the customer.
62
	 *
63
	 * @param string $type Basket type if a customer can have more than one basket
64
	 * @return \Aimeos\MShop\Order\Item\Iface Shopping basket
65
	 */
66
	public function getSession( string $type = 'default' ) : \Aimeos\MShop\Order\Item\Iface
67
	{
68
		$context = $this->context();
69
		$token = $context->token();
70
		$locale = $context->locale();
71
		$currency = $locale->getCurrencyId();
72
		$language = $locale->getLanguageId();
73
		$sitecode = $locale->getSiteItem()->getCode();
74
75
		$key = $token . '-' . $sitecode . '-' . $language . '-' . $currency . '-' . $type;
76
77
		try
78
		{
79
			if( ( $order = \Aimeos\MShop::create( $context, 'order/basket' )->get( $key )->getItem() ) === null ) {
80
				return $this->object()->create();
81
			}
82
83
			\Aimeos\MShop::create( $context, 'plugin' )->register( $order, 'order' );
84
		}
85
		catch( \Exception $e )
86
		{
87
			return $this->object()->create();
88
		}
89
90
		return $order;
91
	}
92
93
94
	/**
95
	 * Returns the current lock status of the basket.
96
	 *
97
	 * @param string $type Basket type if a customer can have more than one basket
98
	 * @return int Lock status (@see \Aimeos\MShop\Order\Manager\Base)
99
	 */
100
	public function getSessionLock( string $type = 'default' ) : int
101
	{
102
		$context = $this->context();
103
		$session = $context->session();
104
		$locale = $context->locale();
105
		$currency = $locale->getCurrencyId();
106
		$language = $locale->getLanguageId();
107
		$sitecode = $locale->getSiteItem()->getCode();
108
		$key = 'aimeos/basket/lock-' . $sitecode . '-' . $language . '-' . $currency . '-' . strval( $type );
109
110
		if( ( $value = $session->get( $key ) ) !== null ) {
111
			return (int) $value;
112
		}
113
114
		return \Aimeos\MShop\Order\Manager\Base::LOCK_DISABLE;
115
	}
116
117
118
	/**
119
	 * Saves the current shopping basket of the customer.
120
	 *
121
	 * @param \Aimeos\MShop\Order\Item\Iface $order Shopping basket
122
	 * @param string $type Order type if a customer can have more than one order at once
123
	 * @return \Aimeos\MShop\Order\Manager\Iface Manager object for chaining method calls
124
	 */
125
	public function setSession( \Aimeos\MShop\Order\Item\Iface $order, string $type = 'default' ) : \Aimeos\MShop\Order\Manager\Iface
126
	{
127
		$context = $this->context();
128
		$token = $context->token();
129
		$locale = $context->locale();
130
		$currency = $locale->getCurrencyId();
131
		$language = $locale->getLanguageId();
132
		$sitecode = $locale->getSiteItem()->getCode();
133
134
		$key = $token . '-' . $sitecode . '-' . $language . '-' . $currency . '-' . strval( $type );
135
136
		$session = $context->session();
137
138
		$list = $session->get( 'aimeos/basket/list', [] );
139
		$list[$key] = $key;
140
141
		$session->set( 'aimeos/basket/list', $list );
142
143
		$manager = \Aimeos\MShop::create( $context, 'order/basket' );
144
		$manager->save( $manager->create()->setId( $key )->setCustomerId( $context->user() )->setItem( clone $order ) );
145
146
		return $this;
147
	}
148
149
150
	/**
151
	 * Locks or unlocks the session by setting the lock value.
152
	 * The lock is a cooperative lock and you have to check the lock value before you proceed.
153
	 *
154
	 * @param int $lock Lock value (@see \Aimeos\MShop\Order\Manager\Base)
155
	 * @param string $type Order type if a customer can have more than one order at once
156
	 * @return \Aimeos\MShop\Order\Manager\Iface Manager object for chaining method calls
157
	 */
158
	public function setSessionLock( int $lock, string $type = 'default' ) : \Aimeos\MShop\Order\Manager\Iface
159
	{
160
		$this->checkLock( $lock );
161
162
		$context = $this->context();
163
		$session = $context->session();
164
		$locale = $context->locale();
165
		$currency = $locale->getCurrencyId();
166
		$language = $locale->getLanguageId();
167
		$sitecode = $locale->getSiteItem()->getCode();
168
		$key = 'aimeos/basket/lock-' . $sitecode . '-' . $language . '-' . $currency . '-' . strval( $type );
169
170
		$session->set( $key, strval( $lock ) );
171
172
		return $this;
173
	}
174
175
176
	/**
177
	 * Checks if the lock value is a valid constant.
178
	 *
179
	 * @param int $value Lock constant
180
	 * @return \Aimeos\MShop\Order\Manager\Iface Manager object for chaining method calls
181
	 * @throws \Aimeos\MShop\Order\Exception If given value is invalid
182
	 */
183
	protected function checkLock( int $value ) : \Aimeos\MShop\Order\Manager\Iface
184
	{
185
		switch( $value )
186
		{
187
			case \Aimeos\MShop\Order\Manager\Base::LOCK_DISABLE:
188
			case \Aimeos\MShop\Order\Manager\Base::LOCK_ENABLE:
189
				return $this;
190
		}
191
192
		$msg = $this->context()->translate( 'mshop', 'Lock flag "%1$d" not within allowed range' );
193
		throw new \Aimeos\MShop\Order\Exception( sprintf( $msg, $value ) );
194
	}
195
196
197
	/**
198
	 * Returns the address item map for the given order IDs
199
	 *
200
	 * @param string[] $ids List of order IDs
201
	 * @param bool $fresh Create new items by copying the existing ones and remove their IDs
202
	 * @return array Multi-dimensional associative list of order IDs as keys and order address type/item pairs as values
203
	 */
204
	protected function getAddresses( array $ids, bool $fresh = false ) : array
205
	{
206
		$items = [];
207
		$manager = $this->object()->getSubManager( 'address' );
208
209
		$criteria = $manager->filter()->slice( 0, 0x7fffffff );
210
		$criteria->setConditions( $criteria->compare( '==', 'order.address.parentid', $ids ) );
211
212
		foreach( $manager->search( $criteria ) as $item )
213
		{
214
			if( $fresh === true )
215
			{
216
				$item->setParentId( null );
217
				$item->setId( null );
218
			}
219
220
			$items[$item->getParentId()][] = $item;
221
		}
222
223
		return $items;
224
	}
225
226
227
	/**
228
	 * Returns the coupon map for the given order IDs
229
	 *
230
	 * @param string[] $ids List of order IDs
231
	 * @param bool $fresh Create new items by copying the existing ones and remove their IDs
232
	 * @param array $products Associative list of IDs and order product ID/item pairs as values
233
	 * @return array Multi-dimensional associative list of order IDs as keys and coupons with product items as values
234
	 */
235
	protected function getCoupons( array $ids, bool $fresh = false, array $products = [] ) : array
236
	{
237
		$map = $productMap = [];
238
		$manager = $this->object()->getSubManager( 'coupon' );
239
240
		foreach( $products as $id => $list )
241
		{
242
			if( !isset( $productMap[$id] ) ) {
243
				$productMap[$id] = [];
244
			}
245
246
			foreach( $list as $key => $product )
247
			{
248
				$productMap[$id][$product->getId()] = $product;
249
250
				if( $fresh === true )
251
				{
252
					$product->setPosition( null );
253
					$product->setParentId( null );
254
					$product->setId( null );
255
				}
256
			}
257
		}
258
259
		$criteria = $manager->filter()->slice( 0, 0x7fffffff );
260
		$criteria->setConditions( $criteria->compare( '==', 'order.coupon.parentid', $ids ) );
261
262
		foreach( $manager->search( $criteria ) as $item )
263
		{
264
			if( !isset( $map[$item->getParentId()][$item->getCode()] ) ) {
265
				$map[$item->getParentId()][$item->getCode()] = [];
266
			}
267
268
			if( $item->getProductId() !== null && isset( $productMap[$item->getParentId()][$item->getProductId()] ) ) {
269
				$map[$item->getParentId()][$item->getCode()][] = $productMap[$item->getParentId()][$item->getProductId()];
270
			}
271
		}
272
273
		return $map;
274
	}
275
276
277
	/**
278
	 * Retrieves the ordered products from the storage.
279
	 *
280
	 * @param string[] $ids List of order IDs
281
	 * @param bool $fresh Create new items by copying the existing ones and remove their IDs
282
	 * @return array Multi-dimensional associative list of order IDs as keys and order product
283
	 *	IDs/items pairs in reversed order as values
284
	 */
285
	protected function getProducts( array $ids, bool $fresh = false ) : array
286
	{
287
		$map = $attributes = $subProducts = [];
288
		$manager = $this->object()->getSubManager( 'product' );
289
		$attrManager = $manager->getSubManager( 'attribute' );
290
291
		$criteria = $manager->filter()->slice( 0, 0x7fffffff );
292
		$criteria->setConditions( $criteria->compare( '==', 'order.product.parentid', $ids ) );
293
		$items = $manager->search( $criteria )->reverse();
294
295
		$search = $attrManager->filter()->slice( 0, 0x7fffffff );
296
		$search->setConditions( $search->compare( '==', 'order.product.attribute.parentid', $items->keys()->toArray() ) );
297
298
		foreach( $attrManager->search( $search ) as $id => $attribute )
299
		{
300
			if( $fresh === true )
301
			{
302
				$attributes[$attribute->getParentId()][] = $attribute;
303
				$attribute->setParentId( null );
304
				$attribute->setId( null );
305
			}
306
			else
307
			{
308
				$attributes[$attribute->getParentId()][$id] = $attribute;
309
			}
310
		}
311
312
		foreach( $items as $id => $item )
313
		{
314
			if( isset( $attributes[$id] ) ) {
315
				$item->setAttributeItems( $attributes[$id] );
316
			}
317
318
			if( $item->getOrderProductId() === null )
319
			{
320
				ksort( $subProducts ); // bring the array into the right order because it's reversed
321
				$item->setProducts( $subProducts );
322
				$map[$item->getParentId()][$item->getPosition()] = $item;
323
324
				$subProducts = [];
325
			}
326
			else
327
			{	// in case it's a sub-product
328
				$subProducts[$item->getPosition()] = $item;
329
			}
330
331
			if( $fresh === true )
332
			{
333
				$item->setPosition( null );
334
				$item->setParentId( null );
335
				$item->setId( null );
336
			}
337
		}
338
339
		foreach( $map as $key => $list ) {
340
			ksort( $map[$key] );
341
		}
342
343
		return $map;
344
	}
345
346
347
	/**
348
	 * Retrieves the order services from the storage.
349
	 *
350
	 * @param string[] $ids List of order IDs
351
	 * @param bool $fresh Create new items by copying the existing ones and remove their IDs
352
	 * @return array Multi-dimensional associative list of order IDs as keys and service type/items pairs as values
353
	 */
354
	protected function getServices( array $ids, bool $fresh = false ) : array
355
	{
356
		$map = [];
357
		$manager = $this->object()->getSubManager( 'service' );
358
359
		$criteria = $manager->filter()->slice( 0, 0x7fffffff );
360
		$criteria->setConditions( $criteria->compare( '==', 'order.service.parentid', $ids ) );
361
362
		foreach( $manager->search( $criteria ) as $item )
363
		{
364
			if( $fresh === true )
365
			{
366
				foreach( $item->getAttributeItems() as $attribute )
367
				{
368
						$attribute->setId( null );
369
						$attribute->setParentId( null );
370
				}
371
372
				$item->setParentId( null );
373
				$item->setId( null );
374
			}
375
376
			$map[$item->getParentId()][] = $item;
377
		}
378
379
		return $map;
380
	}
381
382
383
	/**
384
	 * Load the basket item for the given ID.
385
	 *
386
	 * @param string $id Unique order ID
387
	 * @param \Aimeos\MShop\Price\Item\Iface $price Price object with total order value
388
	 * @param \Aimeos\MShop\Locale\Item\Iface $localeItem Locale object of the order
389
	 * @param array $row Array of values with all relevant order information
390
	 * @param array $ref Basket parts that should be loaded too
391
	 * @return \Aimeos\MShop\Order\Item\Iface The loaded order item for the given ID
392
	 */
393
	protected function loadItems( string $id, \Aimeos\MShop\Price\Item\Iface $price,
394
		\Aimeos\MShop\Locale\Item\Iface $localeItem, array $row, array $ref )
395
	{
396
		$products = $coupons = $addresses = $services = [];
397
398
		if( in_array( 'order/product', $ref ) || in_array( 'order/coupon', $ref ) ) {
399
			$products = $this->loadProducts( $id, false );
400
		}
401
402
		if( in_array( 'order/coupon', $ref ) ) {
403
			$coupons = $this->loadCoupons( $id, false, $products );
404
		}
405
406
		if( in_array( 'order/address', $ref ) ) {
407
			$addresses = $this->loadAddresses( $id, false );
408
		}
409
410
		if( in_array( 'order/service', $ref ) ) {
411
			$services = $this->loadServices( $id, false );
412
		}
413
414
		$basket = $this->createItemBase( $price, $localeItem, $row, $products, $addresses, $services, $coupons );
415
416
		\Aimeos\MShop::create( $this->context(), 'plugin' )->register( $basket, 'order' );
417
418
		return $basket;
419
	}
420
421
422
	/**
423
	 * Create a new basket item as a clone from an existing order ID.
424
	 *
425
	 * @param string $id Unique order ID
426
	 * @param \Aimeos\MShop\Price\Item\Iface $price Price object with total order value
427
	 * @param \Aimeos\MShop\Locale\Item\Iface $localeItem Locale object of the order
428
	 * @param array $row Array of values with all relevant order information
429
	 * @param array $ref Basket parts that should be loaded
430
	 * @return \Aimeos\MShop\Order\Item\Standard The loaded order item for the given ID
431
	 */
432
	protected function loadFresh( string $id, \Aimeos\MShop\Price\Item\Iface $price,
433
		\Aimeos\MShop\Locale\Item\Iface $localeItem, array $row, array $ref )
434
	{
435
		$products = $coupons = $addresses = $services = [];
436
437
		if( in_array( 'order/product', $ref ) ) {
438
			$products = $this->loadProducts( $id, true );
439
		}
440
441
		if( in_array( 'order/coupon', $ref ) ) {
442
			// load coupons with product array containing product ids for coupon/product matching
443
			// not very efficient, a better solution might be considered for 2020.01 release
444
			// see https://github.com/aimeos/aimeos-core/pull/175 for discussion
445
			$coupons = $this->loadCoupons( $id, true, $this->loadProducts( $id, false ) );
446
		}
447
448
		if( in_array( 'order/address', $ref ) ) {
449
			$addresses = $this->loadAddresses( $id, true );
450
		}
451
452
		if( in_array( 'order/service', $ref ) ) {
453
			$services = $this->loadServices( $id, true );
454
		}
455
456
		$basket = $this->createItemBase( $price, $localeItem, $row );
457
		$basket->setId( null );
458
459
		\Aimeos\MShop::create( $this->context(), 'plugin' )->register( $basket, 'order' );
460
461
		foreach( $services as $item ) {
462
			$basket->addService( $item, $item->getType() );
463
		}
464
465
		foreach( $addresses as $item ) {
466
			$basket->addAddress( $item, $item->getType() );
467
		}
468
469
		foreach( $products as $item )
470
		{
471
			if( !( $item->getFlags() & \Aimeos\MShop\Order\Item\Product\Base::FLAG_IMMUTABLE ) ) {
472
				$basket->addProduct( $item );
473
			}
474
		}
475
476
		foreach( $coupons as $code => $items ) {
477
			$basket->addCoupon( $code );
478
		}
479
480
		return $basket;
481
	}
482
483
484
	/**
485
	 * Retrieves the addresses of the order from the storage.
486
	 *
487
	 * @param string $id Order ID
488
	 * @param bool $fresh Create new items by copying the existing ones and remove their IDs
489
	 * @return \Aimeos\MShop\Order\Item\Address\Iface[] List of order address items
490
	 */
491
	protected function loadAddresses( string $id, bool $fresh ) : array
492
	{
493
		$map = $this->getAddresses( [$id], $fresh );
494
495
		if( ( $items = reset( $map ) ) !== false ) {
496
			return $items;
497
		}
498
499
		return [];
500
	}
501
502
503
	/**
504
	 * Retrieves the coupons of the order from the storage.
505
	 *
506
	 * @param string $id Order ID
507
	 * @param bool $fresh Create new items by copying the existing ones and remove their IDs
508
	 * @param array $products Multi-dimensional associative list of order IDs as keys and order product
509
	 *	IDs/items pairs in reversed order as values
510
	 * @return \Aimeos\MShop\Order\Item\Product\Iface[] Associative list of coupon codes as keys and items as values
511
	 */
512
	protected function loadCoupons( string $id, bool $fresh, array $products ) : array
513
	{
514
		$map = $this->getCoupons( [$id], $fresh, [$id => $products] );
515
516
		if( ( $items = reset( $map ) ) !== false ) {
517
			return $items;
518
		}
519
520
		return [];
521
	}
522
523
524
	/**
525
	 * Retrieves the ordered products from the storage.
526
	 *
527
	 * @param string $id Order ID
528
	 * @param bool $fresh Create new items by copying the existing ones and remove their IDs
529
	 * @return \Aimeos\MShop\Order\Item\Product\Iface[] List of product items
530
	 */
531
	protected function loadProducts( string $id, bool $fresh ) : array
532
	{
533
		$items = current( $this->getProducts( [$id], $fresh ) );
534
		return $items ?: [];
535
	}
536
537
538
	/**
539
	 * Retrieves the services of the order from the storage.
540
	 *
541
	 * @param string $id Order ID
542
	 * @param bool $fresh Create new items by copying the existing ones and remove their IDs
543
	 * @return \Aimeos\MShop\Order\Item\Service\Iface[] List of order service items
544
	 */
545
	protected function loadServices( string $id, bool $fresh ) : array
546
	{
547
		$map = $this->getServices( [$id], $fresh );
548
549
		if( ( $items = reset( $map ) ) !== false ) {
550
			return $items;
551
		}
552
553
		return [];
554
	}
555
556
557
	/**
558
	 * Saves the addresses of the order to the storage.
559
	 *
560
	 * @param \Aimeos\MShop\Order\Item\Iface $basket Basket containing address items
561
	 * @return \Aimeos\MShop\Order\Manager\Iface Manager object for chaining method calls
562
	 */
563
	protected function saveAddresses( \Aimeos\MShop\Order\Item\Iface $basket ) : \Aimeos\MShop\Order\Manager\Iface
564
	{
565
		$addresses = $basket->getAddresses()->flat( 1 );
566
567
		foreach( $addresses as $address )
568
		{
569
			if( $address->getParentId() != $basket->getId() ) {
570
				$address->setId( null ); // create new item if copied
571
			}
572
573
			$address->setParentId( $basket->getId() );
574
		}
575
576
		$this->object()->getSubManager( 'address' )->save( $addresses );
577
578
		return $this;
579
	}
580
581
582
	/**
583
	 * Saves the coupons of the order to the storage.
584
	 *
585
	 * @param \Aimeos\MShop\Order\Item\Iface $basket Basket containing coupon items
586
	 * @return \Aimeos\MShop\Order\Manager\Iface Manager object for chaining method calls
587
	 */
588
	protected function saveCoupons( \Aimeos\MShop\Order\Item\Iface $basket ) : \Aimeos\MShop\Order\Manager\Iface
589
	{
590
		$manager = $this->object()->getSubManager( 'coupon' );
591
		$filter = $manager->filter()->add( 'order.coupon.parentid', '==', $basket->getId() )->slice( 0, 0x7fffffff );
592
		$items = $manager->search( $filter )->groupBy( 'order.coupon.code' );
593
594
		foreach( $basket->getCoupons() as $code => $products )
595
		{
596
			if( empty( $products ) )
597
			{
598
				$item = !empty( $items[$code] ) ? current( $items[$code] ) : $manager->create()->setParentId( $basket->getId() );
0 ignored issues
show
Bug introduced by
It seems like $items[$code] can also be of type null; however, parameter $array of current() does only seem to accept array|object, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

598
				$item = !empty( $items[$code] ) ? current( /** @scrutinizer ignore-type */ $items[$code] ) : $manager->create()->setParentId( $basket->getId() );
Loading history...
599
				$manager->save( $item->setCode( $code ) );
600
				continue;
601
			}
602
603
			foreach( $products as $product )
604
			{
605
				foreach( $items[$code] ?? [] as $prodItem )
606
				{
607
					if( $product->getId() === $prodItem->getId() ) {
608
						continue 2;
609
					}
610
				}
611
612
				$manager->save( $manager->create()->setParentId( $basket->getId() )->setCode( $code )->setProductId( $product->getId() ) );
613
			}
614
		}
615
616
		return $this;
617
	}
618
619
620
	/**
621
	 * Saves the ordered products to the storage.
622
	 *
623
	 * @param \Aimeos\MShop\Order\Item\Iface $basket Basket containing ordered products or bundles
624
	 * @return \Aimeos\MShop\Order\Manager\Iface Manager object for chaining method calls
625
	 */
626
	protected function saveProducts( \Aimeos\MShop\Order\Item\Iface $basket ) : \Aimeos\MShop\Order\Manager\Iface
627
	{
628
		$products = $basket->getProducts();
629
		$pos = $products->merge( $products->getProducts()->flat( 1 ) )->max( 'order.product.position' );
630
631
		foreach( $products as $product )
632
		{
633
			if( $product->getParentId() != $basket->getId() ) {
634
				$product->setId( null ); // create new item if copied
635
			}
636
637
			if( !$product->getPosition() ) {
638
				$product->setPosition( ++$pos );
639
			}
640
641
			$product->setParentId( $basket->getId() );
642
643
			foreach( $product->getProducts() as $subProduct )
644
			{
645
				if( $subProduct->getParentId() != $basket->getId() ) {
646
					$subProduct->setId( null ); // create new item if copied
647
				}
648
649
				if( !$subProduct->getPosition() ) {
650
					$subProduct->setPosition( ++$pos );
651
				}
652
653
				$subProduct->setParentId( $basket->getId() );
654
			}
655
		}
656
657
		$this->object()->getSubManager( 'product' )->save( $products );
658
659
		return $this;
660
	}
661
662
663
	/**
664
	 * Saves the services of the order to the storage.
665
	 *
666
	 * @param \Aimeos\MShop\Order\Item\Iface $basket Basket containing service items
667
	 * @return \Aimeos\MShop\Order\Manager\Iface Manager object for chaining method calls
668
	 */
669
	protected function saveServices( \Aimeos\MShop\Order\Item\Iface $basket ) : \Aimeos\MShop\Order\Manager\Iface
670
	{
671
		$services = $basket->getServices()->flat( 1 );
672
673
		foreach( $services as $service )
674
		{
675
			if( $service->getParentId() != $basket->getId() ) {
676
				$service->setId( null ); // create new item if copied
677
			}
678
679
			$service->setParentId( $basket->getId() );
680
		}
681
682
		$this->object()->getSubManager( 'service' )->save( $services );
683
684
		return $this;
685
	}
686
}
687