Completed
Push — master ( ac3c49...e09568 )
by Aimeos
02:38
created

Standard::setService()   B

Complexity

Conditions 5
Paths 5

Size

Total Lines 45
Code Lines 26

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 45
rs 8.439
c 0
b 0
f 0
cc 5
eloc 26
nc 5
nop 3
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-2017
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
23
	implements Iface, \Aimeos\Controller\Frontend\Common\Iface
24
{
25
	private $baskets = [];
26
	private $domainManager;
27
	private $type = 'default';
28
29
30
	/**
31
	 * Initializes the frontend controller.
32
	 *
33
	 * @param \Aimeos\MShop\Context\Item\Iface $context Object storing the required instances for manaing databases
34
	 *  connections, logger, session, etc.
35
	 */
36
	public function __construct( \Aimeos\MShop\Context\Item\Iface $context )
37
	{
38
		parent::__construct( $context );
39
40
		$this->domainManager = \Aimeos\MShop\Factory::createManager( $context, 'order/base' );
41
	}
42
43
44
	/**
45
	 * Empties the basket and removing all products, addresses, services, etc.
46
	 *
47
	 * @return \Aimeos\Controller\Frontend\Basket\Iface Basket frontend object
48
	 */
49
	public function clear()
50
	{
51
		$this->baskets[$this->type] = $this->domainManager->createItem();
52
		$this->domainManager->setSession( $this->baskets[$this->type], $this->type );
53
54
		return $this;
55
	}
56
57
58
	/**
59
	 * Returns the basket object.
60
	 *
61
	 * @return \Aimeos\MShop\Order\Item\Base\Iface Basket holding products, addresses and delivery/payment options
62
	 */
63
	public function get()
64
	{
65
		if( !isset( $this->baskets[$this->type] ) )
66
		{
67
			$this->baskets[$this->type] = $this->domainManager->getSession( $this->type );
68
			$this->checkLocale( $this->type );
69
		}
70
71
		return $this->baskets[$this->type];
72
	}
73
74
75
	/**
76
	 * Explicitely persists the basket content
77
	 *
78
	 * @return \Aimeos\Controller\Frontend\Basket\Iface Basket frontend object
79
	 */
80
	public function save()
81
	{
82
		if( isset( $this->baskets[$this->type] ) && $this->baskets[$this->type]->isModified() ) {
83
			$this->domainManager->setSession( $this->baskets[$this->type], $this->type );
84
		}
85
86
		return $this;
87
	}
88
89
90
	/**
91
	 * Sets the new basket type
92
	 *
93
	 * @param string $type Basket type
94
	 * @return \Aimeos\Controller\Frontend\Basket\Iface Basket frontend object
95
	 */
96
	public function setType( $type )
97
	{
98
		$this->type = $type;
99
		return $this;
100
	}
101
102
103
	/**
104
	 * Creates a new order base object from the current basket
105
	 *
106
	 * @return \Aimeos\MShop\Order\Item\Base\Iface Order base object including products, addresses and services
107
	 */
108
	public function store()
109
	{
110
		$total = 0;
111
		$context = $this->getContext();
112
		$config = $context->getConfig();
113
114
		/** controller/frontend/basket/limit-count
115
		 * Maximum number of orders within the time frame
116
		 *
117
		 * Creating new orders is limited to avoid abuse and mitigate denial of
118
		 * service attacks. The number of orders created within the time frame
119
		 * configured by "controller/frontend/basket/limit-seconds" are counted
120
		 * before a new order of the same user (either logged in or identified
121
		 * by the IP address) is created. If the number of orders is higher than
122
		 * the configured value, an error message will be shown to the user
123
		 * instead of creating a new order.
124
		 *
125
		 * @param integer Number of orders allowed within the time frame
126
		 * @since 2017.05
127
		 * @category Developer
128
		 * @see controller/frontend/basket/limit-seconds
129
		 */
130
		$count = $config->get( 'controller/frontend/basket/limit-count', 5 );
131
132
		/** controller/frontend/basket/limit-seconds
133
		 * Order limitation time frame in seconds
134
		 *
135
		 * Creating new orders is limited to avoid abuse and mitigate denial of
136
		 * service attacks. Within the configured time frame, only a limited
137
		 * number of orders can be created. All orders of the current user
138
		 * (either logged in or identified by the IP address) within the last X
139
		 * seconds are counted. If the total value is higher then the number
140
		 * configured in "controller/frontend/basket/limit-count", an error
141
		 * message will be shown to the user instead of creating a new order.
142
		 *
143
		 * @param integer Number of seconds to check orders within
144
		 * @since 2017.05
145
		 * @category Developer
146
		 * @see controller/frontend/basket/limit-count
147
		 */
148
		$seconds = $config->get( 'controller/frontend/basket/limit-seconds', 300 );
149
150
		$search = $this->domainManager->createSearch();
151
		$expr = [
152
			$search->compare( '==', 'order.base.editor', $context->getEditor() ),
153
			$search->compare( '>=', 'order.base.ctime', date( 'Y-m-d H:i:s', time() - $seconds ) ),
154
		];
155
		$search->setConditions( $search->combine( '&&', $expr ) );
156
		$search->setSlice( 0, 0 );
157
158
		$this->domainManager->searchItems( $search, [], $total );
159
160
		if( $total > $count )
161
		{
162
			$msg = $context->getI18n()->dt( 'controller/frontend', 'Temporary order limit reached' );
163
			throw new \Aimeos\Controller\Frontend\Basket\Exception( $msg );
164
		}
165
166
167
		$basket = $this->get()->finish();
168
		$basket->setCustomerId( (string) $context->getUserId() );
169
170
		$this->domainManager->begin();
171
		$this->domainManager->store( $basket );
172
		$this->domainManager->commit();
173
174
		return $basket;
175
	}
176
177
178
	/**
179
	 * Returns the order base object for the given ID
180
	 *
181
	 * @param string $id Unique ID of the order base object
182
	 * @param integer $parts Constants which parts of the order base object should be loaded
183
	 * @param boolean $default True to add default criteria (user logged in), false if not
184
	 * @return \Aimeos\MShop\Order\Item\Base\Iface Order base object including the given parts
185
	 */
186
	public function load( $id, $parts = \Aimeos\MShop\Order\Manager\Base\Base::PARTS_ALL, $default = true )
187
	{
188
		return $this->domainManager->load( $id, $parts, false, $default );
189
	}
190
191
192
	/**
193
	 * Adds a categorized product to the basket of the user stored in the session.
194
	 *
195
	 * @param string $prodid ID of the base product to add
196
	 * @param integer $quantity Amount of products that should by added
197
	 * @param array $options Option list (unused at the moment)
198
	 * @param array $variantAttributeIds List of variant-building attribute IDs that identify a specific product
199
	 * 	in a selection products
200
	 * @param array $configAttributeIds  List of attribute IDs that doesn't identify a specific product in a
201
	 * 	selection of products but are stored together with the product (e.g. for configurable products)
202
	 * @param array $hiddenAttributeIds List of attribute IDs that should be stored along with the product in the order
203
	 * @param array $customAttributeValues Associative list of attribute IDs and arbitrary values that should be stored
204
	 * 	along with the product in the order
205
	 * @param string $stocktype Unique code of the stock type to deliver the products from
206
	 * @throws \Aimeos\Controller\Frontend\Basket\Exception If the product isn't available
207
	 */
208
	public function addProduct( $prodid, $quantity = 1, array $options = [], array $variantAttributeIds = [],
209
		array $configAttributeIds = [], array $hiddenAttributeIds = [], array $customAttributeValues = [],
210
		$stocktype = 'default' )
211
	{
212
		$attributeMap = [
213
			'custom' => array_keys( $customAttributeValues ),
214
			'config' => $configAttributeIds,
215
			'hidden' => $hiddenAttributeIds,
216
		];
217
		$this->checkListRef( $prodid, 'attribute', $attributeMap );
218
219
220
		$context = $this->getContext();
221
		$productManager = \Aimeos\MShop\Factory::createManager( $context, 'product' );
222
		$productItem = $productManager->getItem( $prodid, array( 'media', 'supplier', 'price', 'product', 'text' ), true );
223
		$prices = $productItem->getRefItems( 'price', 'default', 'default' );
224
225
		$orderBaseProductItem = \Aimeos\MShop\Factory::createManager( $context, 'order/base/product' )->createItem();
226
		$orderBaseProductItem->copyFrom( $productItem )->setQuantity( $quantity )->setStockType( $stocktype );
227
228
		$attr = $this->getOrderProductAttributes( 'custom', array_keys( $customAttributeValues ), $customAttributeValues );
229
		$attr = array_merge( $attr, $this->getOrderProductAttributes( 'config', $configAttributeIds ) );
230
		$attr = array_merge( $attr, $this->getOrderProductAttributes( 'hidden', $hiddenAttributeIds ) );
231
232
		$orderBaseProductItem->setAttributes( $attr );
233
		$orderBaseProductItem->setPrice( $this->calcPrice( $orderBaseProductItem, $prices, $quantity ) );
234
235
		$this->get()->addProduct( $orderBaseProductItem );
236
		$this->save();
237
	}
238
239
240
	/**
241
	 * Deletes a product item from the basket.
242
	 *
243
	 * @param integer $position Position number (key) of the order product item
244
	 */
245
	public function deleteProduct( $position )
246
	{
247
		$product = $this->get()->getProduct( $position );
248
249
		if( $product->getFlags() === \Aimeos\MShop\Order\Item\Base\Product\Base::FLAG_IMMUTABLE )
250
		{
251
			$msg = $this->getContext()->getI18n()->dt( 'controller/frontend', 'Basket item at position "%1$d" cannot be deleted manually' );
252
			throw new \Aimeos\Controller\Frontend\Basket\Exception( sprintf( $msg, $position ) );
253
		}
254
255
		$this->get()->deleteProduct( $position );
256
		$this->save();
257
	}
258
259
260
	/**
261
	 * Edits the quantity of a product item in the basket.
262
	 *
263
	 * @param integer $position Position number (key) of the order product item
264
	 * @param integer $quantity New quantiy of the product item
265
	 * @param array $options Possible options are: 'stock'=>true|false
266
	 * 	The 'stock'=>false option allows adding products without being in stock.
267
	 * @param string[] $configAttributeCodes Codes of the product config attributes that should be REMOVED
268
	 */
269
	public function editProduct( $position, $quantity, array $options = [],
270
		array $configAttributeCodes = [] )
271
	{
272
		$product = $this->get()->getProduct( $position );
273
274
		if( $product->getFlags() & \Aimeos\MShop\Order\Item\Base\Product\Base::FLAG_IMMUTABLE )
275
		{
276
			$msg = $this->getContext()->getI18n()->dt( 'controller/frontend', 'Basket item at position "%1$d" cannot be changed' );
277
			throw new \Aimeos\Controller\Frontend\Basket\Exception( sprintf( $msg, $position ) );
278
		}
279
280
		$product->setQuantity( $quantity );
281
282
		$attributes = $product->getAttributes();
283
		foreach( $attributes as $key => $attribute )
284
		{
285
			if( in_array( $attribute->getCode(), $configAttributeCodes ) ) {
286
				unset( $attributes[$key] );
287
			}
288
		}
289
		$product->setAttributes( $attributes );
290
291
		$manager = \Aimeos\MShop\Factory::createManager( $this->getContext(), 'product' );
292
		$productItem = $manager->findItem( $product->getProductCode(), array( 'price', 'text' ) );
293
		$product->setPrice( $this->calcPrice( $product, $productItem->getRefItems( 'price', 'default' ), $quantity ) );
294
295
		$this->get()->editProduct( $product, $position );
296
297
		$this->save();
298
	}
299
300
301
	/**
302
	 * Adds the given coupon code and updates the basket.
303
	 *
304
	 * @param string $code Coupon code entered by the user
305
	 * @throws \Aimeos\Controller\Frontend\Basket\Exception if the coupon code is invalid or not allowed
306
	 */
307
	public function addCoupon( $code )
308
	{
309
		$context = $this->getContext();
310
311
		/** controller/frontend/basket/standard/coupon/allowed
312
		 * Number of coupon codes a customer is allowed to enter
313
		 *
314
		 * This configuration option enables shop owners to limit the number of coupon
315
		 * codes that can be added by a customer to his current basket. By default, only
316
		 * one coupon code is allowed per order.
317
		 *
318
		 * Coupon codes are valid until a payed order is placed by the customer. The
319
		 * "count" of the codes is decreased afterwards. If codes are not personalized
320
		 * the codes can be reused in the next order until their "count" reaches zero.
321
		 *
322
		 * @param integer Positive number of coupon codes including zero
323
		 * @since 2017.08
324
		 * @category User
325
		 * @category Developer
326
		 */
327
		$allowed = $context->getConfig()->get( 'client/html/basket/standard/coupon/allowed', 1 ); // @deprecated
328
		$allowed = $context->getConfig()->get( 'controller/frontend/basket/standard/coupon/allowed', $allowed );
329
330
		if( $allowed <= count( $this->get()->getCoupons() ) )
331
		{
332
			$msg = $context->getI18n()->dt( 'controller/frontend', 'Number of coupon codes exceeds the limit' );
333
			throw new \Aimeos\Controller\Frontend\Basket\Exception( $msg );
334
		}
335
336
337
		$manager = \Aimeos\MShop\Factory::createManager( $this->getContext(), 'coupon' );
338
		$codeManager = \Aimeos\MShop\Factory::createManager( $this->getContext(), 'coupon/code' );
339
340
341
		$search = $codeManager->createSearch( true );
342
		$expr = array(
343
			$search->compare( '==', 'coupon.code.code', $code ),
344
			$search->getConditions(),
345
		);
346
		$search->setConditions( $search->combine( '&&', $expr ) );
347
		$search->setSlice( 0, 1 );
348
349
		$result = $codeManager->searchItems( $search );
350
351
		if( ( $codeItem = reset( $result ) ) === false )
352
		{
353
			$msg = sprintf( $context->getI18n()->dt( 'controller/frontend', 'Coupon code "%1$s" is invalid or not available any more' ), $code );
354
			throw new \Aimeos\Controller\Frontend\Basket\Exception( $msg );
355
		}
356
357
358
		$search = $manager->createSearch( true );
359
		$expr = array(
360
			$search->compare( '==', 'coupon.id', $codeItem->getParentId() ),
361
			$search->getConditions(),
362
		);
363
		$search->setConditions( $search->combine( '&&', $expr ) );
364
		$search->setSlice( 0, 1 );
365
366
		$result = $manager->searchItems( $search );
367
368
		if( ( $item = reset( $result ) ) === false )
369
		{
370
			$msg = sprintf( $context->getI18n()->dt( 'controller/frontend', 'Coupon for code "%1$s" is not available any more' ), $code );
371
			throw new \Aimeos\Controller\Frontend\Basket\Exception( $msg );
372
		}
373
374
375
		$provider = $manager->getProvider( $item, $codeItem->getCode() );
376
377
		if( $provider->isAvailable( $this->get() ) !== true )
378
		{
379
			$msg = sprintf( $context->getI18n()->dt( 'controller/frontend', 'Requirements for coupon code "%1$s" aren\'t met' ), $code );
380
			throw new \Aimeos\Controller\Frontend\Basket\Exception( $msg );
381
		}
382
383
		$provider->addCoupon( $this->get() );
384
		$this->save();
385
	}
386
387
388
	/**
389
	 * Removes the given coupon code and its effects from the basket.
390
	 *
391
	 * @param string $code Coupon code entered by the user
392
	 * @throws \Aimeos\Controller\Frontend\Basket\Exception if the coupon code is invalid
393
	 */
394
	public function deleteCoupon( $code )
395
	{
396
		$context = $this->getContext();
397
		$manager = \Aimeos\MShop\Factory::createManager( $context, 'coupon' );
398
399
		$search = $manager->createSearch();
400
		$search->setConditions( $search->compare( '==', 'coupon.code.code', $code ) );
401
		$search->setSlice( 0, 1 );
402
403
		$result = $manager->searchItems( $search );
404
405
		if( ( $item = reset( $result ) ) === false )
406
		{
407
			$msg = $context->getI18n()->dt( 'controller/frontend', 'Coupon code "%1$s" is invalid' );
408
			throw new \Aimeos\Controller\Frontend\Basket\Exception( sprintf( $msg, $code ) );
409
		}
410
411
		$manager->getProvider( $item, $code )->deleteCoupon( $this->get() );
412
		$this->save();
413
	}
414
415
416
	/**
417
	 * Sets the address of the customer in the basket.
418
	 *
419
	 * @param string $type Address type constant from \Aimeos\MShop\Order\Item\Base\Address\Base
420
	 * @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
421
	 * @throws \Aimeos\Controller\Frontend\Basket\Exception If the billing or delivery address is not of any required type of
422
	 * 	if one of the keys is invalid when using an array with key/value pairs
423
	 */
424
	public function setAddress( $type, $value )
425
	{
426
		$context = $this->getContext();
427
		$address = \Aimeos\MShop\Factory::createManager( $context, 'order/base/address' )->createItem();
428
		$address->setType( $type );
429
430
		if( $value instanceof \Aimeos\MShop\Common\Item\Address\Iface )
431
		{
432
			$address->copyFrom( $value );
433
			$this->get()->setAddress( $address, $type );
434
		}
435
		else if( is_array( $value ) )
436
		{
437
			$this->setAddressFromArray( $address, $value );
438
			$this->get()->setAddress( $address, $type );
439
		}
440
		else if( $value === null )
441
		{
442
			$this->get()->deleteAddress( $type );
443
		}
444
		else
445
		{
446
			$msg = $context->getI18n()->dt( 'controller/frontend', 'Invalid value for address type "%1$s"' );
447
			throw new \Aimeos\Controller\Frontend\Basket\Exception( sprintf( $msg, $type ) );
448
		}
449
450
		$this->save();
451
	}
452
453
454
	/**
455
	 * Adds the delivery/payment service item based on the service ID.
456
	 *
457
	 * @param string $type Service type code like 'payment' or 'delivery'
458
	 * @param string $id|null Unique ID of the service item or null to remove it
0 ignored issues
show
Bug introduced by
There is no parameter named $id|null. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
459
	 * @param array $attributes Associative list of key/value pairs containing the attributes selected or
460
	 * 	entered by the customer when choosing one of the delivery or payment options
461
	 * @throws \Aimeos\Controller\Frontend\Basket\Exception If there is no price to the service item attached
462
	 */
463
	public function addService( $type, $id, array $attributes = [] )
464
	{
465
		$context = $this->getContext();
466
467
		$serviceManager = \Aimeos\MShop\Factory::createManager( $context, 'service' );
468
		$serviceItem = $serviceManager->getItem( $id, array( 'media', 'price', 'text' ) );
469
470
		$provider = $serviceManager->getProvider( $serviceItem );
471
		$result = $provider->checkConfigFE( $attributes );
472
		$unknown = array_diff_key( $attributes, $result );
473
474
		if( count( $unknown ) > 0 )
475
		{
476
			$msg = $context->getI18n()->dt( 'controller/frontend', 'Unknown attributes "%1$s"' );
477
			$msg = sprintf( $msg, implode( '","', array_keys( $unknown ) ) );
478
			throw new \Aimeos\Controller\Frontend\Basket\Exception( $msg );
479
		}
480
481
		foreach( $result as $key => $value )
482
		{
483
			if( $value !== null ) {
484
				throw new \Aimeos\Controller\Frontend\Basket\Exception( $value );
485
			}
486
		}
487
488
		$orderBaseServiceManager = \Aimeos\MShop\Factory::createManager( $context, 'order/base/service' );
489
		$orderServiceItem = $orderBaseServiceManager->createItem();
490
		$orderServiceItem->copyFrom( $serviceItem );
491
492
		// remove service rebate of original price
493
		$price = $provider->calcPrice( $this->get() )->setRebate( '0.00' );
494
		$orderServiceItem->setPrice( $price );
495
496
		$provider->setConfigFE( $orderServiceItem, $attributes );
497
498
		$this->get()->addService( $orderServiceItem, $type );
0 ignored issues
show
Bug introduced by
The method addService() does not seem to exist on object<Aimeos\MShop\Order\Item\Base\Iface>.

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...
499
		$this->save();
500
	}
501
502
503
	/**
504
	 * Removes the delivery or payment service items from the basket
505
	 *
506
	 * @param string $type Service type code like 'payment' or 'delivery'
507
	 */
508
	public function deleteService( $type )
509
	{
510
		$this->get()->deleteService( $type );
511
		$this->save();
512
	}
513
514
515
	/**
516
	 * Fills the order address object with the values from the array.
517
	 *
518
	 * @param \Aimeos\MShop\Order\Item\Base\Address\Iface $address Address item to store the values into
519
	 * @param array $map Associative array of key/value pairs. The keys must be the same as when calling toArray() from
520
	 * 	an address item.
521
	 * @throws \Aimeos\Controller\Frontend\Basket\Exception
522
	 */
523
	protected function setAddressFromArray( \Aimeos\MShop\Order\Item\Base\Address\Iface $address, array $map )
524
	{
525
		foreach( $map as $key => $value ) {
526
			$map[$key] = strip_tags( $value ); // prevent XSS
527
		}
528
529
		$errors = $address->fromArray( $map );
530
531
		if( count( $errors ) > 0 )
532
		{
533
			$msg = $this->getContext()->getI18n()->dt( 'controller/frontend', 'Invalid address properties, please check your input' );
534
			throw new \Aimeos\Controller\Frontend\Basket\Exception( $msg, 0, null, $errors );
535
		}
536
	}
537
}
538