Completed
Push — master ( 4f8168...8c4809 )
by Aimeos
03:04
created

Standard::addProduct()   B

Complexity

Conditions 3
Paths 3

Size

Total Lines 43
Code Lines 28

Duplication

Lines 0
Ratio 0 %

Importance

Changes 3
Bugs 0 Features 0
Metric Value
c 3
b 0
f 0
dl 0
loc 43
rs 8.8571
cc 3
eloc 28
nc 3
nop 8

How to fix   Many Parameters   

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

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-2016
7
 * @package Controller
8
 * @subpackage Frontend
9
 */
10
11
12
namespace Aimeos\Controller\Frontend\Basket;
13
14
15
/**
16
 * Default implementation of the basket frontend controller.
17
 *
18
 * @package Controller
19
 * @subpackage Frontend
20
 */
21
class Standard
22
	extends Base
2 ignored issues
show
Coding Style introduced by
The extends keyword must be on the same line as the class name
Loading history...
Coding Style introduced by
Expected 0 spaces between "Base" and comma; 1 found
Loading history...
23
	implements Iface, \Aimeos\Controller\Frontend\Common\Iface
1 ignored issue
show
Coding Style introduced by
The implements keyword must be on the same line as the class name
Loading history...
24
{
25
	private $basket;
26
	private $domainManager;
27
28
29
	/**
30
	 * Initializes the frontend controller.
31
	 *
32
	 * @param \Aimeos\MShop\Context\Item\Iface $context Object storing the required instances for manaing databases
33
	 *  connections, logger, session, etc.
34
	 */
35
	public function __construct( \Aimeos\MShop\Context\Item\Iface $context )
36
	{
37
		parent::__construct( $context );
38
39
		$this->domainManager = \Aimeos\MShop\Factory::createManager( $context, 'order/base' );
40
		$this->basket = $this->domainManager->getSession();
41
42
		$this->checkLocale();
43
	}
44
45
46
	/**
47
	 * Explicitely persists the basket content
48
	 */
49
	public function save()
50
	{
51
		if( $this->basket->isModified() ) {
52
			$this->domainManager->setSession( $this->basket );
53
		}
54
	}
55
56
57
	/**
58
	 * Empties the basket and removing all products, addresses, services, etc.
59
	 */
60
	public function clear()
61
	{
62
		$this->basket = $this->domainManager->createItem();
63
		$this->domainManager->setSession( $this->basket );
64
	}
65
66
67
	/**
68
	 * Returns the basket object.
69
	 *
70
	 * @return \Aimeos\MShop\Order\Item\Base\Iface Basket holding products, addresses and delivery/payment options
71
	 */
72
	public function get()
73
	{
74
		return $this->basket;
75
	}
76
77
78
	/**
79
	 * Adds a categorized product to the basket of the user stored in the session.
80
	 *
81
	 * @param string $prodid ID of the base product to add
82
	 * @param integer $quantity Amount of products that should by added
83
	 * @param array $options Possible options are: 'stock'=>true|false and 'variant'=>true|false
84
	 * 	The 'stock'=>false option allows adding products without being in stock.
85
	 * 	The 'variant'=>false option allows adding the selection product to the basket
86
	 * 	instead of the specific sub-product if the variant-building attribute IDs
87
	 * 	doesn't match a specific sub-product or if the attribute IDs are missing.
88
	 * @param array $variantAttributeIds List of variant-building attribute IDs that identify a specific product
89
	 * 	in a selection products
90
	 * @param array $configAttributeIds  List of attribute IDs that doesn't identify a specific product in a
91
	 * 	selection of products but are stored together with the product (e.g. for configurable products)
92
	 * @param array $hiddenAttributeIds List of attribute IDs that should be stored along with the product in the order
93
	 * @param array $customAttributeValues Associative list of attribute IDs and arbitrary values that should be stored
94
	 * 	along with the product in the order
95
	 * @param string $warehouse Unique code of the warehouse to deliver the products from
96
	 * @throws \Aimeos\Controller\Frontend\Basket\Exception If the product isn't available
97
	 */
98
	public function addProduct( $prodid, $quantity = 1, array $options = array(), array $variantAttributeIds = array(),
99
		array $configAttributeIds = array(), array $hiddenAttributeIds = array(), array $customAttributeValues = array(),
100
		$warehouse = 'default' )
101
	{
102
		$context = $this->getContext();
103
104
		$productItem = $this->getDomainItem( 'product', 'product.id', $prodid, array( 'media', 'supplier', 'price', 'product', 'text' ) );
105
106
		$orderBaseProductItem = \Aimeos\MShop\Factory::createManager( $context, 'order/base/product' )->createItem();
107
		$orderBaseProductItem->copyFrom( $productItem );
108
		$orderBaseProductItem->setQuantity( $quantity );
109
		$orderBaseProductItem->setWarehouseCode( $warehouse );
110
111
		$attr = array();
112
		$prices = $productItem->getRefItems( 'price', 'default', 'default' );
113
114
		switch( $productItem->getType() )
115
		{
116
			case 'select':
117
				$attr = $this->getVariantDetails( $orderBaseProductItem, $productItem, $prices, $variantAttributeIds, $options );
118
				break;
119
			case 'bundle':
120
				$this->addBundleProducts( $orderBaseProductItem, $productItem, $variantAttributeIds, $warehouse );
121
				break;
122
		}
123
124
		$priceManager = \Aimeos\MShop\Factory::createManager( $context, 'price' );
125
		$price = $priceManager->getLowestPrice( $prices, $quantity );
126
127
		$attr = array_merge( $attr, $this->createOrderProductAttributes( $price, $prodid, $quantity, $configAttributeIds, 'config' ) );
128
		$attr = array_merge( $attr, $this->createOrderProductAttributes( $price, $prodid, $quantity, $hiddenAttributeIds, 'hidden' ) );
129
		$attr = array_merge( $attr, $this->createOrderProductAttributes( $price, $prodid, $quantity, array_keys( $customAttributeValues ), 'custom', $customAttributeValues ) );
130
131
		// remove product rebate of original price in favor to rebates granted for the order
132
		$price->setRebate( '0.00' );
133
134
		$orderBaseProductItem->setPrice( $price );
135
		$orderBaseProductItem->setAttributes( $attr );
136
137
		$this->addProductInStock( $orderBaseProductItem, $productItem->getId(), $quantity, $options, $warehouse );
138
139
		$this->domainManager->setSession( $this->basket );
140
	}
141
142
143
	/**
144
	 * Deletes a product item from the basket.
145
	 *
146
	 * @param integer $position Position number (key) of the order product item
147
	 */
148
	public function deleteProduct( $position )
149
	{
150
		$product = $this->basket->getProduct( $position );
151
152
		if( $product->getFlags() === \Aimeos\MShop\Order\Item\Base\Product\Base::FLAG_IMMUTABLE )
153
		{
154
			$msg = sprintf( 'Basket item at position "%1$d" cannot be deleted manually', $position );
155
			throw new \Aimeos\Controller\Frontend\Basket\Exception( $msg );
156
		}
157
158
		$this->basket->deleteProduct( $position );
159
		$this->domainManager->setSession( $this->basket );
160
	}
161
162
163
	/**
164
	 * Edits the quantity of a product item in the basket.
165
	 *
166
	 * @param integer $position Position number (key) of the order product item
167
	 * @param integer $quantity New quantiy of the product item
168
	 * @param array $options Possible options are: 'stock'=>true|false
169
	 * 	The 'stock'=>false option allows adding products without being in stock.
170
	 * @param string[] $configAttributeCodes Codes of the product config attributes that should be REMOVED
171
	 */
172
	public function editProduct( $position, $quantity, array $options = array(),
173
		array $configAttributeCodes = array() )
174
	{
175
		$product = $this->basket->getProduct( $position );
176
		$product->setQuantity( $quantity ); // Enforce check immediately
177
178
		if( $product->getFlags() & \Aimeos\MShop\Order\Item\Base\Product\Base::FLAG_IMMUTABLE )
179
		{
180
			$msg = sprintf( 'Basket item at position "%1$d" cannot be changed', $position );
181
			throw new \Aimeos\Controller\Frontend\Basket\Exception( $msg );
182
		}
183
184
		$attributes = $product->getAttributes();
185
		foreach( $attributes as $key => $attribute )
186
		{
187
			if( in_array( $attribute->getCode(), $configAttributeCodes ) ) {
188
				unset( $attributes[$key] );
189
			}
190
		}
191
		$product->setAttributes( $attributes );
192
193
		$productItem = $this->getDomainItem( 'product', 'product.code', $product->getProductCode(), array( 'price', 'text' ) );
194
		$prices = $productItem->getRefItems( 'price', 'default' );
195
196
		$product->setPrice( $this->calcPrice( $product, $prices, $quantity ) );
197
198
		$this->editProductInStock( $product, $productItem, $quantity, $position, $options );
199
200
		$this->domainManager->setSession( $this->basket );
201
	}
202
203
204
	/**
205
	 * Adds the given coupon code and updates the basket.
206
	 *
207
	 * @param string $code Coupon code entered by the user
208
	 * @throws \Aimeos\Controller\Frontend\Basket\Exception if the coupon code is invalid or not allowed
209
	 */
210
	public function addCoupon( $code )
211
	{
212
		$manager = \Aimeos\MShop\Factory::createManager( $this->getContext(), 'coupon' );
213
		$codeManager = \Aimeos\MShop\Factory::createManager( $this->getContext(), 'coupon/code' );
214
215
216
		$search = $codeManager->createSearch( true );
217
		$expr = array(
218
			$search->compare( '==', 'coupon.code.code', $code ),
219
			$search->getConditions(),
220
		);
221
		$search->setConditions( $search->combine( '&&', $expr ) );
222
		$search->setSlice( 0, 1 );
223
224
		$result = $codeManager->searchItems( $search );
225
226
		if( ( $codeItem = reset( $result ) ) === false ) {
227
			throw new \Aimeos\Controller\Frontend\Basket\Exception( sprintf( 'Coupon code "%1$s" is invalid or not available any more', $code ) );
228
		}
229
230
231
		$search = $manager->createSearch( true );
232
		$expr = array(
233
			$search->compare( '==', 'coupon.id', $codeItem->getParentId() ),
234
			$search->getConditions(),
235
		);
236
		$search->setConditions( $search->combine( '&&', $expr ) );
237
		$search->setSlice( 0, 1 );
238
239
		$result = $manager->searchItems( $search );
240
241
		if( ( $item = reset( $result ) ) === false ) {
242
			throw new \Aimeos\Controller\Frontend\Basket\Exception( sprintf( 'Coupon for code "%1$s" is not available any more', $code ) );
243
		}
244
245
246
		$provider = $manager->getProvider( $item, $code );
247
248
		if( $provider->isAvailable( $this->basket ) !== true ) {
249
			throw new \Aimeos\Controller\Frontend\Basket\Exception( sprintf( 'Requirements for coupon code "%1$s" aren\'t met', $code ) );
250
		}
251
252
		$provider->addCoupon( $this->basket );
253
		$this->domainManager->setSession( $this->basket );
254
	}
255
256
257
	/**
258
	 * Removes the given coupon code and its effects from the basket.
259
	 *
260
	 * @param string $code Coupon code entered by the user
261
	 * @throws \Aimeos\Controller\Frontend\Basket\Exception if the coupon code is invalid
262
	 */
263
	public function deleteCoupon( $code )
264
	{
265
		$manager = \Aimeos\MShop\Factory::createManager( $this->getContext(), 'coupon' );
266
267
		$search = $manager->createSearch();
268
		$search->setConditions( $search->compare( '==', 'coupon.code.code', $code ) );
269
		$search->setSlice( 0, 1 );
270
271
		$result = $manager->searchItems( $search );
272
273
		if( ( $item = reset( $result ) ) === false ) {
274
			throw new \Aimeos\Controller\Frontend\Basket\Exception( sprintf( 'Coupon code "%1$s" is invalid', $code ) );
275
		}
276
277
		$manager->getProvider( $item, $code )->deleteCoupon( $this->basket );
278
		$this->domainManager->setSession( $this->basket );
279
	}
280
281
282
	/**
283
	 * Sets the address of the customer in the basket.
284
	 *
285
	 * @param string $type Address type constant from \Aimeos\MShop\Order\Item\Base\Address\Base
286
	 * @param \Aimeos\MShop\Common\Item\Address\Iface|array|null $value Address object or array with key/value pairs of address or null to remove address from basket
287
	 * @throws \Aimeos\Controller\Frontend\Basket\Exception If the billing or delivery address is not of any required type of
288
	 * 	if one of the keys is invalid when using an array with key/value pairs
289
	 */
290
	public function setAddress( $type, $value )
291
	{
292
		$address = \Aimeos\MShop\Factory::createManager( $this->getContext(), 'order/base/address' )->createItem();
293
		$address->setType( $type );
294
295
		if( $value instanceof \Aimeos\MShop\Common\Item\Address\Iface )
296
		{
297
			$address->copyFrom( $value );
298
			$this->basket->setAddress( $address, $type );
299
		}
300
		else if( is_array( $value ) )
301
		{
302
			$this->setAddressFromArray( $address, $value );
303
			$this->basket->setAddress( $address, $type );
304
		}
305
		else if( $value === null )
306
		{
307
			$this->basket->deleteAddress( $type );
308
		}
309
		else
310
		{
311
			throw new \Aimeos\Controller\Frontend\Basket\Exception( sprintf( 'Invalid value for address type "%1$s"', $type ) );
312
		}
313
314
		$this->domainManager->setSession( $this->basket );
315
	}
316
317
318
	/**
319
	 * Sets the delivery/payment service item based on the service ID.
320
	 *
321
	 * @param string $type Service type code like 'payment' or 'delivery'
322
	 * @param string $id Unique ID of the service item
323
	 * @param array $attributes Associative list of key/value pairs containing the attributes selected or
324
	 * 	entered by the customer when choosing one of the delivery or payment options
325
	 * @throws \Aimeos\Controller\Frontend\Basket\Exception If there is no price to the service item attached
326
	 */
327
	public function setService( $type, $id, array $attributes = array() )
328
	{
329
		$context = $this->getContext();
330
331
		$serviceManager = \Aimeos\MShop\Factory::createManager( $context, 'service' );
332
		$serviceItem = $this->getDomainItem( 'service', 'service.id', $id, array( 'media', 'price', 'text' ) );
333
334
		$provider = $serviceManager->getProvider( $serviceItem );
335
		$result = $provider->checkConfigFE( $attributes );
336
		$unknown = array_diff_key( $attributes, $result );
337
338
		if( count( $unknown ) > 0 )
339
		{
340
			$msg = sprintf( 'Unknown attributes "%1$s"', implode( '","', array_keys( $unknown ) ) );
341
			throw new \Aimeos\Controller\Frontend\Basket\Exception( $msg );
342
		}
343
344
		foreach( $result as $key => $value )
345
		{
346
			if( $value !== null ) {
347
				throw new \Aimeos\Controller\Frontend\Basket\Exception( $value );
348
			}
349
		}
350
351
		$orderBaseServiceManager = \Aimeos\MShop\Factory::createManager( $context, 'order/base/service' );
352
		$orderServiceItem = $orderBaseServiceManager->createItem();
353
		$orderServiceItem->copyFrom( $serviceItem );
354
355
		$price = $provider->calcPrice( $this->basket );
356
		// remove service rebate of original price
357
		$price->setRebate( '0.00' );
358
		$orderServiceItem->setPrice( $price );
359
360
		$provider->setConfigFE( $orderServiceItem, $attributes );
361
362
		$this->basket->setService( $orderServiceItem, $type );
363
		$this->domainManager->setSession( $this->basket );
364
	}
365
366
367
	/**
368
	 * Adds the bundled products to the order product item.
369
	 *
370
	 * @param \Aimeos\MShop\Order\Item\Base\Product\Iface $orderBaseProductItem Order product item
371
	 * @param \Aimeos\MShop\Product\Item\Iface $productItem Bundle product item
372
	 * @param array $variantAttributeIds List of product variant attribute IDs
373
	 * @param string $warehouse
374
	 */
375
	protected function addBundleProducts( \Aimeos\MShop\Order\Item\Base\Product\Iface $orderBaseProductItem,
376
		\Aimeos\MShop\Product\Item\Iface $productItem, array $variantAttributeIds, $warehouse )
377
	{
378
		$quantity = $orderBaseProductItem->getQuantity();
379
		$products = $subProductIds = $orderProducts = array();
380
		$orderProductManager = \Aimeos\MShop\Factory::createManager( $this->getContext(), 'order/base/product' );
381
382
		foreach( $productItem->getRefItems( 'product', null, 'default' ) as $item ) {
383
			$subProductIds[] = $item->getId();
384
		}
385
386
		if( count( $subProductIds ) > 0 )
387
		{
388
			$productManager = \Aimeos\MShop\Factory::createManager( $this->getContext(), 'product' );
389
390
			$search = $productManager->createSearch( true );
391
			$expr = array(
392
				$search->compare( '==', 'product.id', $subProductIds ),
393
				$search->getConditions(),
394
			);
395
			$search->setConditions( $search->combine( '&&', $expr ) );
396
397
			$products = $productManager->searchItems( $search, array( 'attribute', 'media', 'price', 'text' ) );
398
		}
399
400
		foreach( $products as $product )
401
		{
402
			$prices = $product->getRefItems( 'price', 'default', 'default' );
403
404
			$orderProduct = $orderProductManager->createItem();
405
			$orderProduct->copyFrom( $product );
406
			$orderProduct->setWarehouseCode( $warehouse );
407
			$orderProduct->setPrice( $this->calcPrice( $orderProduct, $prices, $quantity ) );
408
409
			$orderProducts[] = $orderProduct;
410
		}
411
412
		$orderBaseProductItem->setProducts( $orderProducts );
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Aimeos\MShop\Order\Item\Base\Product\Iface as the method setProducts() does only exist in the following implementations of said interface: Aimeos\MShop\Order\Item\Base\Product\Standard.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
413
	}
414
415
416
	/**
417
	 * Edits the changed product to the basket if it's in stock.
418
	 *
419
	 * @param \Aimeos\MShop\Order\Item\Base\Product\Iface $orderBaseProductItem Old order product from basket
420
	 * @param string $productId Unique ID of the product item that belongs to the order product
421
	 * @param integer $quantity Number of products to add to the basket
422
	 * @param array $options Associative list of options
423
	 * @param string $warehouse Warehouse code for retrieving the stock level
424
	 * @throws \Aimeos\Controller\Frontend\Basket\Exception If there's not enough stock available
425
	 */
426
	protected function addProductInStock( \Aimeos\MShop\Order\Item\Base\Product\Iface $orderBaseProductItem,
427
			$productId, $quantity, array $options, $warehouse )
428
	{
429
		$stocklevel = null;
430
		if( !isset( $options['stock'] ) || $options['stock'] != false ) {
431
			$stocklevel = $this->getStockLevel( $productId, $warehouse );
432
		}
433
434
		if( $stocklevel === null || $stocklevel > 0 )
435
		{
436
			$position = $this->get()->addProduct( $orderBaseProductItem );
437
			$orderBaseProductItem = clone $this->get()->getProduct( $position );
438
			$quantity = $orderBaseProductItem->getQuantity();
439
440
			if( $stocklevel > 0 && $stocklevel < $quantity )
441
			{
442
				$this->get()->deleteProduct( $position );
443
				$orderBaseProductItem->setQuantity( $stocklevel );
444
				$this->get()->addProduct( $orderBaseProductItem, $position );
445
			}
446
		}
447
448
		if( $stocklevel !== null && $stocklevel < $quantity )
449
		{
450
			$msg = sprintf( 'There are not enough products "%1$s" in stock', $orderBaseProductItem->getName() );
451
			throw new \Aimeos\Controller\Frontend\Basket\Exception( $msg );
452
		}
453
	}
454
455
456
	/**
457
	 * Creates the order product attribute items from the given attribute IDs and updates the price item if necessary.
458
	 *
459
	 * @param \Aimeos\MShop\Price\Item\Iface $price Price item of the ordered product
460
	 * @param string $prodid Unique product ID where the given attributes must be attached to
461
	 * @param integer $quantity Number of products that should be added to the basket
462
	 * @param array $attributeIds List of attributes IDs of the given type
463
	 * @param string $type Attribute type
464
	 * @param array $attributeValues Associative list of attribute IDs as keys and their codes as values
465
	 * @return array List of items implementing \Aimeos\MShop\Order\Item\Product\Attribute\Iface
466
	 */
467
	protected function createOrderProductAttributes( \Aimeos\MShop\Price\Item\Iface $price, $prodid, $quantity,
468
			array $attributeIds, $type, array $attributeValues = array() )
469
	{
470
		if( empty( $attributeIds ) ) {
471
			return array();
472
		}
473
474
		$attrTypeId = $this->getProductListTypeItem( 'attribute', $type )->getId();
475
		$this->checkReferences( $prodid, 'attribute', $attrTypeId, $attributeIds );
476
477
		$list = array();
478
		$context = $this->getContext();
479
480
		$priceManager = \Aimeos\MShop\Factory::createManager( $context, 'price' );
481
		$orderProductAttributeManager = \Aimeos\MShop\Factory::createManager( $context, 'order/base/product/attribute' );
482
483
		foreach( $this->getAttributes( $attributeIds ) as $id => $attrItem )
484
		{
485
			$prices = $attrItem->getRefItems( 'price', 'default', 'default' );
486
487
			if( !empty( $prices ) ) {
488
				$price->addItem( $priceManager->getLowestPrice( $prices, $quantity ) );
489
			}
490
491
			$item = $orderProductAttributeManager->createItem();
492
			$item->copyFrom( $attrItem );
493
			$item->setType( $type );
494
495
			if( isset( $attributeValues[$id] ) ) {
496
				$item->setValue( $attributeValues[$id] );
497
			}
498
499
			$list[] = $item;
500
		}
501
502
		return $list;
503
	}
504
505
506
	/**
507
	 * Edits the changed product to the basket if it's in stock.
508
	 *
509
	 * @param \Aimeos\MShop\Order\Item\Base\Product\Iface $product Old order product from basket
510
	 * @param \Aimeos\MShop\Product\Item\Iface $productItem Product item that belongs to the order product
511
	 * @param integer $quantity New product quantity
512
	 * @param integer $position Position of the old order product in the basket
513
	 * @param array Associative list of options
514
	 * @throws \Aimeos\Controller\Frontend\Basket\Exception If there's not enough stock available
515
	 */
516
	protected function editProductInStock( \Aimeos\MShop\Order\Item\Base\Product\Iface $product,
517
			\Aimeos\MShop\Product\Item\Iface $productItem, $quantity, $position, array $options )
518
	{
519
		$stocklevel = null;
520
		if( !isset( $options['stock'] ) || $options['stock'] != false ) {
521
			$stocklevel = $this->getStockLevel( $productItem->getId(), $product->getWarehouseCode() );
522
		}
523
524
		$product->setQuantity( ( $stocklevel !== null && $stocklevel > 0 ? min( $stocklevel, $quantity ) : $quantity ) );
525
526
		$this->get()->deleteProduct( $position );
527
528
		if( $stocklevel === null || $stocklevel > 0 ) {
529
			$this->get()->addProduct( $product, $position );
530
		}
531
532
		if( $stocklevel !== null && $stocklevel < $quantity )
533
		{
534
			$msg = sprintf( 'There are not enough products "%1$s" in stock', $productItem->getName() );
535
			throw new \Aimeos\Controller\Frontend\Basket\Exception( $msg );
536
		}
537
	}
538
539
540
	/**
541
	 * Retrieves the domain item specified by the given key and value.
542
	 *
543
	 * @param string $domain Product manager search key
544
	 * @param string $key Domain manager search key
545
	 * @param string $value Unique domain identifier
546
	 * @param string[] $ref List of referenced items that should be fetched too
547
	 * @return \Aimeos\MShop\Common\Item\Iface Domain item object
548
	 * @throws \Aimeos\Controller\Frontend\Basket\Exception
549
	 */
550
	protected function getDomainItem( $domain, $key, $value, array $ref )
551
	{
552
		$manager = \Aimeos\MShop\Factory::createManager( $this->getContext(), $domain );
553
554
		$search = $manager->createSearch( true );
555
		$expr = array(
556
				$search->compare( '==', $key, $value ),
557
				$search->getConditions(),
558
		);
559
		$search->setConditions( $search->combine( '&&', $expr ) );
560
561
		$result = $manager->searchItems( $search, $ref );
562
563
		if( ( $item = reset( $result ) ) === false )
564
		{
565
			$msg = sprintf( 'No item for "%1$s" (%2$s) found', $value, $key );
566
			throw new \Aimeos\Controller\Frontend\Basket\Exception( $msg );
567
		}
568
569
		return $item;
570
	}
571
572
573
	/**
574
	 * Returns the variant attributes and updates the price list if necessary.
575
	 *
576
	 * @param \Aimeos\MShop\Order\Item\Base\Product\Iface $orderBaseProductItem Order product item
577
	 * @param \Aimeos\MShop\Product\Item\Iface &$productItem Product item which is replaced if necessary
578
	 * @param array &$prices List of product prices that will be updated if necessary
579
	 * @param array $variantAttributeIds List of product variant attribute IDs
580
	 * @param array $options Associative list of options
581
	 * @return \Aimeos\MShop\Order\Item\Base\Product\Attribute\Iface[] List of order product attributes
582
	 * @throws \Aimeos\Controller\Frontend\Basket\Exception If no product variant is found
583
	 */
584
	protected function getVariantDetails( \Aimeos\MShop\Order\Item\Base\Product\Iface $orderBaseProductItem,
585
		\Aimeos\MShop\Product\Item\Iface &$productItem, array &$prices, array $variantAttributeIds, array $options )
586
	{
587
		$attr = array();
588
		$productItems = $this->getProductVariants( $productItem, $variantAttributeIds );
589
590
		if( count( $productItems ) > 1 )
591
		{
592
			$msg = sprintf( 'No unique article found for selected attributes and product ID "%1$s"', $productItem->getId() );
593
			throw new \Aimeos\Controller\Frontend\Basket\Exception( $msg );
594
		}
595
		else if( ( $result = reset( $productItems ) ) !== false ) // count == 1
596
		{
597
			$productItem = $result;
598
			$orderBaseProductItem->setProductCode( $productItem->getCode() );
599
600
			$subprices = $productItem->getRefItems( 'price', 'default', 'default' );
601
602
			if( count( $subprices ) > 0 ) {
603
				$prices = $subprices;
604
			}
605
606
			$orderProductAttrManager = \Aimeos\MShop\Factory::createManager( $this->getContext(), 'order/base/product/attribute' );
607
			$variantAttributes = $productItem->getRefItems( 'attribute', null, 'variant' );
608
609
			foreach( $this->getAttributes( array_keys( $variantAttributes ), array( 'text' ) ) as $attrItem )
610
			{
611
				$orderAttributeItem = $orderProductAttrManager->createItem();
612
				$orderAttributeItem->copyFrom( $attrItem );
613
				$orderAttributeItem->setType( 'variant' );
614
615
				$attr[] = $orderAttributeItem;
616
			}
617
		}
618
		else if( !isset( $options['variant'] ) || $options['variant'] != false ) // count == 0
619
		{
620
			$msg = sprintf( 'No article found for selected attributes and product ID "%1$s"', $productItem->getId() );
621
			throw new \Aimeos\Controller\Frontend\Basket\Exception( $msg );
622
		}
623
624
		return $attr;
625
	}
626
627
628
	/**
629
	 * Fills the order address object with the values from the array.
630
	 *
631
	 * @param \Aimeos\MShop\Order\Item\Base\Address\Iface $address Address item to store the values into
632
	 * @param array $map Associative array of key/value pairs. The keys must be the same as when calling toArray() from
633
	 * 	an address item.
634
	 * @throws \Aimeos\Controller\Frontend\Basket\Exception
635
	 */
636
	protected function setAddressFromArray( \Aimeos\MShop\Order\Item\Base\Address\Iface $address, array $map )
637
	{
638
		foreach( $map as $key => $value ) {
639
			$map[$key] = strip_tags( $value ); // prevent XSS
640
		}
641
642
		$errors = $address->fromArray( $map );
643
644
		if( count( $errors ) > 0 )
645
		{
646
			$msg = sprintf( 'Invalid address properties, please check your input' );
647
			throw new \Aimeos\Controller\Frontend\Basket\Exception( $msg, 0, null, $errors );
648
		}
649
	}
650
}
651