Passed
Push — master ( 146528...f625f1 )
by Aimeos
02:24
created

Base   D

Complexity

Total Complexity 58

Size/Duplication

Total Lines 468
Duplicated Lines 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
wmc 58
eloc 180
c 2
b 0
f 0
dl 0
loc 468
rs 4.5599

13 Methods

Rating   Name   Duplication   Size   Complexity  
B calcPrice() 0 39 7
A checkAttributes() 0 14 3
A checkQuantity() 0 9 2
B checkLocale() 0 39 6
B copyProducts() 0 47 8
A copyAddresses() 0 24 4
A copyCoupons() 0 20 3
B copyServices() 0 33 7
A getOrderProductAttributes() 0 29 4
A createSubscriptions() 0 21 5
A getSiteId() 0 6 2
A getAttributes() 0 25 3
A getAttributeItems() 0 27 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.

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-2022
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
	/**
24
	 * Calculates and returns the current price for the given order product and product prices.
25
	 *
26
	 * @param \Aimeos\MShop\Order\Item\Base\Product\Iface $orderProduct Ordered product item
27
	 * @param \Aimeos\Map $prices List of price items implementing \Aimeos\MShop\Price\Item\Iface
28
	 * @param float $quantity New product quantity
29
	 * @return \Aimeos\MShop\Price\Item\Iface Price item with calculated price
30
	 */
31
	protected function calcPrice( \Aimeos\MShop\Order\Item\Base\Product\Iface $orderProduct,
32
		\Aimeos\Map $prices, float $quantity ) : \Aimeos\MShop\Price\Item\Iface
33
	{
34
		$context = $this->context();
35
36
		$priceManager = \Aimeos\MShop::create( $context, 'price' );
37
		$price = $priceManager->getLowestPrice( $prices, $quantity );
38
39
		// customers can pay what they would like to pay
40
		if( ( $attr = $orderProduct->getAttributeItem( 'price', 'custom' ) ) !== null )
41
		{
42
			$amount = $attr->getValue();
43
44
			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

44
			if( preg_match( '/^[0-9]*(\.[0-9]+)?$/', /** @scrutinizer ignore-type */ $amount ) !== 1 || ( (double) $amount ) < 0.01 )
Loading history...
45
			{
46
				$msg = $context->translate( 'controller/frontend', 'Invalid price value "%1$s"' );
47
				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 $values of sprintf() does only seem to accept double|integer|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

47
				throw new \Aimeos\Controller\Frontend\Basket\Exception( sprintf( $msg, /** @scrutinizer ignore-type */ $amount ) );
Loading history...
48
			}
49
50
			$price = $price->setValue( $amount );
51
		}
52
53
		$orderAttributes = $orderProduct->getAttributeItems();
54
		$attrItems = $this->getAttributeItems( $orderAttributes );
55
56
		// add prices of (optional) attributes
57
		foreach( $orderAttributes as $orderAttrItem )
58
		{
59
			if( ( $attrItem = $attrItems->get( $orderAttrItem->getAttributeId() ) ) !== null
60
				&& !( $prices = $attrItem->getRefItems( 'price', 'default', 'default' ) )->isEmpty()
61
			) {
62
				$attrPrice = $priceManager->getLowestPrice( $prices, $orderAttrItem->getQuantity() );
63
				$price = $price->addItem( clone $attrPrice, $orderAttrItem->getQuantity() );
64
				$orderAttrItem->setPrice( $attrPrice->addItem( $attrPrice, $orderAttrItem->getQuantity() - 1 )->getValue() );
65
			}
66
		}
67
68
		// remove product rebate of original price in favor to rebates granted for the order
69
		return $price->setRebate( '0.00' );
70
	}
71
72
73
	/**
74
	 * Returns the allowed quantity for the given product
75
	 *
76
	 * @param \Aimeos\MShop\Product\Item\Iface $product Product item including referenced items
77
	 * @param float $quantity New product quantity
78
	 * @return float Updated quantity value
79
	 */
80
	protected function checkQuantity( \Aimeos\MShop\Product\Item\Iface $product, float $quantity ) : float
81
	{
82
		$scale = $product->getScale();
83
84
		if( fmod( $quantity, $scale ) >= 0.0005 ) {
85
			return round( ceil( $quantity / $scale ) * $scale, 4 );
86
		}
87
88
		return $quantity;
89
	}
90
91
92
	/**
93
	 * Checks if the attribute IDs are really associated to the product
94
	 *
95
	 * @param \Aimeos\MShop\Product\Item\Iface $product Product item with referenced items
96
	 * @param string $domain Domain the references must be of
97
	 * @param array $refMap Associative list of list type codes as keys and lists of reference IDs as values
98
	 * @throws \Aimeos\Controller\Frontend\Basket\Exception If one or more of the IDs are not associated
99
	 */
100
	protected function checkAttributes( array $products, string $listType, array $refIds )
101
	{
102
		$attrIds = map();
103
104
		foreach( $products as $product ) {
105
			$attrIds->merge( $product->getRefItems( 'attribute', null, $listType )->keys() );
106
		}
107
108
		if( $attrIds->intersect( $refIds )->count() !== count( $refIds ) )
109
		{
110
			$i18n = $this->context()->i18n();
111
			$prodIds = map( $products )->getId()->join( ', ' );
112
			$msg = $i18n->dt( 'controller/frontend', 'Invalid "%1$s" references for product with ID %2$s' );
113
			throw new \Aimeos\Controller\Frontend\Basket\Exception( sprintf( $msg, 'attribute', $prodIds ) );
114
		}
115
	}
116
117
118
	/**
119
	 * Checks for a locale mismatch and migrates the products to the new basket if necessary.
120
	 *
121
	 * @param \Aimeos\MShop\Locale\Item\Iface $locale Locale object from current basket
122
	 * @param string $type Basket type
123
	 */
124
	protected function checkLocale( \Aimeos\MShop\Locale\Item\Iface $locale, string $type )
125
	{
126
		$errors = [];
127
		$context = $this->context();
128
		$session = $context->session();
129
130
		$localeStr = $session->get( 'aimeos/basket/locale' );
131
		$localeKey = $locale->getSiteItem()->getCode() . '|' . $locale->getLanguageId() . '|' . $locale->getCurrencyId();
132
133
		if( $localeStr !== null && $localeStr !== $localeKey )
134
		{
135
			$locParts = explode( '|', $localeStr );
136
			$locSite = ( isset( $locParts[0] ) ? $locParts[0] : '' );
137
			$locLanguage = ( isset( $locParts[1] ) ? $locParts[1] : '' );
138
			$locCurrency = ( isset( $locParts[2] ) ? $locParts[2] : '' );
139
140
			$localeManager = \Aimeos\MShop::create( $context, 'locale' );
141
			$locale = $localeManager->bootstrap( $locSite, $locLanguage, $locCurrency, false );
142
143
			$context = clone $context;
144
			$context->setLocale( $locale );
145
146
			$manager = \Aimeos\MShop::create( $context, 'order/base' );
147
			$basket = $manager->getSession( $type )->off();
148
149
			$this->copyAddresses( $basket, $errors, $localeKey );
150
			$this->copyServices( $basket, $errors );
151
			$this->copyProducts( $basket, $errors, $localeKey );
152
			$this->copyCoupons( $basket, $errors, $localeKey );
153
154
			$this->object()->get()->setCustomerId( $basket->getCustomerId() )
155
				->setCustomerReference( $basket->getCustomerReference() )
156
				->setComment( $basket->getComment() )
157
				->setLocale( $locale );
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, string $localeKey ) : array
175
	{
176
		foreach( $basket->getAddresses() as $type => $items )
177
		{
178
			foreach( $items as $pos => $item )
179
			{
180
				try
181
				{
182
					$this->object()->get()->addAddress( $item, $type, $pos );
183
				}
184
				catch( \Exception $e )
185
				{
186
					$logger = $this->context()->logger();
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->info( sprintf( $str, $type, $localeKey, $e->getMessage() ), 'controller/frontend' );
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, string $localeKey ) : array
210
	{
211
		foreach( $basket->getCoupons() as $code => $list )
212
		{
213
			try
214
			{
215
				$this->object()->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->context()->logger();
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->info( sprintf( $str, $code, $localeKey, $e->getMessage() ), 'controller/frontend' );
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, string $localeKey ) : array
241
	{
242
		$context = $this->context();
243
		$manager = \Aimeos\MShop::create( $context, 'product' );
244
		$ruleManager = \Aimeos\MShop::create( $context, 'rule' );
245
		$domains = ['attribute', 'media', 'price', 'product', 'text'];
246
247
		foreach( $basket->getProducts() as $pos => $product )
248
		{
249
			if( $product->getFlags() & \Aimeos\MShop\Order\Item\Base\Product\Base::FLAG_IMMUTABLE ) {
250
				continue;
251
			}
252
253
			try
254
			{
255
				$variantIds = $configIds = $customIds = [];
256
257
				foreach( $product->getAttributeItems() as $attrItem )
258
				{
259
					switch( $attrItem->getType() )
260
					{
261
						case 'variant': $variantIds[] = $attrItem->getAttributeId(); break;
262
						case 'config': $configIds[$attrItem->getAttributeId()] = $attrItem->getQuantity(); break;
263
						case 'custom': $customIds[$attrItem->getAttributeId()] = $attrItem->getValue(); break;
264
					}
265
				}
266
267
				$item = $manager->get( $product->getProductId(), $domains );
268
				$item = $ruleManager->apply( $item, 'catalog' );
269
				$qty = $product->getQuantity();
270
271
				$this->object()->addProduct( $item, $qty, $variantIds, $configIds, $customIds, $product->getStockType() );
272
273
				$basket->deleteProduct( $pos );
274
			}
275
			catch( \Exception $e )
276
			{
277
				$code = $product->getProductCode();
278
				$logger = $this->context()->logger();
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->info( sprintf( $str, $code, $localeKey, $e->getMessage() ), 'controller/frontend' );
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 ) : array
298
	{
299
		$newBasket = $this->object();
300
		$manager = \Aimeos\MShop::create( $this->context(), 'service' );
301
302
		foreach( $basket->getServices() as $type => $list )
303
		{
304
			foreach( $list as $item )
305
			{
306
				try
307
				{
308
					foreach( $newBasket->get()->getService( $type ) as $pos => $ordService )
309
					{
310
						if( $item->getCode() === $ordService->getCode() ) {
311
							$newBasket->get()->deleteService( $type, $pos );
312
						}
313
					}
314
315
					$attributes = [];
316
317
					foreach( $item->getAttributeItems() as $attrItem ) {
318
						$attributes[$attrItem->getCode()] = $attrItem->getValue();
319
					}
320
321
					$service = $manager->get( $item->getServiceId(), ['media', 'price', 'text'] );
322
					$newBasket->addService( $service, $attributes );
323
					$basket->deleteService( $type );
324
				}
325
				catch( \Exception $e ) { ; } // Don't notify the user as appropriate services can be added automatically
326
			}
327
		}
328
329
		return $errors;
330
	}
331
332
333
	/**
334
	 * Creates the subscription entries for the ordered products with interval attributes
335
	 *
336
	 * @param \Aimeos\MShop\Order\Item\Base\Iface $basket Basket object
337
	 */
338
	protected function createSubscriptions( \Aimeos\MShop\Order\Item\Base\Iface $basket )
339
	{
340
		$types = ['config', 'custom', 'hidden', 'variant'];
341
		$manager = \Aimeos\MShop::create( $this->context(), 'subscription' );
342
343
		foreach( $basket->getProducts() as $orderProduct )
344
		{
345
			if( ( $interval = $orderProduct->getAttribute( 'interval', $types ) ) !== null )
346
			{
347
				$interval = is_array( $interval ) ? reset( $interval ) : $interval;
348
349
				$item = $manager->create()->setInterval( $interval )
350
					->setProductId( $orderProduct->getProductId() )
351
					->setOrderProductId( $orderProduct->getId() )
352
					->setOrderBaseId( $basket->getId() );
353
354
				if( ( $end = $orderProduct->getAttribute( 'intervalend', $types ) ) !== null ) {
355
					$item = $item->setDateEnd( $end );
356
				}
357
358
				$manager->save( $item, false );
359
			}
360
		}
361
	}
362
363
364
	/**
365
	 * Returns the attribute items for the given attribute IDs.
366
	 *
367
	 * @param array $attributeIds List of attribute IDs
368
	 * @param string[] $domains Names of the domain items that should be fetched too
369
	 * @return \Aimeos\Map List of items implementing \Aimeos\MShop\Attribute\Item\Iface
370
	 * @throws \Aimeos\Controller\Frontend\Basket\Exception If the actual attribute number doesn't match the expected one
371
	 */
372
	protected function getAttributes( array $attributeIds, array $domains = ['text'] ) : \Aimeos\Map
373
	{
374
		if( empty( $attributeIds ) ) {
375
			return map();
376
		}
377
378
		$attributeManager = \Aimeos\MShop::create( $this->context(), 'attribute' );
379
380
		$search = $attributeManager->filter( true )
381
			->add( ['attribute.id' => $attributeIds] )
382
			->slice( 0, count( $attributeIds ) );
383
384
		$attrItems = $attributeManager->search( $search, $domains );
385
386
		if( $attrItems->count() !== count( $attributeIds ) )
387
		{
388
			$i18n = $this->context()->i18n();
389
			$expected = implode( ',', $attributeIds );
390
			$actual = $attrItems->keys()->join( ',' );
391
			$msg = $i18n->dt( 'controller/frontend', 'Available attribute IDs "%1$s" do not match the given attribute IDs "%2$s"' );
392
393
			throw new \Aimeos\Controller\Frontend\Basket\Exception( sprintf( $msg, $actual, $expected ) );
394
		}
395
396
		return $attrItems;
397
	}
398
399
400
	/**
401
	 * Returns the attribute items using the given order attribute items.
402
	 *
403
	 * @param \Aimeos\Map $orderAttributes List of items implementing \Aimeos\MShop\Order\Item\Base\Product\Attribute\Iface
404
	 * @return \Aimeos\Map List of attribute IDs as key and attribute items implementing \Aimeos\MShop\Attribute\Item\Iface
405
	 */
406
	protected function getAttributeItems( \Aimeos\Map $orderAttributes ) : \Aimeos\Map
407
	{
408
		if( $orderAttributes->isEmpty() ) {
409
			return map();
410
		}
411
412
		$attributeManager = \Aimeos\MShop::create( $this->context(), 'attribute' );
413
		$search = $attributeManager->filter( true );
414
		$expr = [];
415
416
		foreach( $orderAttributes as $item )
417
		{
418
			if( is_scalar( $item->getValue() ) )
419
			{
420
				$tmp = array(
421
					$search->compare( '==', 'attribute.domain', 'product' ),
422
					$search->compare( '==', 'attribute.code', $item->getValue() ),
423
					$search->compare( '==', 'attribute.type', $item->getCode() ),
424
					$search->compare( '>', 'attribute.status', 0 ),
425
					$search->getConditions(),
426
				);
427
				$expr[] = $search->and( $tmp );
428
			}
429
		}
430
431
		$search->setConditions( $search->or( $expr ) );
432
		return $attributeManager->search( $search, array( 'price' ) );
433
	}
434
435
436
	/**
437
	 * Returns the order product attribute items for the given IDs and values
438
	 *
439
	 * @param string $type Attribute type code
440
	 * @param array $ids List of attributes IDs of the given type
441
	 * @param array $values Associative list of attribute IDs as keys and their codes as values
442
	 * @param array $quantities Associative list of attribute IDs as keys and their quantities as values
443
	 * @return array List of items implementing \Aimeos\MShop\Order\Item\Product\Attribute\Iface
444
	 */
445
	protected function getOrderProductAttributes( string $type, array $ids, array $values = [], array $quantities = [] ) : array
446
	{
447
		if( empty( $ids ) ) {
448
			return [];
449
		}
450
451
		$list = [];
452
		$context = $this->context();
453
454
		$priceManager = \Aimeos\MShop::create( $context, 'price' );
455
		$manager = \Aimeos\MShop::create( $context, 'order/base/product/attribute' );
456
457
		foreach( $this->getAttributes( $ids, ['price', 'text'] ) as $id => $attrItem )
458
		{
459
			$qty = $quantities[$id] ?? 1;
460
			$item = $manager->create()->copyFrom( $attrItem )->setType( $type )
461
				->setValue( $values[$id] ?? $attrItem->getCode() )
462
				->setQuantity( $qty );
463
464
			if( !( $prices = $attrItem->getRefItems( 'price', 'default', 'default' ) )->isEmpty() )
465
			{
466
				$attrPrice = $priceManager->getLowestPrice( $prices, $qty );
467
				$item->setPrice( $attrPrice->addItem( $attrPrice, $qty - 1 )->getValue() );
468
			}
469
470
			$list[] = $item;
471
		}
472
473
		return $list;
474
	}
475
476
477
	/**
478
	 * Returns the site ID of the ordered product
479
	 *
480
	 * @param \Aimeos\MShop\Product\Item\Iface $product Product item
481
	 * @return string Site ID for the ordered product
482
	 */
483
	protected function getSiteId( \Aimeos\MShop\Product\Item\Iface $product ) : string
484
	{
485
		// if product is inherited, use site ID of current site
486
		$siteIds = $this->context()->locale()->getSitePath();
487
488
		return  in_array( $product->getSiteId(), $siteIds ) ? end( $siteIds ) : $product->getSiteId();
489
	}
490
}
491