Completed
Push — master ( ee2c71...0751df )
by Aimeos
03:00
created

Base   C

Complexity

Total Complexity 51

Size/Duplication

Total Lines 511
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 16

Importance

Changes 4
Bugs 1 Features 0
Metric Value
wmc 51
c 4
b 1
f 0
lcom 1
cbo 16
dl 0
loc 511
rs 6.0708

14 Methods

Rating   Name   Duplication   Size   Complexity  
B checkReferences() 0 30 4
B checkLocale() 0 36 6
A copyAddresses() 0 20 3
A copyCoupons() 0 20 3
B copyProducts() 0 41 5
A copyServices() 0 20 4
B calcPrice() 0 29 4
B getAttributes() 0 29 3
B getAttributeItems() 0 26 3
B getProductListTypeItem() 0 27 3
B getProductVariants() 0 38 5
A getValue() 0 8 2
A checkCategory() 0 20 2
B getStockLevel() 0 33 4

How to fix   Complexity   

Complex Class

Complex classes like Base often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Base, and based on these observations, apply Extract Interface, too.

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->getWarehouseCode()
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
	 * Returns the attribute items for the given attribute IDs.
289
	 *
290
	 * @param array $attributeIds List of attribute IDs
291
	 * @param string[] $domains Names of the domain items that should be fetched too
292
	 * @return array List of items implementing \Aimeos\MShop\Attribute\Item\Iface
293
	 * @throws \Aimeos\Controller\Frontend\Basket\Exception If the actual attribute number doesn't match the expected one
294
	 */
295
	protected function getAttributes( array $attributeIds, array $domains = array( 'price', 'text' ) )
296
	{
297
		if( empty( $attributeIds ) ) {
298
			return array();
299
		}
300
301
		$attributeManager = \Aimeos\MShop\Factory::createManager( $this->getContext(), 'attribute' );
302
303
		$search = $attributeManager->createSearch( true );
304
		$expr = array(
305
				$search->compare( '==', 'attribute.id', $attributeIds ),
306
				$search->getConditions(),
307
		);
308
		$search->setConditions( $search->combine( '&&', $expr ) );
309
		$search->setSlice( 0, 0x7fffffff );
310
311
		$attrItems = $attributeManager->searchItems( $search, $domains );
312
313
		if( count( $attrItems ) !== count( $attributeIds ) )
314
		{
315
			$expected = implode( ',', $attributeIds );
316
			$actual = implode( ',', array_keys( $attrItems ) );
317
			$msg = sprintf( 'Available attribute IDs "%1$s" do not match the given attribute IDs "%2$s"', $actual, $expected );
318
319
			throw new \Aimeos\Controller\Frontend\Basket\Exception( $msg );
320
		}
321
322
		return $attrItems;
323
	}
324
325
326
	/**
327
	 * Returns the attribute items using the given order attribute items.
328
	 *
329
	 * @param \Aimeos\MShop\Order\Item\Base\Product\Attribute\Item[] $orderAttributes List of order product attribute items
330
	 * @return \Aimeos\MShop\Attribute\Item\Iface[] Associative list of attribute IDs as key and attribute items as values
331
	 */
332
	protected function getAttributeItems( array $orderAttributes )
333
	{
334
		if( empty( $orderAttributes ) ) {
335
			return array();
336
		}
337
338
		$attributeManager = \Aimeos\MShop\Factory::createManager( $this->getContext(), 'attribute' );
339
		$search = $attributeManager->createSearch( true );
340
		$expr = array();
341
342
		foreach( $orderAttributes as $item )
343
		{
344
			$tmp = array(
345
				$search->compare( '==', 'attribute.domain', 'product' ),
346
				$search->compare( '==', 'attribute.code', $item->getValue() ),
347
				$search->compare( '==', 'attribute.type.domain', 'product' ),
348
				$search->compare( '==', 'attribute.type.code', $item->getCode() ),
349
				$search->compare( '>', 'attribute.type.status', 0 ),
350
				$search->getConditions(),
351
			);
352
			$expr[] = $search->combine( '&&', $tmp );
353
		}
354
355
		$search->setConditions( $search->combine( '||', $expr ) );
356
		return $attributeManager->searchItems( $search, array( 'price' ) );
357
	}
358
359
360
	/**
361
	 * Returns the list type item for the given domain and code.
362
	 *
363
	 * @param string $domain Domain name of the list type
364
	 * @param string $code Code of the list type
365
	 * @return \Aimeos\MShop\Common\Item\Type\Iface List type item
366
	 */
367
	protected function getProductListTypeItem( $domain, $code )
368
	{
369
		if( !isset( $this->listTypeAttributes[$domain][$code] ) )
370
		{
371
			$listTypeManager = \Aimeos\MShop\Factory::createManager( $this->getContext(), 'product/lists/type' );
372
373
			$listTypeSearch = $listTypeManager->createSearch( true );
374
			$expr = array(
375
				$listTypeSearch->compare( '==', 'product.lists.type.domain', $domain ),
376
				$listTypeSearch->compare( '==', 'product.lists.type.code', $code ),
377
				$listTypeSearch->getConditions(),
378
			);
379
			$listTypeSearch->setConditions( $listTypeSearch->combine( '&&', $expr ) );
380
381
			$listTypeItems = $listTypeManager->searchItems( $listTypeSearch );
382
383
			if( ( $listTypeItem = reset( $listTypeItems ) ) === false )
384
			{
385
				$msg = sprintf( 'List type for domain "%1$s" and code "%2$s" not found', $domain, $code );
386
				throw new \Aimeos\Controller\Frontend\Basket\Exception( $msg );
387
			}
388
389
			$this->listTypeAttributes[$domain][$code] = $listTypeItem;
390
		}
391
392
		return $this->listTypeAttributes[$domain][$code];
393
	}
394
395
396
	/**
397
	 * Returns the product variants of a selection product that match the given attributes.
398
	 *
399
	 * @param \Aimeos\MShop\Product\Item\Iface $productItem Product item including sub-products
400
	 * @param array $variantAttributeIds IDs for the variant-building attributes
401
	 * @param array $domains Names of the domain items that should be fetched too
402
	 * @return array List of products matching the given attributes
403
	 */
404
	protected function getProductVariants( \Aimeos\MShop\Product\Item\Iface $productItem, array $variantAttributeIds,
405
			array $domains = array( 'attribute', 'media', 'price', 'text' ) )
406
	{
407
		$subProductIds = array();
408
		foreach( $productItem->getRefItems( 'product', 'default', 'default' ) as $item ) {
409
			$subProductIds[] = $item->getId();
410
		}
411
412
		if( count( $subProductIds ) === 0 ) {
413
			return array();
414
		}
415
416
		$productManager = \Aimeos\MShop\Factory::createManager( $this->getContext(), 'product' );
417
		$search = $productManager->createSearch( true );
418
419
		$expr = array(
420
				$search->compare( '==', 'product.id', $subProductIds ),
421
				$search->getConditions(),
422
		);
423
424
		if( count( $variantAttributeIds ) > 0 )
425
		{
426
			foreach( $variantAttributeIds as $key => $id ) {
427
				$variantAttributeIds[$key] = (string) $id;
428
			}
429
430
			$listTypeItem = $this->getProductListTypeItem( 'attribute', 'variant' );
431
432
			$param = array( 'attribute', $listTypeItem->getId(), $variantAttributeIds );
433
			$cmpfunc = $search->createFunction( 'product.contains', $param );
434
435
			$expr[] = $search->compare( '==', $cmpfunc, count( $variantAttributeIds ) );
436
		}
437
438
		$search->setConditions( $search->combine( '&&', $expr ) );
439
440
		return $productManager->searchItems( $search, $domains );
441
	}
442
443
444
	/**
445
	 * Returns the value of an array or the default value if it's not available.
446
	 *
447
	 * @param array $values Associative list of key/value pairs
448
	 * @param string $name Name of the key to return the value for
449
	 * @param mixed $default Default value if no value is available for the given name
450
	 * @return mixed Value from the array or default value
451
	 */
452
	protected function getValue( array $values, $name, $default = null )
453
	{
454
		if( isset( $values[$name] ) ) {
455
			return $values[$name];
456
		}
457
458
		return $default;
459
	}
460
461
462
	/**
463
	 * Checks if the product is part of at least one category in the product catalog.
464
	 *
465
	 * @param string $prodid Unique ID of the product
466
	 * @throws \Aimeos\Controller\Frontend\Basket\Exception If product is not associated to at least one category
467
	 * @deprecated 2016.05
468
	 */
469
	protected function checkCategory( $prodid )
470
	{
471
		$catalogListManager = \Aimeos\MShop\Factory::createManager( $this->getContext(), 'catalog/lists' );
472
473
		$search = $catalogListManager->createSearch( true );
474
		$expr = array(
475
				$search->compare( '==', 'catalog.lists.refid', $prodid ),
476
				$search->getConditions()
477
		);
478
		$search->setConditions( $search->combine( '&&', $expr ) );
479
		$search->setSlice( 0, 1 );
480
481
		$result = $catalogListManager->searchItems( $search );
482
483
		if( reset( $result ) === false )
484
		{
485
			$msg = sprintf( 'Adding product with ID "%1$s" is not allowed', $prodid );
486
			throw new \Aimeos\Controller\Frontend\Basket\Exception( $msg );
487
		}
488
	}
489
490
491
	/**
492
	 * Returns the highest stock level for the product.
493
	 *
494
	 * @param string $prodid Unique ID of the product
495
	 * @param string $warehouse Unique code of the warehouse
496
	 * @return integer|null Number of available items in stock (null for unlimited stock)
497
	 */
498
	protected function getStockLevel( $prodid, $warehouse )
499
	{
500
		$manager = \Aimeos\MShop\Factory::createManager( $this->getContext(), 'product/stock' );
501
502
		$search = $manager->createSearch( true );
503
		$expr = array(
504
				$search->compare( '==', 'product.stock.parentid', $prodid ),
505
				$search->getConditions(),
506
				$search->compare( '==', 'product.stock.warehouse.code', $warehouse ),
507
		);
508
		$search->setConditions( $search->combine( '&&', $expr ) );
509
510
		$result = $manager->searchItems( $search );
511
512
		if( empty( $result ) )
513
		{
514
			$msg = sprintf( 'No stock for product ID "%1$s" and warehouse "%2$s" available', $prodid, $warehouse );
515
			throw new \Aimeos\Controller\Frontend\Basket\Exception( $msg );
516
		}
517
518
		$stocklevel = null;
519
520
		foreach( $result as $item )
521
		{
522
			if( ( $stock = $item->getStockLevel() ) === null ) {
523
				return null;
524
			}
525
526
			$stocklevel = max( (int) $stocklevel, $item->getStockLevel() );
527
		}
528
529
		return $stocklevel;
530
	}
531
}
532