Passed
Push — master ( 2d37a0...b4057e )
by Aimeos
11:58 queued 04:03
created

Base::getSiteId()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 6
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 2
dl 0
loc 6
rs 10
c 0
b 0
f 0
cc 2
nc 2
nop 1
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 ceil( $quantity / $scale ) * $scale;
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
158
			$manager->setSession( $basket, $type );
159
		}
160
161
		$session->set( 'aimeos/basket/locale', $localeKey );
162
	}
163
164
165
	/**
166
	 * Migrates the addresses from the old basket to the current one.
167
	 *
168
	 * @param \Aimeos\MShop\Order\Item\Base\Iface $basket Basket object
169
	 * @param array $errors Associative list of previous errors
170
	 * @param string $localeKey Unique identifier of the site, language and currency
171
	 * @return array Associative list of errors occured
172
	 */
173
	protected function copyAddresses( \Aimeos\MShop\Order\Item\Base\Iface $basket, array $errors, string $localeKey ) : array
174
	{
175
		foreach( $basket->getAddresses() as $type => $items )
176
		{
177
			foreach( $items as $pos => $item )
178
			{
179
				try
180
				{
181
					$this->object()->get()->addAddress( $item, $type, $pos );
182
				}
183
				catch( \Exception $e )
184
				{
185
					$logger = $this->context()->logger();
186
					$errors['address'][$type] = $e->getMessage();
187
188
					$str = 'Error migrating address with type "%1$s" in basket to locale "%2$s": %3$s';
189
					$logger->info( sprintf( $str, $type, $localeKey, $e->getMessage() ), 'controller/frontend' );
190
				}
191
			}
192
193
			$basket->deleteAddress( $type );
194
		}
195
196
		return $errors;
197
	}
198
199
200
	/**
201
	 * Migrates the coupons from the old basket to the current one.
202
	 *
203
	 * @param \Aimeos\MShop\Order\Item\Base\Iface $basket Basket object
204
	 * @param array $errors Associative list of previous errors
205
	 * @param string $localeKey Unique identifier of the site, language and currency
206
	 * @return array Associative list of errors occured
207
	 */
208
	protected function copyCoupons( \Aimeos\MShop\Order\Item\Base\Iface $basket, array $errors, string $localeKey ) : array
209
	{
210
		foreach( $basket->getCoupons() as $code => $list )
211
		{
212
			try
213
			{
214
				$this->object()->addCoupon( $code );
215
				$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

215
				$basket->/** @scrutinizer ignore-call */ 
216
             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...
216
			}
217
			catch( \Exception $e )
218
			{
219
				$logger = $this->context()->logger();
220
				$errors['coupon'][$code] = $e->getMessage();
221
222
				$str = 'Error migrating coupon with code "%1$s" in basket to locale "%2$s": %3$s';
223
				$logger->info( sprintf( $str, $code, $localeKey, $e->getMessage() ), 'controller/frontend' );
224
			}
225
		}
226
227
		return $errors;
228
	}
229
230
231
	/**
232
	 * Migrates the products from the old basket to the current one.
233
	 *
234
	 * @param \Aimeos\MShop\Order\Item\Base\Iface $basket Basket object
235
	 * @param array $errors Associative list of previous errors
236
	 * @param string $localeKey Unique identifier of the site, language and currency
237
	 * @return array Associative list of errors occured
238
	 */
239
	protected function copyProducts( \Aimeos\MShop\Order\Item\Base\Iface $basket, array $errors, string $localeKey ) : array
240
	{
241
		$context = $this->context();
242
		$manager = \Aimeos\MShop::create( $context, 'product' );
243
		$ruleManager = \Aimeos\MShop::create( $context, 'rule' );
244
		$domains = ['attribute', 'media', 'price', 'product', 'text'];
245
246
		foreach( $basket->getProducts() as $pos => $product )
247
		{
248
			if( $product->getFlags() & \Aimeos\MShop\Order\Item\Base\Product\Base::FLAG_IMMUTABLE ) {
249
				continue;
250
			}
251
252
			try
253
			{
254
				$variantIds = $configIds = $customIds = [];
255
256
				foreach( $product->getAttributeItems() as $attrItem )
257
				{
258
					switch( $attrItem->getType() )
259
					{
260
						case 'variant': $variantIds[] = $attrItem->getAttributeId(); break;
261
						case 'config': $configIds[$attrItem->getAttributeId()] = $attrItem->getQuantity(); break;
262
						case 'custom': $customIds[$attrItem->getAttributeId()] = $attrItem->getValue(); break;
263
					}
264
				}
265
266
				$item = $manager->get( $product->getProductId(), $domains );
267
				$item = $ruleManager->apply( $item, 'catalog' );
268
				$qty = $product->getQuantity();
269
270
				$this->object()->addProduct( $item, $qty, $variantIds, $configIds, $customIds, $product->getStockType() );
271
272
				$basket->deleteProduct( $pos );
273
			}
274
			catch( \Exception $e )
275
			{
276
				$code = $product->getProductCode();
277
				$logger = $this->context()->logger();
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->info( sprintf( $str, $code, $localeKey, $e->getMessage() ), 'controller/frontend' );
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 ) : array
297
	{
298
		$manager = \Aimeos\MShop::create( $this->context(), 'service' );
299
300
		foreach( $basket->getServices() as $type => $list )
301
		{
302
			foreach( $list as $item )
303
			{
304
				try
305
				{
306
					$attributes = [];
307
308
					foreach( $item->getAttributeItems() as $attrItem ) {
309
						$attributes[$attrItem->getCode()] = $attrItem->getValue();
310
					}
311
312
					$service = $manager->get( $item->getServiceId(), ['media', 'price', 'text'] );
313
					$this->object()->addService( $service, $attributes );
314
					$basket->deleteService( $type );
315
				}
316
				catch( \Exception $e ) { ; } // Don't notify the user as appropriate services can be added automatically
317
			}
318
		}
319
320
		return $errors;
321
	}
322
323
324
	/**
325
	 * Creates the subscription entries for the ordered products with interval attributes
326
	 *
327
	 * @param \Aimeos\MShop\Order\Item\Base\Iface $basket Basket object
328
	 */
329
	protected function createSubscriptions( \Aimeos\MShop\Order\Item\Base\Iface $basket )
330
	{
331
		$types = ['config', 'custom', 'hidden', 'variant'];
332
		$manager = \Aimeos\MShop::create( $this->context(), 'subscription' );
333
334
		foreach( $basket->getProducts() as $orderProduct )
335
		{
336
			if( ( $interval = $orderProduct->getAttribute( 'interval', $types ) ) !== null )
337
			{
338
				$interval = is_array( $interval ) ? reset( $interval ) : $interval;
339
340
				$item = $manager->create()->setInterval( $interval )
341
					->setProductId( $orderProduct->getProductId() )
342
					->setOrderProductId( $orderProduct->getId() )
343
					->setOrderBaseId( $basket->getId() );
344
345
				if( ( $end = $orderProduct->getAttribute( 'intervalend', $types ) ) !== null ) {
346
					$item = $item->setDateEnd( $end );
347
				}
348
349
				$manager->save( $item, false );
350
			}
351
		}
352
	}
353
354
355
	/**
356
	 * Returns the attribute items for the given attribute IDs.
357
	 *
358
	 * @param array $attributeIds List of attribute IDs
359
	 * @param string[] $domains Names of the domain items that should be fetched too
360
	 * @return \Aimeos\Map List of items implementing \Aimeos\MShop\Attribute\Item\Iface
361
	 * @throws \Aimeos\Controller\Frontend\Basket\Exception If the actual attribute number doesn't match the expected one
362
	 */
363
	protected function getAttributes( array $attributeIds, array $domains = ['text'] ) : \Aimeos\Map
364
	{
365
		if( empty( $attributeIds ) ) {
366
			return map();
367
		}
368
369
		$attributeManager = \Aimeos\MShop::create( $this->context(), 'attribute' );
370
371
		$search = $attributeManager->filter( true )
372
			->add( ['attribute.id' => $attributeIds] )
373
			->slice( 0, count( $attributeIds ) );
374
375
		$attrItems = $attributeManager->search( $search, $domains );
376
377
		if( $attrItems->count() !== count( $attributeIds ) )
378
		{
379
			$i18n = $this->context()->i18n();
380
			$expected = implode( ',', $attributeIds );
381
			$actual = $attrItems->keys()->join( ',' );
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\Map $orderAttributes List of items implementing \Aimeos\MShop\Order\Item\Base\Product\Attribute\Iface
395
	 * @return \Aimeos\Map List of attribute IDs as key and attribute items implementing \Aimeos\MShop\Attribute\Item\Iface
396
	 */
397
	protected function getAttributeItems( \Aimeos\Map $orderAttributes ) : \Aimeos\Map
398
	{
399
		if( $orderAttributes->isEmpty() ) {
400
			return map();
401
		}
402
403
		$attributeManager = \Aimeos\MShop::create( $this->context(), 'attribute' );
404
		$search = $attributeManager->filter( true );
405
		$expr = [];
406
407
		foreach( $orderAttributes as $item )
408
		{
409
			if( is_scalar( $item->getValue() ) )
410
			{
411
				$tmp = array(
412
					$search->compare( '==', 'attribute.domain', 'product' ),
413
					$search->compare( '==', 'attribute.code', $item->getValue() ),
414
					$search->compare( '==', 'attribute.type', $item->getCode() ),
415
					$search->compare( '>', 'attribute.status', 0 ),
416
					$search->getConditions(),
417
				);
418
				$expr[] = $search->and( $tmp );
419
			}
420
		}
421
422
		$search->setConditions( $search->or( $expr ) );
423
		return $attributeManager->search( $search, array( 'price' ) );
424
	}
425
426
427
	/**
428
	 * Returns the order product attribute items for the given IDs and values
429
	 *
430
	 * @param string $type Attribute type code
431
	 * @param array $ids List of attributes IDs of the given type
432
	 * @param array $values Associative list of attribute IDs as keys and their codes as values
433
	 * @param array $quantities Associative list of attribute IDs as keys and their quantities as values
434
	 * @return array List of items implementing \Aimeos\MShop\Order\Item\Product\Attribute\Iface
435
	 */
436
	protected function getOrderProductAttributes( string $type, array $ids, array $values = [], array $quantities = [] ) : array
437
	{
438
		if( empty( $ids ) ) {
439
			return [];
440
		}
441
442
		$list = [];
443
		$context = $this->context();
444
445
		$priceManager = \Aimeos\MShop::create( $context, 'price' );
446
		$manager = \Aimeos\MShop::create( $context, 'order/base/product/attribute' );
447
448
		foreach( $this->getAttributes( $ids, ['price', 'text'] ) as $id => $attrItem )
449
		{
450
			$qty = $quantities[$id] ?? 1;
451
			$item = $manager->create()->copyFrom( $attrItem )->setType( $type )
452
				->setValue( $values[$id] ?? $attrItem->getCode() )
453
				->setQuantity( $qty );
454
455
			if( !( $prices = $attrItem->getRefItems( 'price', 'default', 'default' ) )->isEmpty() )
456
			{
457
				$attrPrice = $priceManager->getLowestPrice( $prices, $qty );
458
				$item->setPrice( $attrPrice->addItem( $attrPrice, $qty - 1 )->getValue() );
459
			}
460
461
			$list[] = $item;
462
		}
463
464
		return $list;
465
	}
466
467
468
	/**
469
	 * Returns the site ID of the ordered product
470
	 *
471
	 * @param \Aimeos\MShop\Product\Item\Iface $product Product item
472
	 * @return string Site ID for the ordered product
473
	 */
474
	protected function getSiteId( \Aimeos\MShop\Product\Item\Iface $product ) : string
475
	{
476
		// if product is inherited, use site ID of current site
477
		$siteIds = $this->context()->locale()->getSitePath();
478
479
		return  in_array( $product->getSiteId(), $siteIds ) ? end( $siteIds ) : $product->getSiteId();
480
	}
481
}
482