Passed
Push — master ( 9a0039...2f021f )
by Aimeos
07:58
created

Standard::addProduct()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 34
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
eloc 20
c 2
b 0
f 0
dl 0
loc 34
rs 9.6
cc 3
nc 4
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-2021
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 $manager;
26
	private $baskets = [];
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->manager = \Aimeos\MShop::create( $context, 'order/base' );
41
	}
42
43
44
	/**
45
	 * Adds values like comments to the basket
46
	 *
47
	 * @param array $values Order base values like comment
48
	 * @return \Aimeos\Controller\Frontend\Basket\Iface Basket frontend object for fluent interface
49
	 */
50
	public function add( array $values ) : Iface
51
	{
52
		$this->baskets[$this->type] = $this->get()->fromArray( $values );
53
		return $this;
54
	}
55
56
57
	/**
58
	 * Empties the basket and removing all products, addresses, services, etc.
59
	 *
60
	 * @return \Aimeos\Controller\Frontend\Basket\Iface Basket frontend object for fluent interface
61
	 */
62
	public function clear() : Iface
63
	{
64
		$this->baskets[$this->type] = $this->manager->create();
65
		$this->manager->setSession( $this->baskets[$this->type], $this->type );
66
67
		return $this;
68
	}
69
70
71
	/**
72
	 * Returns the basket object.
73
	 *
74
	 * @return \Aimeos\MShop\Order\Item\Base\Iface Basket holding products, addresses and delivery/payment options
75
	 */
76
	public function get() : \Aimeos\MShop\Order\Item\Base\Iface
77
	{
78
		if( !isset( $this->baskets[$this->type] ) )
79
		{
80
			$this->baskets[$this->type] = $this->manager->getSession( $this->type );
81
			$this->checkLocale( $this->baskets[$this->type]->getLocale(), $this->type );
82
			$this->baskets[$this->type]->setCustomerId( (string) $this->getContext()->getUserId() );
83
		}
84
85
		return $this->baskets[$this->type];
86
	}
87
88
89
	/**
90
	 * Explicitely persists the basket content
91
	 *
92
	 * @return \Aimeos\Controller\Frontend\Basket\Iface Basket frontend object for fluent interface
93
	 */
94
	public function save() : Iface
95
	{
96
		if( isset( $this->baskets[$this->type] ) && $this->baskets[$this->type]->isModified() ) {
97
			$this->manager->setSession( $this->baskets[$this->type], $this->type );
98
		}
99
100
		return $this;
101
	}
102
103
104
	/**
105
	 * Sets the new basket type
106
	 *
107
	 * @param string $type Basket type
108
	 * @return \Aimeos\Controller\Frontend\Basket\Iface Basket frontend object for fluent interface
109
	 */
110
	public function setType( string $type ) : Iface
111
	{
112
		$this->type = $type;
113
		return $this;
114
	}
115
116
117
	/**
118
	 * Creates a new order base object from the current basket
119
	 *
120
	 * @return \Aimeos\MShop\Order\Item\Base\Iface Order base object including products, addresses and services
121
	 */
122
	public function store() : \Aimeos\MShop\Order\Item\Base\Iface
123
	{
124
		$total = 0;
125
		$context = $this->getContext();
126
		$config = $context->getConfig();
127
128
		/** controller/frontend/basket/limit-count
129
		 * Maximum number of orders within the time frame
130
		 *
131
		 * Creating new orders is limited to avoid abuse and mitigate denial of
132
		 * service attacks. The number of orders created within the time frame
133
		 * configured by "controller/frontend/basket/limit-seconds" are counted
134
		 * before a new order of the same user (either logged in or identified
135
		 * by the IP address) is created. If the number of orders is higher than
136
		 * the configured value, an error message will be shown to the user
137
		 * instead of creating a new order.
138
		 *
139
		 * @param integer Number of orders allowed within the time frame
140
		 * @since 2017.05
141
		 * @category Developer
142
		 * @see controller/frontend/basket/limit-seconds
143
		 */
144
		$count = $config->get( 'controller/frontend/basket/limit-count', 5 );
145
146
		/** controller/frontend/basket/limit-seconds
147
		 * Order limitation time frame in seconds
148
		 *
149
		 * Creating new orders is limited to avoid abuse and mitigate denial of
150
		 * service attacks. Within the configured time frame, only a limited
151
		 * number of orders can be created. All orders of the current user
152
		 * (either logged in or identified by the IP address) within the last X
153
		 * seconds are counted. If the total value is higher then the number
154
		 * configured in "controller/frontend/basket/limit-count", an error
155
		 * message will be shown to the user instead of creating a new order.
156
		 *
157
		 * @param integer Number of seconds to check orders within
158
		 * @since 2017.05
159
		 * @category Developer
160
		 * @see controller/frontend/basket/limit-count
161
		 */
162
		$seconds = $config->get( 'controller/frontend/basket/limit-seconds', 900 );
163
164
		$search = $this->manager->filter()->slice( 0, 0 );
165
		$expr = [
166
			$search->compare( '==', 'order.base.editor', $context->getEditor() ),
167
			$search->compare( '>=', 'order.base.ctime', date( 'Y-m-d H:i:s', time() - $seconds ) ),
168
		];
169
		$search->setConditions( $search->and( $expr ) );
170
171
		$this->manager->search( $search, [], $total );
172
173
		if( $total >= $count )
174
		{
175
			$msg = $context->translate( 'controller/frontend', 'Temporary order limit reached' );
176
			throw new \Aimeos\Controller\Frontend\Basket\Exception( $msg );
177
		}
178
179
180
		$basket = $this->get()->setCustomerId( (string) $context->getUserId() )->finish()->check();
181
182
		$this->manager->begin();
183
		$this->manager->store( $basket );
184
		$this->manager->commit();
185
186
		$this->save(); // for reusing unpaid orders, might have side effects (!)
187
		$this->createSubscriptions( $basket );
188
189
		return $basket;
190
	}
191
192
193
	/**
194
	 * Returns the order base object for the given ID
195
	 *
196
	 * @param string $id Unique ID of the order base object
197
	 * @param array $ref References items that should be fetched too
198
	 * @param bool $default True to add default criteria (user logged in), false if not
199
	 * @return \Aimeos\MShop\Order\Item\Base\Iface Order base object including the given parts
200
	 * @todo 2021.01 Use array type hint for $ref
201
	 */
202
	public function load( string $id, $ref = \Aimeos\MShop\Order\Item\Base\Base::PARTS_ALL,
203
		bool $default = true ) : \Aimeos\MShop\Order\Item\Base\Iface
204
	{
205
		if( is_int( $ref ) ) {
206
			return $this->manager->load( $id, $ref, false, $default );
207
		}
208
209
		return $this->manager->get( $id, $ref, $default );
210
	}
211
212
213
	/**
214
	 * Adds a product to the basket of the customer stored in the session
215
	 *
216
	 * @param \Aimeos\MShop\Product\Item\Iface $product Product to add including texts, media, prices, attributes, etc.
217
	 * @param float $quantity Amount of products that should by added
218
	 * @param array $variant List of variant-building attribute IDs that identify an article in a selection product
219
	 * @param array $config List of configurable attribute IDs the customer has chosen from
220
	 * @param array $custom Associative list of attribute IDs as keys and arbitrary values that will be added to the ordered product
221
	 * @param string $stocktype Unique code of the stock type to deliver the products from
222
	 * @param string|null $supplierid Unique supplier ID the product is from
223
	 * @param string|null $siteid Unique site ID the product is from or null for siteid of the product item
224
	 * @return \Aimeos\Controller\Frontend\Basket\Iface Basket frontend object for fluent interface
225
	 * @throws \Aimeos\Controller\Frontend\Basket\Exception If the product isn't available
226
	 */
227
	public function addProduct( \Aimeos\MShop\Product\Item\Iface $product,
228
		float $quantity = 1, array $variant = [], array $config = [], array $custom = [],
229
		string $stocktype = 'default', string $supplierid = null, string $siteid = null ) : Iface
230
	{
231
		$quantity = $this->checkQuantity( $product, $quantity );
232
		$this->checkAttributes( [$product], 'custom', array_keys( $custom ) );
233
		$this->checkAttributes( [$product], 'config', array_keys( $config ) );
234
235
		$prices = $product->getRefItems( 'price', 'default', 'default' );
236
		$hidden = $product->getRefItems( 'attribute', null, 'hidden' );
237
238
		$custAttr = $this->getOrderProductAttributes( 'custom', array_keys( $custom ), $custom );
239
		$confAttr = $this->getOrderProductAttributes( 'config', array_keys( $config ), [], $config );
240
		$hideAttr = $this->getOrderProductAttributes( 'hidden', $hidden->keys()->toArray() );
241
242
		$orderBaseProductItem = \Aimeos\MShop::create( $this->getContext(), 'order/base/product' )->create()
243
			->copyFrom( $product )->setQuantity( $quantity )->setStockType( $stocktype )
244
			->setAttributeItems( array_merge( $custAttr, $confAttr, $hideAttr ) );
245
246
		$orderBaseProductItem = $orderBaseProductItem
247
			->setPrice( $this->calcPrice( $orderBaseProductItem, $prices, $quantity ) );
248
249
		if( $siteid ) {
250
			$orderBaseProductItem->setSiteId( $siteid );
251
		}
252
253
		if( $supplierid )
254
		{
255
			$name = \Aimeos\MShop::create( $this->getContext(), 'supplier' )->get( $supplierid, ['text'] )->getName();
256
			$orderBaseProductItem->setSupplierId( $supplierid )->setSupplierName( $name );
257
		}
258
259
		$this->baskets[$this->type] = $this->get()->addProduct( $orderBaseProductItem );
260
		return $this->save();
261
	}
262
263
264
	/**
265
	 * Deletes a product item from the basket.
266
	 *
267
	 * @param int $position Position number (key) of the order product item
268
	 * @return \Aimeos\Controller\Frontend\Basket\Iface Basket frontend object for fluent interface
269
	 */
270
	public function deleteProduct( int $position ) : Iface
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()->translate( 'controller/frontend', 'Basket item at position "%1$d" cannot be deleted manually' );
277
			throw new \Aimeos\Controller\Frontend\Basket\Exception( sprintf( $msg, $position ) );
278
		}
279
280
		$this->baskets[$this->type] = $this->get()->deleteProduct( $position );
281
		return $this->save();
282
	}
283
284
285
	/**
286
	 * Edits the quantity of a product item in the basket.
287
	 *
288
	 * @param int $position Position number (key) of the order product item
289
	 * @param float $quantity New quantiy of the product item
290
	 * @return \Aimeos\Controller\Frontend\Basket\Iface Basket frontend object for fluent interface
291
	 */
292
	public function updateProduct( int $position, float $quantity ) : Iface
293
	{
294
		$context = $this->getContext();
295
		$orderProduct = $this->get()->getProduct( $position );
296
297
		if( $orderProduct->getFlags() & \Aimeos\MShop\Order\Item\Base\Product\Base::FLAG_IMMUTABLE )
298
		{
299
			$msg = $context->translate( 'controller/frontend', 'Basket item at position "%1$d" cannot be changed' );
300
			throw new \Aimeos\Controller\Frontend\Basket\Exception( sprintf( $msg, $position ) );
301
		}
302
303
		$manager = \Aimeos\MShop::create( $context, 'product' );
304
		$product = $manager->find( $orderProduct->getProductCode(), ['price', 'text'], true );
305
		$product = \Aimeos\MShop::create( $context, 'rule' )->apply( $product, 'catalog' );
306
307
		$quantity = $this->checkQuantity( $product, $quantity );
308
		$price = $this->calcPrice( $orderProduct, $product->getRefItems( 'price', 'default' ), $quantity );
309
		$orderProduct = $orderProduct->setQuantity( $quantity )->setPrice( $price );
310
311
		$this->baskets[$this->type] = $this->get()->addProduct( $orderProduct, $position );
312
		return $this->save();
313
	}
314
315
316
	/**
317
	 * Adds the given coupon code and updates the basket.
318
	 *
319
	 * @param string $code Coupon code entered by the user
320
	 * @return \Aimeos\Controller\Frontend\Basket\Iface Basket frontend object for fluent interface
321
	 * @throws \Aimeos\Controller\Frontend\Basket\Exception if the coupon code is invalid or not allowed
322
	 */
323
	public function addCoupon( string $code ) : Iface
324
	{
325
		$context = $this->getContext();
326
327
		/** controller/frontend/basket/coupon/allowed
328
		 * Number of coupon codes a customer is allowed to enter
329
		 *
330
		 * This configuration option enables shop owners to limit the number of coupon
331
		 * codes that can be added by a customer to his current basket. By default, only
332
		 * one coupon code is allowed per order.
333
		 *
334
		 * Coupon codes are valid until a payed order is placed by the customer. The
335
		 * "count" of the codes is decreased afterwards. If codes are not personalized
336
		 * the codes can be reused in the next order until their "count" reaches zero.
337
		 *
338
		 * @param integer Positive number of coupon codes including zero
339
		 * @since 2017.08
340
		 * @category User
341
		 * @category Developer
342
		 */
343
		$allowed = $context->getConfig()->get( 'controller/frontend/basket/coupon/allowed', 1 );
344
345
		if( $allowed <= count( $this->get()->getCoupons() ) )
346
		{
347
			$msg = $context->translate( 'controller/frontend', 'Number of coupon codes exceeds the limit' );
348
			throw new \Aimeos\Controller\Frontend\Basket\Exception( $msg );
349
		}
350
351
		$this->baskets[$this->type] = $this->get()->addCoupon( $code );
352
		return $this->save();
353
	}
354
355
356
	/**
357
	 * Removes the given coupon code and its effects from the basket.
358
	 *
359
	 * @param string $code Coupon code entered by the user
360
	 * @return \Aimeos\Controller\Frontend\Basket\Iface Basket frontend object for fluent interface
361
	 * @throws \Aimeos\Controller\Frontend\Basket\Exception if the coupon code is invalid
362
	 */
363
	public function deleteCoupon( string $code ) : Iface
364
	{
365
		$this->baskets[$this->type] = $this->get()->deleteCoupon( $code );
366
		return $this->save();
367
	}
368
369
370
	/**
371
	 * Adds an address of the customer to the basket
372
	 *
373
	 * @param string $type Address type code like 'payment' or 'delivery'
374
	 * @param array $values Associative list of key/value pairs with address details
375
	 * @param int|null $position Position number (key) of the order address item
376
	 * @return \Aimeos\Controller\Frontend\Basket\Iface Basket frontend object for fluent interface
377
	 */
378
	public function addAddress( string $type, array $values = [], int $position = null ) : Iface
379
	{
380
		foreach( $values as $key => $value )
381
		{
382
			if( is_scalar( $value ) ) {
383
				$values[$key] = strip_tags( (string) $value ); // prevent XSS
384
			}
385
		}
386
387
		$context = $this->getContext();
388
		$address = \Aimeos\MShop::create( $context, 'order/base/address' )->create()->fromArray( $values );
389
		$address->set( 'nostore', ( $values['nostore'] ?? false ) ? true : false );
390
391
		$this->baskets[$this->type] = $this->get()->addAddress( $address, $type, $position );
392
		return $this->save();
393
	}
394
395
396
	/**
397
	 * Removes the address of the given type and position if available
398
	 *
399
	 * @param string $type Address type code like 'payment' or 'delivery'
400
	 * @param int|null $position Position of the address in the list to overwrite
401
	 * @return \Aimeos\Controller\Frontend\Basket\Iface Basket frontend object for fluent interface
402
	 */
403
	public function deleteAddress( string $type, int $position = null ) : Iface
404
	{
405
		$this->baskets[$this->type] = $this->get()->deleteAddress( $type, $position );
406
		return $this->save();
407
	}
408
409
410
	/**
411
	 * Adds the delivery/payment service including the given configuration
412
	 *
413
	 * @param \Aimeos\MShop\Service\Item\Iface $service Service item selected by the customer
414
	 * @param array $config Associative list of key/value pairs with the options selected by the customer
415
	 * @param int|null $position Position of the address in the list to overwrite
416
	 * @return \Aimeos\Controller\Frontend\Basket\Iface Basket frontend object for fluent interface
417
	 * @throws \Aimeos\Controller\Frontend\Basket\Exception If given service attributes are invalid
418
	 */
419
	public function addService( \Aimeos\MShop\Service\Item\Iface $service, array $config = [], int $position = null ) : Iface
420
	{
421
		$context = $this->getContext();
422
		$manager = \Aimeos\MShop::create( $context, 'service' );
423
424
		$provider = $manager->getProvider( $service, $service->getType() );
425
		$errors = $provider->checkConfigFE( $config );
426
		$unknown = array_diff_key( $config, $errors );
427
428
		if( count( $unknown ) > 0 )
429
		{
430
			$msg = $context->translate( 'controller/frontend', 'Unknown service attributes' );
431
			throw new \Aimeos\Controller\Frontend\Basket\Exception( $msg, -1, null, $unknown );
432
		}
433
434
		if( count( array_filter( $errors ) ) > 0 )
435
		{
436
			$msg = $context->translate( 'controller/frontend', 'Invalid service attributes' );
437
			throw new \Aimeos\Controller\Frontend\Basket\Exception( $msg, -1, null, array_filter( $errors ) );
438
		}
439
440
		// remove service rebate of original price
441
		$price = $provider->calcPrice( $this->get() )->setRebate( '0.00' );
442
443
		$orderBaseServiceManager = \Aimeos\MShop::create( $context, 'order/base/service' );
444
445
		$orderServiceItem = $orderBaseServiceManager->create()->copyFrom( $service )->setPrice( $price );
446
		$orderServiceItem = $provider->setConfigFE( $orderServiceItem, $config );
447
448
		$this->baskets[$this->type] = $this->get()->addService( $orderServiceItem, $service->getType(), $position );
0 ignored issues
show
Bug introduced by
It seems like $service->getType() can also be of type null; however, parameter $type of Aimeos\MShop\Order\Item\Base\Iface::addService() 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

448
		$this->baskets[$this->type] = $this->get()->addService( $orderServiceItem, /** @scrutinizer ignore-type */ $service->getType(), $position );
Loading history...
449
		return $this->save();
450
	}
451
452
453
	/**
454
	 * Removes the delivery or payment service items from the basket
455
	 *
456
	 * @param string $type Service type code like 'payment' or 'delivery'
457
	 * @param int|null $position Position of the address in the list to overwrite
458
	 * @return \Aimeos\Controller\Frontend\Basket\Iface Basket frontend object for fluent interface
459
	 */
460
	public function deleteService( string $type, int $position = null ) : Iface
461
	{
462
		$this->baskets[$this->type] = $this->get()->deleteService( $type, $position );
463
		return $this->save();
464
	}
465
466
467
	/**
468
	 * Returns the manager used by the controller
469
	 *
470
	 * @return \Aimeos\MShop\Common\Manager\Iface Manager object
471
	 */
472
	protected function getManager() : \Aimeos\MShop\Common\Manager\Iface
473
	{
474
		return $this->manager;
475
	}
476
}
477