Completed
Push — master ( b46c0d...fe698d )
by Aimeos
06:27
created

Base::getAttributeItems()   B

Complexity

Conditions 3
Paths 3

Size

Total Lines 26
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

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