Passed
Push — master ( 8a9822...bee67c )
by Aimeos
04:54
created

Base::offsetExists()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
c 0
b 0
f 0
nc 1
nop 1
dl 0
loc 3
rs 10
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\Item\Base;
13
14
15
/**
16
 * Abstract order base class with necessary constants and basic methods.
17
 *
18
 * @package MShop
19
 * @subpackage Order
20
 */
21
abstract class Base
22
	extends \Aimeos\MShop\Common\Item\Base
23
	implements \Aimeos\MShop\Order\Item\Base\Iface, \Aimeos\Macro\Iface, \ArrayAccess, \JsonSerializable
24
{
25
	use \Aimeos\MW\Observer\Publisher\Traits;
26
	use \Aimeos\Macro\Macroable;
27
28
29
	// protected is a workaround for serialize problem
30
	protected $coupons;
31
	protected $products;
32
	protected $services = [];
33
	protected $addresses = [];
34
35
36
	/**
37
	 * Initializes the basket object
38
	 *
39
	 * @param \Aimeos\MShop\Price\Item\Iface $price Default price of the basket (usually 0.00)
40
	 * @param \Aimeos\MShop\Locale\Item\Iface $locale Locale item containing the site, language and currency
41
	 * @param array $values Associative list of key/value pairs containing, e.g. the order or user ID
42
	 * @param array $products List of ordered products implementing \Aimeos\MShop\Order\Item\Base\Product\Iface
43
	 * @param array $addresses List of order addresses implementing \Aimeos\MShop\Order\Item\Base\Address\Iface
44
	 * @param array $services List of order services implementing \Aimeos\MShop\Order\Item\Base\Service\Iface
45
	 * @param array $coupons Associative list of coupon codes as keys and ordered products implementing \Aimeos\MShop\Order\Item\Base\Product\Iface as values
46
	 */
47
	public function __construct( \Aimeos\MShop\Price\Item\Iface $price, \Aimeos\MShop\Locale\Item\Iface $locale,
48
		array $values = [], array $products = [], array $addresses = [],
49
		array $services = [], array $coupons = [] )
50
	{
51
		map( $addresses )->implements( \Aimeos\MShop\Order\Item\Base\Address\Iface::class, true );
52
		map( $products )->implements( \Aimeos\MShop\Order\Item\Base\Product\Iface::class, true );
53
		map( $services )->implements( \Aimeos\MShop\Order\Item\Base\Service\Iface::class, true );
54
55
		foreach( $coupons as $couponProducts ) {
56
			map( $couponProducts )->implements( \Aimeos\MShop\Order\Item\Base\Product\Iface::class, true );
57
		}
58
59
		parent::__construct( 'order.base.', $values );
60
61
		$this->coupons = $coupons;
62
		$this->products = $products;
63
64
		foreach( $addresses as $address ) {
65
			$this->addresses[$address->getType()][] = $address;
66
		}
67
68
		foreach( $services as $service ) {
69
			$this->services[$service->getType()][] = $service;
70
		}
71
	}
72
73
74
	/**
75
	 * Specifies the data which should be serialized to JSON by json_encode().
76
	 *
77
	 * @return array<string,mixed> Data to serialize to JSON
78
	 */
79
	#[\ReturnTypeWillChange]
80
	public function jsonSerialize()
81
	{
82
		return parent::jsonSerialize() + [
83
			'coupons' => $this->coupons,
84
			'products' => $this->products,
85
			'services' => $this->services,
86
			'addresses' => $this->addresses,
87
		];
88
	}
89
90
91
	/**
92
	 * Prepares the object for serialization.
93
	 *
94
	 * @return array List of properties that should be serialized
95
	 */
96
	public function __sleep() : array
97
	{
98
		/*
99
		 * Workaround because database connections can't be serialized
100
		 * Listeners will be reattached on wakeup by the order base manager
101
		 */
102
		$this->off();
103
104
		return array_keys( get_object_vars( $this ) );
105
	}
106
107
108
	/**
109
	 * Returns the ID of the items
110
	 *
111
	 * @return string ID of the item or null
112
	 */
113
	public function __toString() : string
114
	{
115
		return (string) $this->getId();
116
	}
117
118
119
	/**
120
	 * Returns the item type
121
	 *
122
	 * @return string Item type, subtypes are separated by slashes
123
	 */
124
	public function getResourceType() : string
125
	{
126
		return 'order/base';
127
	}
128
129
130
	/**
131
	 * Adds the address of the given type to the basket
132
	 *
133
	 * @param \Aimeos\MShop\Order\Item\Base\Address\Iface $address Order address item for the given type
134
	 * @param string $type Address type, usually "billing" or "delivery"
135
	 * @param int|null $position Position of the address in the list
136
	 * @return \Aimeos\MShop\Order\Item\Base\Iface Order base item for method chaining
137
	 */
138
	public function addAddress( \Aimeos\MShop\Order\Item\Base\Address\Iface $address, string $type, int $position = null ) : \Aimeos\MShop\Order\Item\Base\Iface
139
	{
140
		$address = $this->notify( 'addAddress.before', $address );
141
142
		$address = clone $address;
143
		$address = $address->setType( $type );
144
145
		if( $position !== null ) {
146
			$this->addresses[$type][$position] = $address;
147
		} else {
148
			$this->addresses[$type][] = $address;
149
		}
150
151
		$this->setModified();
152
153
		$this->notify( 'addAddress.after', $address );
154
155
		return $this;
156
	}
157
158
159
	/**
160
	 * Deletes an order address from the basket
161
	 *
162
	 * @param string $type Address type defined in \Aimeos\MShop\Order\Item\Base\Address\Base
163
	 * @param int|null $position Position of the address in the list
164
	 * @return \Aimeos\MShop\Order\Item\Base\Iface Order base item for method chaining
165
	 */
166
	public function deleteAddress( string $type, int $position = null ) : \Aimeos\MShop\Order\Item\Base\Iface
167
	{
168
		if( $position === null && isset( $this->addresses[$type] ) || isset( $this->addresses[$type][$position] ) )
0 ignored issues
show
introduced by
Consider adding parentheses for clarity. Current Interpretation: ($position === null && IssetNode) || IssetNode, Probably Intended Meaning: $position === null && (IssetNode || IssetNode)
Loading history...
169
		{
170
			$old = ( isset( $this->addresses[$type][$position] ) ? $this->addresses[$type][$position] : $this->addresses[$type] );
171
			$old = $this->notify( 'deleteAddress.before', $old );
172
173
			if( $position !== null ) {
174
				unset( $this->addresses[$type][$position] );
175
			} else {
176
				unset( $this->addresses[$type] );
177
			}
178
179
			$this->setModified();
180
181
			$this->notify( 'deleteAddress.after', $old );
182
		}
183
184
		return $this;
185
	}
186
187
188
	/**
189
	 * Returns the order address depending on the given type
190
	 *
191
	 * @param string $type Address type, usually "billing" or "delivery"
192
	 * @param int|null $position Address position in list of addresses
193
	 * @return \Aimeos\MShop\Order\Item\Base\Address\Iface[]|\Aimeos\MShop\Order\Item\Base\Address\Iface Order address item or list of
194
	 */
195
	public function getAddress( string $type, int $position = null )
196
	{
197
		if( $position !== null )
198
		{
199
			if( isset( $this->addresses[$type][$position] ) ) {
200
				return $this->addresses[$type][$position];
201
			}
202
203
			throw new \Aimeos\MShop\Order\Exception( sprintf( 'Address not available' ) );
204
		}
205
206
		return ( isset( $this->addresses[$type] ) ? $this->addresses[$type] : [] );
207
	}
208
209
210
	/**
211
	 * Returns all addresses that are part of the basket
212
	 *
213
	 * @return \Aimeos\Map Associative list of address items implementing
214
	 *  \Aimeos\MShop\Order\Item\Base\Address\Iface with "billing" or "delivery" as key
215
	 */
216
	public function getAddresses() : \Aimeos\Map
217
	{
218
		return map( $this->addresses );
219
	}
220
221
222
	/**
223
	 * Replaces all addresses in the current basket with the new ones
224
	 *
225
	 * @param \Aimeos\Map|array $map Associative list of order addresses as returned by getAddresses()
226
	 * @return \Aimeos\MShop\Order\Item\Base\Iface Order base item for method chaining
227
	 */
228
	public function setAddresses( iterable $map ) : \Aimeos\MShop\Order\Item\Base\Iface
229
	{
230
		$map = $this->notify( 'setAddresses.before', $map );
231
232
		foreach( $map as $type => $items ) {
233
			$this->checkAddresses( $items, $type );
234
		}
235
236
		$old = $this->addresses;
237
		$this->addresses = is_map( $map ) ? $map->toArray() : $map;
238
		$this->setModified();
239
240
		$this->notify( 'setAddresses.after', $old );
241
242
		return $this;
243
	}
244
245
246
	/**
247
	 * Adds a coupon code and the given product item to the basket
248
	 *
249
	 * @param string $code Coupon code
250
	 * @return \Aimeos\MShop\Order\Item\Base\Iface Order base item for method chaining
251
	 */
252
	public function addCoupon( string $code ) : \Aimeos\MShop\Order\Item\Base\Iface
253
	{
254
		if( !isset( $this->coupons[$code] ) )
255
		{
256
			$code = $this->notify( 'addCoupon.before', $code );
257
258
			$this->coupons[$code] = [];
259
			$this->setModified();
260
261
			$this->notify( 'addCoupon.after', $code );
262
		}
263
264
		return $this;
265
	}
266
267
268
	/**
269
	 * Removes a coupon and the related product items from the basket
270
	 *
271
	 * @param string $code Coupon code
272
	 * @return \Aimeos\MShop\Order\Item\Base\Iface Order base item for method chaining
273
	 */
274
	public function deleteCoupon( string $code ) : \Aimeos\MShop\Order\Item\Base\Iface
275
	{
276
		if( isset( $this->coupons[$code] ) )
277
		{
278
			$old = [$code => $this->coupons[$code]];
279
			$old = $this->notify( 'deleteCoupon.before', $old );
280
281
			foreach( $this->coupons[$code] as $product )
282
			{
283
				if( ( $key = array_search( $product, $this->products, true ) ) !== false ) {
284
					unset( $this->products[$key] );
285
				}
286
			}
287
288
			unset( $this->coupons[$code] );
289
			$this->setModified();
290
291
			$this->notify( 'deleteCoupon.after', $old );
292
		}
293
294
		return $this;
295
	}
296
297
298
	/**
299
	 * Returns the available coupon codes and the lists of affected product items
300
	 *
301
	 * @return \Aimeos\Map Associative array of codes and lists of product items
302
	 *  implementing \Aimeos\MShop\Order\Product\Iface
303
	 */
304
	public function getCoupons() : \Aimeos\Map
305
	{
306
		return map( $this->coupons );
307
	}
308
309
310
	/**
311
	 * Sets a coupon code and the given product items in the basket.
312
	 *
313
	 * @param string $code Coupon code
314
	 * @param \Aimeos\MShop\Order\Item\Base\Product\Iface[] $products List of coupon products
315
	 * @return \Aimeos\MShop\Order\Item\Base\Iface Order base item for method chaining
316
	 */
317
	public function setCoupon( string $code, iterable $products = [] ) : \Aimeos\MShop\Order\Item\Base\Iface
318
	{
319
		$new = $this->notify( 'setCoupon.before', [$code => $products] );
320
321
		$products = $this->checkProducts( map( $new )->first( [] ) );
322
323
		if( isset( $this->coupons[$code] ) )
324
		{
325
			foreach( $this->coupons[$code] as $product )
326
			{
327
				if( ( $key = array_search( $product, $this->products, true ) ) !== false ) {
328
					unset( $this->products[$key] );
329
				}
330
			}
331
		}
332
333
		foreach( $products as $product ) {
334
			$this->products[] = $product;
335
		}
336
337
		$old = isset( $this->coupons[$code] ) ? [$code => $this->coupons[$code]] : [];
338
		$this->coupons[$code] = is_map( $products ) ? $products->toArray() : $products;
339
		$this->setModified();
340
341
		$this->notify( 'setCoupon.after', $old );
342
343
		return $this;
344
	}
345
346
347
	/**
348
	 * Replaces all coupons in the current basket with the new ones
349
	 *
350
	 * @param iterable $map Associative list of order coupons as returned by getCoupons()
351
	 * @return \Aimeos\MShop\Order\Item\Base\Iface Order base item for method chaining
352
	 */
353
	public function setCoupons( iterable $map ) : \Aimeos\MShop\Order\Item\Base\Iface
354
	{
355
		$map = $this->notify( 'setCoupons.before', $map );
356
357
		foreach( $map as $code => $products ) {
358
			$map[$code] = $this->checkProducts( $products );
359
		}
360
361
		foreach( $this->coupons as $code => $products )
362
		{
363
			foreach( $products as $product )
364
			{
365
				if( ( $key = array_search( $product, $this->products, true ) ) !== false ) {
366
					unset( $this->products[$key] );
367
				}
368
			}
369
		}
370
371
		foreach( $map as $code => $products )
372
		{
373
			foreach( $products as $product ) {
374
				$this->products[] = $product;
375
			}
376
		}
377
378
		$old = $this->coupons;
379
		$this->coupons = is_map( $map ) ? $map->toArray() : $map;
380
		$this->setModified();
381
382
		$this->notify( 'setCoupons.after', $old );
383
384
		return $this;
385
	}
386
387
388
	/**
389
	 * Adds an order product item to the basket
390
	 * If a similar item is found, only the quantity is increased.
391
	 *
392
	 * @param \Aimeos\MShop\Order\Item\Base\Product\Iface $item Order product item to be added
393
	 * @param int|null $position position of the new order product item
394
	 * @return \Aimeos\MShop\Order\Item\Base\Iface Order base item for method chaining
395
	 */
396
	public function addProduct( \Aimeos\MShop\Order\Item\Base\Product\Iface $item, int $position = null ) : \Aimeos\MShop\Order\Item\Base\Iface
397
	{
398
		$item = $this->notify( 'addProduct.before', $item );
399
400
		$this->checkProducts( [$item] );
401
402
		if( $position !== null ) {
403
			$this->products[$position] = $item;
404
		} elseif( ( $pos = $this->getSameProduct( $item, $this->products ) ) !== null ) {
405
			$item = $this->products[$pos]->setQuantity( $this->products[$pos]->getQuantity() + $item->getQuantity() );
406
		} else {
407
			$this->products[] = $item;
408
		}
409
410
		ksort( $this->products );
411
		$this->setModified();
412
413
		$this->notify( 'addProduct.after', $item );
414
415
		return $this;
416
	}
417
418
419
	/**
420
	 * Deletes an order product item from the basket
421
	 *
422
	 * @param int $position Position of the order product item
423
	 * @return \Aimeos\MShop\Order\Item\Base\Iface Order base item for method chaining
424
	 */
425
	public function deleteProduct( int $position ) : \Aimeos\MShop\Order\Item\Base\Iface
426
	{
427
		if( isset( $this->products[$position] ) )
428
		{
429
			$old = $this->products[$position];
430
			$old = $this->notify( 'deleteProduct.before', $old );
431
432
			unset( $this->products[$position] );
433
			$this->setModified();
434
435
			$this->notify( 'deleteProduct.after', $old );
436
		}
437
438
		return $this;
439
	}
440
441
442
	/**
443
	 * Returns the product item of an basket specified by its key
444
	 *
445
	 * @param int $key Key returned by getProducts() identifying the requested product
446
	 * @return \Aimeos\MShop\Order\Item\Base\Product\Iface Product item of an order
447
	 */
448
	public function getProduct( int $key ) : \Aimeos\MShop\Order\Item\Base\Product\Iface
449
	{
450
		if( !isset( $this->products[$key] ) ) {
451
			throw new \Aimeos\MShop\Order\Exception( sprintf( 'Product not available' ) );
452
		}
453
454
		return $this->products[$key];
455
	}
456
457
458
	/**
459
	 * Returns the product items that are or should be part of a basket
460
	 *
461
	 * @return \Aimeos\Map List of order product items implementing \Aimeos\MShop\Order\Item\Base\Product\Iface
462
	 */
463
	public function getProducts() : \Aimeos\Map
464
	{
465
		return map( $this->products );
466
	}
467
468
469
	/**
470
	 * Replaces all products in the current basket with the new ones
471
	 *
472
	 * @param \Aimeos\MShop\Order\Item\Base\Product\Iface[] $map Associative list of ordered products as returned by getProducts()
473
	 * @return \Aimeos\MShop\Order\Item\Base\Iface Order base item for method chaining
474
	 */
475
	public function setProducts( iterable $map ) : \Aimeos\MShop\Order\Item\Base\Iface
476
	{
477
		$map = $this->notify( 'setProducts.before', $map );
478
479
		$this->checkProducts( $map );
480
481
		$old = $this->products;
482
		$this->products = is_map( $map ) ? $map->toArray() : $map;
483
		$this->setModified();
484
485
		$this->notify( 'setProducts.after', $old );
486
487
		return $this;
488
	}
489
490
491
	/**
492
	 * Adds an order service to the basket
493
	 *
494
	 * @param \Aimeos\MShop\Order\Item\Base\Service\Iface $service Order service item for the given domain
495
	 * @param string $type Service type constant from \Aimeos\MShop\Order\Item\Service\Base
496
	 * @param int|null $position Position of the service in the list to overwrite
497
	 * @return \Aimeos\MShop\Order\Item\Base\Iface Order base item for method chaining
498
	 */
499
	public function addService( \Aimeos\MShop\Order\Item\Base\Service\Iface $service, string $type, int $position = null ) : \Aimeos\MShop\Order\Item\Base\Iface
500
	{
501
		$service = $this->notify( 'addService.before', $service );
502
503
		$this->checkPrice( $service->getPrice() );
504
505
		$service = clone $service;
506
		$service = $service->setType( $type );
507
508
		if( $position !== null ) {
509
			$this->services[$type][$position] = $service;
510
		} else {
511
			$this->services[$type][] = $service;
512
		}
513
514
		$this->setModified();
515
516
		$this->notify( 'addService.after', $service );
517
518
		return $this;
519
	}
520
521
522
	/**
523
	 * Deletes an order service from the basket
524
	 *
525
	 * @param string $type Service type constant from \Aimeos\MShop\Order\Item\Service\Base
526
	 * @param int|null $position Position of the service in the list to delete
527
	 * @return \Aimeos\MShop\Order\Item\Base\Iface Order base item for method chaining
528
	 */
529
	public function deleteService( string $type, int $position = null ) : \Aimeos\MShop\Order\Item\Base\Iface
530
	{
531
		if( $position === null && isset( $this->services[$type] ) || isset( $this->services[$type][$position] ) )
0 ignored issues
show
introduced by
Consider adding parentheses for clarity. Current Interpretation: ($position === null && IssetNode) || IssetNode, Probably Intended Meaning: $position === null && (IssetNode || IssetNode)
Loading history...
532
		{
533
			$old = ( isset( $this->services[$type][$position] ) ? $this->services[$type][$position] : $this->services[$type] );
534
			$old = $this->notify( 'deleteService.before', $old );
535
536
			if( $position !== null ) {
537
				unset( $this->services[$type][$position] );
538
			} else {
539
				unset( $this->services[$type] );
540
			}
541
542
			$this->setModified();
543
544
			$this->notify( 'deleteService.after', $old );
545
		}
546
547
		return $this;
548
	}
549
550
551
	/**
552
	 * Returns the order services depending on the given type
553
	 *
554
	 * @param string $type Service type constant from \Aimeos\MShop\Order\Item\Service\Base
555
	 * @param int|null $position Position of the service in the list to retrieve
556
	 * @return \Aimeos\MShop\Order\Item\Base\Service\Iface[]|\Aimeos\MShop\Order\Item\Base\Service\Iface
557
	 * 	Order service item or list of items for the requested type
558
	 * @throws \Aimeos\MShop\Order\Exception If no service for the given type and position is found
559
	 */
560
	public function getService( string $type, int $position = null )
561
	{
562
		if( $position !== null )
563
		{
564
			if( isset( $this->services[$type][$position] ) ) {
565
				return $this->services[$type][$position];
566
			}
567
568
			throw new \Aimeos\MShop\Order\Exception( sprintf( 'Service not available' ) );
569
		}
570
571
		return ( isset( $this->services[$type] ) ? $this->services[$type] : [] );
572
	}
573
574
575
	/**
576
	 * Returns all services that are part of the basket
577
	 *
578
	 * @return \Aimeos\Map Associative list of service types ("delivery" or "payment") as keys and list of
579
	 *	service items implementing \Aimeos\MShop\Order\Service\Iface as values
580
	 */
581
	public function getServices() : \Aimeos\Map
582
	{
583
		return map( $this->services );
584
	}
585
586
587
	/**
588
	 * Replaces all services in the current basket with the new ones
589
	 *
590
	 * @param \Aimeos\MShop\Order\Item\Base\Service\Iface[] $map Associative list of order services as returned by getServices()
591
	 * @return \Aimeos\MShop\Order\Item\Base\Iface Order base item for method chaining
592
	 */
593
	public function setServices( iterable $map ) : \Aimeos\MShop\Order\Item\Base\Iface
594
	{
595
		$map = $this->notify( 'setServices.before', $map );
596
597
		foreach( $map as $type => $services ) {
598
			$map[$type] = $this->checkServices( $services, $type );
599
		}
600
601
		$old = $this->services;
602
		$this->services = is_map( $map ) ? $map->toArray() : $map;
603
		$this->setModified();
604
605
		$this->notify( 'setServices.after', $old );
606
607
		return $this;
608
	}
609
610
611
	/**
612
	 * Returns the service costs
613
	 *
614
	 * @param string $type Service type like "delivery" or "payment"
615
	 * @return float Service costs value
616
	 */
617
	public function getCosts( string $type = 'delivery' ) : float
618
	{
619
		$costs = 0;
620
621
		if( $type === 'delivery' )
622
		{
623
			foreach( $this->getProducts() as $product ) {
624
				$costs += $product->getPrice()->getCosts() * $product->getQuantity();
625
			}
626
		}
627
628
		foreach( $this->getService( $type ) as $service ) {
629
			$costs += $service->getPrice()->getCosts();
630
		}
631
632
		return $costs;
633
	}
634
635
636
	/**
637
	 * Returns a list of tax names and values
638
	 *
639
	 * @return array Associative list of tax names as key and price items as value
640
	 */
641
	public function getTaxes() : array
642
	{
643
		$taxes = [];
644
645
		foreach( $this->getProducts() as $product )
646
		{
647
			$price = $product->getPrice();
648
649
			foreach( $price->getTaxrates() as $name => $taxrate )
650
			{
651
				$price = (clone $price)->setTaxRate( $taxrate );
652
653
				if( isset( $taxes[$name][$taxrate] ) ) {
654
					$taxes[$name][$taxrate]->addItem( $price, $product->getQuantity() );
655
				} else {
656
					$taxes[$name][$taxrate] = $price->addItem( $price, $product->getQuantity() - 1 );
657
				}
658
			}
659
		}
660
661
		foreach( $this->getServices() as $services )
662
		{
663
			foreach( $services as $service )
664
			{
665
				$price = $service->getPrice();
666
667
				foreach( $price->getTaxrates() as $name => $taxrate )
668
				{
669
					$price = (clone $price)->setTaxRate( $taxrate );
670
671
					if( isset( $taxes[$name][$taxrate] ) ) {
672
						$taxes[$name][$taxrate]->addItem( $price );
673
					} else {
674
						$taxes[$name][$taxrate] = $price;
675
					}
676
				}
677
			}
678
		}
679
680
		return $taxes;
681
	}
682
683
684
	/**
685
	 * Checks if the price uses the same currency as the price in the basket.
686
	 *
687
	 * @param \Aimeos\MShop\Price\Item\Iface $item Price item
688
	 * @return void
689
	 */
690
	abstract protected function checkPrice( \Aimeos\MShop\Price\Item\Iface $item );
691
692
693
	/**
694
	 * Checks if all order addresses are valid
695
	 *
696
	 * @param \Aimeos\MShop\Order\Item\Base\Address\Iface[] $items Order address items
697
	 * @param string $type Address type constant from \Aimeos\MShop\Order\Item\Base\Address\Base
698
	 * @return \Aimeos\MShop\Order\Item\Base\Address\Iface[] List of checked items
699
	 * @throws \Aimeos\MShop\Exception If one of the order addresses is invalid
700
	 */
701
	protected function checkAddresses( iterable $items, string $type ) : iterable
702
	{
703
		map( $items )->implements( \Aimeos\MShop\Order\Item\Base\Address\Iface::class, true );
704
705
		foreach( $items as $key => $item ) {
706
			$items[$key] = $item->setType( $type );
707
		}
708
709
		return $items;
710
	}
711
712
713
	/**
714
	 * Checks if all order products are valid
715
	 *
716
	 * @param \Aimeos\MShop\Order\Item\Base\Product\Iface[] $items Order product items
717
	 * @return \Aimeos\MShop\Order\Item\Base\Product\Iface[] List of checked items
718
	 * @throws \Aimeos\MShop\Exception If one of the order products is invalid
719
	 */
720
	protected function checkProducts( iterable $items ) : \Aimeos\Map
721
	{
722
		map( $items )->implements( \Aimeos\MShop\Order\Item\Base\Product\Iface::class, true );
723
724
		foreach( $items as $key => $item )
725
		{
726
			if( $item->getProductCode() === '' ) {
727
				throw new \Aimeos\MShop\Order\Exception( sprintf( 'Product does not contain the SKU code' ) );
728
			}
729
730
			$this->checkPrice( $item->getPrice() );
731
		}
732
733
		return map( $items );
734
	}
735
736
737
	/**
738
	 * Checks if all order services are valid
739
	 *
740
	 * @param \Aimeos\MShop\Order\Item\Base\Service\Iface[] $items Order service items
741
	 * @param string $type Service type constant from \Aimeos\MShop\Order\Item\Base\Service\Base
742
	 * @return \Aimeos\MShop\Order\Item\Base\Service\Iface[] List of checked items
743
	 * @throws \Aimeos\MShop\Exception If one of the order services is invalid
744
	 */
745
	protected function checkServices( iterable $items, string $type ) : iterable
746
	{
747
		map( $items )->implements( \Aimeos\MShop\Order\Item\Base\Service\Iface::class, true );
748
749
		foreach( $items as $key => $item )
750
		{
751
			$this->checkPrice( $item->getPrice() );
752
			$items[$key] = $item->setType( $type );
753
		}
754
755
		return $items;
756
	}
757
758
759
	/**
760
	 * Tests if the given product is similar to an existing one.
761
	 * Similarity is described by the equality of properties so the quantity of
762
	 * the existing product can be updated.
763
	 *
764
	 * @param \Aimeos\MShop\Order\Item\Base\Product\Iface $item Order product item
765
	 * @param \Aimeos\MShop\Order\Item\Base\Product\Iface[] $products List of order product items to check against
766
	 * @return int|null Positon of the same product in the product list of false if product is unique
767
	 * @throws \Aimeos\MShop\Order\Exception If no similar item was found
768
	 */
769
	protected function getSameProduct( \Aimeos\MShop\Order\Item\Base\Product\Iface $item, iterable $products ) : ?int
770
	{
771
		$map = [];
772
		$count = 0;
773
774
		foreach( $item->getAttributeItems() as $attributeItem )
775
		{
776
			$key = md5( $attributeItem->getCode() . json_encode( $attributeItem->getValue() ) );
777
			$map[$key] = $attributeItem;
778
			$count++;
779
		}
780
781
		foreach( $products as $position => $product )
782
		{
783
			if( $product->compare( $item ) === false ) {
784
				continue;
785
			}
786
787
			$prodAttributes = $product->getAttributeItems();
788
789
			if( count( $prodAttributes ) !== $count ) {
790
				continue;
791
			}
792
793
			foreach( $prodAttributes as $attribute )
794
			{
795
				$key = md5( $attribute->getCode() . json_encode( $attribute->getValue() ) );
796
797
				if( isset( $map[$key] ) === false || $map[$key]->getQuantity() != $attribute->getQuantity() ) {
798
					continue 2; // jump to outer loop
799
				}
800
			}
801
802
			return $position;
803
		}
804
805
		return null;
806
	}
807
}
808