Completed
Push — master ( 272a5c...1fc904 )
by Aimeos
08:57
created

Base::addProduct()   C

Complexity

Conditions 7
Paths 4

Size

Total Lines 46
Code Lines 25

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 7
eloc 25
nc 4
nop 2
dl 0
loc 46
rs 6.7272
c 0
b 0
f 0
1
<?php
2
3
/**
4
 * @license LGPLv3, http://opensource.org/licenses/LGPL-3.0
5
 * @copyright Metaways Infosystems GmbH, 2011
6
 * @copyright Aimeos (aimeos.org), 2015-2016
7
 * @package MShop
8
 * @subpackage Order
9
 */
10
11
12
namespace Aimeos\MShop\Order\Item\Base;
13
14
15
/**
16
 * Abstract order base class with necessary constants and basic methods.
17
 *
18
 * @package MShop
19
 * @subpackage Order
20
 */
21
abstract class Base
22
	extends \Aimeos\MW\Observer\Publisher\Base
0 ignored issues
show
Coding Style introduced by
Expected 0 spaces between "Base" and comma; 1 found
Loading history...
23
	implements \Aimeos\MShop\Order\Item\Base\Iface
24
{
25
	/**
26
	 * Check no basket content.
27
	 * Don't check if the basket is ready for checkout or ordering.
28
	 */
29
	const PARTS_NONE = 0;
30
31
	/**
32
	 * Check basket for products.
33
	 * Checks if the basket complies to the product related requirements.
34
	 */
35
	const PARTS_PRODUCT = 1;
36
37
	/**
38
	 * Check basket for addresses.
39
	 * Checks if the basket complies to the address related requirements.
40
	 */
41
	const PARTS_ADDRESS = 2;
42
43
	/**
44
	 * Check basket for delivery/payment.
45
	 * Checks if the basket complies to the delivery/payment related
46
	 * requirements.
47
	 */
48
	const PARTS_SERVICE = 4;
49
50
	/**
51
	 * Check basket for all parts.
52
	 * This constant matches all other part constants.
53
	 */
54
	const PARTS_ALL = 7;
55
56
57
	protected $products;
58
	protected $addresses;
59
	protected $services;
60
	protected $coupons;
61
	private $modified = false;
62
63
64
	/**
65
	 * Initializes the basket object
66
	 *
67
	 * @param \Aimeos\MShop\Price\Item\Iface $price Default price of the basket (usually 0.00)
68
	 * @param \Aimeos\MShop\Locale\Item\Iface $locale Locale item containing the site, language and currency
69
	 * @param array $values Associative list of key/value pairs containing, e.g. the order or user ID
70
	 * @param array $products List of ordered products implementing \Aimeos\MShop\Order\Item\Base\Product\Iface
71
	 * @param array $addresses List of order addresses implementing \Aimeos\MShop\Order\Item\Base\Address\Iface
72
	 * @param array $services List of order services implementing \Aimeos\MShop\Order\Item\Base\Service\Iface
73
	 * @param array $coupons Associative list of coupon codes as keys and ordered products implementing \Aimeos\MShop\Order\Item\Base\Product\Iface as values
74
	 */
75
	public function __construct( \Aimeos\MShop\Price\Item\Iface $price, \Aimeos\MShop\Locale\Item\Iface $locale,
76
			array $values = [], array $products = [], array $addresses = [],
77
			array $services = [], array $coupons = [] )
78
	{
79
		\Aimeos\MW\Common\Base::checkClassList( '\Aimeos\MShop\Order\Item\Base\Product\Iface', $products );
80
		\Aimeos\MW\Common\Base::checkClassList( '\Aimeos\MShop\Order\Item\Base\Address\Iface', $addresses );
81
		\Aimeos\MW\Common\Base::checkClassList( '\Aimeos\MShop\Order\Item\Base\Service\Iface', $services );
82
83
		foreach( $coupons as $couponProducts ) {
84
			\Aimeos\MW\Common\Base::checkClassList( '\Aimeos\MShop\Order\Item\Base\Product\Iface', $couponProducts );
85
		}
86
87
		$this->products = $products;
88
		$this->addresses = $addresses;
89
		$this->services = $services;
90
		$this->coupons = $coupons;
91
	}
92
93
94
	/**
95
	 * Clones internal objects of the order base item.
96
	 */
97
	public function __clone()
98
	{
99
		foreach( $this->products as $key => $value ) {
100
			$this->products[$key] = $value;
101
		}
102
103
		foreach( $this->addresses as $key => $value ) {
104
			$this->addresses[$key] = $value;
105
		}
106
107
		foreach( $this->services as $key => $value ) {
108
			$this->services[$key] = $value;
109
		}
110
111
		foreach( $this->coupons as $key => $value ) {
112
			$this->coupons[$key] = $value;
113
		}
114
	}
115
116
117
	/**
118
	 * Returns the item type
119
	 *
120
	 * @return string Item type, subtypes are separated by slashes
121
	 */
122
	public function getResourceType()
123
	{
124
		return 'order/base';
125
	}
126
127
128
	/**
129
	 * Returns the product items that are or should be part of an (future) order.
130
	 *
131
	 * @return array Array of order product items implementing \Aimeos\MShop\Order\Item\Base\Product\Iface
132
	 */
133
	public function getProducts()
134
	{
135
		return $this->products;
136
	}
137
138
139
	/**
140
	 * Returns the product item of an (future) order specified by its key.
141
	 *
142
	 * @param integer $key Key returned by getProducts() identifying the requested product
143
	 * @return \Aimeos\MShop\Order\Item\Base\Product\Iface Product item of an order
144
	 */
145
	public function getProduct( $key )
146
	{
147
		if( !isset( $this->products[$key] ) ) {
148
			throw new \Aimeos\MShop\Order\Exception( sprintf( 'Product with array key "%1$d" not available', $key ) );
149
		}
150
151
		return $this->products[$key];
152
	}
153
154
155
	/**
156
	 * Adds an order product item to the (future) order.
157
	 * If a similar item is found, only the quantity is increased.
158
	 *
159
	 * @param \Aimeos\MShop\Order\Item\Base\Product\Iface $item Order product item to be added
160
	 * @param integer|null $position position of the new order product item
161
	 */
162
	public function addProduct( \Aimeos\MShop\Order\Item\Base\Product\Iface $item, $position = null )
163
	{
164
		$this->checkProduct( $item );
165
		$this->checkPrice( $item->getPrice() );
166
167
		$this->notifyListeners( 'addProduct.before', $item );
168
169
		if( ( $pos = $this->getSameProduct( $item, $this->products ) ) !== false )
170
		{
171
			$quantity = $item->getQuantity();
172
			$item = $this->products[$pos];
173
			$item->setQuantity( $item->getQuantity() + $quantity );
174
		}
175
		else if( $position !== null )
176
		{
177
			if( isset( $this->products[$position] ) )
178
			{
179
				$products = [];
180
181
				foreach( $this->products as $key => $product )
182
				{
183
					if( $key < $position ) {
184
						$products[$key] = $product;
185
					} else if( $key >= $position ) {
186
						$products[$key + 1] = $product;
187
					}
188
				}
189
190
				$products[$position] = $item;
191
				$this->products = $products;
192
			}
193
			else
194
			{
195
				$this->products[$position] = $item;
196
			}
197
		}
198
		else
199
		{
200
			$this->products[] = $item;
201
		}
202
203
		ksort( $this->products );
204
		$this->setModified();
205
206
		$this->notifyListeners( 'addProduct.after', $item );
207
	}
208
209
210
	/**
211
	 * Sets a modified order product item to the (future) order.
212
	 *
213
	 * @param \Aimeos\MShop\Order\Item\Base\Product\Iface $item Order product item to be added
214
	 * @param integer $position Position id of the order product item
0 ignored issues
show
Bug introduced by
There is no parameter named $position. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
215
	 */
216
	public function editProduct( \Aimeos\MShop\Order\Item\Base\Product\Iface $item, $pos )
217
	{
218
		if( !array_key_exists( $pos, $this->products ) ) {
219
			throw new \Aimeos\MShop\Order\Exception( sprintf( 'Product with array key "%1$d" not available', $pos ) );
220
		}
221
222
		$this->notifyListeners( 'editProduct.before', $item );
223
224
		if( ( $pos = $this->getSameProduct( $item, $this->products ) ) !== false )
225
		{
226
			$this->products[$pos] = $item;
227
			$this->setModified();
228
		}
229
		else
230
		{
231
			$this->products[$pos] = $item;
232
		}
233
234
		$this->notifyListeners( 'editProduct.after', $item );
235
	}
236
237
238
	/**
239
	 * Deletes an order product item from the (future) order.
240
	 *
241
	 * @param integer $position Position id of the order product item
242
	 */
243
	public function deleteProduct( $position )
244
	{
245
		if( !array_key_exists( $position, $this->products ) ) {
246
			return;
247
		}
248
249
		$product = $this->products[$position];
250
251
		$this->notifyListeners( 'deleteProduct.before', $product );
252
253
		unset( $this->products[$position] );
254
		$this->setModified();
255
256
		$this->notifyListeners( 'deleteProduct.after', $product );
257
	}
258
259
260
	/**
261
	 * Returns all addresses that are part of the basket.
262
	 *
263
	 * @return array Associative list of address items implementing
264
	 *  \Aimeos\MShop\Order\Item\Base\Address\Iface with "billing" or "delivery" as key
265
	 */
266
	public function getAddresses()
267
	{
268
		return $this->addresses;
269
	}
270
271
272
	/**
273
	 * Returns the billing or delivery address depending on the given type.
274
	 *
275
	 * @param string $type Address type, usually "billing" or "delivery"
276
	 * @return \Aimeos\MShop\Order\Item\Base\Address\Iface Order address item for the requested type
277
	 */
278
	public function getAddress( $type = \Aimeos\MShop\Order\Item\Base\Address\Base::TYPE_PAYMENT )
279
	{
280
		if( !isset( $this->addresses[$type] ) ) {
281
			throw new \Aimeos\MShop\Order\Exception( sprintf( 'Address for type "%1$s" not available', $type ) );
282
		}
283
284
		return $this->addresses[$type];
285
	}
286
287
288
	/**
289
	 * Sets a customer address as billing or delivery address for an order.
290
	 *
291
	 * @param \Aimeos\MShop\Order\Item\Base\Address\Iface $address Order address item for the given type
292
	 * @param string $type Address type, usually "billing" or "delivery"
293
	 */
294
	public function setAddress( \Aimeos\MShop\Order\Item\Base\Address\Iface $address, $type )
0 ignored issues
show
Documentation introduced by
The return type could not be reliably inferred; please add a @return annotation.

Our type inference engine in quite powerful, but sometimes the code does not provide enough clues to go by. In these cases we request you to add a @return annotation as described here.

Loading history...
295
	{
296
		if( isset( $this->addresses[$type] ) && $this->addresses[$type] === $address ) { return $address; }
297
298
		$this->notifyListeners( 'setAddress.before', $address );
299
300
		$address = clone $address;
301
		$address->setType( $type ); // enforce that the type is the same as the given one
302
		$address->setId( null ); // enforce saving as new item
303
304
		$this->addresses[$type] = $address;
305
		$this->setModified();
306
307
		$this->notifyListeners( 'setAddress.after', $address );
308
	}
309
310
311
	/**
312
	 * Deleted a customer address for billing or delivery of an order.
313
	 *
314
	 * @param string $type Address type defined in \Aimeos\MShop\Order\Item\Base\Address\Base
315
	 */
316
	public function deleteAddress( $type )
317
	{
318
		if( !isset( $this->addresses[$type] ) ) {
319
			return;
320
		}
321
322
		$this->notifyListeners( 'deleteAddress.before', $type );
323
324
		$address = $this->addresses[$type];
325
		unset( $this->addresses[$type] );
326
		$this->setModified();
327
328
		$this->notifyListeners( 'deleteAddress.after', $address );
329
	}
330
331
332
	/**
333
	 * Returns all services that are part of the basket.
334
	 *
335
	 * @return array Associative list of service items implementing \Aimeos\MShop\Order\Service\Iface
336
	 *  with "delivery" or "payment" as key
337
	 */
338
	public function getServices()
339
	{
340
		return $this->services;
341
	}
342
343
344
	/**
345
	 * Returns the delivery or payment service depending on the given type.
346
	 *
347
	 * @param string $type Service type constant from \Aimeos\MShop\Order\Item\Service\Base
348
	 * @return \Aimeos\MShop\Order\Item\Base\Serive\Iface Order service item for the requested type
349
	 */
350
	public function getService( $type )
351
	{
352
		if( !isset( $this->services[$type] ) ) {
353
			throw new \Aimeos\MShop\Order\Exception( sprintf( 'Service of type "%1$s" not available', $type ) );
354
		}
355
356
		return $this->services[$type];
357
	}
358
359
360
	/**
361
	 * Sets a service as delivery or payment service for an order.
362
	 *
363
	 * @param \Aimeos\MShop\Order\Item\Base\Service\Iface $service Order service item for the given domain
364
	 * @param string $type Service type constant from \Aimeos\MShop\Order\Item\Service\Base
365
	 */
366
	public function setService( \Aimeos\MShop\Order\Item\Base\Service\Iface $service, $type )
367
	{
368
		$this->checkPrice( $service->getPrice() );
369
370
		$this->notifyListeners( 'setService.before', $service );
371
372
		$service = clone $service;
373
		$service->setType( $type ); // enforce that the type is the same as the given one
374
		$service->setId( null ); // enforce saving as new item
375
376
		$this->services[$type] = $service;
377
		$this->setModified();
378
379
		$this->notifyListeners( 'setService.after', $service );
380
	}
381
382
383
	/**
384
	 * Deletes the delivery or payment service from the basket.
385
	 *
386
	 * @param string $type Service type constant from \Aimeos\MShop\Order\Item\Service\Base
387
	 */
388
	public function deleteService( $type )
389
	{
390
		if( !isset( $this->services[$type] ) ) {
391
			return;
392
		}
393
394
		$this->notifyListeners( 'deleteService.before', $type );
395
396
		$service = $this->services[$type];
397
		unset( $this->services[$type] );
398
		$this->setModified();
399
400
		$this->notifyListeners( 'deleteService.after', $service );
401
	}
402
403
404
	/**
405
	 * Returns the available coupon codes and the lists of affected product items.
406
	 *
407
	 * @return array Associative array of codes and lists of product items
408
	 *  implementing \Aimeos\MShop\Order\Product\Iface
409
	 */
410
	public function getCoupons()
411
	{
412
		return $this->coupons;
413
	}
414
415
416
	/**
417
	 * Adds a coupon code entered by the customer and the given product item to the basket.
418
	 *
419
	 * @param string $code Coupon code
420
	 * @param \Aimeos\MShop\Order\Item\Base\Product\Iface[] $products List of coupon products
421
	 */
422
	public function addCoupon( $code, array $products = [] )
423
	{
424
		if( isset( $this->coupons[$code] ) ) {
425
			throw new \Aimeos\MShop\Order\Exception( sprintf( 'Duplicate coupon code "%1$s"', $code ) );
426
		}
427
428
		foreach( $products as $product )
429
		{
430
			$this->checkProduct( $product );
431
			$this->checkPrice( $product->getPrice() );
432
		}
433
434
		$this->notifyListeners( 'addCoupon.before', $products );
435
436
		$this->coupons[$code] = $products;
437
438
		foreach( $products as $product ) {
439
			$this->products[] = $product;
440
		}
441
442
		$this->setModified();
443
444
		$this->notifyListeners( 'addCoupon.after', $code );
445
	}
446
447
448
	/**
449
	 * Removes a coupon and the related product items from the basket.
450
	 *
451
	 * @param string $code Coupon code
452
	 * @param boolean $removecode If the coupon code should also be removed
453
	 * @return array List of affected product items implementing \Aimeos\MShop\Order\Item\Base\Product\Iface
454
	 *  or an empty list if no products are affected by a coupon
455
	 */
456
	public function deleteCoupon( $code, $removecode = false )
457
	{
458
		$products = [];
459
460
		if( isset( $this->coupons[$code] ) )
461
		{
462
			$this->notifyListeners( 'deleteCoupon.before', $code );
463
464
			$products = $this->coupons[$code];
465
466
			foreach( $products as $product )
467
			{
468
				if( ( $key = array_search( $product, $this->products, true ) ) !== false ) {
469
					unset( $this->products[$key] );
470
				}
471
			}
472
473
			if( $removecode === true ) {
474
				unset( $this->coupons[$code] );
475
			} else {
476
				$this->coupons[$code] = [];
477
			}
478
479
			$this->setModified();
480
481
			$this->notifyListeners( 'deleteCoupon.after', $code );
482
		}
483
484
		return $products;
485
	}
486
487
488
	/**
489
	 * Tests if all necessary items are available to create the order.
490
	 *
491
	 * @param integer $what Test for the specific type of completeness
492
	 * @throws \Aimeos\MShop\Order\Exception if there are no products in the basket
493
	 */
494
	public function check( $what = self::PARTS_ALL )
495
	{
496
		$this->checkParts( $what );
497
498
		$this->notifyListeners( 'check.before', $what );
499
500
		if( ( $what & self::PARTS_PRODUCT ) && ( count( $this->products ) < 1 ) ) {
501
			throw new \Aimeos\MShop\Order\Exception( sprintf( 'Basket empty' ) );
502
		}
503
504
		$this->notifyListeners( 'check.after', $what );
505
	}
506
507
508
	/**
509
	 * Tests if the order object was modified.
510
	 *
511
	 * @return bool True if modified, false if not
512
	 */
513
	public function isModified()
514
	{
515
		return $this->modified;
516
	}
517
518
519
	/**
520
	 * Sets the modified flag of the object.
521
	 */
522
	public function setModified()
523
	{
524
		$this->modified = true;
525
	}
526
527
528
	/**
529
	 * Checks the constants for the different parts of the basket.
530
	 *
531
	 * @param integer $value Part constant
532
	 * @throws \Aimeos\MShop\Order\Exception If parts constant is invalid
533
	 */
534
	protected function checkParts( $value )
535
	{
536
		$value = (int) $value;
537
538
		if( $value < self::PARTS_NONE || $value > self::PARTS_ALL ) {
539
			throw new \Aimeos\MShop\Order\Exception( sprintf( 'Flags "%1$s" not within allowed range', $value ) );
540
		}
541
	}
542
543
544
	/**
545
	 * Checks if the price uses the same currency as the price in the basket.
546
	 *
547
	 * @param \Aimeos\MShop\Price\Item\Iface $item Price item
548
	 * @return null
549
	 */
550
	abstract protected function checkPrice( \Aimeos\MShop\Price\Item\Iface $item );
551
552
553
	/**
554
	 * Checks if a order product contains all required values.
555
	 *
556
	 * @param \Aimeos\MShop\Order\Item\Base\Product\Iface $item Order product item
557
	 * @throws \Aimeos\MShop\Exception if the price item or product code is missing
558
	 */
559
	protected function checkProduct( \Aimeos\MShop\Order\Item\Base\Product\Iface $item )
560
	{
561
		if( $item->getProductCode() === '' ) {
562
			throw new \Aimeos\MShop\Order\Exception( sprintf( 'Product does not contain all required values. Product code for item not available.' ) );
563
		}
564
	}
565
566
567
	/**
568
	 * Tests if the given product is similar to an existing one.
569
	 * Similarity is described by the equality of properties so the quantity of
570
	 * the existing product can be updated.
571
	 *
572
	 * @param \Aimeos\MShop\Order\Item\Base\Product\Iface $item Order product item
573
	 * @param array $products List of order product items to check against
574
	 * @return integer|false Positon of the same product in the product list of false if product is unique
575
	 * @throws \Aimeos\MShop\Order\Exception If no similar item was found
576
	 */
577
	protected function getSameProduct( \Aimeos\MShop\Order\Item\Base\Product\Iface $item, array $products )
578
	{
579
		$attributeMap = [];
580
581
		foreach( $item->getAttributes() as $attributeItem ) {
582
			$attributeMap[$attributeItem->getCode()] = $attributeItem;
583
		}
584
585
		foreach( $products as $position => $product )
586
		{
587
			if( $product->compare( $item ) === false ) {
588
				continue;
589
			}
590
591
			$prodAttributes = $product->getAttributes();
592
593
			if( count( $prodAttributes ) !== count( $attributeMap ) ) {
594
				continue;
595
			}
596
597
			foreach( $prodAttributes as $attribute )
598
			{
599
				if( array_key_exists( $attribute->getCode(), $attributeMap ) === false
600
					|| $attributeMap[$attribute->getCode()]->getValue() != $attribute->getValue() ) {
601
					continue 2; // jump to outer loop
602
				}
603
			}
604
605
			return $position;
606
		}
607
608
		return false;
609
	}
610
}
611