Base   D
last analyzed

Complexity

Total Complexity 59

Size/Duplication

Total Lines 472
Duplicated Lines 0 %

Importance

Changes 7
Bugs 0 Features 0
Metric Value
wmc 59
eloc 183
c 7
b 0
f 0
dl 0
loc 472
rs 4.08

13 Methods

Rating   Name   Duplication   Size   Complexity  
B checkLocale() 0 39 6
B copyServices() 0 33 7
A getOrderProductAttributes() 0 29 4
A createSubscriptions() 0 21 5
B calcPrice() 0 43 7
A checkAttributes() 0 14 3
A checkQuantity() 0 9 2
A getAttributes() 0 25 3
A getAttributeItems() 0 27 4
A copyAddresses() 0 24 4
A copyCoupons() 0 20 3
A getVendor() 0 6 2
B copyProducts() 0 47 9

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-2024
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\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\Product\Iface $orderProduct,
32
		\Aimeos\Map $prices, float $quantity ) : \Aimeos\MShop\Price\Item\Iface
33
	{
34
		$context = $this->context();
35
		$priceManager = \Aimeos\MShop::create( $context, 'price' );
36
		$price = $priceManager->getLowestPrice( $prices, $quantity, null, $orderProduct->getSiteId() );
37
38
		// customers can pay what they would like to pay
39
		if( ( $attr = $orderProduct->getAttributeItem( 'price', 'custom' ) ) !== null )
40
		{
41
			$amount = $attr->getValue();
42
43
			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

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

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