Passed
Push — master ( a3e004...b231c1 )
by Aimeos
07:52 queued 02:33
created

Base::get()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

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

450
				if( ( $key = array_search( $product, /** @scrutinizer ignore-type */ $this->products, true ) ) !== false ) {
Loading history...
451
					unset( $this->products[$key] );
452
				}
453
			}
454
455
			unset( $this->coupons[$code] );
456
			$this->setModified();
457
458
			$this->notify( 'deleteCoupon.after', $old );
459
		}
460
461
		return $this;
462
	}
463
464
465
	/**
466
	 * Returns the available coupon codes and the lists of affected product items
467
	 *
468
	 * @return \Aimeos\Map Associative array of codes and lists of product items
469
	 *  implementing \Aimeos\MShop\Order\Product\Iface
470
	 */
471
	public function getCoupons() : \Aimeos\Map
472
	{
473
		return map( $this->coupons );
474
	}
475
476
477
	/**
478
	 * Sets a coupon code and the given product items in the basket.
479
	 *
480
	 * @param string $code Coupon code
481
	 * @param \Aimeos\MShop\Order\Item\Base\Product\Iface[] $products List of coupon products
482
	 * @return \Aimeos\MShop\Order\Item\Base\Iface Order base item for method chaining
483
	 */
484
	public function setCoupon( string $code, iterable $products = [] ) : \Aimeos\MShop\Order\Item\Base\Iface
485
	{
486
		$new = $this->notify( 'setCoupon.before', [$code => $products] );
487
488
		$products = $this->checkProducts( map( $new )->first( [] ) );
489
490
		if( isset( $this->coupons[$code] ) )
491
		{
492
			foreach( $this->coupons[$code] as $product )
493
			{
494
				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

494
				if( ( $key = array_search( $product, /** @scrutinizer ignore-type */ $this->products, true ) ) !== false ) {
Loading history...
495
					unset( $this->products[$key] );
496
				}
497
			}
498
		}
499
500
		foreach( $products as $product ) {
501
			$this->products[] = $product;
502
		}
503
504
		$old = isset( $this->coupons[$code] ) ? [$code => $this->coupons[$code]] : [];
505
		$this->coupons[$code] = is_map( $products ) ? $products->toArray() : $products;
506
		$this->setModified();
507
508
		$this->notify( 'setCoupon.after', $old );
509
510
		return $this;
511
	}
512
513
514
	/**
515
	 * Replaces all coupons in the current basket with the new ones
516
	 *
517
	 * @param iterable $map Associative list of order coupons as returned by getCoupons()
518
	 * @return \Aimeos\MShop\Order\Item\Base\Iface Order base item for method chaining
519
	 */
520
	public function setCoupons( iterable $map ) : \Aimeos\MShop\Order\Item\Base\Iface
521
	{
522
		$map = $this->notify( 'setCoupons.before', $map );
523
524
		foreach( $map as $code => $products ) {
525
			$map[$code] = $this->checkProducts( $products );
526
		}
527
528
		foreach( $this->coupons as $code => $products )
529
		{
530
			foreach( $products as $product )
531
			{
532
				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

532
				if( ( $key = array_search( $product, /** @scrutinizer ignore-type */ $this->products, true ) ) !== false ) {
Loading history...
533
					unset( $this->products[$key] );
534
				}
535
			}
536
		}
537
538
		foreach( $map as $code => $products )
539
		{
540
			foreach( $products as $product ) {
541
				$this->products[] = $product;
542
			}
543
		}
544
545
		$old = $this->coupons;
546
		$this->coupons = is_map( $map ) ? $map->toArray() : $map;
547
		$this->setModified();
548
549
		$this->notify( 'setCoupons.after', $old );
550
551
		return $this;
552
	}
553
554
555
	/**
556
	 * Adds an order product item to the basket
557
	 * If a similar item is found, only the quantity is increased.
558
	 *
559
	 * @param \Aimeos\MShop\Order\Item\Base\Product\Iface $item Order product item to be added
560
	 * @param int|null $position position of the new order product item
561
	 * @return \Aimeos\MShop\Order\Item\Base\Iface Order base item for method chaining
562
	 */
563
	public function addProduct( \Aimeos\MShop\Order\Item\Base\Product\Iface $item, int $position = null ) : \Aimeos\MShop\Order\Item\Base\Iface
564
	{
565
		$item = $this->notify( 'addProduct.before', $item );
566
567
		$this->checkProducts( [$item] );
568
569
		if( $position !== null ) {
570
			$this->products[$position] = $item;
571
		} elseif( ( $pos = $this->getSameProduct( $item, $this->products ) ) !== null ) {
572
			$item = $this->products[$pos]->setQuantity( $this->products[$pos]->getQuantity() + $item->getQuantity() );
573
		} else {
574
			$this->products[] = $item;
575
		}
576
577
		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

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