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

Standard::editProduct()   B

Complexity

Conditions 4
Paths 4

Size

Total Lines 30
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

Changes 3
Bugs 0 Features 0
Metric Value
c 3
b 0
f 0
dl 0
loc 30
rs 8.5806
cc 4
eloc 17
nc 4
nop 4
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