Passed
Push — master ( 5788d1...3b28d2 )
by Aimeos
04:17
created

Base   F

Complexity

Total Complexity 122

Size/Duplication

Total Lines 969
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 122
eloc 263
c 0
b 0
f 0
dl 0
loc 969
rs 2

44 Methods

Rating   Name   Duplication   Size   Complexity  
A __clone() 0 2 1
A __construct() 0 22 4
A __set() 0 7 3
A __isset() 0 3 1
A __get() 0 7 2
A jsonSerialize() 0 8 1
A __sleep() 0 9 1
A __toString() 0 3 1
A getAddresses() 0 3 1
A isModified() 0 3 1
A offsetGet() 0 8 2
A deleteService() 0 19 6
A checkAddresses() 0 9 2
A getResourceType() 0 3 1
B setCoupons() 0 32 8
A setAddresses() 0 15 3
A getProducts() 0 3 1
A setServices() 0 15 3
A assign() 0 7 2
A getService() 0 12 4
A get() 0 7 2
A getServices() 0 3 1
A checkProducts() 0 14 3
A getAddress() 0 12 4
A deleteCoupon() 0 21 4
A addService() 0 20 2
A checkServices() 0 11 2
A offsetSet() 0 7 3
A set() 0 9 3
A setProducts() 0 13 2
A getProduct() 0 7 2
A addAddress() 0 18 2
A addCoupon() 0 13 2
B setCoupon() 0 27 7
A setModified() 0 4 1
A addProduct() 0 20 3
A deleteProduct() 0 14 2
A offsetUnset() 0 3 1
A deleteAddress() 0 19 6
A getCosts() 0 16 4
B getSameProduct() 0 37 8
A getCoupons() 0 3 1
B getTaxes() 0 40 8
A offsetExists() 0 3 1

How to fix   Complexity   

Complex Class

Complex classes like Base often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Base, and based on these observations, apply Extract Interface, too.

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, \JsonSerializable
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
		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
		$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
	 * Specifies the data which should be serialized to JSON by json_encode().
127
	 *
128
	 * @return array<string,mixed> Data to serialize to JSON
129
	 */
130
	#[\ReturnTypeWillChange]
131
	public function jsonSerialize()
132
	{
133
		return $this->bdata + [
134
			'coupons' => $this->coupons,
135
			'products' => $this->products,
136
			'services' => $this->services,
137
			'addresses' => $this->addresses,
138
		];
139
	}
140
141
142
	/**
143
	 * Tests if the item property for the given name is available
144
	 *
145
	 * @param string $name Name of the property
146
	 * @return bool True if the property exists, false if not
147
	 */
148
	public function offsetExists( $name ) : bool
149
	{
150
		return array_key_exists( $name, $this->bdata );
151
	}
152
153
154
	/**
155
	 * Returns the item property for the given name
156
	 *
157
	 * @param string $name Name of the property
158
	 * @return mixed|null Property value or null if property is unknown
159
	 */
160
	#[\ReturnTypeWillChange]
161
	public function offsetGet( $name )
162
	{
163
		if( array_key_exists( $name, $this->bdata ) ) {
164
			return $this->bdata[$name];
165
		}
166
167
		return null;
168
	}
169
170
171
	/**
172
	 * Sets the new item property for the given name
173
	 *
174
	 * @param string $name Name of the property
175
	 * @param mixed $value New property value
176
	 */
177
	public function offsetSet( $name, $value ) : void
178
	{
179
		if( !array_key_exists( $name, $this->bdata ) || $this->bdata[$name] !== $value ) {
180
			$this->setModified();
181
		}
182
183
		$this->bdata[$name] = $value;
184
	}
185
186
187
	/**
188
	 * Removes an item property
189
	 * This is not supported by items
190
	 *
191
	 * @param string $name Name of the property
192
	 * @throws \LogicException Always thrown because this method isn't supported
193
	 */
194
	public function offsetUnset( $name ) : void
195
	{
196
		throw new \LogicException( 'Not implemented' );
197
	}
198
199
200
	/**
201
	 * Prepares the object for serialization.
202
	 *
203
	 * @return array List of properties that should be serialized
204
	 */
205
	public function __sleep() : array
206
	{
207
		/*
208
		 * Workaround because database connections can't be serialized
209
		 * Listeners will be reattached on wakeup by the order base manager
210
		 */
211
		$this->off();
212
213
		return array_keys( get_object_vars( $this ) );
214
	}
215
216
217
	/**
218
	 * Returns the ID of the items
219
	 *
220
	 * @return string ID of the item or null
221
	 */
222
	public function __toString() : string
223
	{
224
		return (string) $this->getId();
225
	}
226
227
228
	/**
229
	 * Assigns multiple key/value pairs to the item
230
	 *
231
	 * @param iterable $pairs Associative list of key/value pairs
232
	 * @return \Aimeos\MShop\Common\Item\Iface Item for method chaining
233
	 */
234
	public function assign( iterable $pairs ) : \Aimeos\MShop\Common\Item\Iface
235
	{
236
		foreach( $pairs as $key => $value ) {
237
			$this->set( $key, $value );
238
		}
239
240
		return $this;
241
	}
242
243
244
	/**
245
	 * Returns the item property for the given name
246
	 *
247
	 * @param string $name Name of the property
248
	 * @param mixed $default Default value if property is unknown
249
	 * @return mixed|null Property value or default value if property is unknown
250
	 */
251
	public function get( string $name, $default = null )
252
	{
253
		if( isset( $this->bdata[$name] ) ) {
254
			return $this->bdata[$name];
255
		}
256
257
		return $default;
258
	}
259
260
261
	/**
262
	 * Sets the new item property for the given name
263
	 *
264
	 * @param string $name Name of the property
265
	 * @param mixed $value New property value
266
	 * @return \Aimeos\MShop\Order\Item\Base\Iface Order base item for method chaining
267
	 */
268
	public function set( string $name, $value ) : \Aimeos\MShop\Common\Item\Iface
269
	{
270
		if( !array_key_exists( $name, $this->bdata ) || $this->bdata[$name] !== $value )
271
		{
272
			$this->bdata[$name] = $value;
273
			$this->setModified();
274
		}
275
276
		return $this;
277
	}
278
279
280
	/**
281
	 * Returns the item type
282
	 *
283
	 * @return string Item type, subtypes are separated by slashes
284
	 */
285
	public function getResourceType() : string
286
	{
287
		return 'order/base';
288
	}
289
290
291
	/**
292
	 * Tests if the order object was modified.
293
	 *
294
	 * @return bool True if modified, false if not
295
	 */
296
	public function isModified() : bool
297
	{
298
		return $this->modified;
299
	}
300
301
302
	/**
303
	 * Sets the modified flag of the object.
304
	 *
305
	 * @return \Aimeos\MShop\Order\Item\Base\Iface Order base item for method chaining
306
	 */
307
	public function setModified() : \Aimeos\MShop\Order\Item\Base\Iface
308
	{
309
		$this->modified = true;
310
		return $this;
311
	}
312
313
314
	/**
315
	 * Adds the address of the given type to the basket
316
	 *
317
	 * @param \Aimeos\MShop\Order\Item\Base\Address\Iface $address Order address item for the given type
318
	 * @param string $type Address type, usually "billing" or "delivery"
319
	 * @param int|null $position Position of the address in the list
320
	 * @return \Aimeos\MShop\Order\Item\Base\Iface Order base item for method chaining
321
	 */
322
	public function addAddress( \Aimeos\MShop\Order\Item\Base\Address\Iface $address, string $type, int $position = null ) : \Aimeos\MShop\Order\Item\Base\Iface
323
	{
324
		$address = $this->notify( 'addAddress.before', $address );
325
326
		$address = clone $address;
327
		$address = $address->setType( $type );
328
329
		if( $position !== null ) {
330
			$this->addresses[$type][$position] = $address;
331
		} else {
332
			$this->addresses[$type][] = $address;
333
		}
334
335
		$this->setModified();
336
337
		$this->notify( 'addAddress.after', $address );
338
339
		return $this;
340
	}
341
342
343
	/**
344
	 * Deletes an order address from the basket
345
	 *
346
	 * @param string $type Address type defined in \Aimeos\MShop\Order\Item\Base\Address\Base
347
	 * @param int|null $position Position of the address in the list
348
	 * @return \Aimeos\MShop\Order\Item\Base\Iface Order base item for method chaining
349
	 */
350
	public function deleteAddress( string $type, int $position = null ) : \Aimeos\MShop\Order\Item\Base\Iface
351
	{
352
		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...
353
		{
354
			$old = ( isset( $this->addresses[$type][$position] ) ? $this->addresses[$type][$position] : $this->addresses[$type] );
355
			$old = $this->notify( 'deleteAddress.before', $old );
356
357
			if( $position !== null ) {
358
				unset( $this->addresses[$type][$position] );
359
			} else {
360
				unset( $this->addresses[$type] );
361
			}
362
363
			$this->setModified();
364
365
			$this->notify( 'deleteAddress.after', $old );
366
		}
367
368
		return $this;
369
	}
370
371
372
	/**
373
	 * Returns the order address depending on the given type
374
	 *
375
	 * @param string $type Address type, usually "billing" or "delivery"
376
	 * @param int|null $position Address position in list of addresses
377
	 * @return \Aimeos\MShop\Order\Item\Base\Address\Iface[]|\Aimeos\MShop\Order\Item\Base\Address\Iface Order address item or list of
378
	 */
379
	public function getAddress( string $type, int $position = null )
380
	{
381
		if( $position !== null )
382
		{
383
			if( isset( $this->addresses[$type][$position] ) ) {
384
				return $this->addresses[$type][$position];
385
			}
386
387
			throw new \Aimeos\MShop\Order\Exception( sprintf( 'Address not available' ) );
388
		}
389
390
		return ( isset( $this->addresses[$type] ) ? $this->addresses[$type] : [] );
391
	}
392
393
394
	/**
395
	 * Returns all addresses that are part of the basket
396
	 *
397
	 * @return \Aimeos\Map Associative list of address items implementing
398
	 *  \Aimeos\MShop\Order\Item\Base\Address\Iface with "billing" or "delivery" as key
399
	 */
400
	public function getAddresses() : \Aimeos\Map
401
	{
402
		return map( $this->addresses );
403
	}
404
405
406
	/**
407
	 * Replaces all addresses in the current basket with the new ones
408
	 *
409
	 * @param \Aimeos\Map|array $map Associative list of order addresses as returned by getAddresses()
410
	 * @return \Aimeos\MShop\Order\Item\Base\Iface Order base item for method chaining
411
	 */
412
	public function setAddresses( iterable $map ) : \Aimeos\MShop\Order\Item\Base\Iface
413
	{
414
		$map = $this->notify( 'setAddresses.before', $map );
415
416
		foreach( $map as $type => $items ) {
417
			$this->checkAddresses( $items, $type );
418
		}
419
420
		$old = $this->addresses;
421
		$this->addresses = is_map( $map ) ? $map->toArray() : $map;
422
		$this->setModified();
423
424
		$this->notify( 'setAddresses.after', $old );
425
426
		return $this;
427
	}
428
429
430
	/**
431
	 * Adds a coupon code and the given product item to the basket
432
	 *
433
	 * @param string $code Coupon code
434
	 * @return \Aimeos\MShop\Order\Item\Base\Iface Order base item for method chaining
435
	 */
436
	public function addCoupon( string $code ) : \Aimeos\MShop\Order\Item\Base\Iface
437
	{
438
		if( !isset( $this->coupons[$code] ) )
439
		{
440
			$code = $this->notify( 'addCoupon.before', $code );
441
442
			$this->coupons[$code] = [];
443
			$this->setModified();
444
445
			$this->notify( 'addCoupon.after', $code );
446
		}
447
448
		return $this;
449
	}
450
451
452
	/**
453
	 * Removes a coupon and the related product items from the basket
454
	 *
455
	 * @param string $code Coupon code
456
	 * @return \Aimeos\MShop\Order\Item\Base\Iface Order base item for method chaining
457
	 */
458
	public function deleteCoupon( string $code ) : \Aimeos\MShop\Order\Item\Base\Iface
459
	{
460
		if( isset( $this->coupons[$code] ) )
461
		{
462
			$old = [$code => $this->coupons[$code]];
463
			$old = $this->notify( 'deleteCoupon.before', $old );
464
465
			foreach( $this->coupons[$code] as $product )
466
			{
467
				if( ( $key = array_search( $product, $this->products, true ) ) !== false ) {
468
					unset( $this->products[$key] );
469
				}
470
			}
471
472
			unset( $this->coupons[$code] );
473
			$this->setModified();
474
475
			$this->notify( 'deleteCoupon.after', $old );
476
		}
477
478
		return $this;
479
	}
480
481
482
	/**
483
	 * Returns the available coupon codes and the lists of affected product items
484
	 *
485
	 * @return \Aimeos\Map Associative array of codes and lists of product items
486
	 *  implementing \Aimeos\MShop\Order\Product\Iface
487
	 */
488
	public function getCoupons() : \Aimeos\Map
489
	{
490
		return map( $this->coupons );
491
	}
492
493
494
	/**
495
	 * Sets a coupon code and the given product items in the basket.
496
	 *
497
	 * @param string $code Coupon code
498
	 * @param \Aimeos\MShop\Order\Item\Base\Product\Iface[] $products List of coupon products
499
	 * @return \Aimeos\MShop\Order\Item\Base\Iface Order base item for method chaining
500
	 */
501
	public function setCoupon( string $code, iterable $products = [] ) : \Aimeos\MShop\Order\Item\Base\Iface
502
	{
503
		$new = $this->notify( 'setCoupon.before', [$code => $products] );
504
505
		$products = $this->checkProducts( map( $new )->first( [] ) );
506
507
		if( isset( $this->coupons[$code] ) )
508
		{
509
			foreach( $this->coupons[$code] as $product )
510
			{
511
				if( ( $key = array_search( $product, $this->products, true ) ) !== false ) {
512
					unset( $this->products[$key] );
513
				}
514
			}
515
		}
516
517
		foreach( $products as $product ) {
518
			$this->products[] = $product;
519
		}
520
521
		$old = isset( $this->coupons[$code] ) ? [$code => $this->coupons[$code]] : [];
522
		$this->coupons[$code] = is_map( $products ) ? $products->toArray() : $products;
523
		$this->setModified();
524
525
		$this->notify( 'setCoupon.after', $old );
526
527
		return $this;
528
	}
529
530
531
	/**
532
	 * Replaces all coupons in the current basket with the new ones
533
	 *
534
	 * @param iterable $map Associative list of order coupons as returned by getCoupons()
535
	 * @return \Aimeos\MShop\Order\Item\Base\Iface Order base item for method chaining
536
	 */
537
	public function setCoupons( iterable $map ) : \Aimeos\MShop\Order\Item\Base\Iface
538
	{
539
		$map = $this->notify( 'setCoupons.before', $map );
540
541
		foreach( $map as $code => $products ) {
542
			$map[$code] = $this->checkProducts( $products );
543
		}
544
545
		foreach( $this->coupons as $code => $products )
546
		{
547
			foreach( $products as $product )
548
			{
549
				if( ( $key = array_search( $product, $this->products, true ) ) !== false ) {
550
					unset( $this->products[$key] );
551
				}
552
			}
553
		}
554
555
		foreach( $map as $code => $products )
556
		{
557
			foreach( $products as $product ) {
558
				$this->products[] = $product;
559
			}
560
		}
561
562
		$old = $this->coupons;
563
		$this->coupons = is_map( $map ) ? $map->toArray() : $map;
564
		$this->setModified();
565
566
		$this->notify( 'setCoupons.after', $old );
567
568
		return $this;
569
	}
570
571
572
	/**
573
	 * Adds an order product item to the basket
574
	 * If a similar item is found, only the quantity is increased.
575
	 *
576
	 * @param \Aimeos\MShop\Order\Item\Base\Product\Iface $item Order product item to be added
577
	 * @param int|null $position position of the new order product item
578
	 * @return \Aimeos\MShop\Order\Item\Base\Iface Order base item for method chaining
579
	 */
580
	public function addProduct( \Aimeos\MShop\Order\Item\Base\Product\Iface $item, int $position = null ) : \Aimeos\MShop\Order\Item\Base\Iface
581
	{
582
		$item = $this->notify( 'addProduct.before', $item );
583
584
		$this->checkProducts( [$item] );
585
586
		if( $position !== null ) {
587
			$this->products[$position] = $item;
588
		} elseif( ( $pos = $this->getSameProduct( $item, $this->products ) ) !== null ) {
589
			$item = $this->products[$pos]->setQuantity( $this->products[$pos]->getQuantity() + $item->getQuantity() );
590
		} else {
591
			$this->products[] = $item;
592
		}
593
594
		ksort( $this->products );
595
		$this->setModified();
596
597
		$this->notify( 'addProduct.after', $item );
598
599
		return $this;
600
	}
601
602
603
	/**
604
	 * Deletes an order product item from the basket
605
	 *
606
	 * @param int $position Position of the order product item
607
	 * @return \Aimeos\MShop\Order\Item\Base\Iface Order base item for method chaining
608
	 */
609
	public function deleteProduct( int $position ) : \Aimeos\MShop\Order\Item\Base\Iface
610
	{
611
		if( isset( $this->products[$position] ) )
612
		{
613
			$old = $this->products[$position];
614
			$old = $this->notify( 'deleteProduct.before', $old );
615
616
			unset( $this->products[$position] );
617
			$this->setModified();
618
619
			$this->notify( 'deleteProduct.after', $old );
620
		}
621
622
		return $this;
623
	}
624
625
626
	/**
627
	 * Returns the product item of an basket specified by its key
628
	 *
629
	 * @param int $key Key returned by getProducts() identifying the requested product
630
	 * @return \Aimeos\MShop\Order\Item\Base\Product\Iface Product item of an order
631
	 */
632
	public function getProduct( int $key ) : \Aimeos\MShop\Order\Item\Base\Product\Iface
633
	{
634
		if( !isset( $this->products[$key] ) ) {
635
			throw new \Aimeos\MShop\Order\Exception( sprintf( 'Product not available' ) );
636
		}
637
638
		return $this->products[$key];
639
	}
640
641
642
	/**
643
	 * Returns the product items that are or should be part of a basket
644
	 *
645
	 * @return \Aimeos\Map List of order product items implementing \Aimeos\MShop\Order\Item\Base\Product\Iface
646
	 */
647
	public function getProducts() : \Aimeos\Map
648
	{
649
		return map( $this->products );
650
	}
651
652
653
	/**
654
	 * Replaces all products in the current basket with the new ones
655
	 *
656
	 * @param \Aimeos\MShop\Order\Item\Base\Product\Iface[] $map Associative list of ordered products as returned by getProducts()
657
	 * @return \Aimeos\MShop\Order\Item\Base\Iface Order base item for method chaining
658
	 */
659
	public function setProducts( iterable $map ) : \Aimeos\MShop\Order\Item\Base\Iface
660
	{
661
		$map = $this->notify( 'setProducts.before', $map );
662
663
		$this->checkProducts( $map );
664
665
		$old = $this->products;
666
		$this->products = is_map( $map ) ? $map->toArray() : $map;
667
		$this->setModified();
668
669
		$this->notify( 'setProducts.after', $old );
670
671
		return $this;
672
	}
673
674
675
	/**
676
	 * Adds an order service to the basket
677
	 *
678
	 * @param \Aimeos\MShop\Order\Item\Base\Service\Iface $service Order service item for the given domain
679
	 * @param string $type Service type constant from \Aimeos\MShop\Order\Item\Service\Base
680
	 * @param int|null $position Position of the service in the list to overwrite
681
	 * @return \Aimeos\MShop\Order\Item\Base\Iface Order base item for method chaining
682
	 */
683
	public function addService( \Aimeos\MShop\Order\Item\Base\Service\Iface $service, string $type, int $position = null ) : \Aimeos\MShop\Order\Item\Base\Iface
684
	{
685
		$service = $this->notify( 'addService.before', $service );
686
687
		$this->checkPrice( $service->getPrice() );
688
689
		$service = clone $service;
690
		$service = $service->setType( $type );
691
692
		if( $position !== null ) {
693
			$this->services[$type][$position] = $service;
694
		} else {
695
			$this->services[$type][] = $service;
696
		}
697
698
		$this->setModified();
699
700
		$this->notify( 'addService.after', $service );
701
702
		return $this;
703
	}
704
705
706
	/**
707
	 * Deletes an order service from the basket
708
	 *
709
	 * @param string $type Service type constant from \Aimeos\MShop\Order\Item\Service\Base
710
	 * @param int|null $position Position of the service in the list to delete
711
	 * @return \Aimeos\MShop\Order\Item\Base\Iface Order base item for method chaining
712
	 */
713
	public function deleteService( string $type, int $position = null ) : \Aimeos\MShop\Order\Item\Base\Iface
714
	{
715
		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...
716
		{
717
			$old = ( isset( $this->services[$type][$position] ) ? $this->services[$type][$position] : $this->services[$type] );
718
			$old = $this->notify( 'deleteService.before', $old );
719
720
			if( $position !== null ) {
721
				unset( $this->services[$type][$position] );
722
			} else {
723
				unset( $this->services[$type] );
724
			}
725
726
			$this->setModified();
727
728
			$this->notify( 'deleteService.after', $old );
729
		}
730
731
		return $this;
732
	}
733
734
735
	/**
736
	 * Returns the order services depending on the given type
737
	 *
738
	 * @param string $type Service type constant from \Aimeos\MShop\Order\Item\Service\Base
739
	 * @param int|null $position Position of the service in the list to retrieve
740
	 * @return \Aimeos\MShop\Order\Item\Base\Service\Iface[]|\Aimeos\MShop\Order\Item\Base\Service\Iface
741
	 * 	Order service item or list of items for the requested type
742
	 * @throws \Aimeos\MShop\Order\Exception If no service for the given type and position is found
743
	 */
744
	public function getService( string $type, int $position = null )
745
	{
746
		if( $position !== null )
747
		{
748
			if( isset( $this->services[$type][$position] ) ) {
749
				return $this->services[$type][$position];
750
			}
751
752
			throw new \Aimeos\MShop\Order\Exception( sprintf( 'Service not available' ) );
753
		}
754
755
		return ( isset( $this->services[$type] ) ? $this->services[$type] : [] );
756
	}
757
758
759
	/**
760
	 * Returns all services that are part of the basket
761
	 *
762
	 * @return \Aimeos\Map Associative list of service types ("delivery" or "payment") as keys and list of
763
	 *	service items implementing \Aimeos\MShop\Order\Service\Iface as values
764
	 */
765
	public function getServices() : \Aimeos\Map
766
	{
767
		return map( $this->services );
768
	}
769
770
771
	/**
772
	 * Replaces all services in the current basket with the new ones
773
	 *
774
	 * @param \Aimeos\MShop\Order\Item\Base\Service\Iface[] $map Associative list of order services as returned by getServices()
775
	 * @return \Aimeos\MShop\Order\Item\Base\Iface Order base item for method chaining
776
	 */
777
	public function setServices( iterable $map ) : \Aimeos\MShop\Order\Item\Base\Iface
778
	{
779
		$map = $this->notify( 'setServices.before', $map );
780
781
		foreach( $map as $type => $services ) {
782
			$map[$type] = $this->checkServices( $services, $type );
783
		}
784
785
		$old = $this->services;
786
		$this->services = is_map( $map ) ? $map->toArray() : $map;
787
		$this->setModified();
788
789
		$this->notify( 'setServices.after', $old );
790
791
		return $this;
792
	}
793
794
795
	/**
796
	 * Returns the service costs
797
	 *
798
	 * @param string $type Service type like "delivery" or "payment"
799
	 * @return float Service costs value
800
	 */
801
	public function getCosts( string $type = 'delivery' ) : float
802
	{
803
		$costs = 0;
804
805
		if( $type === 'delivery' )
806
		{
807
			foreach( $this->getProducts() as $product ) {
808
				$costs += $product->getPrice()->getCosts() * $product->getQuantity();
809
			}
810
		}
811
812
		foreach( $this->getService( $type ) as $service ) {
813
			$costs += $service->getPrice()->getCosts();
814
		}
815
816
		return $costs;
817
	}
818
819
820
	/**
821
	 * Returns a list of tax names and values
822
	 *
823
	 * @return array Associative list of tax names as key and price items as value
824
	 */
825
	public function getTaxes() : array
826
	{
827
		$taxes = [];
828
829
		foreach( $this->getProducts() as $product )
830
		{
831
			$price = $product->getPrice();
832
833
			foreach( $price->getTaxrates() as $name => $taxrate )
834
			{
835
				$price = (clone $price)->setTaxRate( $taxrate );
836
837
				if( isset( $taxes[$name][$taxrate] ) ) {
838
					$taxes[$name][$taxrate]->addItem( $price, $product->getQuantity() );
839
				} else {
840
					$taxes[$name][$taxrate] = $price->addItem( $price, $product->getQuantity() - 1 );
841
				}
842
			}
843
		}
844
845
		foreach( $this->getServices() as $services )
846
		{
847
			foreach( $services as $service )
848
			{
849
				$price = $service->getPrice();
850
851
				foreach( $price->getTaxrates() as $name => $taxrate )
852
				{
853
					$price = (clone $price)->setTaxRate( $taxrate );
854
855
					if( isset( $taxes[$name][$taxrate] ) ) {
856
						$taxes[$name][$taxrate]->addItem( $price );
857
					} else {
858
						$taxes[$name][$taxrate] = $price;
859
					}
860
				}
861
			}
862
		}
863
864
		return $taxes;
865
	}
866
867
868
	/**
869
	 * Checks if the price uses the same currency as the price in the basket.
870
	 *
871
	 * @param \Aimeos\MShop\Price\Item\Iface $item Price item
872
	 * @return void
873
	 */
874
	abstract protected function checkPrice( \Aimeos\MShop\Price\Item\Iface $item );
875
876
877
	/**
878
	 * Checks if all order addresses are valid
879
	 *
880
	 * @param \Aimeos\MShop\Order\Item\Base\Address\Iface[] $items Order address items
881
	 * @param string $type Address type constant from \Aimeos\MShop\Order\Item\Base\Address\Base
882
	 * @return \Aimeos\MShop\Order\Item\Base\Address\Iface[] List of checked items
883
	 * @throws \Aimeos\MShop\Exception If one of the order addresses is invalid
884
	 */
885
	protected function checkAddresses( iterable $items, string $type ) : iterable
886
	{
887
		map( $items )->implements( \Aimeos\MShop\Order\Item\Base\Address\Iface::class, true );
888
889
		foreach( $items as $key => $item ) {
890
			$items[$key] = $item->setType( $type );
891
		}
892
893
		return $items;
894
	}
895
896
897
	/**
898
	 * Checks if all order products are valid
899
	 *
900
	 * @param \Aimeos\MShop\Order\Item\Base\Product\Iface[] $items Order product items
901
	 * @return \Aimeos\MShop\Order\Item\Base\Product\Iface[] List of checked items
902
	 * @throws \Aimeos\MShop\Exception If one of the order products is invalid
903
	 */
904
	protected function checkProducts( iterable $items ) : \Aimeos\Map
905
	{
906
		map( $items )->implements( \Aimeos\MShop\Order\Item\Base\Product\Iface::class, true );
907
908
		foreach( $items as $key => $item )
909
		{
910
			if( $item->getProductCode() === '' ) {
911
				throw new \Aimeos\MShop\Order\Exception( sprintf( 'Product does not contain the SKU code' ) );
912
			}
913
914
			$this->checkPrice( $item->getPrice() );
915
		}
916
917
		return map( $items );
918
	}
919
920
921
	/**
922
	 * Checks if all order services are valid
923
	 *
924
	 * @param \Aimeos\MShop\Order\Item\Base\Service\Iface[] $items Order service items
925
	 * @param string $type Service type constant from \Aimeos\MShop\Order\Item\Base\Service\Base
926
	 * @return \Aimeos\MShop\Order\Item\Base\Service\Iface[] List of checked items
927
	 * @throws \Aimeos\MShop\Exception If one of the order services is invalid
928
	 */
929
	protected function checkServices( iterable $items, string $type ) : iterable
930
	{
931
		map( $items )->implements( \Aimeos\MShop\Order\Item\Base\Service\Iface::class, true );
932
933
		foreach( $items as $key => $item )
934
		{
935
			$this->checkPrice( $item->getPrice() );
936
			$items[$key] = $item->setType( $type );
937
		}
938
939
		return $items;
940
	}
941
942
943
	/**
944
	 * Tests if the given product is similar to an existing one.
945
	 * Similarity is described by the equality of properties so the quantity of
946
	 * the existing product can be updated.
947
	 *
948
	 * @param \Aimeos\MShop\Order\Item\Base\Product\Iface $item Order product item
949
	 * @param \Aimeos\MShop\Order\Item\Base\Product\Iface[] $products List of order product items to check against
950
	 * @return int|null Positon of the same product in the product list of false if product is unique
951
	 * @throws \Aimeos\MShop\Order\Exception If no similar item was found
952
	 */
953
	protected function getSameProduct( \Aimeos\MShop\Order\Item\Base\Product\Iface $item, iterable $products ) : ?int
954
	{
955
		$map = [];
956
		$count = 0;
957
958
		foreach( $item->getAttributeItems() as $attributeItem )
959
		{
960
			$key = md5( $attributeItem->getCode() . json_encode( $attributeItem->getValue() ) );
961
			$map[$key] = $attributeItem;
962
			$count++;
963
		}
964
965
		foreach( $products as $position => $product )
966
		{
967
			if( $product->compare( $item ) === false ) {
968
				continue;
969
			}
970
971
			$prodAttributes = $product->getAttributeItems();
972
973
			if( count( $prodAttributes ) !== $count ) {
974
				continue;
975
			}
976
977
			foreach( $prodAttributes as $attribute )
978
			{
979
				$key = md5( $attribute->getCode() . json_encode( $attribute->getValue() ) );
980
981
				if( isset( $map[$key] ) === false || $map[$key]->getQuantity() != $attribute->getQuantity() ) {
982
					continue 2; // jump to outer loop
983
				}
984
			}
985
986
			return $position;
987
		}
988
989
		return null;
990
	}
991
}
992