Passed
Push — master ( dbf483...09711f )
by Aimeos
07:52
created

Base::setCoupon()   B

Complexity

Conditions 7
Paths 16

Size

Total Lines 27
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 7
eloc 13
nc 16
nop 2
dl 0
loc 27
rs 8.8333
c 0
b 0
f 0
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-2021
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 implements \Aimeos\MShop\Order\Item\Base\Iface, \ArrayAccess
22
{
23
	use \Aimeos\MW\Observer\Publisher\Traits;
24
25
26
	/**
27
	 * Check and load/store only basic basket content
28
	 */
29
	const PARTS_NONE = 0;
30
31
	/**
32
	 * Check and load/store basket with addresses
33
	 */
34
	const PARTS_ADDRESS = 1;
35
36
	/**
37
	 * Load/store basket with coupons
38
	 */
39
	const PARTS_COUPON = 2;
40
41
	/**
42
	 * Check and load/store basket with products
43
	 */
44
	const PARTS_PRODUCT = 4;
45
46
	/**
47
	 * Check and load/store basket with delivery/payment
48
	 */
49
	const PARTS_SERVICE = 8;
50
51
	/**
52
	 * Check and load/store basket with all parts.
53
	 */
54
	const PARTS_ALL = 15;
55
56
57
	// protected is a workaround for serialize problem
58
	protected $bdata;
59
	protected $coupons;
60
	protected $products;
61
	protected $services = [];
62
	protected $addresses = [];
63
	protected $modified = false;
64
65
66
	/**
67
	 * Initializes the basket object
68
	 *
69
	 * @param \Aimeos\MShop\Price\Item\Iface $price Default price of the basket (usually 0.00)
70
	 * @param \Aimeos\MShop\Locale\Item\Iface $locale Locale item containing the site, language and currency
71
	 * @param array $values Associative list of key/value pairs containing, e.g. the order or user ID
72
	 * @param array $products List of ordered products implementing \Aimeos\MShop\Order\Item\Base\Product\Iface
73
	 * @param array $addresses List of order addresses implementing \Aimeos\MShop\Order\Item\Base\Address\Iface
74
	 * @param array $services List of order services implementing \Aimeos\MShop\Order\Item\Base\Service\Iface
75
	 * @param array $coupons Associative list of coupon codes as keys and ordered products implementing \Aimeos\MShop\Order\Item\Base\Product\Iface as values
76
	 */
77
	public function __construct( \Aimeos\MShop\Price\Item\Iface $price, \Aimeos\MShop\Locale\Item\Iface $locale,
78
		array $values = [], array $products = [], array $addresses = [],
79
		array $services = [], array $coupons = [] )
80
	{
81
		\Aimeos\MW\Common\Base::checkClassList( \Aimeos\MShop\Order\Item\Base\Address\Iface::class, $addresses );
82
		\Aimeos\MW\Common\Base::checkClassList( \Aimeos\MShop\Order\Item\Base\Product\Iface::class, $products );
83
		\Aimeos\MW\Common\Base::checkClassList( \Aimeos\MShop\Order\Item\Base\Service\Iface::class, $services );
84
85
		foreach( $coupons as $couponProducts ) {
86
			\Aimeos\MW\Common\Base::checkClassList( \Aimeos\MShop\Order\Item\Base\Product\Iface::class, $couponProducts );
87
		}
88
89
		$this->bdata = $values;
90
		$this->coupons = $coupons;
91
		$this->products = $products;
92
93
		foreach( $addresses as $address ) {
94
			$this->addresses[$address->getType()][$address->getPosition()] = $address;
95
		}
96
97
		foreach( $services as $service ) {
98
			$this->services[$service->getType()][$service->getPosition()] = $service;
99
		}
100
	}
101
102
103
	/**
104
	 * Clones internal objects of the order base item.
105
	 */
106
	public function __clone()
107
	{
108
	}
109
110
111
	/**
112
	 * Returns the item property for the given name
113
	 *
114
	 * @param string $name Name of the property
115
	 * @return mixed|null Property value or null if property is unknown
116
	 */
117
	public function __get( string $name )
118
	{
119
		if( array_key_exists( $name, $this->bdata ) ) {
120
			return $this->bdata[$name];
121
		}
122
123
		return null;
124
	}
125
126
127
	/**
128
	 * Tests if the item property for the given name is available
129
	 *
130
	 * @param string $name Name of the property
131
	 * @return bool True if the property exists, false if not
132
	 */
133
	public function __isset( string $name ) : bool
134
	{
135
		return array_key_exists( $name, $this->bdata );
136
	}
137
138
139
	/**
140
	 * Sets the new item property for the given name
141
	 *
142
	 * @param string $name Name of the property
143
	 * @param mixed $value New property value
144
	 */
145
	public function __set( string $name, $value )
146
	{
147
		if( !array_key_exists( $name, $this->bdata ) || $this->bdata[$name] !== $value ) {
148
			$this->setModified();
149
		}
150
151
		$this->bdata[$name] = $value;
152
	}
153
154
155
	/**
156
	 * Tests if the item property for the given name is available
157
	 *
158
	 * @param string $name Name of the property
159
	 * @return bool True if the property exists, false if not
160
	 */
161
	public function offsetExists( $name )
162
	{
163
		return array_key_exists( $name, $this->bdata );
164
	}
165
166
167
	/**
168
	 * Returns the item property for the given name
169
	 *
170
	 * @param string $name Name of the property
171
	 * @return mixed|null Property value or null if property is unknown
172
	 */
173
	public function offsetGet( $name )
174
	{
175
		if( array_key_exists( $name, $this->bdata ) ) {
176
			return $this->bdata[$name];
177
		}
178
179
		return null;
180
	}
181
182
183
	/**
184
	 * Sets the new item property for the given name
185
	 *
186
	 * @param string $name Name of the property
187
	 * @param mixed $value New property value
188
	 */
189
	public function offsetSet( $name, $value )
190
	{
191
		if( !array_key_exists( $name, $this->bdata ) || $this->bdata[$name] !== $value ) {
192
			$this->setModified();
193
		}
194
195
		$this->bdata[$name] = $value;
196
	}
197
198
199
	/**
200
	 * Removes an item property
201
	 * This is not supported by items
202
	 *
203
	 * @param string $name Name of the property
204
	 * @throws \LogicException Always thrown because this method isn't supported
205
	 */
206
	public function offsetUnset( $name )
207
	{
208
		throw new \LogicException( 'Not implemented' );
209
	}
210
211
212
	/**
213
	 * Prepares the object for serialization.
214
	 *
215
	 * @return array List of properties that should be serialized
216
	 */
217
	public function __sleep() : array
218
	{
219
		/*
220
		 * Workaround because database connections can't be serialized
221
		 * Listeners will be reattached on wakeup by the order base manager
222
		 */
223
		$this->off();
224
225
		return array_keys( get_object_vars( $this ) );
226
	}
227
228
229
	/**
230
	 * Returns the ID of the items
231
	 *
232
	 * @return string ID of the item or null
233
	 */
234
	public function __toString() : string
235
	{
236
		return (string) $this->getId();
237
	}
238
239
240
	/**
241
	 * Assigns multiple key/value pairs to the item
242
	 *
243
	 * @param iterable $pairs Associative list of key/value pairs
244
	 * @return \Aimeos\MShop\Common\Item\Iface Item for method chaining
245
	 */
246
	public function assign( iterable $pairs ) : \Aimeos\MShop\Common\Item\Iface
247
	{
248
		foreach( $pairs as $key => $value ) {
249
			$this->set( $key, $value );
250
		}
251
252
		return $this;
253
	}
254
255
256
	/**
257
	 * Returns the item property for the given name
258
	 *
259
	 * @param string $name Name of the property
260
	 * @param mixed $default Default value if property is unknown
261
	 * @return mixed|null Property value or default value if property is unknown
262
	 */
263
	public function get( string $name, $default = null )
264
	{
265
		if( isset( $this->bdata[$name] ) ) {
266
			return $this->bdata[$name];
267
		}
268
269
		return $default;
270
	}
271
272
273
	/**
274
	 * Sets the new item property for the given name
275
	 *
276
	 * @param string $name Name of the property
277
	 * @param mixed $value New property value
278
	 * @return \Aimeos\MShop\Order\Item\Base\Iface Order base item for method chaining
279
	 */
280
	public function set( string $name, $value ) : \Aimeos\MShop\Common\Item\Iface
281
	{
282
		if( !array_key_exists( $name, $this->bdata ) || $this->bdata[$name] !== $value )
283
		{
284
			$this->bdata[$name] = $value;
285
			$this->setModified();
286
		}
287
288
		return $this;
289
	}
290
291
292
	/**
293
	 * Returns the item type
294
	 *
295
	 * @return string Item type, subtypes are separated by slashes
296
	 */
297
	public function getResourceType() : string
298
	{
299
		return 'order/base';
300
	}
301
302
303
	/**
304
	 * Tests if the order object was modified.
305
	 *
306
	 * @return bool True if modified, false if not
307
	 */
308
	public function isModified() : bool
309
	{
310
		return $this->modified;
311
	}
312
313
314
	/**
315
	 * Sets the modified flag of the object.
316
	 *
317
	 * @return \Aimeos\MShop\Order\Item\Base\Iface Order base item for method chaining
318
	 */
319
	public function setModified() : \Aimeos\MShop\Order\Item\Base\Iface
320
	{
321
		$this->modified = true;
322
		return $this;
323
	}
324
325
326
	/**
327
	 * Adds the address of the given type to the basket
328
	 *
329
	 * @param \Aimeos\MShop\Order\Item\Base\Address\Iface $address Order address item for the given type
330
	 * @param string $type Address type, usually "billing" or "delivery"
331
	 * @param int|null $position Position of the address in the list
332
	 * @return \Aimeos\MShop\Order\Item\Base\Iface Order base item for method chaining
333
	 */
334
	public function addAddress( \Aimeos\MShop\Order\Item\Base\Address\Iface $address, string $type, int $position = null ) : \Aimeos\MShop\Order\Item\Base\Iface
335
	{
336
		$address = $this->notify( 'addAddress.before', $address );
337
338
		$address = clone $address;
339
		$address = $address->setType( $type );
340
341
		if( $position !== null ) {
342
			$this->addresses[$type][$position] = $address;
343
		} else {
344
			$this->addresses[$type][] = $address;
345
		}
346
347
		$this->setModified();
348
349
		$this->notify( 'addAddress.after', $address );
350
351
		return $this;
352
	}
353
354
355
	/**
356
	 * Deletes an order address from the basket
357
	 *
358
	 * @param string $type Address type defined in \Aimeos\MShop\Order\Item\Base\Address\Base
359
	 * @param int|null $position Position of the address in the list
360
	 * @return \Aimeos\MShop\Order\Item\Base\Iface Order base item for method chaining
361
	 */
362
	public function deleteAddress( string $type, int $position = null ) : \Aimeos\MShop\Order\Item\Base\Iface
363
	{
364
		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...
365
		{
366
			$old = ( isset( $this->addresses[$type][$position] ) ? $this->addresses[$type][$position] : $this->addresses[$type] );
367
			$old = $this->notify( 'deleteAddress.before', $old );
368
369
			if( $position !== null ) {
370
				unset( $this->addresses[$type][$position] );
371
			} else {
372
				unset( $this->addresses[$type] );
373
			}
374
375
			$this->setModified();
376
377
			$this->notify( 'deleteAddress.after', $old );
378
		}
379
380
		return $this;
381
	}
382
383
384
	/**
385
	 * Returns the order address depending on the given type
386
	 *
387
	 * @param string $type Address type, usually "billing" or "delivery"
388
	 * @param int|null $position Address position in list of addresses
389
	 * @return \Aimeos\MShop\Order\Item\Base\Address\Iface[]|\Aimeos\MShop\Order\Item\Base\Address\Iface Order address item or list of
390
	 */
391
	public function getAddress( string $type, int $position = null )
392
	{
393
		if( $position !== null )
394
		{
395
			if( isset( $this->addresses[$type][$position] ) ) {
396
				return $this->addresses[$type][$position];
397
			}
398
399
			throw new \Aimeos\MShop\Order\Exception( sprintf( 'Address not available' ) );
400
		}
401
402
		return ( isset( $this->addresses[$type] ) ? $this->addresses[$type] : [] );
403
	}
404
405
406
	/**
407
	 * Returns all addresses that are part of the basket
408
	 *
409
	 * @return \Aimeos\Map Associative list of address items implementing
410
	 *  \Aimeos\MShop\Order\Item\Base\Address\Iface with "billing" or "delivery" as key
411
	 */
412
	public function getAddresses() : \Aimeos\Map
413
	{
414
		return map( $this->addresses );
415
	}
416
417
418
	/**
419
	 * Replaces all addresses in the current basket with the new ones
420
	 *
421
	 * @param \Aimeos\Map|array $map Associative list of order addresses as returned by getAddresses()
422
	 * @return \Aimeos\MShop\Order\Item\Base\Iface Order base item for method chaining
423
	 */
424
	public function setAddresses( iterable $map ) : \Aimeos\MShop\Order\Item\Base\Iface
425
	{
426
		$map = $this->notify( 'setAddresses.before', $map );
427
428
		foreach( $map as $type => $items ) {
429
			$this->checkAddresses( $items, $type );
430
		}
431
432
		$old = $this->addresses;
433
		$this->addresses = is_map( $map ) ? $map->toArray() : $map;
434
		$this->setModified();
435
436
		$this->notify( 'setAddresses.after', $old );
437
438
		return $this;
439
	}
440
441
442
	/**
443
	 * Adds a coupon code and the given product item to the basket
444
	 *
445
	 * @param string $code Coupon code
446
	 * @return \Aimeos\MShop\Order\Item\Base\Iface Order base item for method chaining
447
	 */
448
	public function addCoupon( string $code ) : \Aimeos\MShop\Order\Item\Base\Iface
449
	{
450
		if( !isset( $this->coupons[$code] ) )
451
		{
452
			$code = $this->notify( 'addCoupon.before', $code );
453
454
			$this->coupons[$code] = [];
455
			$this->setModified();
456
457
			$this->notify( 'addCoupon.after', $code );
458
		}
459
460
		return $this;
461
	}
462
463
464
	/**
465
	 * Removes a coupon and the related product items from the basket
466
	 *
467
	 * @param string $code Coupon code
468
	 * @return \Aimeos\MShop\Order\Item\Base\Iface Order base item for method chaining
469
	 */
470
	public function deleteCoupon( string $code ) : \Aimeos\MShop\Order\Item\Base\Iface
471
	{
472
		if( isset( $this->coupons[$code] ) )
473
		{
474
			$old = [$code => $this->coupons[$code]];
475
			$old = $this->notify( 'deleteCoupon.before', $old );
476
477
			foreach( $this->coupons[$code] as $product )
478
			{
479
				if( ( $key = array_search( $product, $this->products, true ) ) !== false ) {
0 ignored issues
show
Bug introduced by
$this->products of type iterable is incompatible with the type array expected by parameter $haystack of array_search(). ( Ignorable by Annotation )

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

479
				if( ( $key = array_search( $product, /** @scrutinizer ignore-type */ $this->products, true ) ) !== false ) {
Loading history...
480
					unset( $this->products[$key] );
481
				}
482
			}
483
484
			unset( $this->coupons[$code] );
485
			$this->setModified();
486
487
			$this->notify( 'deleteCoupon.after', $old );
488
		}
489
490
		return $this;
491
	}
492
493
494
	/**
495
	 * Returns the available coupon codes and the lists of affected product items
496
	 *
497
	 * @return \Aimeos\Map Associative array of codes and lists of product items
498
	 *  implementing \Aimeos\MShop\Order\Product\Iface
499
	 */
500
	public function getCoupons() : \Aimeos\Map
501
	{
502
		return map( $this->coupons );
503
	}
504
505
506
	/**
507
	 * Sets a coupon code and the given product items in the basket.
508
	 *
509
	 * @param string $code Coupon code
510
	 * @param \Aimeos\MShop\Order\Item\Base\Product\Iface[] $products List of coupon products
511
	 * @return \Aimeos\MShop\Order\Item\Base\Iface Order base item for method chaining
512
	 */
513
	public function setCoupon( string $code, iterable $products = [] ) : \Aimeos\MShop\Order\Item\Base\Iface
514
	{
515
		$new = $this->notify( 'setCoupon.before', [$code => $products] );
516
517
		$products = $this->checkProducts( map( $new )->first( [] ) );
518
519
		if( isset( $this->coupons[$code] ) )
520
		{
521
			foreach( $this->coupons[$code] as $product )
522
			{
523
				if( ( $key = array_search( $product, $this->products, true ) ) !== false ) {
0 ignored issues
show
Bug introduced by
$this->products of type iterable is incompatible with the type array expected by parameter $haystack of array_search(). ( Ignorable by Annotation )

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

523
				if( ( $key = array_search( $product, /** @scrutinizer ignore-type */ $this->products, true ) ) !== false ) {
Loading history...
524
					unset( $this->products[$key] );
525
				}
526
			}
527
		}
528
529
		foreach( $products as $product ) {
530
			$this->products[] = $product;
531
		}
532
533
		$old = isset( $this->coupons[$code] ) ? [$code => $this->coupons[$code]] : [];
534
		$this->coupons[$code] = is_map( $products ) ? $products->toArray() : $products;
535
		$this->setModified();
536
537
		$this->notify( 'setCoupon.after', $old );
538
539
		return $this;
540
	}
541
542
543
	/**
544
	 * Replaces all coupons in the current basket with the new ones
545
	 *
546
	 * @param iterable $map Associative list of order coupons as returned by getCoupons()
547
	 * @return \Aimeos\MShop\Order\Item\Base\Iface Order base item for method chaining
548
	 */
549
	public function setCoupons( iterable $map ) : \Aimeos\MShop\Order\Item\Base\Iface
550
	{
551
		$map = $this->notify( 'setCoupons.before', $map );
552
553
		foreach( $map as $code => $products ) {
554
			$map[$code] = $this->checkProducts( $products );
555
		}
556
557
		foreach( $this->coupons as $code => $products )
558
		{
559
			foreach( $products as $product )
560
			{
561
				if( ( $key = array_search( $product, $this->products, true ) ) !== false ) {
0 ignored issues
show
Bug introduced by
$this->products of type iterable is incompatible with the type array expected by parameter $haystack of array_search(). ( Ignorable by Annotation )

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

561
				if( ( $key = array_search( $product, /** @scrutinizer ignore-type */ $this->products, true ) ) !== false ) {
Loading history...
562
					unset( $this->products[$key] );
563
				}
564
			}
565
		}
566
567
		foreach( $map as $code => $products )
568
		{
569
			foreach( $products as $product ) {
570
				$this->products[] = $product;
571
			}
572
		}
573
574
		$old = $this->coupons;
575
		$this->coupons = is_map( $map ) ? $map->toArray() : $map;
576
		$this->setModified();
577
578
		$this->notify( 'setCoupons.after', $old );
579
580
		return $this;
581
	}
582
583
584
	/**
585
	 * Adds an order product item to the basket
586
	 * If a similar item is found, only the quantity is increased.
587
	 *
588
	 * @param \Aimeos\MShop\Order\Item\Base\Product\Iface $item Order product item to be added
589
	 * @param int|null $position position of the new order product item
590
	 * @return \Aimeos\MShop\Order\Item\Base\Iface Order base item for method chaining
591
	 */
592
	public function addProduct( \Aimeos\MShop\Order\Item\Base\Product\Iface $item, int $position = null ) : \Aimeos\MShop\Order\Item\Base\Iface
593
	{
594
		$item = $this->notify( 'addProduct.before', $item );
595
596
		$this->checkProducts( [$item] );
597
598
		if( $position !== null ) {
599
			$this->products[$position] = $item;
600
		} elseif( ( $pos = $this->getSameProduct( $item, $this->products ) ) !== null ) {
601
			$item = $this->products[$pos]->setQuantity( $this->products[$pos]->getQuantity() + $item->getQuantity() );
602
		} else {
603
			$this->products[] = $item;
604
		}
605
606
		ksort( $this->products );
0 ignored issues
show
Bug introduced by
$this->products of type iterable is incompatible with the type array expected by parameter $array of ksort(). ( Ignorable by Annotation )

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

606
		ksort( /** @scrutinizer ignore-type */ $this->products );
Loading history...
607
		$this->setModified();
608
609
		$this->notify( 'addProduct.after', $item );
610
611
		return $this;
612
	}
613
614
615
	/**
616
	 * Deletes an order product item from the basket
617
	 *
618
	 * @param int $position Position of the order product item
619
	 * @return \Aimeos\MShop\Order\Item\Base\Iface Order base item for method chaining
620
	 */
621
	public function deleteProduct( int $position ) : \Aimeos\MShop\Order\Item\Base\Iface
622
	{
623
		if( isset( $this->products[$position] ) )
624
		{
625
			$old = $this->products[$position];
626
			$old = $this->notify( 'deleteProduct.before', $old );
627
628
			unset( $this->products[$position] );
629
			$this->setModified();
630
631
			$this->notify( 'deleteProduct.after', $old );
632
		}
633
634
		return $this;
635
	}
636
637
638
	/**
639
	 * Returns the product item of an basket specified by its key
640
	 *
641
	 * @param int $key Key returned by getProducts() identifying the requested product
642
	 * @return \Aimeos\MShop\Order\Item\Base\Product\Iface Product item of an order
643
	 */
644
	public function getProduct( int $key ) : \Aimeos\MShop\Order\Item\Base\Product\Iface
645
	{
646
		if( !isset( $this->products[$key] ) ) {
647
			throw new \Aimeos\MShop\Order\Exception( sprintf( 'Product not available' ) );
648
		}
649
650
		return $this->products[$key];
651
	}
652
653
654
	/**
655
	 * Returns the product items that are or should be part of a basket
656
	 *
657
	 * @return \Aimeos\Map List of order product items implementing \Aimeos\MShop\Order\Item\Base\Product\Iface
658
	 */
659
	public function getProducts() : \Aimeos\Map
660
	{
661
		return map( $this->products );
662
	}
663
664
665
	/**
666
	 * Replaces all products in the current basket with the new ones
667
	 *
668
	 * @param \Aimeos\MShop\Order\Item\Base\Product\Iface[] $map Associative list of ordered products as returned by getProducts()
669
	 * @return \Aimeos\MShop\Order\Item\Base\Iface Order base item for method chaining
670
	 */
671
	public function setProducts( iterable $map ) : \Aimeos\MShop\Order\Item\Base\Iface
672
	{
673
		$map = $this->notify( 'setProducts.before', $map );
674
675
		$this->checkProducts( $map );
676
677
		$old = $this->products;
678
		$this->products = is_map( $map ) ? $map->toArray() : $map;
679
		$this->setModified();
680
681
		$this->notify( 'setProducts.after', $old );
682
683
		return $this;
684
	}
685
686
687
	/**
688
	 * Adds an order service to the basket
689
	 *
690
	 * @param \Aimeos\MShop\Order\Item\Base\Service\Iface $service Order service item for the given domain
691
	 * @param string $type Service type constant from \Aimeos\MShop\Order\Item\Service\Base
692
	 * @param int|null $position Position of the service in the list to overwrite
693
	 * @return \Aimeos\MShop\Order\Item\Base\Iface Order base item for method chaining
694
	 */
695
	public function addService( \Aimeos\MShop\Order\Item\Base\Service\Iface $service, string $type, int $position = null ) : \Aimeos\MShop\Order\Item\Base\Iface
696
	{
697
		$service = $this->notify( 'addService.before', $service );
698
699
		$this->checkPrice( $service->getPrice() );
700
701
		$service = clone $service;
702
		$service = $service->setType( $type );
703
704
		if( $position !== null ) {
705
			$this->services[$type][$position] = $service;
706
		} else {
707
			$this->services[$type][] = $service;
708
		}
709
710
		$this->setModified();
711
712
		$this->notify( 'addService.after', $service );
713
714
		return $this;
715
	}
716
717
718
	/**
719
	 * Deletes an order service from the basket
720
	 *
721
	 * @param string $type Service type constant from \Aimeos\MShop\Order\Item\Service\Base
722
	 * @param int|null $position Position of the service in the list to delete
723
	 * @return \Aimeos\MShop\Order\Item\Base\Iface Order base item for method chaining
724
	 */
725
	public function deleteService( string $type, int $position = null ) : \Aimeos\MShop\Order\Item\Base\Iface
726
	{
727
		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...
728
		{
729
			$old = ( isset( $this->services[$type][$position] ) ? $this->services[$type][$position] : $this->services[$type] );
730
			$old = $this->notify( 'deleteService.before', $old );
731
732
			if( $position !== null ) {
733
				unset( $this->services[$type][$position] );
734
			} else {
735
				unset( $this->services[$type] );
736
			}
737
738
			$this->setModified();
739
740
			$this->notify( 'deleteService.after', $old );
741
		}
742
743
		return $this;
744
	}
745
746
747
	/**
748
	 * Returns the order services depending on the given type
749
	 *
750
	 * @param string $type Service type constant from \Aimeos\MShop\Order\Item\Service\Base
751
	 * @param int|null $position Position of the service in the list to retrieve
752
	 * @return \Aimeos\MShop\Order\Item\Base\Service\Iface[]|\Aimeos\MShop\Order\Item\Base\Service\Iface
753
	 * 	Order service item or list of items for the requested type
754
	 * @throws \Aimeos\MShop\Order\Exception If no service for the given type and position is found
755
	 */
756
	public function getService( string $type, int $position = null )
757
	{
758
		if( $position !== null )
759
		{
760
			if( isset( $this->services[$type][$position] ) ) {
761
				return $this->services[$type][$position];
762
			}
763
764
			throw new \Aimeos\MShop\Order\Exception( sprintf( 'Service not available' ) );
765
		}
766
767
		return ( isset( $this->services[$type] ) ? $this->services[$type] : [] );
768
	}
769
770
771
	/**
772
	 * Returns all services that are part of the basket
773
	 *
774
	 * @return \Aimeos\Map Associative list of service types ("delivery" or "payment") as keys and list of
775
	 *	service items implementing \Aimeos\MShop\Order\Service\Iface as values
776
	 */
777
	public function getServices() : \Aimeos\Map
778
	{
779
		return map( $this->services );
780
	}
781
782
783
	/**
784
	 * Replaces all services in the current basket with the new ones
785
	 *
786
	 * @param \Aimeos\MShop\Order\Item\Base\Service\Iface[] $map Associative list of order services as returned by getServices()
787
	 * @return \Aimeos\MShop\Order\Item\Base\Iface Order base item for method chaining
788
	 */
789
	public function setServices( iterable $map ) : \Aimeos\MShop\Order\Item\Base\Iface
790
	{
791
		$map = $this->notify( 'setServices.before', $map );
792
793
		foreach( $map as $type => $services ) {
794
			$map[$type] = $this->checkServices( $services, $type );
795
		}
796
797
		$old = $this->services;
798
		$this->services = is_map( $map ) ? $map->toArray() : $map;
799
		$this->setModified();
800
801
		$this->notify( 'setServices.after', $old );
802
803
		return $this;
804
	}
805
806
807
	/**
808
	 * Checks if the price uses the same currency as the price in the basket.
809
	 *
810
	 * @param \Aimeos\MShop\Price\Item\Iface $item Price item
811
	 * @return void
812
	 */
813
	abstract protected function checkPrice( \Aimeos\MShop\Price\Item\Iface $item );
814
815
816
	/**
817
	 * Checks if all order addresses are valid
818
	 *
819
	 * @param \Aimeos\MShop\Order\Item\Base\Address\Iface[] $items Order address items
820
	 * @param string $type Address type constant from \Aimeos\MShop\Order\Item\Base\Address\Base
821
	 * @return \Aimeos\MShop\Order\Item\Base\Address\Iface[] List of checked items
822
	 * @throws \Aimeos\MShop\Exception If one of the order addresses is invalid
823
	 */
824
	protected function checkAddresses( iterable $items, string $type ) : iterable
825
	{
826
		foreach( $items as $key => $item )
827
		{
828
			\Aimeos\MW\Common\Base::checkClass( \Aimeos\MShop\Order\Item\Base\Address\Iface::class, $item );
829
			$items[$key] = $item->setType( $type );
830
		}
831
832
		return $items;
833
	}
834
835
836
	/**
837
	 * Checks if all order products are valid
838
	 *
839
	 * @param \Aimeos\MShop\Order\Item\Base\Product\Iface[] $items Order product items
840
	 * @return \Aimeos\MShop\Order\Item\Base\Product\Iface[] List of checked items
841
	 * @throws \Aimeos\MShop\Exception If one of the order products is invalid
842
	 */
843
	protected function checkProducts( iterable $items ) : \Aimeos\Map
844
	{
845
		foreach( $items as $key => $item )
846
		{
847
			\Aimeos\MW\Common\Base::checkClass( \Aimeos\MShop\Order\Item\Base\Product\Iface::class, $item );
848
849
			if( $item->getProductCode() === '' ) {
850
				throw new \Aimeos\MShop\Order\Exception( sprintf( 'Product does not contain the SKU code' ) );
851
			}
852
853
			$this->checkPrice( $item->getPrice() );
854
		}
855
856
		return map( $items );
857
	}
858
859
860
	/**
861
	 * Checks if all order services are valid
862
	 *
863
	 * @param \Aimeos\MShop\Order\Item\Base\Service\Iface[] $items Order service items
864
	 * @param string $type Service type constant from \Aimeos\MShop\Order\Item\Base\Service\Base
865
	 * @return \Aimeos\MShop\Order\Item\Base\Service\Iface[] List of checked items
866
	 * @throws \Aimeos\MShop\Exception If one of the order services is invalid
867
	 */
868
	protected function checkServices( iterable $items, string $type ) : iterable
869
	{
870
		foreach( $items as $key => $item )
871
		{
872
			\Aimeos\MW\Common\Base::checkClass( \Aimeos\MShop\Order\Item\Base\Service\Iface::class, $item );
873
874
			$this->checkPrice( $item->getPrice() );
875
			$items[$key] = $item->setType( $type );
876
		}
877
878
		return $items;
879
	}
880
881
882
	/**
883
	 * Tests if the given product is similar to an existing one.
884
	 * Similarity is described by the equality of properties so the quantity of
885
	 * the existing product can be updated.
886
	 *
887
	 * @param \Aimeos\MShop\Order\Item\Base\Product\Iface $item Order product item
888
	 * @param \Aimeos\MShop\Order\Item\Base\Product\Iface[] $products List of order product items to check against
889
	 * @return int|null Positon of the same product in the product list of false if product is unique
890
	 * @throws \Aimeos\MShop\Order\Exception If no similar item was found
891
	 */
892
	protected function getSameProduct( \Aimeos\MShop\Order\Item\Base\Product\Iface $item, iterable $products ) : ?int
893
	{
894
		$map = [];
895
		$count = 0;
896
897
		foreach( $item->getAttributeItems() as $attributeItem )
898
		{
899
			$key = md5( $attributeItem->getCode() . json_encode( $attributeItem->getValue() ) );
900
			$map[$key] = $attributeItem;
901
			$count++;
902
		}
903
904
		foreach( $products as $position => $product )
905
		{
906
			if( $product->compare( $item ) === false ) {
907
				continue;
908
			}
909
910
			$prodAttributes = $product->getAttributeItems();
911
912
			if( count( $prodAttributes ) !== $count ) {
913
				continue;
914
			}
915
916
			foreach( $prodAttributes as $attribute )
917
			{
918
				$key = md5( $attribute->getCode() . json_encode( $attribute->getValue() ) );
919
920
				if( isset( $map[$key] ) === false || $map[$key]->getQuantity() != $attribute->getQuantity() ) {
921
					continue 2; // jump to outer loop
922
				}
923
			}
924
925
			return $position;
926
		}
927
928
		return null;
929
	}
930
}
931