Completed
Push — master ( 24efbd...02b932 )
by Aimeos
01:53
created

Base::getProductVariants()   A

Complexity

Conditions 4
Paths 6

Size

Total Lines 30

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 30
rs 9.44
c 0
b 0
f 0
cc 4
nc 6
nop 3
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();
0 ignored issues
show
Bug introduced by
The method getAttributes() does not exist on Aimeos\MShop\Order\Item\Base\Product\Iface. Did you maybe mean getAttribute()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
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
			foreach( $refIds as $key => $refId )
111
			{
112
				$cmpfunc = $search->createFunction( 'product.list', [$domain, $listType, (string) $refId] );
113
				$expr[] = $search->compare( '!=', $cmpfunc, null );
114
			}
115
		}
116
117
		$search->setConditions( $search->combine( '&&', $expr ) );
118
119
		if( count( $productManager->searchItems( $search, [] ) ) === 0 )
120
		{
121
			$msg = $context->getI18n()->dt( 'controller/frontend', 'Invalid "%1$s" references for product with ID %2$s' );
122
			throw new \Aimeos\Controller\Frontend\Basket\Exception( sprintf( $msg, $domain, json_encode( $prodId ) ) );
123
		}
124
	}
125
126
127
	/**
128
	 * Checks for a locale mismatch and migrates the products to the new basket if necessary.
129
	 *
130
	 * @param \Aimeos\MShop\Locale\Item\Iface $locale Locale object from current basket
131
	 * @param string $type Basket type
132
	 */
133
	protected function checkLocale( \Aimeos\MShop\Locale\Item\Iface $locale, $type )
134
	{
135
		$errors = [];
136
		$context = $this->getContext();
137
		$session = $context->getSession();
138
139
		$localeStr = $session->get( 'aimeos/basket/locale' );
140
		$localeKey = $locale->getSite()->getCode() . '|' . $locale->getLanguageId() . '|' . $locale->getCurrencyId();
141
142
		if( $localeStr !== null && $localeStr !== $localeKey )
143
		{
144
			$locParts = explode( '|', $localeStr );
145
			$locSite = ( isset( $locParts[0] ) ? $locParts[0] : '' );
146
			$locLanguage = ( isset( $locParts[1] ) ? $locParts[1] : '' );
147
			$locCurrency = ( isset( $locParts[2] ) ? $locParts[2] : '' );
148
149
			$localeManager = \Aimeos\MShop\Factory::createManager( $context, 'locale' );
150
			$locale = $localeManager->bootstrap( $locSite, $locLanguage, $locCurrency, false );
151
152
			$context = clone $context;
153
			$context->setLocale( $locale );
154
155
			$manager = \Aimeos\MShop\Factory::createManager( $context, 'order/base' );
156
			$basket = $manager->getSession( $type );
157
158
			$this->copyAddresses( $basket, $errors, $localeKey );
159
			$this->copyServices( $basket, $errors );
160
			$this->copyProducts( $basket, $errors, $localeKey );
161
			$this->copyCoupons( $basket, $errors, $localeKey );
162
163
			$manager->setSession( $basket, $type );
164
		}
165
166
		$session->set( 'aimeos/basket/locale', $localeKey );
167
	}
168
169
170
	/**
171
	 * Migrates the addresses from the old basket to the current one.
172
	 *
173
	 * @param \Aimeos\MShop\Order\Item\Base\Iface $basket Basket object
174
	 * @param array $errors Associative list of previous errors
175
	 * @param string $localeKey Unique identifier of the site, language and currency
176
	 * @return array Associative list of errors occured
177
	 */
178
	protected function copyAddresses( \Aimeos\MShop\Order\Item\Base\Iface $basket, array $errors, $localeKey )
179
	{
180
		foreach( $basket->getAddresses() as $type => $item )
181
		{
182
			try
183
			{
184
				$this->setAddress( $type, $item->toArray() );
185
				$basket->deleteAddress( $type );
186
			}
187
			catch( \Exception $e )
188
			{
189
				$logger = $this->getContext()->getLogger();
190
				$errors['address'][$type] = $e->getMessage();
191
192
				$str = 'Error migrating address with type "%1$s" in basket to locale "%2$s": %3$s';
193
				$logger->log( sprintf( $str, $type, $localeKey, $e->getMessage() ), \Aimeos\MW\Logger\Base::INFO );
194
			}
195
		}
196
197
		return $errors;
198
	}
199
200
201
	/**
202
	 * Migrates the coupons from the old basket to the current one.
203
	 *
204
	 * @param \Aimeos\MShop\Order\Item\Base\Iface $basket Basket object
205
	 * @param array $errors Associative list of previous errors
206
	 * @param string $localeKey Unique identifier of the site, language and currency
207
	 * @return array Associative list of errors occured
208
	 */
209
	protected function copyCoupons( \Aimeos\MShop\Order\Item\Base\Iface $basket, array $errors, $localeKey )
210
	{
211
		foreach( $basket->getCoupons() as $code => $list )
212
		{
213
			try
214
			{
215
				$this->addCoupon( $code );
216
				$basket->deleteCoupon( $code, true );
217
			}
218
			catch( \Exception $e )
219
			{
220
				$logger = $this->getContext()->getLogger();
221
				$errors['coupon'][$code] = $e->getMessage();
222
223
				$str = 'Error migrating coupon with code "%1$s" in basket to locale "%2$s": %3$s';
224
				$logger->log( sprintf( $str, $code, $localeKey, $e->getMessage() ), \Aimeos\MW\Logger\Base::INFO );
225
			}
226
		}
227
228
		return $errors;
229
	}
230
231
232
	/**
233
	 * Migrates the products from the old basket to the current one.
234
	 *
235
	 * @param \Aimeos\MShop\Order\Item\Base\Iface $basket Basket object
236
	 * @param array $errors Associative list of previous errors
237
	 * @param string $localeKey Unique identifier of the site, language and currency
238
	 * @return array Associative list of errors occured
239
	 */
240
	protected function copyProducts( \Aimeos\MShop\Order\Item\Base\Iface $basket, array $errors, $localeKey )
241
	{
242
		foreach( $basket->getProducts() as $pos => $product )
243
		{
244
			if( $product->getFlags() & \Aimeos\MShop\Order\Item\Base\Product\Base::FLAG_IMMUTABLE ) {
245
				continue;
246
			}
247
248
			try
249
			{
250
				$variantIds = $configIds = $customIds = [];
251
252
				foreach( $product->getAttributeItems() as $attrItem )
253
				{
254
					switch( $attrItem->getType() )
255
					{
256
						case 'variant': $variantIds[] = $attrItem->getAttributeId(); break;
257
						case 'config': $configIds[$attrItem->getAttributeId()] = $attrItem->getQuantity(); break;
258
						case 'custom': $customIds[$attrItem->getAttributeId()] = $attrItem->getValue(); break;
259
					}
260
				}
261
262
				$this->addProduct(
263
					$product->getProductId(), $product->getQuantity(), $product->getStockType(),
264
					$variantIds, $configIds, [], $customIds
265
				);
266
267
				$basket->deleteProduct( $pos );
268
			}
269
			catch( \Exception $e )
270
			{
271
				$code = $product->getProductCode();
272
				$logger = $this->getContext()->getLogger();
273
				$errors['product'][$pos] = $e->getMessage();
274
275
				$str = 'Error migrating product with code "%1$s" in basket to locale "%2$s": %3$s';
276
				$logger->log( sprintf( $str, $code, $localeKey, $e->getMessage() ), \Aimeos\MW\Logger\Base::INFO );
277
			}
278
		}
279
280
		return $errors;
281
	}
282
283
284
	/**
285
	 * Migrates the services from the old basket to the current one.
286
	 *
287
	 * @param \Aimeos\MShop\Order\Item\Base\Iface $basket Basket object
288
	 * @param array $errors Associative list of previous errors
289
	 * @return array Associative list of errors occured
290
	 */
291
	protected function copyServices( \Aimeos\MShop\Order\Item\Base\Iface $basket, array $errors )
292
	{
293
		foreach( $basket->getServices() as $type => $list )
294
		{
295
			foreach( $list as $item )
296
			{
297
				try
298
				{
299
					$attributes = [];
300
301
					foreach( $item->getAttributes() as $attrItem ) {
302
						$attributes[$attrItem->getCode()] = $attrItem->getValue();
303
					}
304
305
					$this->addService( $type, $item->getServiceId(), $attributes );
306
					$basket->deleteService( $type );
307
				}
308
				catch( \Exception $e ) { ; } // Don't notify the user as appropriate services can be added automatically
309
			}
310
		}
311
312
		return $errors;
313
	}
314
315
316
	/**
317
	 * Creates the subscription entries for the ordered products with interval attributes
318
	 *
319
	 * @param \Aimeos\MShop\Order\Item\Base\Iface $basket Basket object
320
	 */
321
	protected function createSubscriptions( \Aimeos\MShop\Order\Item\Base\Iface $basket )
322
	{
323
		$manager = \Aimeos\MShop\Factory::createManager( $this->getContext(), 'subscription' );
324
325
		foreach( $basket->getProducts() as $orderProduct )
326
		{
327
			if( ( $interval = $orderProduct->getAttribute( 'interval', 'config' ) ) !== null )
328
			{
329
				$item = $manager->createItem();
330
				$item->setOrderBaseId( $basket->getId() );
0 ignored issues
show
Bug introduced by
The method setOrderBaseId() does not seem to exist on object<Aimeos\MShop\Attribute\Item\Iface>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
331
				$item->setOrderProductId( $orderProduct->getId() );
0 ignored issues
show
Bug introduced by
The method setOrderProductId() does not seem to exist on object<Aimeos\MShop\Attribute\Item\Iface>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
332
				$item->setInterval( $interval );
0 ignored issues
show
Bug introduced by
The method setInterval() does not seem to exist on object<Aimeos\MShop\Attribute\Item\Iface>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
333
				$item->setStatus( 1 );
334
335
				if( ( $end = $orderProduct->getAttribute( 'intervalend', 'custom' ) ) !== null
336
					|| ( $end = $orderProduct->getAttribute( 'intervalend', 'config' ) ) !== null
337
					|| ( $end = $orderProduct->getAttribute( 'intervalend', 'hidden' ) ) !== null
338
				) {
339
					$item->setDateEnd( $end );
0 ignored issues
show
Bug introduced by
The method setDateEnd() does not seem to exist on object<Aimeos\MShop\Attribute\Item\Iface>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
340
				}
341
342
				$manager->saveItem( $item, false );
343
			}
344
		}
345
	}
346
347
348
	/**
349
	 * Returns the attribute items for the given attribute IDs.
350
	 *
351
	 * @param array $attributeIds List of attribute IDs
352
	 * @param string[] $domains Names of the domain items that should be fetched too
353
	 * @return array List of items implementing \Aimeos\MShop\Attribute\Item\Iface
354
	 * @throws \Aimeos\Controller\Frontend\Basket\Exception If the actual attribute number doesn't match the expected one
355
	 */
356
	protected function getAttributes( array $attributeIds, array $domains = array( 'price', 'text' ) )
357
	{
358
		if( empty( $attributeIds ) ) {
359
			return [];
360
		}
361
362
		$attributeManager = \Aimeos\MShop\Factory::createManager( $this->getContext(), 'attribute' );
363
364
		$search = $attributeManager->createSearch( true );
365
		$expr = array(
366
			$search->compare( '==', 'attribute.id', $attributeIds ),
367
			$search->getConditions(),
368
		);
369
		$search->setConditions( $search->combine( '&&', $expr ) );
370
		$search->setSlice( 0, 0x7fffffff );
371
372
		$attrItems = $attributeManager->searchItems( $search, $domains );
373
374
		if( count( $attrItems ) !== count( $attributeIds ) )
375
		{
376
			$i18n = $this->getContext()->getI18n();
377
			$expected = implode( ',', $attributeIds );
378
			$actual = implode( ',', array_keys( $attrItems ) );
379
			$msg = $i18n->dt( 'controller/frontend', 'Available attribute IDs "%1$s" do not match the given attribute IDs "%2$s"' );
380
381
			throw new \Aimeos\Controller\Frontend\Basket\Exception( sprintf( $msg, $actual, $expected ) );
382
		}
383
384
		return $attrItems;
385
	}
386
387
388
	/**
389
	 * Returns the attribute items using the given order attribute items.
390
	 *
391
	 * @param \Aimeos\MShop\Order\Item\Base\Product\Attribute\Item[] $orderAttributes List of order product attribute items
392
	 * @return \Aimeos\MShop\Attribute\Item\Iface[] Associative list of attribute IDs as key and attribute items as values
393
	 */
394
	protected function getAttributeItems( array $orderAttributes )
395
	{
396
		if( empty( $orderAttributes ) ) {
397
			return [];
398
		}
399
400
		$attributeManager = \Aimeos\MShop\Factory::createManager( $this->getContext(), 'attribute' );
401
		$search = $attributeManager->createSearch( true );
402
		$expr = [];
403
404
		foreach( $orderAttributes as $item )
405
		{
406
			$tmp = array(
407
				$search->compare( '==', 'attribute.domain', 'product' ),
408
				$search->compare( '==', 'attribute.code', $item->getValue() ),
409
				$search->compare( '==', 'attribute.type.domain', 'product' ),
410
				$search->compare( '==', 'attribute.type.code', $item->getCode() ),
411
				$search->compare( '>', 'attribute.type.status', 0 ),
412
				$search->getConditions(),
413
			);
414
			$expr[] = $search->combine( '&&', $tmp );
415
		}
416
417
		$search->setConditions( $search->combine( '||', $expr ) );
418
		return $attributeManager->searchItems( $search, array( 'price' ) );
419
	}
420
421
422
	/**
423
	 * Returns the order product attribute items for the given IDs and values
424
	 *
425
	 * @param string $type Attribute type code
426
	 * @param array $ids List of attributes IDs of the given type
427
	 * @param array $values Associative list of attribute IDs as keys and their codes as values
428
	 * @param array $quantities Associative list of attribute IDs as keys and their quantities as values
429
	 * @return array List of items implementing \Aimeos\MShop\Order\Item\Product\Attribute\Iface
430
	 */
431
	protected function getOrderProductAttributes( $type, array $ids, array $values = [], array $quantities = [] )
432
	{
433
		if( empty( $ids ) ) {
434
			return [];
435
		}
436
437
		$list = [];
438
		$manager = \Aimeos\MShop\Factory::createManager( $this->getContext(), 'order/base/product/attribute' );
439
440
		foreach( $this->getAttributes( $ids ) as $id => $attrItem )
441
		{
442
			$item = $manager->createItem();
443
			$item->copyFrom( $attrItem );
0 ignored issues
show
Bug introduced by
The method copyFrom() does not seem to exist on object<Aimeos\MShop\Attribute\Item\Iface>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
444
			$item->setType( $type );
0 ignored issues
show
Bug introduced by
The method setType() does not exist on Aimeos\MShop\Attribute\Item\Iface. Did you maybe mean setTypeId()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
445
			$item->setQuantity( isset( $quantities[$id] ) ? $quantities[$id] : 1 );
0 ignored issues
show
Bug introduced by
The method setQuantity() does not seem to exist on object<Aimeos\MShop\Attribute\Item\Iface>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
446
447
			if( isset( $values[$id] ) ) {
448
				$item->setValue( $values[$id] );
0 ignored issues
show
Bug introduced by
The method setValue() does not seem to exist on object<Aimeos\MShop\Attribute\Item\Iface>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
449
			}
450
451
			$list[] = $item;
452
		}
453
454
		return $list;
455
	}
456
457
458
	/**
459
	 * Returns the list type item for the given domain and code.
460
	 *
461
	 * @param string $domain Domain name of the list type
462
	 * @param string $code Code of the list type
463
	 * @return \Aimeos\MShop\Common\Item\Type\Iface List type item
464
	 */
465
	protected function getProductListTypeItem( $domain, $code )
466
	{
467
		$context = $this->getContext();
468
469
		if( empty( $this->listTypeItems ) )
470
		{
471
			$manager = \Aimeos\MShop\Factory::createManager( $context, 'product/lists/type' );
472
473
			foreach( $manager->searchItems( $manager->createSearch( true ) ) as $item ) {
474
				$this->listTypeItems[ $item->getDomain() ][ $item->getCode() ] = $item;
475
			}
476
		}
477
478
		if( !isset( $this->listTypeItems[$domain][$code] ) )
479
		{
480
			$msg = $context->getI18n()->dt( 'controller/frontend', 'List type for domain "%1$s" and code "%2$s" not found' );
481
			throw new \Aimeos\Controller\Frontend\Basket\Exception( sprintf( $msg, $domain, $code ) );
482
		}
483
484
		return $this->listTypeItems[$domain][$code];
485
	}
486
487
488
	/**
489
	 * Returns the product variants of a selection product that match the given attributes.
490
	 *
491
	 * @param \Aimeos\MShop\Product\Item\Iface $productItem Product item including sub-products
492
	 * @param array $variantAttributeIds IDs for the variant-building attributes
493
	 * @param array $domains Names of the domain items that should be fetched too
494
	 * @return array List of products matching the given attributes
495
	 */
496
	protected function getProductVariants( \Aimeos\MShop\Product\Item\Iface $productItem, array $variantAttributeIds,
497
			array $domains = array( 'attribute', 'media', 'price', 'text' ) )
498
	{
499
		$subProductIds = [];
500
		foreach( $productItem->getRefItems( 'product', 'default', 'default' ) as $item ) {
501
			$subProductIds[] = $item->getId();
502
		}
503
504
		if( count( $subProductIds ) === 0 ) {
505
			return [];
506
		}
507
508
		$productManager = \Aimeos\MShop\Factory::createManager( $this->getContext(), 'product' );
509
		$search = $productManager->createSearch( true );
510
511
		$expr = array(
512
			$search->compare( '==', 'product.id', $subProductIds ),
513
			$search->getConditions(),
514
		);
515
516
		foreach( $variantAttributeIds as $id )
517
		{
518
			$cmpfunc = $search->createFunction( 'product.list', ['attribute', 'variant', (string) $id] );
519
			$expr[] = $search->compare( '!=', $cmpfunc, null );
520
		}
521
522
		$search->setConditions( $search->combine( '&&', $expr ) );
523
524
		return $productManager->searchItems( $search, $domains );
525
	}
526
527
528
	/**
529
	 * Returns the value of an array or the default value if it's not available.
530
	 *
531
	 * @param array $values Associative list of key/value pairs
532
	 * @param string $name Name of the key to return the value for
533
	 * @param mixed $default Default value if no value is available for the given name
534
	 * @return mixed Value from the array or default value
535
	 */
536
	protected function getValue( array $values, $name, $default = null )
537
	{
538
		if( isset( $values[$name] ) ) {
539
			return $values[$name];
540
		}
541
542
		return $default;
543
	}
544
}
545