Passed
Push — master ( dfe0bf...8fa14d )
by Aimeos
02:47
created

Base::getAttributes()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 29
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
eloc 17
c 2
b 0
f 0
dl 0
loc 29
rs 9.7
cc 3
nc 3
nop 2
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
	/**
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 $product Ordered product item
27
	 * @param \Aimeos\MShop\Price\Item\Iface[] $prices List of price items
28
	 * @param int $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 $product,
32
		array $prices, int $quantity ) : \Aimeos\MShop\Price\Item\Iface
33
	{
34
		$context = $this->getContext();
35
36
		if( empty( $prices ) )
37
		{
38
			$item = \Aimeos\MShop::create( $context, 'product' )->getItem( $product->getProductId(), ['price'] );
39
			$prices = $item->getRefItems( 'price', 'default', 'default' );
40
		}
41
42
43
		$priceManager = \Aimeos\MShop::create( $context, 'price' );
44
		$price = $priceManager->getLowestPrice( $prices, $quantity );
45
46
		// customers can pay what they would like to pay
47
		if( ( $attr = $product->getAttributeItem( 'price', 'custom' ) ) !== null )
48
		{
49
			$amount = $attr->getValue();
50
51
			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

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

54
				throw new \Aimeos\Controller\Frontend\Basket\Exception( sprintf( $msg, /** @scrutinizer ignore-type */ $amount ) );
Loading history...
55
			}
56
57
			$price = $price->setValue( $amount );
58
		}
59
60
		$orderAttributes = $product->getAttributeItems();
61
		$attrItems = $this->getAttributeItems( $orderAttributes );
62
63
		// add prices of (optional) attributes
64
		foreach( $orderAttributes as $orderAttrItem )
65
		{
66
			$attrId = $orderAttrItem->getAttributeId();
67
68
			if( isset( $attrItems[$attrId] )
69
				&& ( $prices = $attrItems[$attrId]->getRefItems( 'price', 'default', 'default' ) ) !== []
70
			) {
71
				$attrPrice = $priceManager->getLowestPrice( $prices, $orderAttrItem->getQuantity() );
72
				$price = $price->addItem( $attrPrice, $orderAttrItem->getQuantity() );
73
			}
74
		}
75
76
		// remove product rebate of original price in favor to rebates granted for the order
77
		return $price->setRebate( '0.00' );
78
	}
79
80
81
	/**
82
	 * Checks if the attribute IDs are really associated to the product
83
	 *
84
	 * @param \Aimeos\MShop\Product\Item\Iface $product Product item with referenced items
85
	 * @param string $domain Domain the references must be of
86
	 * @param array $refMap Associative list of list type codes as keys and lists of reference IDs as values
87
	 * @throws \Aimeos\Controller\Frontend\Basket\Exception If one or more of the IDs are not associated
88
	 */
89
	protected function checkAttributes( array $products, string $listType, array $refIds )
90
	{
91
		$attrIds = [];
92
93
		foreach( $products as $product ) {
94
			$attrIds += array_keys( $product->getRefItems( 'attribute', null, $listType ) );
95
		}
96
97
		if( count( array_intersect( $refIds, $attrIds ) ) !== count( $refIds ) )
98
		{
99
			$i18n = $this->getContext()->getI18n();
100
			$prodIds = \Aimeos\Map::from( $products )->getId()->join( ', ' );
101
			$msg = $i18n->dt( 'controller/frontend', 'Invalid "%1$s" references for product with ID %2$s' );
102
			throw new \Aimeos\Controller\Frontend\Basket\Exception( sprintf( $msg, 'attribute', $prodIds ) );
103
		}
104
	}
105
106
107
	/**
108
	 * Checks for a locale mismatch and migrates the products to the new basket if necessary.
109
	 *
110
	 * @param \Aimeos\MShop\Locale\Item\Iface $locale Locale object from current basket
111
	 * @param string $type Basket type
112
	 */
113
	protected function checkLocale( \Aimeos\MShop\Locale\Item\Iface $locale, string $type )
114
	{
115
		$errors = [];
116
		$context = $this->getContext();
117
		$session = $context->getSession();
118
119
		$localeStr = $session->get( 'aimeos/basket/locale' );
120
		$localeKey = $locale->getSiteItem()->getCode() . '|' . $locale->getLanguageId() . '|' . $locale->getCurrencyId();
121
122
		if( $localeStr !== null && $localeStr !== $localeKey )
123
		{
124
			$locParts = explode( '|', $localeStr );
125
			$locSite = ( isset( $locParts[0] ) ? $locParts[0] : '' );
126
			$locLanguage = ( isset( $locParts[1] ) ? $locParts[1] : '' );
127
			$locCurrency = ( isset( $locParts[2] ) ? $locParts[2] : '' );
128
129
			$localeManager = \Aimeos\MShop::create( $context, 'locale' );
130
			$locale = $localeManager->bootstrap( $locSite, $locLanguage, $locCurrency, false );
131
132
			$context = clone $context;
133
			$context->setLocale( $locale );
134
135
			$manager = \Aimeos\MShop\Order\Manager\Factory::create( $context )->getSubManager( 'base' );
136
			$basket = $manager->getSession( $type )->off();
137
138
			$this->copyAddresses( $basket, $errors, $localeKey );
139
			$this->copyServices( $basket, $errors );
140
			$this->copyProducts( $basket, $errors, $localeKey );
141
			$this->copyCoupons( $basket, $errors, $localeKey );
142
143
			$this->getObject()->get()->setCustomerId( $basket->getCustomerId() )
144
				->setCustomerReference( $basket->getCustomerReference() )
145
				->setComment( $basket->getComment() );
146
147
			$manager->setSession( $basket, $type );
148
		}
149
150
		$session->set( 'aimeos/basket/locale', $localeKey );
151
	}
152
153
154
	/**
155
	 * Migrates the addresses from the old basket to the current one.
156
	 *
157
	 * @param \Aimeos\MShop\Order\Item\Base\Iface $basket Basket object
158
	 * @param array $errors Associative list of previous errors
159
	 * @param string $localeKey Unique identifier of the site, language and currency
160
	 * @return array Associative list of errors occured
161
	 */
162
	protected function copyAddresses( \Aimeos\MShop\Order\Item\Base\Iface $basket, array $errors, string $localeKey ) : array
163
	{
164
		foreach( $basket->getAddresses() as $type => $items )
165
		{
166
			foreach( $items as $pos => $item )
167
			{
168
				try
169
				{
170
					$this->getObject()->get()->addAddress( $item, $type, $pos );
171
				}
172
				catch( \Exception $e )
173
				{
174
					$logger = $this->getContext()->getLogger();
175
					$errors['address'][$type] = $e->getMessage();
176
177
					$str = 'Error migrating address with type "%1$s" in basket to locale "%2$s": %3$s';
178
					$logger->log( sprintf( $str, $type, $localeKey, $e->getMessage() ), \Aimeos\MW\Logger\Base::INFO );
179
				}
180
			}
181
182
			$basket->deleteAddress( $type );
183
		}
184
185
		return $errors;
186
	}
187
188
189
	/**
190
	 * Migrates the coupons from the old basket to the current one.
191
	 *
192
	 * @param \Aimeos\MShop\Order\Item\Base\Iface $basket Basket object
193
	 * @param array $errors Associative list of previous errors
194
	 * @param string $localeKey Unique identifier of the site, language and currency
195
	 * @return array Associative list of errors occured
196
	 */
197
	protected function copyCoupons( \Aimeos\MShop\Order\Item\Base\Iface $basket, array $errors, string $localeKey ) : array
198
	{
199
		foreach( $basket->getCoupons() as $code => $list )
200
		{
201
			try
202
			{
203
				$this->getObject()->addCoupon( $code );
204
				$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

204
				$basket->/** @scrutinizer ignore-call */ 
205
             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...
205
			}
206
			catch( \Exception $e )
207
			{
208
				$logger = $this->getContext()->getLogger();
209
				$errors['coupon'][$code] = $e->getMessage();
210
211
				$str = 'Error migrating coupon with code "%1$s" in basket to locale "%2$s": %3$s';
212
				$logger->log( sprintf( $str, $code, $localeKey, $e->getMessage() ), \Aimeos\MW\Logger\Base::INFO );
213
			}
214
		}
215
216
		return $errors;
217
	}
218
219
220
	/**
221
	 * Migrates the products from the old basket to the current one.
222
	 *
223
	 * @param \Aimeos\MShop\Order\Item\Base\Iface $basket Basket object
224
	 * @param array $errors Associative list of previous errors
225
	 * @param string $localeKey Unique identifier of the site, language and currency
226
	 * @return array Associative list of errors occured
227
	 */
228
	protected function copyProducts( \Aimeos\MShop\Order\Item\Base\Iface $basket, array $errors, string $localeKey ) : array
229
	{
230
		$domains = ['attribute', 'media', 'price', 'product', 'text'];
231
		$manager = \Aimeos\MShop::create( $this->getContext(), 'product' );
232
233
		foreach( $basket->getProducts() as $pos => $product )
234
		{
235
			if( $product->getFlags() & \Aimeos\MShop\Order\Item\Base\Product\Base::FLAG_IMMUTABLE ) {
236
				continue;
237
			}
238
239
			try
240
			{
241
				$variantIds = $configIds = $customIds = [];
242
243
				foreach( $product->getAttributeItems() as $attrItem )
244
				{
245
					switch( $attrItem->getType() )
246
					{
247
						case 'variant': $variantIds[] = $attrItem->getAttributeId(); break;
248
						case 'config': $configIds[$attrItem->getAttributeId()] = $attrItem->getQuantity(); break;
249
						case 'custom': $customIds[$attrItem->getAttributeId()] = $attrItem->getValue(); break;
250
					}
251
				}
252
253
				$item = $manager->getItem( $product->getProductId(), $domains );
254
255
				$this->getObject()->addProduct(
256
					$item, $product->getQuantity(), $variantIds, $configIds, $customIds,
257
					$product->getStockType(), $product->getSupplierCode()
258
				);
259
260
				$basket->deleteProduct( $pos );
261
			}
262
			catch( \Exception $e )
263
			{
264
				$code = $product->getProductCode();
265
				$logger = $this->getContext()->getLogger();
266
				$errors['product'][$pos] = $e->getMessage();
267
268
				$str = 'Error migrating product with code "%1$s" in basket to locale "%2$s": %3$s';
269
				$logger->log( sprintf( $str, $code, $localeKey, $e->getMessage() ), \Aimeos\MW\Logger\Base::INFO );
270
			}
271
		}
272
273
		return $errors;
274
	}
275
276
277
	/**
278
	 * Migrates the services from the old basket to the current one.
279
	 *
280
	 * @param \Aimeos\MShop\Order\Item\Base\Iface $basket Basket object
281
	 * @param array $errors Associative list of previous errors
282
	 * @return array Associative list of errors occured
283
	 */
284
	protected function copyServices( \Aimeos\MShop\Order\Item\Base\Iface $basket, array $errors ) : array
285
	{
286
		$manager = \Aimeos\MShop::create( $this->getContext(), 'service' );
287
288
		foreach( $basket->getServices() as $type => $list )
289
		{
290
			foreach( $list as $item )
291
			{
292
				try
293
				{
294
					$attributes = [];
295
296
					foreach( $item->getAttributeItems() as $attrItem ) {
297
						$attributes[$attrItem->getCode()] = $attrItem->getValue();
298
					}
299
300
					$service = $manager->getItem( $item->getServiceId(), ['media', 'price', 'text'] );
301
					$this->getObject()->addService( $service, $attributes );
302
					$basket->deleteService( $type );
303
				}
304
				catch( \Exception $e ) {; } // Don't notify the user as appropriate services can be added automatically
305
			}
306
		}
307
308
		return $errors;
309
	}
310
311
312
	/**
313
	 * Creates the subscription entries for the ordered products with interval attributes
314
	 *
315
	 * @param \Aimeos\MShop\Order\Item\Base\Iface $basket Basket object
316
	 */
317
	protected function createSubscriptions( \Aimeos\MShop\Order\Item\Base\Iface $basket )
318
	{
319
		$types = ['config', 'custom', 'hidden', 'variant'];
320
		$manager = \Aimeos\MShop::create( $this->getContext(), 'subscription' );
321
322
		foreach( $basket->getProducts() as $orderProduct )
323
		{
324
			if( ( $interval = $orderProduct->getAttribute( 'interval', $types ) ) !== null )
325
			{
326
				$interval = is_array( $interval ) ? reset( $interval ) : $interval;
327
328
				$item = $manager->createItem()->setInterval( $interval )
329
					->setProductId( $orderProduct->getProductId() )
330
					->setOrderProductId( $orderProduct->getId() )
331
					->setOrderBaseId( $basket->getId() );
332
333
				if( ( $end = $orderProduct->getAttribute( 'intervalend', $types ) ) !== null ) {
334
					$item = $item->setDateEnd( $end );
335
				}
336
337
				$manager->saveItem( $item, false );
0 ignored issues
show
Bug introduced by
The method saveItem() does not exist on Aimeos\MShop\Common\Manager\Iface. Did you maybe mean saveItems()? ( Ignorable by Annotation )

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

337
				$manager->/** @scrutinizer ignore-call */ 
338
              saveItem( $item, false );

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