Passed
Push — master ( b5a62a...377249 )
by Aimeos
02:14
created

Base   C

Complexity

Total Complexity 56

Size/Duplication

Total Lines 429
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 56
eloc 166
dl 0
loc 429
rs 5.5199
c 0
b 0
f 0

11 Methods

Rating   Name   Duplication   Size   Complexity  
A copyCoupons() 0 20 3
B checkLocale() 0 34 6
A getOrderProductAttributes() 0 17 5
A copyServices() 0 22 5
A createSubscriptions() 0 20 6
B copyProducts() 0 46 8
B calcPrice() 0 46 8
A getAttributes() 0 29 3
A getAttributeItems() 0 24 3
A copyAddresses() 0 24 4
A checkListRef() 0 28 5

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.

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-2018
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 = [];
0 ignored issues
show
introduced by
The private property $listTypeItems is not used, and could be removed.
Loading history...
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
			$item = \Aimeos\MShop::create( $context, 'product' )->getItem( $product->getProductId(), ['price'] );
41
			$prices = $item->getRefItems( 'price', 'default', 'default' );
42
		}
43
44
45
		$priceManager = \Aimeos\MShop::create( $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 )
0 ignored issues
show
Bug introduced by
It seems like $amount can also be of type array; however, parameter $subject of preg_match() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

53
			if( preg_match( '/^[0-9]*(\.[0-9]+)?$/', /** @scrutinizer ignore-type */ $amount ) !== 1 || ((double) $amount) < 0.01 )
Loading history...
54
			{
55
				$msg = $context->getI18n()->dt( 'controller/frontend', 'Invalid price value "%1$s"' );
56
				throw new \Aimeos\Controller\Frontend\Basket\Exception( sprintf( $msg, $amount ) );
0 ignored issues
show
Bug introduced by
It seems like $amount can also be of type array; however, parameter $args of sprintf() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

56
				throw new \Aimeos\Controller\Frontend\Basket\Exception( sprintf( $msg, /** @scrutinizer ignore-type */ $amount ) );
Loading history...
57
			}
58
59
			$price = $price->setValue( $amount );
60
		}
61
62
		$orderAttributes = $product->getAttributeItems();
63
		$attrItems = $this->getAttributeItems( $orderAttributes, ['price'] );
0 ignored issues
show
Unused Code introduced by
The call to Aimeos\Controller\Fronte...se::getAttributeItems() has too many arguments starting with array('price'). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

63
		/** @scrutinizer ignore-call */ 
64
  $attrItems = $this->getAttributeItems( $orderAttributes, ['price'] );

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
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', 'default' ) ) !== []
72
			) {
73
				$attrPrice = $priceManager->getLowestPrice( $prices, $orderAttrItem->getQuantity() );
74
				$price = $price->addItem( $attrPrice, $orderAttrItem->getQuantity() );
75
			}
76
		}
77
78
		// remove product rebate of original price in favor to rebates granted for the order
79
		return $price->setRebate( '0.00' );
80
	}
81
82
83
	/**
84
	 * Checks if the reference IDs are really associated to the product
85
	 *
86
	 * @param string|array $prodId Unique ID of the product or list of product IDs
87
	 * @param string $domain Domain the references must be of
88
	 * @param array $refMap Associative list of list type codes as keys and lists of reference IDs as values
89
	 * @throws \Aimeos\Controller\Frontend\Basket\Exception If one or more of the IDs are not associated
90
	 */
91
	protected function checkListRef( $prodId, $domain, array $refMap )
92
	{
93
		if( empty( $refMap ) ) {
94
			return;
95
		}
96
97
		$context = $this->getContext();
98
		$productManager = \Aimeos\MShop::create( $context, 'product' );
99
		$search = $productManager->createSearch( true );
100
101
		$expr = [$search->getConditions()];
102
		$expr[] = $search->compare( '==', 'product.id', $prodId );
103
104
		foreach( $refMap as $listType => $refIds )
105
		{
106
			foreach( $refIds as $refId )
107
			{
108
				$cmpfunc = $search->createFunction( 'product:has', [$domain, $listType, (string) $refId] );
109
				$expr[] = $search->compare( '!=', $cmpfunc, null );
110
			}
111
		}
112
113
		$search->setConditions( $search->combine( '&&', $expr ) );
114
115
		if( count( $productManager->searchItems( $search, [] ) ) === 0 )
116
		{
117
			$msg = $context->getI18n()->dt( 'controller/frontend', 'Invalid "%1$s" references for product with ID %2$s' );
118
			throw new \Aimeos\Controller\Frontend\Basket\Exception( sprintf( $msg, $domain, json_encode( $prodId ) ) );
119
		}
120
	}
121
122
123
	/**
124
	 * Checks for a locale mismatch and migrates the products to the new basket if necessary.
125
	 *
126
	 * @param \Aimeos\MShop\Locale\Item\Iface $locale Locale object from current basket
127
	 * @param string $type Basket type
128
	 */
129
	protected function checkLocale( \Aimeos\MShop\Locale\Item\Iface $locale, $type )
130
	{
131
		$errors = [];
132
		$context = $this->getContext();
133
		$session = $context->getSession();
134
135
		$localeStr = $session->get( 'aimeos/basket/locale' );
136
		$localeKey = $locale->getSite()->getCode() . '|' . $locale->getLanguageId() . '|' . $locale->getCurrencyId();
137
138
		if( $localeStr !== null && $localeStr !== $localeKey )
139
		{
140
			$locParts = explode( '|', $localeStr );
141
			$locSite = ( isset( $locParts[0] ) ? $locParts[0] : '' );
142
			$locLanguage = ( isset( $locParts[1] ) ? $locParts[1] : '' );
143
			$locCurrency = ( isset( $locParts[2] ) ? $locParts[2] : '' );
144
145
			$localeManager = \Aimeos\MShop::create( $context, 'locale' );
146
			$locale = $localeManager->bootstrap( $locSite, $locLanguage, $locCurrency, false );
147
148
			$context = clone $context;
149
			$context->setLocale( $locale );
150
151
			$manager = \Aimeos\MShop::create( $context, 'order/base' );
152
			$basket = $manager->getSession( $type )->off();
153
154
			$this->copyAddresses( $basket, $errors, $localeKey );
155
			$this->copyServices( $basket, $errors );
156
			$this->copyProducts( $basket, $errors, $localeKey );
157
			$this->copyCoupons( $basket, $errors, $localeKey );
158
159
			$manager->setSession( $basket, $type );
160
		}
161
162
		$session->set( 'aimeos/basket/locale', $localeKey );
163
	}
164
165
166
	/**
167
	 * Migrates the addresses from the old basket to the current one.
168
	 *
169
	 * @param \Aimeos\MShop\Order\Item\Base\Iface $basket Basket object
170
	 * @param array $errors Associative list of previous errors
171
	 * @param string $localeKey Unique identifier of the site, language and currency
172
	 * @return array Associative list of errors occured
173
	 */
174
	protected function copyAddresses( \Aimeos\MShop\Order\Item\Base\Iface $basket, array $errors, $localeKey )
175
	{
176
		foreach( $basket->getAddresses() as $type => $items )
177
		{
178
			foreach( $items as $pos => $item )
179
			{
180
				try
181
				{
182
					$this->get()->addAddress( $item, $type, $pos );
183
				}
184
				catch( \Exception $e )
185
				{
186
					$logger = $this->getContext()->getLogger();
187
					$errors['address'][$type] = $e->getMessage();
188
189
					$str = 'Error migrating address with type "%1$s" in basket to locale "%2$s": %3$s';
190
					$logger->log( sprintf( $str, $type, $localeKey, $e->getMessage() ), \Aimeos\MW\Logger\Base::INFO );
191
				}
192
			}
193
194
			$basket->deleteAddress( $type );
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 );
0 ignored issues
show
Unused Code introduced by
The call to Aimeos\MShop\Order\Item\Base\Iface::deleteCoupon() has too many arguments starting with true. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

216
				$basket->/** @scrutinizer ignore-call */ 
217
             deleteCoupon( $code, true );

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
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
		$domains = ['attribute', 'media', 'price', 'product', 'text'];
243
		$manager = \Aimeos\MShop::create( $this->getContext(), 'product' );
244
245
		foreach( $basket->getProducts() as $pos => $product )
246
		{
247
			if( $product->getFlags() & \Aimeos\MShop\Order\Item\Base\Product\Base::FLAG_IMMUTABLE ) {
248
				continue;
249
			}
250
251
			try
252
			{
253
				$variantIds = $configIds = $customIds = [];
254
255
				foreach( $product->getAttributeItems() as $attrItem )
256
				{
257
					switch( $attrItem->getType() )
258
					{
259
						case 'variant': $variantIds[] = $attrItem->getAttributeId(); break;
260
						case 'config': $configIds[$attrItem->getAttributeId()] = $attrItem->getQuantity(); break;
261
						case 'custom': $customIds[$attrItem->getAttributeId()] = $attrItem->getValue(); break;
262
					}
263
				}
264
265
				$item = $manager->getItem( $product->getProductId(), $domains );
266
267
				$this->addProduct(
268
					$item, $product->getQuantity(), $variantIds, $configIds, $customIds,
269
					$product->getStockType(), $product->getSupplierCode()
270
				);
271
272
				$basket->deleteProduct( $pos );
273
			}
274
			catch( \Exception $e )
275
			{
276
				$code = $product->getProductCode();
277
				$logger = $this->getContext()->getLogger();
278
				$errors['product'][$pos] = $e->getMessage();
279
280
				$str = 'Error migrating product with code "%1$s" in basket to locale "%2$s": %3$s';
281
				$logger->log( sprintf( $str, $code, $localeKey, $e->getMessage() ), \Aimeos\MW\Logger\Base::INFO );
282
			}
283
		}
284
285
		return $errors;
286
	}
287
288
289
	/**
290
	 * Migrates the services from the old basket to the current one.
291
	 *
292
	 * @param \Aimeos\MShop\Order\Item\Base\Iface $basket Basket object
293
	 * @param array $errors Associative list of previous errors
294
	 * @return array Associative list of errors occured
295
	 */
296
	protected function copyServices( \Aimeos\MShop\Order\Item\Base\Iface $basket, array $errors )
297
	{
298
		foreach( $basket->getServices() as $type => $list )
299
		{
300
			foreach( $list as $item )
301
			{
302
				try
303
				{
304
					$attributes = [];
305
306
					foreach( $item->getAttributeItems() as $attrItem ) {
307
						$attributes[$attrItem->getCode()] = $attrItem->getValue();
308
					}
309
310
					$this->addService( $type, $item->getServiceId(), $attributes );
311
					$basket->deleteService( $type );
312
				}
313
				catch( \Exception $e ) { ; } // Don't notify the user as appropriate services can be added automatically
314
			}
315
		}
316
317
		return $errors;
318
	}
319
320
321
	/**
322
	 * Creates the subscription entries for the ordered products with interval attributes
323
	 *
324
	 * @param \Aimeos\MShop\Order\Item\Base\Iface $basket Basket object
325
	 */
326
	protected function createSubscriptions( \Aimeos\MShop\Order\Item\Base\Iface $basket )
327
	{
328
		$manager = \Aimeos\MShop::create( $this->getContext(), 'subscription' );
329
330
		foreach( $basket->getProducts() as $orderProduct )
331
		{
332
			if( ( $interval = $orderProduct->getAttribute( 'interval', 'config' ) ) !== null )
333
			{
334
				$item = $manager->createItem()->setInterval( $interval )
0 ignored issues
show
Bug introduced by
The method setInterval() does not exist on Aimeos\MShop\Attribute\Item\Iface. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

334
				$item = $manager->createItem()->/** @scrutinizer ignore-call */ setInterval( $interval )

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

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

443
				$list[] = $manager->createItem()->/** @scrutinizer ignore-call */ copyFrom( $attrItem )->setType( $type )

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
					->setValue( isset( $values[$id] ) ? $values[$id] : $attrItem->getCode() )
445
					->setQuantity( isset( $quantities[$id] ) ? $quantities[$id] : 1 );
446
			}
447
		}
448
449
		return $list;
450
	}
451
}
452