Completed
Push — master ( afe8d8...095350 )
by Aimeos
02:02
created

Base::createSubscriptions()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 18
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 18
rs 9.4285
c 0
b 0
f 0
cc 3
eloc 10
nc 3
nop 1
1
<?php
2
3
/**
4
 * @license LGPLv3, http://opensource.org/licenses/LGPL-3.0
5
 * @copyright Metaways Infosystems GmbH, 2012
6
 * @copyright Aimeos (aimeos.org), 2015-2017
7
 * @package Controller
8
 * @subpackage Frontend
9
 */
10
11
12
namespace Aimeos\Controller\Frontend\Basket;
13
14
15
/**
16
 * Base class for the basket frontend controller
17
 *
18
 * @package Controller
19
 * @subpackage Frontend
20
 */
21
abstract class Base extends \Aimeos\Controller\Frontend\Base implements Iface
22
{
23
	private $listTypeItems = [];
24
25
26
	/**
27
	 * Calculates and returns the current price for the given order product and product prices.
28
	 *
29
	 * @param \Aimeos\MShop\Order\Item\Base\Product\Iface $product Ordered product item
30
	 * @param \Aimeos\MShop\Price\Item\Iface[] $prices List of price items
31
	 * @param integer $quantity New product quantity
32
	 * @return \Aimeos\MShop\Price\Item\Iface Price item with calculated price
33
	 */
34
	protected function calcPrice( \Aimeos\MShop\Order\Item\Base\Product\Iface $product, array $prices, $quantity )
35
	{
36
		$context = $this->getContext();
37
38
		if( empty( $prices ) )
39
		{
40
			$manager = \Aimeos\MShop\Factory::createManager( $context, 'product' );
41
			$prices = $manager->getItem( $product->getProductId(), array( 'price' ) )->getRefItems( 'price', 'default' );
42
		}
43
44
45
		$priceManager = \Aimeos\MShop\Factory::createManager( $context, 'price' );
46
		$price = $priceManager->getLowestPrice( $prices, $quantity );
47
48
		// customers can pay what they would like to pay
49
		if( ( $attr = $product->getAttributeItem( 'price', 'custom' ) ) !== null )
50
		{
51
			$amount = $attr->getValue();
52
53
			if( preg_match( '/^[0-9]*(\.[0-9]+)?$/', $amount ) !== 1 || ((double) $amount) < 0.01 )
54
			{
55
				$msg = $context->getI18n()->dt( 'controller/frontend', 'Invalid price value "%1$s"' );
56
				throw new \Aimeos\Controller\Frontend\Basket\Exception( sprintf( $msg, $amount ) );
57
			}
58
59
			$price->setValue( $amount );
60
		}
61
62
		$orderAttributes = $product->getAttributes();
63
		$attrItems = $this->getAttributeItems( $orderAttributes );
64
65
		// add prices of (optional) attributes
66
		foreach( $orderAttributes as $orderAttrItem )
67
		{
68
			$attrId = $orderAttrItem->getAttributeId();
69
70
			if( isset( $attrItems[$attrId] )
71
				&& ( $prices = $attrItems[$attrId]->getRefItems( 'price', 'default' ) ) !== []
72
			) {
73
				$attrPrice = $priceManager->getLowestPrice( $prices, $orderAttrItem->getQuantity() );
74
				$price->addItem( $attrPrice, $orderAttrItem->getQuantity() );
75
			}
76
		}
77
78
		// remove product rebate of original price in favor to rebates granted for the order
79
		$price->setRebate( '0.00' );
80
81
		return $price;
82
	}
83
84
85
	/**
86
	 * Checks if the reference IDs are really associated to the product
87
	 *
88
	 * @param string|array $prodId Unique ID of the product or list of product IDs
89
	 * @param string $domain Domain the references must be of
90
	 * @param array $refMap Associative list of list type codes as keys and lists of reference IDs as values
91
	 * @throws \Aimeos\Controller\Frontend\Basket\Exception If one or more of the IDs are not associated
92
	 */
93
	protected function checkListRef( $prodId, $domain, array $refMap )
94
	{
95
		if( empty( $refMap ) ) {
96
			return;
97
		}
98
99
		$context = $this->getContext();
100
		$productManager = \Aimeos\MShop\Factory::createManager( $context, 'product' );
101
		$search = $productManager->createSearch( true );
102
103
		$expr = array(
104
			$search->compare( '==', 'product.id', $prodId ),
105
			$search->getConditions(),
106
		);
107
108
		foreach( $refMap as $listType => $refIds )
109
		{
110
			if( empty( $refIds ) ) {
111
				continue;
112
			}
113
114
			foreach( $refIds as $key => $refId ) {
115
				$refIds[$key] = (string) $refId;
116
			}
117
118
			$param = array( $domain, $this->getProductListTypeItem( $domain, $listType )->getId(), $refIds );
119
			$cmpfunc = $search->createFunction( 'product.contains', $param );
120
121
			$expr[] = $search->compare( '==', $cmpfunc, count( $refIds ) );
122
		}
123
124
		$search->setConditions( $search->combine( '&&', $expr ) );
125
126
		if( count( $productManager->searchItems( $search, [] ) ) === 0 )
127
		{
128
			$msg = $context->getI18n()->dt( 'controller/frontend', 'Invalid "%1$s" references for product with ID %2$s' );
129
			throw new \Aimeos\Controller\Frontend\Basket\Exception( sprintf( $msg, $domain, json_encode( $prodId ) ) );
130
		}
131
	}
132
133
134
	/**
135
	 * Checks for a locale mismatch and migrates the products to the new basket if necessary.
136
	 *
137
	 * @param \Aimeos\MShop\Locale\Item\Iface $locale Locale object from current basket
138
	 * @param string $type Basket type
139
	 */
140
	protected function checkLocale( \Aimeos\MShop\Locale\Item\Iface $locale, $type )
141
	{
142
		$errors = [];
143
		$context = $this->getContext();
144
		$session = $context->getSession();
145
146
		$localeStr = $session->get( 'aimeos/basket/locale' );
147
		$localeKey = $locale->getSite()->getCode() . '|' . $locale->getLanguageId() . '|' . $locale->getCurrencyId();
148
149
		if( $localeStr !== null && $localeStr !== $localeKey )
150
		{
151
			$locParts = explode( '|', $localeStr );
152
			$locSite = ( isset( $locParts[0] ) ? $locParts[0] : '' );
153
			$locLanguage = ( isset( $locParts[1] ) ? $locParts[1] : '' );
154
			$locCurrency = ( isset( $locParts[2] ) ? $locParts[2] : '' );
155
156
			$localeManager = \Aimeos\MShop\Factory::createManager( $context, 'locale' );
157
			$locale = $localeManager->bootstrap( $locSite, $locLanguage, $locCurrency, false );
158
159
			$context = clone $context;
160
			$context->setLocale( $locale );
161
162
			$manager = \Aimeos\MShop\Factory::createManager( $context, 'order/base' );
163
			$basket = $manager->getSession( $type );
164
165
			$this->copyAddresses( $basket, $errors, $localeKey );
166
			$this->copyServices( $basket, $errors );
167
			$this->copyProducts( $basket, $errors, $localeKey );
168
			$this->copyCoupons( $basket, $errors, $localeKey );
169
170
			$manager->setSession( $basket, $type );
171
		}
172
173
		$session->set( 'aimeos/basket/locale', $localeKey );
174
	}
175
176
177
	/**
178
	 * Migrates the addresses from the old basket to the current one.
179
	 *
180
	 * @param \Aimeos\MShop\Order\Item\Base\Iface $basket Basket object
181
	 * @param array $errors Associative list of previous errors
182
	 * @param string $localeKey Unique identifier of the site, language and currency
183
	 * @return array Associative list of errors occured
184
	 */
185
	protected function copyAddresses( \Aimeos\MShop\Order\Item\Base\Iface $basket, array $errors, $localeKey )
186
	{
187
		foreach( $basket->getAddresses() as $type => $item )
188
		{
189
			try
190
			{
191
				$this->setAddress( $type, $item->toArray() );
192
				$basket->deleteAddress( $type );
193
			}
194
			catch( \Exception $e )
195
			{
196
				$logger = $this->getContext()->getLogger();
197
				$errors['address'][$type] = $e->getMessage();
198
199
				$str = 'Error migrating address with type "%1$s" in basket to locale "%2$s": %3$s';
200
				$logger->log( sprintf( $str, $type, $localeKey, $e->getMessage() ), \Aimeos\MW\Logger\Base::INFO );
201
			}
202
		}
203
204
		return $errors;
205
	}
206
207
208
	/**
209
	 * Migrates the coupons from the old basket to the current one.
210
	 *
211
	 * @param \Aimeos\MShop\Order\Item\Base\Iface $basket Basket object
212
	 * @param array $errors Associative list of previous errors
213
	 * @param string $localeKey Unique identifier of the site, language and currency
214
	 * @return array Associative list of errors occured
215
	 */
216
	protected function copyCoupons( \Aimeos\MShop\Order\Item\Base\Iface $basket, array $errors, $localeKey )
217
	{
218
		foreach( $basket->getCoupons() as $code => $list )
219
		{
220
			try
221
			{
222
				$this->addCoupon( $code );
223
				$basket->deleteCoupon( $code, true );
224
			}
225
			catch( \Exception $e )
226
			{
227
				$logger = $this->getContext()->getLogger();
228
				$errors['coupon'][$code] = $e->getMessage();
229
230
				$str = 'Error migrating coupon with code "%1$s" in basket to locale "%2$s": %3$s';
231
				$logger->log( sprintf( $str, $code, $localeKey, $e->getMessage() ), \Aimeos\MW\Logger\Base::INFO );
232
			}
233
		}
234
235
		return $errors;
236
	}
237
238
239
	/**
240
	 * Migrates the products from the old basket to the current one.
241
	 *
242
	 * @param \Aimeos\MShop\Order\Item\Base\Iface $basket Basket object
243
	 * @param array $errors Associative list of previous errors
244
	 * @param string $localeKey Unique identifier of the site, language and currency
245
	 * @return array Associative list of errors occured
246
	 */
247
	protected function copyProducts( \Aimeos\MShop\Order\Item\Base\Iface $basket, array $errors, $localeKey )
248
	{
249
		foreach( $basket->getProducts() as $pos => $product )
250
		{
251
			if( $product->getFlags() & \Aimeos\MShop\Order\Item\Base\Product\Base::FLAG_IMMUTABLE ) {
252
				continue;
253
			}
254
255
			try
256
			{
257
				$attrIds = [];
258
259
				foreach( $product->getAttributes() as $attrItem ) {
260
					$attrIds[$attrItem->getType()][] = $attrItem->getAttributeId();
261
				}
262
263
				$this->addProduct(
264
					$product->getProductId(),
265
					$product->getQuantity(),
266
					$product->getStockType(),
267
					$this->getValue( $attrIds, 'variant', [] ),
268
					$this->getValue( $attrIds, 'config', [] ),
269
					$this->getValue( $attrIds, 'hidden', [] ),
270
					$this->getValue( $attrIds, 'custom', [] )
271
				);
272
273
				$basket->deleteProduct( $pos );
274
			}
275
			catch( \Exception $e )
276
			{
277
				$code = $product->getProductCode();
278
				$logger = $this->getContext()->getLogger();
279
				$errors['product'][$pos] = $e->getMessage();
280
281
				$str = 'Error migrating product with code "%1$s" in basket to locale "%2$s": %3$s';
282
				$logger->log( sprintf( $str, $code, $localeKey, $e->getMessage() ), \Aimeos\MW\Logger\Base::INFO );
283
			}
284
		}
285
286
		return $errors;
287
	}
288
289
290
	/**
291
	 * Migrates the services from the old basket to the current one.
292
	 *
293
	 * @param \Aimeos\MShop\Order\Item\Base\Iface $basket Basket object
294
	 * @param array $errors Associative list of previous errors
295
	 * @return array Associative list of errors occured
296
	 */
297
	protected function copyServices( \Aimeos\MShop\Order\Item\Base\Iface $basket, array $errors )
298
	{
299
		foreach( $basket->getServices() as $type => $list )
300
		{
301
			foreach( $list as $item )
302
			{
303
				try
304
				{
305
					$attributes = [];
306
307
					foreach( $item->getAttributes() as $attrItem ) {
308
						$attributes[$attrItem->getCode()] = $attrItem->getValue();
309
					}
310
311
					$this->addService( $type, $item->getServiceId(), $attributes );
312
					$basket->deleteService( $type );
313
				}
314
				catch( \Exception $e ) { ; } // Don't notify the user as appropriate services can be added automatically
315
			}
316
		}
317
318
		return $errors;
319
	}
320
321
322
	/**
323
	 * Creates the subscription entries for the ordered products with interval attributes
324
	 *
325
	 * @param \Aimeos\MShop\Order\Item\Base\Iface $basket Basket object
326
	 */
327
	protected function createSubscriptions( \Aimeos\MShop\Order\Item\Base\Iface $basket )
328
	{
329
		$manager = \Aimeos\MShop\Factory::createManager( $this->getContext(), 'subscription' );
330
331
		foreach( $basket->getProducts() as $orderProduct )
332
		{
333
			if( ( $interval = $orderProduct->getAttribute( 'interval', 'subscription' ) ) !== null )
334
			{
335
				$item = $manager->createItem();
336
				$item->setOrderBaseId( $basket->getId() );
337
				$item->setOrderProductId( $orderProduct->getId() );
338
				$item->setInterval( $interval );
339
				$item->setStatus( 1 );
340
341
				$manager->saveItem( $item, false );
342
			}
343
		}
344
	}
345
346
347
	/**
348
	 * Returns the attribute items for the given attribute IDs.
349
	 *
350
	 * @param array $attributeIds List of attribute IDs
351
	 * @param string[] $domains Names of the domain items that should be fetched too
352
	 * @return array List of items implementing \Aimeos\MShop\Attribute\Item\Iface
353
	 * @throws \Aimeos\Controller\Frontend\Basket\Exception If the actual attribute number doesn't match the expected one
354
	 */
355
	protected function getAttributes( array $attributeIds, array $domains = array( 'price', 'text' ) )
356
	{
357
		if( empty( $attributeIds ) ) {
358
			return [];
359
		}
360
361
		$attributeManager = \Aimeos\MShop\Factory::createManager( $this->getContext(), 'attribute' );
362
363
		$search = $attributeManager->createSearch( true );
364
		$expr = array(
365
			$search->compare( '==', 'attribute.id', $attributeIds ),
366
			$search->getConditions(),
367
		);
368
		$search->setConditions( $search->combine( '&&', $expr ) );
369
		$search->setSlice( 0, 0x7fffffff );
370
371
		$attrItems = $attributeManager->searchItems( $search, $domains );
372
373
		if( count( $attrItems ) !== count( $attributeIds ) )
374
		{
375
			$i18n = $this->getContext()->getI18n();
376
			$expected = implode( ',', $attributeIds );
377
			$actual = implode( ',', array_keys( $attrItems ) );
378
			$msg = $i18n->dt( 'controller/frontend', 'Available attribute IDs "%1$s" do not match the given attribute IDs "%2$s"' );
379
380
			throw new \Aimeos\Controller\Frontend\Basket\Exception( sprintf( $msg, $actual, $expected ) );
381
		}
382
383
		return $attrItems;
384
	}
385
386
387
	/**
388
	 * Returns the attribute items using the given order attribute items.
389
	 *
390
	 * @param \Aimeos\MShop\Order\Item\Base\Product\Attribute\Item[] $orderAttributes List of order product attribute items
391
	 * @return \Aimeos\MShop\Attribute\Item\Iface[] Associative list of attribute IDs as key and attribute items as values
392
	 */
393
	protected function getAttributeItems( array $orderAttributes )
394
	{
395
		if( empty( $orderAttributes ) ) {
396
			return [];
397
		}
398
399
		$attributeManager = \Aimeos\MShop\Factory::createManager( $this->getContext(), 'attribute' );
400
		$search = $attributeManager->createSearch( true );
401
		$expr = [];
402
403
		foreach( $orderAttributes as $item )
404
		{
405
			$tmp = array(
406
				$search->compare( '==', 'attribute.domain', 'product' ),
407
				$search->compare( '==', 'attribute.code', $item->getValue() ),
408
				$search->compare( '==', 'attribute.type.domain', 'product' ),
409
				$search->compare( '==', 'attribute.type.code', $item->getCode() ),
410
				$search->compare( '>', 'attribute.type.status', 0 ),
411
				$search->getConditions(),
412
			);
413
			$expr[] = $search->combine( '&&', $tmp );
414
		}
415
416
		$search->setConditions( $search->combine( '||', $expr ) );
417
		return $attributeManager->searchItems( $search, array( 'price' ) );
418
	}
419
420
421
	/**
422
	 * Returns the order product attribute items for the given IDs and values
423
	 *
424
	 * @param string $type Attribute type code
425
	 * @param array $ids List of attributes IDs of the given type
426
	 * @param array $values Associative list of attribute IDs as keys and their codes as values
427
	 * @param array $quantities Associative list of attribute IDs as keys and their quantities as values
428
	 * @return array List of items implementing \Aimeos\MShop\Order\Item\Product\Attribute\Iface
429
	 */
430
	protected function getOrderProductAttributes( $type, array $ids, array $values = [], array $quantities = [] )
431
	{
432
		if( empty( $ids ) ) {
433
			return [];
434
		}
435
436
		$list = [];
437
		$manager = \Aimeos\MShop\Factory::createManager( $this->getContext(), 'order/base/product/attribute' );
438
439
		foreach( $this->getAttributes( $ids ) as $id => $attrItem )
440
		{
441
			$item = $manager->createItem();
442
			$item->copyFrom( $attrItem );
443
			$item->setType( $type );
444
			$item->setQuantity( isset( $quantities[$id] ) ? $quantities[$id] : 1 );
445
446
			if( isset( $values[$id] ) ) {
447
				$item->setValue( $values[$id] );
448
			}
449
450
			$list[] = $item;
451
		}
452
453
		return $list;
454
	}
455
456
457
	/**
458
	 * Returns the list type item for the given domain and code.
459
	 *
460
	 * @param string $domain Domain name of the list type
461
	 * @param string $code Code of the list type
462
	 * @return \Aimeos\MShop\Common\Item\Type\Iface List type item
463
	 */
464
	protected function getProductListTypeItem( $domain, $code )
465
	{
466
		$context = $this->getContext();
467
468
		if( empty( $this->listTypeItems ) )
469
		{
470
			$manager = \Aimeos\MShop\Factory::createManager( $context, 'product/lists/type' );
471
472
			foreach( $manager->searchItems( $manager->createSearch( true ) ) as $item ) {
473
				$this->listTypeItems[ $item->getDomain() ][ $item->getCode() ] = $item;
474
			}
475
		}
476
477
		if( !isset( $this->listTypeItems[$domain][$code] ) )
478
		{
479
			$msg = $context->getI18n()->dt( 'controller/frontend', 'List type for domain "%1$s" and code "%2$s" not found' );
480
			throw new \Aimeos\Controller\Frontend\Basket\Exception( sprintf( $msg, $domain, $code ) );
481
		}
482
483
		return $this->listTypeItems[$domain][$code];
484
	}
485
486
487
	/**
488
	 * Returns the product variants of a selection product that match the given attributes.
489
	 *
490
	 * @param \Aimeos\MShop\Product\Item\Iface $productItem Product item including sub-products
491
	 * @param array $variantAttributeIds IDs for the variant-building attributes
492
	 * @param array $domains Names of the domain items that should be fetched too
493
	 * @return array List of products matching the given attributes
494
	 */
495
	protected function getProductVariants( \Aimeos\MShop\Product\Item\Iface $productItem, array $variantAttributeIds,
496
			array $domains = array( 'attribute', 'media', 'price', 'text' ) )
497
	{
498
		$subProductIds = [];
499
		foreach( $productItem->getRefItems( 'product', 'default', 'default' ) as $item ) {
500
			$subProductIds[] = $item->getId();
501
		}
502
503
		if( count( $subProductIds ) === 0 ) {
504
			return [];
505
		}
506
507
		$productManager = \Aimeos\MShop\Factory::createManager( $this->getContext(), 'product' );
508
		$search = $productManager->createSearch( true );
509
510
		$expr = array(
511
			$search->compare( '==', 'product.id', $subProductIds ),
512
			$search->getConditions(),
513
		);
514
515
		if( count( $variantAttributeIds ) > 0 )
516
		{
517
			foreach( $variantAttributeIds as $key => $id ) {
518
				$variantAttributeIds[$key] = (string) $id;
519
			}
520
521
			$listTypeItem = $this->getProductListTypeItem( 'attribute', 'variant' );
522
523
			$param = array( 'attribute', $listTypeItem->getId(), $variantAttributeIds );
524
			$cmpfunc = $search->createFunction( 'product.contains', $param );
525
526
			$expr[] = $search->compare( '==', $cmpfunc, count( $variantAttributeIds ) );
527
		}
528
529
		$search->setConditions( $search->combine( '&&', $expr ) );
530
531
		return $productManager->searchItems( $search, $domains );
532
	}
533
534
535
	/**
536
	 * Returns the value of an array or the default value if it's not available.
537
	 *
538
	 * @param array $values Associative list of key/value pairs
539
	 * @param string $name Name of the key to return the value for
540
	 * @param mixed $default Default value if no value is available for the given name
541
	 * @return mixed Value from the array or default value
542
	 */
543
	protected function getValue( array $values, $name, $default = null )
544
	{
545
		if( isset( $values[$name] ) ) {
546
			return $values[$name];
547
		}
548
549
		return $default;
550
	}
551
}
552