Completed
Push — master ( 24a86a...771b97 )
by Aimeos
10:31
created

Base::getService()   A

Complexity

Conditions 6
Paths 5

Size

Total Lines 19

Duplication

Lines 0
Ratio 0 %

Importance

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