Completed
Push — master ( 5f83b4...7d4729 )
by Aimeos
07:44
created

Base::__get()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 3
nc 2
nop 1
dl 0
loc 6
rs 9.4285
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-2017
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 $bdata;
58
	protected $products;
59
	protected $addresses;
60
	protected $services;
61
	protected $coupons;
62
	private $modified = false;
63
64
65
	/**
66
	 * Initializes the basket object
67
	 *
68
	 * @param \Aimeos\MShop\Price\Item\Iface $price Default price of the basket (usually 0.00)
69
	 * @param \Aimeos\MShop\Locale\Item\Iface $locale Locale item containing the site, language and currency
70
	 * @param array $values Associative list of key/value pairs containing, e.g. the order or user ID
71
	 * @param array $products List of ordered products implementing \Aimeos\MShop\Order\Item\Base\Product\Iface
72
	 * @param array $addresses List of order addresses implementing \Aimeos\MShop\Order\Item\Base\Address\Iface
73
	 * @param array $services List of order services implementing \Aimeos\MShop\Order\Item\Base\Service\Iface
74
	 * @param array $coupons Associative list of coupon codes as keys and ordered products implementing \Aimeos\MShop\Order\Item\Base\Product\Iface as values
75
	 */
76
	public function __construct( \Aimeos\MShop\Price\Item\Iface $price, \Aimeos\MShop\Locale\Item\Iface $locale,
77
			array $values = [], array $products = [], array $addresses = [],
78
			array $services = [], array $coupons = [] )
79
	{
80
		\Aimeos\MW\Common\Base::checkClassList( '\Aimeos\MShop\Order\Item\Base\Product\Iface', $products );
81
		\Aimeos\MW\Common\Base::checkClassList( '\Aimeos\MShop\Order\Item\Base\Address\Iface', $addresses );
82
		\Aimeos\MW\Common\Base::checkClassList( '\Aimeos\MShop\Order\Item\Base\Service\Iface', $services );
83
84
		foreach( $coupons as $couponProducts ) {
85
			\Aimeos\MW\Common\Base::checkClassList( '\Aimeos\MShop\Order\Item\Base\Product\Iface', $couponProducts );
86
		}
87
88
		$this->bdata = $values;
89
		$this->products = $products;
90
		$this->addresses = $addresses;
91
		$this->services = $services;
92
		$this->coupons = $coupons;
93
	}
94
95
96
	/**
97
	 * Clones internal objects of the order base item.
98
	 */
99
	public function __clone()
100
	{
101
		foreach( $this->products as $key => $value ) {
102
			$this->products[$key] = $value;
103
		}
104
105
		foreach( $this->addresses as $key => $value ) {
106
			$this->addresses[$key] = $value;
107
		}
108
109
		foreach( $this->services as $key => $value ) {
110
			$this->services[$key] = $value;
111
		}
112
113
		foreach( $this->coupons as $key => $value ) {
114
			$this->coupons[$key] = $value;
115
		}
116
	}
117
118
119
	/**
120
	 * Returns the item property for the given name
121
	 *
122
	 * @param string $name Name of the property
123
	 * @return mixed|null Property value or null if property is unknown
124
	 */
125
	public function __get( $name )
126
	{
127
		if( isset( $this->bdata[$name] ) ) {
128
			return $this->bdata[$name];
129
		}
130
	}
131
132
133
	/**
134
	 * Tests if the item property for the given name is available
135
	 *
136
	 * @param string $name Name of the property
137
	 * @return boolean True if the property exists, false if not
138
	 */
139
	public function __isset( $name )
140
	{
141
		if( array_key_exists( $name, $this->bdata ) ) {
142
			return true;
143
		}
144
145
		return false;
146
	}
147
148
149
	/**
150
	 * Returns the item type
151
	 *
152
	 * @return string Item type, subtypes are separated by slashes
153
	 */
154
	public function getResourceType()
155
	{
156
		return 'order/base';
157
	}
158
159
160
	/**
161
	 * Returns the product items that are or should be part of an (future) order.
162
	 *
163
	 * @return array Array of order product items implementing \Aimeos\MShop\Order\Item\Base\Product\Iface
164
	 */
165
	public function getProducts()
166
	{
167
		return $this->products;
168
	}
169
170
171
	/**
172
	 * Returns the product item of an (future) order specified by its key.
173
	 *
174
	 * @param integer $key Key returned by getProducts() identifying the requested product
175
	 * @return \Aimeos\MShop\Order\Item\Base\Product\Iface Product item of an order
176
	 */
177
	public function getProduct( $key )
178
	{
179
		if( !isset( $this->products[$key] ) ) {
180
			throw new \Aimeos\MShop\Order\Exception( sprintf( 'Product with array key "%1$d" not available', $key ) );
181
		}
182
183
		return $this->products[$key];
184
	}
185
186
187
	/**
188
	 * Adds an order product item to the (future) order.
189
	 * If a similar item is found, only the quantity is increased.
190
	 *
191
	 * @param \Aimeos\MShop\Order\Item\Base\Product\Iface $item Order product item to be added
192
	 * @param integer|null $position position of the new order product item
193
	 */
194
	public function addProduct( \Aimeos\MShop\Order\Item\Base\Product\Iface $item, $position = null )
195
	{
196
		$this->checkProduct( $item );
197
		$this->checkPrice( $item->getPrice() );
198
199
		$this->notifyListeners( 'addProduct.before', $item );
200
201
		if( ( $pos = $this->getSameProduct( $item, $this->products ) ) !== false )
202
		{
203
			$quantity = $item->getQuantity();
204
			$item = $this->products[$pos];
205
			$item->setQuantity( $item->getQuantity() + $quantity );
206
		}
207
		else if( $position !== null )
208
		{
209
			if( isset( $this->products[$position] ) )
210
			{
211
				$products = [];
212
213
				foreach( $this->products as $key => $product )
214
				{
215
					if( $key < $position ) {
216
						$products[$key] = $product;
217
					} else if( $key >= $position ) {
218
						$products[$key + 1] = $product;
219
					}
220
				}
221
222
				$products[$position] = $item;
223
				$this->products = $products;
224
			}
225
			else
226
			{
227
				$this->products[$position] = $item;
228
			}
229
		}
230
		else
231
		{
232
			$this->products[] = $item;
233
		}
234
235
		ksort( $this->products );
236
		$this->setModified();
237
238
		$this->notifyListeners( 'addProduct.after', $item );
239
	}
240
241
242
	/**
243
	 * Sets a modified order product item to the (future) order.
244
	 *
245
	 * @param \Aimeos\MShop\Order\Item\Base\Product\Iface $item Order product item to be added
246
	 * @param integer $pos Position id of the order product item
247
	 */
248
	public function editProduct( \Aimeos\MShop\Order\Item\Base\Product\Iface $item, $pos )
249
	{
250
		if( !array_key_exists( $pos, $this->products ) ) {
251
			throw new \Aimeos\MShop\Order\Exception( sprintf( 'Product with array key "%1$d" not available', $pos ) );
252
		}
253
254
		$this->notifyListeners( 'editProduct.before', $item );
255
256
		if( ( $pos = $this->getSameProduct( $item, $this->products ) ) !== false )
257
		{
258
			$this->products[$pos] = $item;
259
			$this->setModified();
260
		}
261
		else
262
		{
263
			$this->products[$pos] = $item;
264
		}
265
266
		$this->notifyListeners( 'editProduct.after', $item );
267
	}
268
269
270
	/**
271
	 * Deletes an order product item from the (future) order.
272
	 *
273
	 * @param integer $position Position id of the order product item
274
	 */
275
	public function deleteProduct( $position )
276
	{
277
		if( !array_key_exists( $position, $this->products ) ) {
278
			return;
279
		}
280
281
		$product = $this->products[$position];
282
283
		$this->notifyListeners( 'deleteProduct.before', $product );
284
285
		unset( $this->products[$position] );
286
		$this->setModified();
287
288
		$this->notifyListeners( 'deleteProduct.after', $product );
289
	}
290
291
292
	/**
293
	 * Returns all addresses that are part of the basket.
294
	 *
295
	 * @return array Associative list of address items implementing
296
	 *  \Aimeos\MShop\Order\Item\Base\Address\Iface with "billing" or "delivery" as key
297
	 */
298
	public function getAddresses()
299
	{
300
		return $this->addresses;
301
	}
302
303
304
	/**
305
	 * Returns the billing or delivery address depending on the given type.
306
	 *
307
	 * @param string $type Address type, usually "billing" or "delivery"
308
	 * @return \Aimeos\MShop\Order\Item\Base\Address\Iface Order address item for the requested type
309
	 */
310
	public function getAddress( $type = \Aimeos\MShop\Order\Item\Base\Address\Base::TYPE_PAYMENT )
311
	{
312
		if( !isset( $this->addresses[$type] ) ) {
313
			throw new \Aimeos\MShop\Order\Exception( sprintf( 'Address for type "%1$s" not available', $type ) );
314
		}
315
316
		return $this->addresses[$type];
317
	}
318
319
320
	/**
321
	 * Sets a customer address as billing or delivery address for an order.
322
	 *
323
	 * @param \Aimeos\MShop\Order\Item\Base\Address\Iface $address Order address item for the given type
324
	 * @param string $type Address type, usually "billing" or "delivery"
325
	 */
326
	public function setAddress( \Aimeos\MShop\Order\Item\Base\Address\Iface $address, $type )
327
	{
328
		if( isset( $this->addresses[$type] ) && $this->addresses[$type] === $address ) { return; }
329
330
		$this->notifyListeners( 'setAddress.before', $address );
331
332
		$address = clone $address;
333
		$address->setType( $type ); // enforce that the type is the same as the given one
334
		$address->setId( null ); // enforce saving as new item
335
336
		$this->addresses[$type] = $address;
337
		$this->setModified();
338
339
		$this->notifyListeners( 'setAddress.after', $address );
340
	}
341
342
343
	/**
344
	 * Deleted a customer address for billing or delivery of an order.
345
	 *
346
	 * @param string $type Address type defined in \Aimeos\MShop\Order\Item\Base\Address\Base
347
	 */
348
	public function deleteAddress( $type )
349
	{
350
		if( !isset( $this->addresses[$type] ) ) {
351
			return;
352
		}
353
354
		$this->notifyListeners( 'deleteAddress.before', $type );
355
356
		$address = $this->addresses[$type];
357
		unset( $this->addresses[$type] );
358
		$this->setModified();
359
360
		$this->notifyListeners( 'deleteAddress.after', $address );
361
	}
362
363
364
	/**
365
	 * Returns all services that are part of the basket.
366
	 *
367
	 * @return array Associative list of service items implementing \Aimeos\MShop\Order\Service\Iface
368
	 *  with "delivery" or "payment" as key
369
	 */
370
	public function getServices()
371
	{
372
		return $this->services;
373
	}
374
375
376
	/**
377
	 * Returns the delivery or payment service depending on the given type.
378
	 *
379
	 * @param string $type Service type constant from \Aimeos\MShop\Order\Item\Service\Base
380
	 * @return \Aimeos\MShop\Order\Item\Base\Serive\Iface Order service item for the requested type
381
	 */
382
	public function getService( $type )
383
	{
384
		if( !isset( $this->services[$type] ) ) {
385
			throw new \Aimeos\MShop\Order\Exception( sprintf( 'Service of type "%1$s" not available', $type ) );
386
		}
387
388
		return $this->services[$type];
389
	}
390
391
392
	/**
393
	 * Sets a service as delivery or payment service for an order.
394
	 *
395
	 * @param \Aimeos\MShop\Order\Item\Base\Service\Iface $service Order service item for the given domain
396
	 * @param string $type Service type constant from \Aimeos\MShop\Order\Item\Service\Base
397
	 */
398
	public function setService( \Aimeos\MShop\Order\Item\Base\Service\Iface $service, $type )
399
	{
400
		$this->checkPrice( $service->getPrice() );
401
402
		$this->notifyListeners( 'setService.before', $service );
403
404
		$service = clone $service;
405
		$service->setType( $type ); // enforce that the type is the same as the given one
406
		$service->setId( null ); // enforce saving as new item
407
408
		$this->services[$type] = $service;
409
		$this->setModified();
410
411
		$this->notifyListeners( 'setService.after', $service );
412
	}
413
414
415
	/**
416
	 * Deletes the delivery or payment service from the basket.
417
	 *
418
	 * @param string $type Service type constant from \Aimeos\MShop\Order\Item\Service\Base
419
	 */
420
	public function deleteService( $type )
421
	{
422
		if( !isset( $this->services[$type] ) ) {
423
			return;
424
		}
425
426
		$this->notifyListeners( 'deleteService.before', $type );
427
428
		$service = $this->services[$type];
429
		unset( $this->services[$type] );
430
		$this->setModified();
431
432
		$this->notifyListeners( 'deleteService.after', $service );
433
	}
434
435
436
	/**
437
	 * Returns the available coupon codes and the lists of affected product items.
438
	 *
439
	 * @return array Associative array of codes and lists of product items
440
	 *  implementing \Aimeos\MShop\Order\Product\Iface
441
	 */
442
	public function getCoupons()
443
	{
444
		return $this->coupons;
445
	}
446
447
448
	/**
449
	 * Adds a coupon code entered by the customer and the given product item to the basket.
450
	 *
451
	 * @param string $code Coupon code
452
	 * @param \Aimeos\MShop\Order\Item\Base\Product\Iface[] $products List of coupon products
453
	 */
454
	public function addCoupon( $code, array $products = [] )
455
	{
456
		if( isset( $this->coupons[$code] ) ) {
457
			throw new \Aimeos\MShop\Order\Exception( sprintf( 'Duplicate coupon code "%1$s"', $code ) );
458
		}
459
460
		foreach( $products as $product )
461
		{
462
			$this->checkProduct( $product );
463
			$this->checkPrice( $product->getPrice() );
464
		}
465
466
		$this->notifyListeners( 'addCoupon.before', $products );
467
468
		$this->coupons[$code] = $products;
469
470
		foreach( $products as $product ) {
471
			$this->products[] = $product;
472
		}
473
474
		$this->setModified();
475
476
		$this->notifyListeners( 'addCoupon.after', $code );
477
	}
478
479
480
	/**
481
	 * Removes a coupon and the related product items from the basket.
482
	 *
483
	 * @param string $code Coupon code
484
	 * @param boolean $removecode If the coupon code should also be removed
485
	 * @return array List of affected product items implementing \Aimeos\MShop\Order\Item\Base\Product\Iface
486
	 *  or an empty list if no products are affected by a coupon
487
	 */
488
	public function deleteCoupon( $code, $removecode = false )
489
	{
490
		$products = [];
491
492
		if( isset( $this->coupons[$code] ) )
493
		{
494
			$this->notifyListeners( 'deleteCoupon.before', $code );
495
496
			$products = $this->coupons[$code];
497
498
			foreach( $products as $product )
499
			{
500
				if( ( $key = array_search( $product, $this->products, true ) ) !== false ) {
501
					unset( $this->products[$key] );
502
				}
503
			}
504
505
			if( $removecode === true ) {
506
				unset( $this->coupons[$code] );
507
			} else {
508
				$this->coupons[$code] = [];
509
			}
510
511
			$this->setModified();
512
513
			$this->notifyListeners( 'deleteCoupon.after', $code );
514
		}
515
516
		return $products;
517
	}
518
519
520
	/**
521
	 * Tests if all necessary items are available to create the order.
522
	 *
523
	 * @param integer $what Test for the specific type of completeness
524
	 * @throws \Aimeos\MShop\Order\Exception if there are no products in the basket
525
	 */
526
	public function check( $what = self::PARTS_ALL )
527
	{
528
		$this->checkParts( $what );
529
530
		$this->notifyListeners( 'check.before', $what );
531
532
		if( ( $what & self::PARTS_PRODUCT ) && ( count( $this->products ) < 1 ) ) {
533
			throw new \Aimeos\MShop\Order\Exception( sprintf( 'Basket empty' ) );
534
		}
535
536
		$this->notifyListeners( 'check.after', $what );
537
	}
538
539
540
	/**
541
	 * Tests if the order object was modified.
542
	 *
543
	 * @return bool True if modified, false if not
544
	 */
545
	public function isModified()
546
	{
547
		return $this->modified;
548
	}
549
550
551
	/**
552
	 * Sets the modified flag of the object.
553
	 */
554
	public function setModified()
555
	{
556
		$this->modified = true;
557
	}
558
559
560
	/**
561
	 * Checks the constants for the different parts of the basket.
562
	 *
563
	 * @param integer $value Part constant
564
	 * @throws \Aimeos\MShop\Order\Exception If parts constant is invalid
565
	 */
566
	protected function checkParts( $value )
567
	{
568
		$value = (int) $value;
569
570
		if( $value < self::PARTS_NONE || $value > self::PARTS_ALL ) {
571
			throw new \Aimeos\MShop\Order\Exception( sprintf( 'Flags "%1$s" not within allowed range', $value ) );
572
		}
573
	}
574
575
576
	/**
577
	 * Checks if the price uses the same currency as the price in the basket.
578
	 *
579
	 * @param \Aimeos\MShop\Price\Item\Iface $item Price item
580
	 * @return null
581
	 */
582
	abstract protected function checkPrice( \Aimeos\MShop\Price\Item\Iface $item );
583
584
585
	/**
586
	 * Checks if a order product contains all required values.
587
	 *
588
	 * @param \Aimeos\MShop\Order\Item\Base\Product\Iface $item Order product item
589
	 * @throws \Aimeos\MShop\Exception if the price item or product code is missing
590
	 */
591
	protected function checkProduct( \Aimeos\MShop\Order\Item\Base\Product\Iface $item )
592
	{
593
		if( $item->getProductCode() === '' ) {
594
			throw new \Aimeos\MShop\Order\Exception( sprintf( 'Product does not contain all required values. Product code for item not available.' ) );
595
		}
596
	}
597
598
599
	/**
600
	 * Tests if the given product is similar to an existing one.
601
	 * Similarity is described by the equality of properties so the quantity of
602
	 * the existing product can be updated.
603
	 *
604
	 * @param \Aimeos\MShop\Order\Item\Base\Product\Iface $item Order product item
605
	 * @param array $products List of order product items to check against
606
	 * @return integer|false Positon of the same product in the product list of false if product is unique
607
	 * @throws \Aimeos\MShop\Order\Exception If no similar item was found
608
	 */
609
	protected function getSameProduct( \Aimeos\MShop\Order\Item\Base\Product\Iface $item, array $products )
610
	{
611
		$attributeMap = [];
612
613
		foreach( $item->getAttributes() as $attributeItem ) {
614
			$attributeMap[$attributeItem->getCode()] = $attributeItem;
615
		}
616
617
		foreach( $products as $position => $product )
618
		{
619
			if( $product->compare( $item ) === false ) {
620
				continue;
621
			}
622
623
			$prodAttributes = $product->getAttributes();
624
625
			if( count( $prodAttributes ) !== count( $attributeMap ) ) {
626
				continue;
627
			}
628
629
			foreach( $prodAttributes as $attribute )
630
			{
631
				if( array_key_exists( $attribute->getCode(), $attributeMap ) === false
632
					|| $attributeMap[$attribute->getCode()]->getValue() != $attribute->getValue() ) {
633
					continue 2; // jump to outer loop
634
				}
635
			}
636
637
			return $position;
638
		}
639
640
		return false;
641
	}
642
}
643