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

Standard::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
eloc 2
c 2
b 0
f 0
dl 0
loc 5
rs 10
cc 1
nc 1
nop 1
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