Completed
Push — master ( 167152...107be5 )
by Aimeos
05:29
created

Base::checkReferences()   B

Complexity

Conditions 4
Paths 4

Size

Total Lines 31
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 31
rs 8.5806
c 0
b 0
f 0
cc 4
eloc 17
nc 4
nop 4
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 string $type Basket type
138
	 */
139
	protected function checkLocale( $type )
140
	{
141
		$errors = [];
142
		$context = $this->getContext();
143
		$session = $context->getSession();
144
		$locale = $this->get()->getLocale();
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\Order\Manager\Factory::createManager( $context )->getSubManager( '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
	 * Returns the attribute items for the given attribute IDs.
324
	 *
325
	 * @param array $attributeIds List of attribute IDs
326
	 * @param string[] $domains Names of the domain items that should be fetched too
327
	 * @return array List of items implementing \Aimeos\MShop\Attribute\Item\Iface
328
	 * @throws \Aimeos\Controller\Frontend\Basket\Exception If the actual attribute number doesn't match the expected one
329
	 */
330
	protected function getAttributes( array $attributeIds, array $domains = array( 'price', 'text' ) )
331
	{
332
		if( empty( $attributeIds ) ) {
333
			return [];
334
		}
335
336
		$attributeManager = \Aimeos\MShop\Factory::createManager( $this->getContext(), 'attribute' );
337
338
		$search = $attributeManager->createSearch( true );
339
		$expr = array(
340
			$search->compare( '==', 'attribute.id', $attributeIds ),
341
			$search->getConditions(),
342
		);
343
		$search->setConditions( $search->combine( '&&', $expr ) );
344
		$search->setSlice( 0, 0x7fffffff );
345
346
		$attrItems = $attributeManager->searchItems( $search, $domains );
347
348
		if( count( $attrItems ) !== count( $attributeIds ) )
349
		{
350
			$i18n = $this->getContext()->getI18n();
351
			$expected = implode( ',', $attributeIds );
352
			$actual = implode( ',', array_keys( $attrItems ) );
353
			$msg = $i18n->dt( 'controller/frontend', 'Available attribute IDs "%1$s" do not match the given attribute IDs "%2$s"' );
354
355
			throw new \Aimeos\Controller\Frontend\Basket\Exception( sprintf( $msg, $actual, $expected ) );
356
		}
357
358
		return $attrItems;
359
	}
360
361
362
	/**
363
	 * Returns the attribute items using the given order attribute items.
364
	 *
365
	 * @param \Aimeos\MShop\Order\Item\Base\Product\Attribute\Item[] $orderAttributes List of order product attribute items
366
	 * @return \Aimeos\MShop\Attribute\Item\Iface[] Associative list of attribute IDs as key and attribute items as values
367
	 */
368
	protected function getAttributeItems( array $orderAttributes )
369
	{
370
		if( empty( $orderAttributes ) ) {
371
			return [];
372
		}
373
374
		$attributeManager = \Aimeos\MShop\Factory::createManager( $this->getContext(), 'attribute' );
375
		$search = $attributeManager->createSearch( true );
376
		$expr = [];
377
378
		foreach( $orderAttributes as $item )
379
		{
380
			$tmp = array(
381
				$search->compare( '==', 'attribute.domain', 'product' ),
382
				$search->compare( '==', 'attribute.code', $item->getValue() ),
383
				$search->compare( '==', 'attribute.type.domain', 'product' ),
384
				$search->compare( '==', 'attribute.type.code', $item->getCode() ),
385
				$search->compare( '>', 'attribute.type.status', 0 ),
386
				$search->getConditions(),
387
			);
388
			$expr[] = $search->combine( '&&', $tmp );
389
		}
390
391
		$search->setConditions( $search->combine( '||', $expr ) );
392
		return $attributeManager->searchItems( $search, array( 'price' ) );
393
	}
394
395
396
	/**
397
	 * Returns the order product attribute items for the given IDs and values
398
	 *
399
	 * @param string $type Attribute type code
400
	 * @param array $ids List of attributes IDs of the given type
401
	 * @param array $values Associative list of attribute IDs as keys and their codes as values
402
	 * @param array $quantities Associative list of attribute IDs as keys and their quantities as values
403
	 * @return array List of items implementing \Aimeos\MShop\Order\Item\Product\Attribute\Iface
404
	 */
405
	protected function getOrderProductAttributes( $type, array $ids, array $values = [], array $quantities = [] )
406
	{
407
		if( empty( $ids ) ) {
408
			return [];
409
		}
410
411
		$list = [];
412
		$manager = \Aimeos\MShop\Factory::createManager( $this->getContext(), 'order/base/product/attribute' );
413
414
		foreach( $this->getAttributes( $ids ) as $id => $attrItem )
415
		{
416
			$item = $manager->createItem();
417
			$item->copyFrom( $attrItem );
418
			$item->setType( $type );
419
			$item->setQuantity( isset( $quantities[$id] ) ? $quantities[$id] : 1 );
420
421
			if( isset( $values[$id] ) ) {
422
				$item->setValue( $values[$id] );
423
			}
424
425
			$list[] = $item;
426
		}
427
428
		return $list;
429
	}
430
431
432
	/**
433
	 * Returns the list type item for the given domain and code.
434
	 *
435
	 * @param string $domain Domain name of the list type
436
	 * @param string $code Code of the list type
437
	 * @return \Aimeos\MShop\Common\Item\Type\Iface List type item
438
	 */
439
	protected function getProductListTypeItem( $domain, $code )
440
	{
441
		$context = $this->getContext();
442
443
		if( empty( $this->listTypeItems ) )
444
		{
445
			$manager = \Aimeos\MShop\Factory::createManager( $context, 'product/lists/type' );
446
447
			foreach( $manager->searchItems( $manager->createSearch( true ) ) as $item ) {
448
				$this->listTypeItems[ $item->getDomain() ][ $item->getCode() ] = $item;
449
			}
450
		}
451
452
		if( !isset( $this->listTypeItems[$domain][$code] ) )
453
		{
454
			$msg = $context->getI18n()->dt( 'controller/frontend', 'List type for domain "%1$s" and code "%2$s" not found' );
455
			throw new \Aimeos\Controller\Frontend\Basket\Exception( sprintf( $msg, $domain, $code ) );
456
		}
457
458
		return $this->listTypeItems[$domain][$code];
459
	}
460
461
462
	/**
463
	 * Returns the product variants of a selection product that match the given attributes.
464
	 *
465
	 * @param \Aimeos\MShop\Product\Item\Iface $productItem Product item including sub-products
466
	 * @param array $variantAttributeIds IDs for the variant-building attributes
467
	 * @param array $domains Names of the domain items that should be fetched too
468
	 * @return array List of products matching the given attributes
469
	 */
470
	protected function getProductVariants( \Aimeos\MShop\Product\Item\Iface $productItem, array $variantAttributeIds,
471
			array $domains = array( 'attribute', 'media', 'price', 'text' ) )
472
	{
473
		$subProductIds = [];
474
		foreach( $productItem->getRefItems( 'product', 'default', 'default' ) as $item ) {
475
			$subProductIds[] = $item->getId();
476
		}
477
478
		if( count( $subProductIds ) === 0 ) {
479
			return [];
480
		}
481
482
		$productManager = \Aimeos\MShop\Factory::createManager( $this->getContext(), 'product' );
483
		$search = $productManager->createSearch( true );
484
485
		$expr = array(
486
			$search->compare( '==', 'product.id', $subProductIds ),
487
			$search->getConditions(),
488
		);
489
490
		if( count( $variantAttributeIds ) > 0 )
491
		{
492
			foreach( $variantAttributeIds as $key => $id ) {
493
				$variantAttributeIds[$key] = (string) $id;
494
			}
495
496
			$listTypeItem = $this->getProductListTypeItem( 'attribute', 'variant' );
497
498
			$param = array( 'attribute', $listTypeItem->getId(), $variantAttributeIds );
499
			$cmpfunc = $search->createFunction( 'product.contains', $param );
500
501
			$expr[] = $search->compare( '==', $cmpfunc, count( $variantAttributeIds ) );
502
		}
503
504
		$search->setConditions( $search->combine( '&&', $expr ) );
505
506
		return $productManager->searchItems( $search, $domains );
507
	}
508
509
510
	/**
511
	 * Returns the value of an array or the default value if it's not available.
512
	 *
513
	 * @param array $values Associative list of key/value pairs
514
	 * @param string $name Name of the key to return the value for
515
	 * @param mixed $default Default value if no value is available for the given name
516
	 * @return mixed Value from the array or default value
517
	 */
518
	protected function getValue( array $values, $name, $default = null )
519
	{
520
		if( isset( $values[$name] ) ) {
521
			return $values[$name];
522
		}
523
524
		return $default;
525
	}
526
}
527