Passed
Push — master ( 62fc03...bcc204 )
by Aimeos
04:26
created

Standard::createItemBase()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 2
c 0
b 0
f 0
dl 0
loc 6
rs 10
cc 1
nc 1
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, https://opensource.org/licenses/LGPL-3.0
5
 * @copyright Metaways Infosystems GmbH, 2011
6
 * @copyright Aimeos (aimeos.org), 2015-2024
7
 * @package MShop
8
 * @subpackage Order
9
 */
10
11
12
namespace Aimeos\MShop\Order\Manager;
13
14
15
/**
16
 * Default order manager implementation.
17
 *
18
 * @package MShop
19
 * @subpackage Order
20
 */
21
class Standard extends Base
22
	implements \Aimeos\MShop\Order\Manager\Iface, \Aimeos\MShop\Common\Manager\Factory\Iface
23
{
24
	use Session;
25
	use Update;
26
27
28
	/** mshop/order/manager/name
29
	 * Class name of the used order manager implementation
30
	 *
31
	 * Each default manager can be replace by an alternative imlementation.
32
	 * To use this implementation, you have to set the last part of the class
33
	 * name as configuration value so the manager factory knows which class it
34
	 * has to instantiate.
35
	 *
36
	 * For example, if the name of the default class is
37
	 *
38
	 *  \Aimeos\MShop\Order\Manager\Standard
39
	 *
40
	 * and you want to replace it with your own version named
41
	 *
42
	 *  \Aimeos\MShop\Order\Manager\Mymanager
43
	 *
44
	 * then you have to set the this configuration option:
45
	 *
46
	 *  mshop/order/manager/name = Mymanager
47
	 *
48
	 * The value is the last part of your own class name and it's case sensitive,
49
	 * so take care that the configuration value is exactly named like the last
50
	 * part of the class name.
51
	 *
52
	 * The allowed characters of the class name are A-Z, a-z and 0-9. No other
53
	 * characters are possible! You should always start the last part of the class
54
	 * name with an upper case character and continue only with lower case characters
55
	 * or numbers. Avoid chamel case names like "MyManager"!
56
	 *
57
	 * @param string Last part of the class name
58
	 * @since 2014.03
59
	 */
60
61
	/** mshop/order/manager/decorators/excludes
62
	 * Excludes decorators added by the "common" option from the order manager
63
	 *
64
	 * Decorators extend the functionality of a class by adding new aspects
65
	 * (e.g. log what is currently done), executing the methods of the underlying
66
	 * class only in certain conditions (e.g. only for logged in users) or
67
	 * modify what is returned to the caller.
68
	 *
69
	 * This option allows you to remove a decorator added via
70
	 * "mshop/common/manager/decorators/default" before they are wrapped
71
	 * around the order manager.
72
	 *
73
	 *  mshop/order/manager/decorators/excludes = array( 'decorator1' )
74
	 *
75
	 * This would remove the decorator named "decorator1" from the list of
76
	 * common decorators ("\Aimeos\MShop\Common\Manager\Decorator\*") added via
77
	 * "mshop/common/manager/decorators/default" for the order manager.
78
	 *
79
	 * @param array List of decorator names
80
	 * @since 2014.03
81
	 * @see mshop/common/manager/decorators/default
82
	 * @see mshop/order/manager/decorators/global
83
	 * @see mshop/order/manager/decorators/local
84
	 */
85
86
	/** mshop/order/manager/decorators/global
87
	 * Adds a list of globally available decorators only to the order manager
88
	 *
89
	 * Decorators extend the functionality of a class by adding new aspects
90
	 * (e.g. log what is currently done), executing the methods of the underlying
91
	 * class only in certain conditions (e.g. only for logged in users) or
92
	 * modify what is returned to the caller.
93
	 *
94
	 * This option allows you to wrap global decorators
95
	 * ("\Aimeos\MShop\Common\Manager\Decorator\*") around the order manager.
96
	 *
97
	 *  mshop/order/manager/decorators/global = array( 'decorator1' )
98
	 *
99
	 * This would add the decorator named "decorator1" defined by
100
	 * "\Aimeos\MShop\Common\Manager\Decorator\Decorator1" only to the order
101
	 * manager.
102
	 *
103
	 * @param array List of decorator names
104
	 * @since 2014.03
105
	 * @see mshop/common/manager/decorators/default
106
	 * @see mshop/order/manager/decorators/excludes
107
	 * @see mshop/order/manager/decorators/local
108
	 */
109
110
	/** mshop/order/manager/decorators/local
111
	 * Adds a list of local decorators only to the order manager
112
	 *
113
	 * Decorators extend the functionality of a class by adding new aspects
114
	 * (e.g. log what is currently done), executing the methods of the underlying
115
	 * class only in certain conditions (e.g. only for logged in users) or
116
	 * modify what is returned to the caller.
117
	 *
118
	 * This option allows you to wrap local decorators
119
	 * ("\Aimeos\MShop\Order\Manager\Decorator\*") around the order manager.
120
	 *
121
	 *  mshop/order/manager/decorators/local = array( 'decorator2' )
122
	 *
123
	 * This would add the decorator named "decorator2" defined by
124
	 * "\Aimeos\MShop\Order\Manager\Decorator\Decorator2" only to the order
125
	 * manager.
126
	 *
127
	 * @param array List of decorator names
128
	 * @since 2014.03
129
	 * @see mshop/common/manager/decorators/default
130
	 * @see mshop/order/manager/decorators/excludes
131
	 * @see mshop/order/manager/decorators/global
132
	 */
133
134
135
	private array $searchConfig = array(
136
		'order.id' => array(
137
			'code' => 'order.id',
138
			'internalcode' => 'mord."id"',
139
			'label' => 'Invoice ID',
140
			'type' => 'int',
141
		),
142
		'order.siteid' => array(
143
			'code' => 'order.siteid',
144
			'internalcode' => 'mord."siteid"',
145
			'label' => 'Invoice site ID',
146
			'public' => false,
147
		),
148
		'order.invoiceno' => array(
149
			'code' => 'order.invoiceno',
150
			'internalcode' => 'mord."invoiceno"',
151
			'label' => 'Invoice number',
152
		),
153
		'order.relatedid' => array(
154
			'code' => 'order.relatedid',
155
			'internalcode' => 'mord."relatedid"',
156
			'label' => 'Related invoice ID',
157
		),
158
		'order.channel' => array(
159
			'code' => 'order.channel',
160
			'internalcode' => 'mord."channel"',
161
			'label' => 'Order channel',
162
		),
163
		'order.datepayment' => array(
164
			'code' => 'order.datepayment',
165
			'internalcode' => 'mord."datepayment"',
166
			'label' => 'Purchase date',
167
			'type' => 'datetime',
168
		),
169
		'order.datedelivery' => array(
170
			'code' => 'order.datedelivery',
171
			'internalcode' => 'mord."datedelivery"',
172
			'label' => 'Delivery date',
173
			'type' => 'datetime',
174
		),
175
		'order.statusdelivery' => array(
176
			'code' => 'order.statusdelivery',
177
			'internalcode' => 'mord."statusdelivery"',
178
			'label' => 'Delivery status',
179
			'type' => 'int',
180
		),
181
		'order.statuspayment' => array(
182
			'code' => 'order.statuspayment',
183
			'internalcode' => 'mord."statuspayment"',
184
			'label' => 'Payment status',
185
			'type' => 'int',
186
		),
187
		'order.sitecode' => array(
188
			'code' => 'order.sitecode',
189
			'internalcode' => 'mord."sitecode"',
190
			'label' => 'Order site code',
191
			'public' => false,
192
		),
193
		'order.customerid' => array(
194
			'code' => 'order.customerid',
195
			'internalcode' => 'mord."customerid"',
196
			'label' => 'Order customer ID',
197
		),
198
		'order.customerref' => array(
199
			'code' => 'order.customerref',
200
			'internalcode' => 'mord."customerref"',
201
			'label' => 'Order customer reference',
202
		),
203
		'order.languageid' => array(
204
			'code' => 'order.languageid',
205
			'internalcode' => 'mord."langid"',
206
			'label' => 'Order language code',
207
		),
208
		'order.currencyid' => array(
209
			'code' => 'order.currencyid',
210
			'internalcode' => 'mord."currencyid"',
211
			'label' => 'Order currencyid code',
212
		),
213
		'order.price' => array(
214
			'code' => 'order.price',
215
			'internalcode' => 'mord."price"',
216
			'label' => 'Order price amount',
217
		),
218
		'order.costs' => array(
219
			'code' => 'order.costs',
220
			'internalcode' => 'mord."costs"',
221
			'label' => 'Order shipping amount',
222
		),
223
		'order.rebate' => array(
224
			'code' => 'order.rebate',
225
			'internalcode' => 'mord."rebate"',
226
			'label' => 'Order rebate amount',
227
		),
228
		'order.taxvalue' => array(
229
			'code' => 'order.taxvalue',
230
			'internalcode' => 'mord."tax"',
231
			'label' => 'Order tax amount',
232
		),
233
		'order.taxflag' => array(
234
			'code' => 'order.taxflag',
235
			'internalcode' => 'mord."taxflag"',
236
			'label' => 'Order tax flag (0=net, 1=gross)',
237
		),
238
		'order.comment' => array(
239
			'code' => 'order.comment',
240
			'internalcode' => 'mord."comment"',
241
			'label' => 'Order comment',
242
		),
243
		'order.cdate' => array(
244
			'code' => 'order.cdate',
245
			'internalcode' => 'mord."cdate"',
246
			'label' => 'Create date',
247
		),
248
		'order.cmonth' => array(
249
			'code' => 'order.cmonth',
250
			'internalcode' => 'mord."cmonth"',
251
			'label' => 'Create month',
252
		),
253
		'order.cweek' => array(
254
			'code' => 'order.cweek',
255
			'internalcode' => 'mord."cweek"',
256
			'label' => 'Create week',
257
		),
258
		'order.cwday' => array(
259
			'code' => 'order.cwday',
260
			'internalcode' => 'mord."cwday"',
261
			'label' => 'Create weekday',
262
		),
263
		'order.chour' => array(
264
			'code' => 'order.chour',
265
			'internalcode' => 'mord."chour"',
266
			'label' => 'Create hour',
267
		),
268
		'order.ctime' => array(
269
			'code' => 'order.ctime',
270
			'internalcode' => 'mord."ctime"',
271
			'label' => 'Create date/time',
272
			'type' => 'datetime',
273
			'public' => false,
274
		),
275
		'order.mtime' => array(
276
			'code' => 'order.mtime',
277
			'internalcode' => 'mord."mtime"',
278
			'label' => 'Modify date/time',
279
			'type' => 'datetime',
280
			'public' => false,
281
		),
282
		'order.editor' => array(
283
			'code' => 'order.editor',
284
			'internalcode' => 'mord."editor"',
285
			'label' => 'Editor',
286
			'public' => false,
287
		),
288
		'order:status' => array(
289
			'code' => 'order:status()',
290
			'internalcode' => '( SELECT COUNT(mordst_cs."parentid")
291
				FROM "mshop_order_status" AS mordst_cs
292
				WHERE mord."id" = mordst_cs."parentid" AND :site
293
				AND mordst_cs."type" = $1 AND mordst_cs."value" IN ( $2 ) )',
294
			'label' => 'Number of order status items, parameter(<type>,<value>)',
295
			'type' => 'int',
296
			'public' => false,
297
		),
298
	);
299
300
301
	/**
302
	 * Creates the manager that will use the given context object.
303
	 *
304
	 * @param \Aimeos\MShop\ContextIface $context Context object with required objects
305
	 */
306
	public function __construct( \Aimeos\MShop\ContextIface $context )
307
	{
308
		parent::__construct( $context );
309
310
		/** mshop/order/manager/resource
311
		 * Name of the database connection resource to use
312
		 *
313
		 * You can configure a different database connection for each data domain
314
		 * and if no such connection name exists, the "db" connection will be used.
315
		 * It's also possible to use the same database connection for different
316
		 * data domains by configuring the same connection name using this setting.
317
		 *
318
		 * @param string Database connection name
319
		 * @since 2023.04
320
		 */
321
		$this->setResourceName( $context->config()->get( 'mshop/order/manager/resource', 'db-order' ) );
322
323
		$level = \Aimeos\MShop\Locale\Manager\Base::SITE_ALL;
324
		$level = $context->config()->get( 'mshop/order/manager/sitemode', $level );
325
326
		$name = 'order:status';
327
		$expr = $this->siteString( 'mordst_cs."siteid"', $level );
328
		$this->searchConfig[$name]['internalcode'] = str_replace( ':site', $expr, $this->searchConfig[$name]['internalcode'] );
329
	}
330
331
332
	/**
333
	 * Counts the number items that are available for the values of the given key.
334
	 *
335
	 * @param \Aimeos\Base\Criteria\Iface $search Search criteria
336
	 * @param array|string $key Search key or list of key to aggregate items for
337
	 * @param string|null $value Search key for aggregating the value column
338
	 * @param string|null $type Type of the aggregation, empty string for count or "sum" or "avg" (average)
339
	 * @return \Aimeos\Map List of the search keys as key and the number of counted items as value
340
	 */
341
	public function aggregate( \Aimeos\Base\Criteria\Iface $search, $key, string $value = null, string $type = null ) : \Aimeos\Map
342
	{
343
		/** mshop/order/manager/aggregate/mysql
344
		 * Counts the number of records grouped by the values in the key column and matched by the given criteria
345
		 *
346
		 * @see mshop/order/manager/aggregate/ansi
347
		 */
348
349
		/** mshop/order/manager/aggregate/ansi
350
		 * Counts the number of records grouped by the values in the key column and matched by the given criteria
351
		 *
352
		 * Groups all records by the values in the key column and counts their
353
		 * occurence. The matched records can be limited by the given criteria
354
		 * from the order database. The records must be from one of the sites
355
		 * that are configured via the context item. If the current site is part
356
		 * of a tree of sites, the statement can count all records from the
357
		 * current site and the complete sub-tree of sites.
358
		 *
359
		 * As the records can normally be limited by criteria from sub-managers,
360
		 * their tables must be joined in the SQL context. This is done by
361
		 * using the "internaldeps" property from the definition of the ID
362
		 * column of the sub-managers. These internal dependencies specify
363
		 * the JOIN between the tables and the used columns for joining. The
364
		 * ":joins" placeholder is then replaced by the JOIN strings from
365
		 * the sub-managers.
366
		 *
367
		 * To limit the records matched, conditions can be added to the given
368
		 * criteria object. It can contain comparisons like column names that
369
		 * must match specific values which can be combined by AND, OR or NOT
370
		 * operators. The resulting string of SQL conditions replaces the
371
		 * ":cond" placeholder before the statement is sent to the database
372
		 * server.
373
		 *
374
		 * This statement doesn't return any records. Instead, it returns pairs
375
		 * of the different values found in the key column together with the
376
		 * number of records that have been found for that key values.
377
		 *
378
		 * The SQL statement should conform to the ANSI standard to be
379
		 * compatible with most relational database systems. This also
380
		 * includes using double quotes for table and column names.
381
		 *
382
		 * @param string SQL statement for aggregating order items
383
		 * @since 2014.09
384
		 * @see mshop/order/manager/insert/ansi
385
		 * @see mshop/order/manager/update/ansi
386
		 * @see mshop/order/manager/newid/ansi
387
		 * @see mshop/order/manager/delete/ansi
388
		 * @see mshop/order/manager/search/ansi
389
		 * @see mshop/order/manager/count/ansi
390
		 */
391
392
		/** mshop/order/manager/aggregateavg/mysql
393
		 * Computes the average of all values grouped by the key column and matched by the given criteria
394
		 *
395
		 * @param string SQL statement for aggregating the order items and computing the average value
396
		 * @since 2017.10
397
		 * @see mshop/order/manager/aggregateavg/ansi
398
		 * @see mshop/order/manager/aggregate/mysql
399
		 */
400
401
		/** mshop/order/manager/aggregateavg/ansi
402
		 * Computes the average of all values grouped by the key column and matched by the given criteria
403
		 *
404
		 * @param string SQL statement for aggregating the order items and computing the average value
405
		 * @since 2017.10
406
		 * @see mshop/order/manager/aggregate/ansi
407
		 */
408
409
		/** mshop/order/manager/aggregatesum/mysql
410
		 * Computes the sum of all values grouped by the key column and matched by the given criteria
411
		 *
412
		 * @param string SQL statement for aggregating the order items and computing the sum
413
		 * @since 2017.10
414
		 * @see mshop/order/manager/aggregatesum/ansi
415
		 * @see mshop/order/manager/aggregate/mysql
416
		 */
417
418
		/** mshop/order/manager/aggregatesum/ansi
419
		 * Computes the sum of all values grouped by the key column and matched by the given criteria
420
		 *
421
		 * @param string SQL statement for aggregating the order items and computing the sum
422
		 * @since 2017.10
423
		 * @see mshop/order/manager/aggregate/ansi
424
		 */
425
426
		$cfgkey = 'mshop/order/manager/aggregate';
427
		return $this->aggregateBase( $search, $key, $cfgkey, ['order'], $value, $type );
428
	}
429
430
431
	/**
432
	 * Removes old entries from the storage.
433
	 *
434
	 * @param iterable $siteids List of IDs for sites whose entries should be deleted
435
	 * @return \Aimeos\MShop\Order\Manager\Iface Manager object for chaining method calls
436
	 */
437
	public function clear( iterable $siteids ) : \Aimeos\MShop\Common\Manager\Iface
438
	{
439
		$path = 'mshop/order/manager/submanagers';
440
		$default = ['address', 'coupon', 'product', 'service', 'status'];
441
442
		foreach( $this->context()->config()->get( $path, $default ) as $domain ) {
443
			$this->object()->getSubManager( $domain )->clear( $siteids );
444
		}
445
446
		return $this->clearBase( $siteids, 'mshop/order/manager/delete' );
447
	}
448
449
450
	/**
451
	 * Creates a new empty item instance
452
	 *
453
	 * @param array $values Values the item should be initialized with
454
	 * @return \Aimeos\MShop\Order\Item\Iface New order item object
455
	 */
456
	public function create( array $values = [] ) : \Aimeos\MShop\Common\Item\Iface
457
	{
458
		$context = $this->context();
459
		$locale = $context->locale();
460
461
		$values['.locale'] = $values['.locale'] ?? $locale;
462
		$values['.price'] = $values['.price'] ?? \Aimeos\MShop::create( $context, 'price' )->create();
463
		$values['order.siteid'] = $values['order.siteid'] ?? $locale->getSiteId();
464
465
		$item = new \Aimeos\MShop\Order\Item\Standard( 'order.', $values );
466
		\Aimeos\MShop::create( $context, 'plugin' )->register( $item, 'order' );
467
468
		return $item;
469
	}
470
471
472
	/**
473
	 * Removes multiple items.
474
	 *
475
	 * @param \Aimeos\MShop\Common\Item\Iface[]|string[] $itemIds List of item objects or IDs of the items
476
	 * @return \Aimeos\MShop\Order\Manager\Iface Manager object for chaining method calls
477
	 */
478
	public function delete( $itemIds ) : \Aimeos\MShop\Common\Manager\Iface
479
	{
480
		/** mshop/order/manager/delete/mysql
481
		 * Deletes the items matched by the given IDs from the database
482
		 *
483
		 * @see mshop/order/manager/delete/ansi
484
		 */
485
486
		/** mshop/order/manager/delete/ansi
487
		 * Deletes the items matched by the given IDs from the database
488
		 *
489
		 * Removes the records specified by the given IDs from the order database.
490
		 * The records must be from the site that is configured via the
491
		 * context item.
492
		 *
493
		 * The ":cond" placeholder is replaced by the name of the ID column and
494
		 * the given ID or list of IDs while the site ID is bound to the question
495
		 * mark.
496
		 *
497
		 * The SQL statement should conform to the ANSI standard to be
498
		 * compatible with most relational database systems. This also
499
		 * includes using double quotes for table and column names.
500
		 *
501
		 * @param string SQL statement for deleting items
502
		 * @since 2014.03
503
		 * @see mshop/order/manager/insert/ansi
504
		 * @see mshop/order/manager/update/ansi
505
		 * @see mshop/order/manager/newid/ansi
506
		 * @see mshop/order/manager/search/ansi
507
		 * @see mshop/order/manager/count/ansi
508
		 */
509
		$path = 'mshop/order/manager/delete';
510
511
		return $this->deleteItemsBase( $itemIds, $path );
512
	}
513
514
515
	/**
516
	 * Creates a search critera object
517
	 *
518
	 * @param bool|null $default Add default criteria or NULL for relaxed default criteria
519
	 * @param bool $site TRUE to add site criteria to show orders with available products only
520
	 * @return \Aimeos\Base\Criteria\Iface New search criteria object
521
	 */
522
	public function filter( ?bool $default = false, bool $site = false ) : \Aimeos\Base\Criteria\Iface
523
	{
524
		$search = parent::filter( $default );
525
		$context = $this->context();
526
527
		if( $default !== false ) {
528
			$search->add( ['order.customerid' => $context->user()] );
529
		}
530
531
		if( $site === true )
532
		{
533
			$level = \Aimeos\MShop\Locale\Manager\Base::SITE_SUBTREE;
534
			$search->add( $this->siteCondition( 'order.product.siteid', $level ) );
535
		}
536
537
		return $search;
538
	}
539
540
541
	/**
542
	 * Returns an order invoice item built from database values.
543
	 *
544
	 * @param string $id Unique id of the order invoice
545
	 * @param string[] $ref List of domains to fetch list items and referenced items for
546
	 * @param bool|null $default Add default criteria or NULL for relaxed default criteria
547
	 * @return \Aimeos\MShop\Order\Item\Iface Returns order invoice item of the given id
548
	 * @throws \Aimeos\MShop\Order\Exception If item couldn't be found
549
	 */
550
	public function get( string $id, array $ref = [], ?bool $default = false ) : \Aimeos\MShop\Common\Item\Iface
551
	{
552
		return $this->getItemBase( 'order.id', $id, $ref, $default );
553
	}
554
555
556
	/**
557
	 * Returns the available manager types
558
	 *
559
	 * @param bool $withsub Return also the resource type of sub-managers if true
560
	 * @return string[] Type of the manager and submanagers, subtypes are separated by slashes
561
	 */
562
	public function getResourceType( bool $withsub = true ) : array
563
	{
564
		$path = 'mshop/order/manager/submanagers';
565
		$default = ['address', 'coupon', 'product', 'service', 'status'];
566
567
		return $this->getResourceTypeBase( 'order', $path, $default, $withsub );
568
	}
569
570
571
	/**
572
	 * Returns the attributes that can be used for searching.
573
	 *
574
	 * @param bool $withsub Return also attributes of sub-managers if true
575
	 * @return \Aimeos\Base\Criteria\Attribute\Iface[] List of search attribute items
576
	 */
577
	public function getSearchAttributes( bool $withsub = true ) : array
578
	{
579
		/** mshop/order/manager/submanagers
580
		 * List of manager names that can be instantiated by the order manager
581
		 *
582
		 * Managers provide a generic interface to the underlying storage.
583
		 * Each manager has or can have sub-managers caring about particular
584
		 * aspects. Each of these sub-managers can be instantiated by its
585
		 * parent manager using the getSubManager() method.
586
		 *
587
		 * The search keys from sub-managers can be normally used in the
588
		 * manager as well. It allows you to search for items of the manager
589
		 * using the search keys of the sub-managers to further limit the
590
		 * retrieved list of items.
591
		 *
592
		 * @param array List of sub-manager names
593
		 * @since 2014.03
594
		 */
595
		$path = 'mshop/order/manager/submanagers';
596
		$default = ['address', 'coupon', 'product', 'service', 'status'];
597
598
		return $this->getSearchAttributesBase( $this->searchConfig, $path, $default, $withsub );
599
	}
600
601
602
	/**
603
	 * Creates a one-time order in the storage from the given invoice object.
604
	 *
605
	 * @param \Aimeos\MShop\Order\Item\Iface $item Order item with necessary values
606
	 * @param bool $fetch True if the new ID should be returned in the item
607
	 * @return \Aimeos\MShop\Order\Item\Iface $item Updated item including the generated ID
608
	 */
609
	protected function saveItem( \Aimeos\MShop\Order\Item\Iface $item, bool $fetch = true ) : \Aimeos\MShop\Order\Item\Iface
610
	{
611
		if( !$item->isModified() ) {
612
			return $this->saveBasket( $item );
613
		}
614
615
		if( empty( $item->getInvoiceNumber() ) && $item->getStatusPayment() >= \Aimeos\MShop\Order\Item\Base::PAY_PENDING )
616
		{
617
			try {
618
				$item->setInvoiceNumber( $this->createInvoiceNumber( $item ) );
619
			} catch( \Exception $e ) { // redo on transaction deadlock
620
				$item->setInvoiceNumber( $this->createInvoiceNumber( $item ) );
621
			}
622
		}
623
624
		$context = $this->context();
625
		$conn = $context->db( $this->getResourceName() );
626
627
		$id = $item->getId();
628
		$columns = $this->object()->getSaveAttributes();
629
630
		if( $id === null )
631
		{
632
			/** mshop/order/manager/insert/mysql
633
			 * Inserts a new order record into the database table
634
			 *
635
			 * @see mshop/order/manager/insert/ansi
636
			 */
637
638
			/** mshop/order/manager/insert/ansi
639
			 * Inserts a new order record into the database table
640
			 *
641
			 * Items with no ID yet (i.e. the ID is NULL) will be created in
642
			 * the database and the newly created ID retrieved afterwards
643
			 * using the "newid" SQL statement.
644
			 *
645
			 * The SQL statement must be a string suitable for being used as
646
			 * prepared statement. It must include question marks for binding
647
			 * the values from the order item to the statement before they are
648
			 * sent to the database server. The number of question marks must
649
			 * be the same as the number of columns listed in the INSERT
650
			 * statement. The order of the columns must correspond to the
651
			 * order in the save() method, so the correct values are
652
			 * bound to the columns.
653
			 *
654
			 * The SQL statement should conform to the ANSI standard to be
655
			 * compatible with most relational database systems. This also
656
			 * includes using double quotes for table and column names.
657
			 *
658
			 * @param string SQL statement for inserting records
659
			 * @since 2014.03
660
			 * @see mshop/order/manager/update/ansi
661
			 * @see mshop/order/manager/newid/ansi
662
			 * @see mshop/order/manager/delete/ansi
663
			 * @see mshop/order/manager/search/ansi
664
			 * @see mshop/order/manager/count/ansi
665
			 */
666
			$path = 'mshop/order/manager/insert';
667
			$sql = $this->addSqlColumns( array_keys( $columns ), $this->getSqlConfig( $path ) );
0 ignored issues
show
Bug introduced by
It seems like $this->getSqlConfig($path) can also be of type array; however, parameter $sql of Aimeos\MShop\Common\Manager\Base::addSqlColumns() 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

667
			$sql = $this->addSqlColumns( array_keys( $columns ), /** @scrutinizer ignore-type */ $this->getSqlConfig( $path ) );
Loading history...
668
		}
669
		else
670
		{
671
			/** mshop/order/manager/update/mysql
672
			 * Updates an existing order record in the database
673
			 *
674
			 * @see mshop/order/manager/update/ansi
675
			 */
676
677
			/** mshop/order/manager/update/ansi
678
			 * Updates an existing order record in the database
679
			 *
680
			 * Items which already have an ID (i.e. the ID is not NULL) will
681
			 * be updated in the database.
682
			 *
683
			 * The SQL statement must be a string suitable for being used as
684
			 * prepared statement. It must include question marks for binding
685
			 * the values from the order item to the statement before they are
686
			 * sent to the database server. The order of the columns must
687
			 * correspond to the order in the save() method, so the
688
			 * correct values are bound to the columns.
689
			 *
690
			 * The SQL statement should conform to the ANSI standard to be
691
			 * compatible with most relational database systems. This also
692
			 * includes using double quotes for table and column names.
693
			 *
694
			 * @param string SQL statement for updating records
695
			 * @since 2014.03
696
			 * @see mshop/order/manager/insert/ansi
697
			 * @see mshop/order/manager/newid/ansi
698
			 * @see mshop/order/manager/delete/ansi
699
			 * @see mshop/order/manager/search/ansi
700
			 * @see mshop/order/manager/count/ansi
701
			 */
702
			$path = 'mshop/order/manager/update';
703
			$sql = $this->addSqlColumns( array_keys( $columns ), $this->getSqlConfig( $path ), false );
704
		}
705
706
		$idx = 1;
707
		$priceItem = $item->getPrice();
708
		$stmt = $this->getCachedStatement( $conn, $path, $sql );
709
710
		foreach( $columns as $name => $entry ) {
711
			$stmt->bind( $idx++, $item->get( $name ), \Aimeos\Base\Criteria\SQL::type( $entry->getType() ) );
712
		}
713
714
		$stmt->bind( $idx++, $item->getInvoiceNumber() );
715
		$stmt->bind( $idx++, $item->getChannel() );
716
		$stmt->bind( $idx++, $item->getDatePayment() );
717
		$stmt->bind( $idx++, $item->getDateDelivery() );
718
		$stmt->bind( $idx++, $item->getStatusDelivery(), \Aimeos\Base\DB\Statement\Base::PARAM_INT );
719
		$stmt->bind( $idx++, $item->getStatusPayment(), \Aimeos\Base\DB\Statement\Base::PARAM_INT );
720
		$stmt->bind( $idx++, $item->getRelatedId(), \Aimeos\Base\DB\Statement\Base::PARAM_STR );
721
		$stmt->bind( $idx++, $item->getCustomerId() );
722
		$stmt->bind( $idx++, $context->locale()->getSiteItem()->getCode() );
723
		$stmt->bind( $idx++, $item->locale()->getLanguageId() );
724
		$stmt->bind( $idx++, $priceItem->getCurrencyId() );
725
		$stmt->bind( $idx++, $priceItem->getValue() );
726
		$stmt->bind( $idx++, $priceItem->getCosts() );
727
		$stmt->bind( $idx++, $priceItem->getRebate() );
728
		$stmt->bind( $idx++, $priceItem->getTaxValue() );
729
		$stmt->bind( $idx++, $priceItem->getTaxFlag(), \Aimeos\Base\DB\Statement\Base::PARAM_INT );
730
		$stmt->bind( $idx++, $item->getCustomerReference() );
731
		$stmt->bind( $idx++, $item->getComment() );
732
		$stmt->bind( $idx++, $context->datetime() ); // mtime
733
		$stmt->bind( $idx++, $context->editor() );
734
735
		if( $id !== null ) {
736
			$stmt->bind( $idx++, $context->locale()->getSiteId() . '%' );
737
			$stmt->bind( $idx++, $id, \Aimeos\Base\DB\Statement\Base::PARAM_INT );
738
		} else {
739
			$date = date_create_from_format( 'Y-m-d H:i:s', $context->datetime() );
740
			$stmt->bind( $idx++, $this->siteId( $item->getSiteId(), \Aimeos\MShop\Locale\Manager\Base::SITE_SUBTREE ) );
741
			$stmt->bind( $idx++, $context->datetime() ); // ctime
742
			$stmt->bind( $idx++, $date->format( 'Y-m-d' ) ); // cdate
743
			$stmt->bind( $idx++, $date->format( 'Y-m' ) ); // cmonth
744
			$stmt->bind( $idx++, $date->format( 'Y-W' ) ); // cweek
745
			$stmt->bind( $idx++, $date->format( 'w' ) ); // cwday
746
			$stmt->bind( $idx++, $date->format( 'G' ) ); // chour
747
		}
748
749
		$stmt->execute()->finish();
750
751
		if( $id === null && $fetch === true )
752
		{
753
			/** mshop/order/manager/newid/mysql
754
			 * Retrieves the ID generated by the database when inserting a new record
755
			 *
756
			 * @see mshop/order/manager/newid/ansi
757
			 */
758
759
			/** mshop/order/manager/newid/ansi
760
			 * Retrieves the ID generated by the database when inserting a new record
761
			 *
762
			 * As soon as a new record is inserted into the database table,
763
			 * the database server generates a new and unique identifier for
764
			 * that record. This ID can be used for retrieving, updating and
765
			 * deleting that specific record from the table again.
766
			 *
767
			 * For MySQL:
768
			 *  SELECT LAST_INSERT_ID()
769
			 * For PostgreSQL:
770
			 *  SELECT currval('seq_mord_id')
771
			 * For SQL Server:
772
			 *  SELECT SCOPE_IDENTITY()
773
			 * For Oracle:
774
			 *  SELECT "seq_mord_id".CURRVAL FROM DUAL
775
			 *
776
			 * There's no way to retrive the new ID by a SQL statements that
777
			 * fits for most database servers as they implement their own
778
			 * specific way.
779
			 *
780
			 * @param string SQL statement for retrieving the last inserted record ID
781
			 * @since 2014.03
782
			 * @see mshop/order/manager/insert/ansi
783
			 * @see mshop/order/manager/update/ansi
784
			 * @see mshop/order/manager/delete/ansi
785
			 * @see mshop/order/manager/search/ansi
786
			 * @see mshop/order/manager/count/ansi
787
			 */
788
			$path = 'mshop/order/manager/newid';
789
			$id = $this->newId( $conn, $path );
790
		}
791
792
		$item->setId( $id );
793
794
		$this->addStatus( $item );
795
796
		return $this->saveBasket( $item );
797
	}
798
799
800
	/**
801
	 * Searches for orders based on the given criteria.
802
	 *
803
	 * @param \Aimeos\Base\Criteria\Iface $search Search criteria object
804
	 * @param string[] $ref List of domains to fetch list items and referenced items for
805
	 * @param int|null &$total Number of items that are available in total
806
	 * @return \Aimeos\Map List of items implementing \Aimeos\MShop\Order\Item\Iface with ids as keys
807
	 */
808
	public function search( \Aimeos\Base\Criteria\Iface $search, array $ref = [], int &$total = null ) : \Aimeos\Map
809
	{
810
		$context = $this->context();
811
		$conn = $context->db( $this->getResourceName() );
812
813
		$priceManager = \Aimeos\MShop::create( $context, 'price' );
814
		$localeManager = \Aimeos\MShop::create( $context, 'locale' );
815
816
		$map = $custItems = [];
0 ignored issues
show
Unused Code introduced by
The assignment to $custItems is dead and can be removed.
Loading history...
817
		$required = ['order'];
818
819
		/** mshop/order/manager/sitemode
820
		 * Mode how items from levels below or above in the site tree are handled
821
		 *
822
		 * By default, only items from the current site are fetched from the
823
		 * storage. If the ai-sites extension is installed, you can create a
824
		 * tree of sites. Then, this setting allows you to define for the
825
		 * whole order domain if items from parent sites are inherited,
826
		 * sites from child sites are aggregated or both.
827
		 *
828
		 * Available constants for the site mode are:
829
		 * * 0 = only items from the current site
830
		 * * 1 = inherit items from parent sites
831
		 * * 2 = aggregate items from child sites
832
		 * * 3 = inherit and aggregate items at the same time
833
		 *
834
		 * You also need to set the mode in the locale manager
835
		 * (mshop/locale/manager/sitelevel) to one of the constants.
836
		 * If you set it to the same value, it will work as described but you
837
		 * can also use different modes. For example, if inheritance and
838
		 * aggregation is configured the locale manager but only inheritance
839
		 * in the domain manager because aggregating items makes no sense in
840
		 * this domain, then items wil be only inherited. Thus, you have full
841
		 * control over inheritance and aggregation in each domain.
842
		 *
843
		 * @param int Constant from Aimeos\MShop\Locale\Manager\Base class
844
		 * @since 2018.01
845
		 * @see mshop/locale/manager/sitelevel
846
		 */
847
		$level = \Aimeos\MShop\Locale\Manager\Base::SITE_ALL;
848
		$level = $context->config()->get( 'mshop/order/manager/sitemode', $level );
849
850
		/** mshop/order/manager/search/mysql
851
		 * Retrieves the records matched by the given criteria in the database
852
		 *
853
		 * @see mshop/order/manager/search/ansi
854
		 */
855
856
		/** mshop/order/manager/search/ansi
857
		 * Retrieves the records matched by the given criteria in the database
858
		 *
859
		 * Fetches the records matched by the given criteria from the order
860
		 * database. The records must be from one of the sites that are
861
		 * configured via the context item. If the current site is part of
862
		 * a tree of sites, the SELECT statement can retrieve all records
863
		 * from the current site and the complete sub-tree of sites.
864
		 *
865
		 * As the records can normally be limited by criteria from sub-managers,
866
		 * their tables must be joined in the SQL context. This is done by
867
		 * using the "internaldeps" property from the definition of the ID
868
		 * column of the sub-managers. These internal dependencies specify
869
		 * the JOIN between the tables and the used columns for joining. The
870
		 * ":joins" placeholder is then replaced by the JOIN strings from
871
		 * the sub-managers.
872
		 *
873
		 * To limit the records matched, conditions can be added to the given
874
		 * criteria object. It can contain comparisons like column names that
875
		 * must match specific values which can be combined by AND, OR or NOT
876
		 * operators. The resulting string of SQL conditions replaces the
877
		 * ":cond" placeholder before the statement is sent to the database
878
		 * server.
879
		 *
880
		 * If the records that are retrieved should be ordered by one or more
881
		 * columns, the generated string of column / sort direction pairs
882
		 * replaces the ":order" placeholder. Columns of
883
		 * sub-managers can also be used for ordering the result set but then
884
		 * no index can be used.
885
		 *
886
		 * The number of returned records can be limited and can start at any
887
		 * number between the begining and the end of the result set. For that
888
		 * the ":size" and ":start" placeholders are replaced by the
889
		 * corresponding values from the criteria object. The default values
890
		 * are 0 for the start and 100 for the size value.
891
		 *
892
		 * The SQL statement should conform to the ANSI standard to be
893
		 * compatible with most relational database systems. This also
894
		 * includes using double quotes for table and column names.
895
		 *
896
		 * @param string SQL statement for searching items
897
		 * @since 2014.03
898
		 * @see mshop/order/manager/insert/ansi
899
		 * @see mshop/order/manager/update/ansi
900
		 * @see mshop/order/manager/newid/ansi
901
		 * @see mshop/order/manager/delete/ansi
902
		 * @see mshop/order/manager/count/ansi
903
		 */
904
		$cfgPathSearch = 'mshop/order/manager/search';
905
906
		/** mshop/order/manager/count/mysql
907
		 * Counts the number of records matched by the given criteria in the database
908
		 *
909
		 * @see mshop/order/manager/count/ansi
910
		 */
911
912
		/** mshop/order/manager/count/ansi
913
		 * Counts the number of records matched by the given criteria in the database
914
		 *
915
		 * Counts all records matched by the given criteria from the order
916
		 * database. The records must be from one of the sites that are
917
		 * configured via the context item. If the current site is part of
918
		 * a tree of sites, the statement can count all records from the
919
		 * current site and the complete sub-tree of sites.
920
		 *
921
		 * As the records can normally be limited by criteria from sub-managers,
922
		 * their tables must be joined in the SQL context. This is done by
923
		 * using the "internaldeps" property from the definition of the ID
924
		 * column of the sub-managers. These internal dependencies specify
925
		 * the JOIN between the tables and the used columns for joining. The
926
		 * ":joins" placeholder is then replaced by the JOIN strings from
927
		 * the sub-managers.
928
		 *
929
		 * To limit the records matched, conditions can be added to the given
930
		 * criteria object. It can contain comparisons like column names that
931
		 * must match specific values which can be combined by AND, OR or NOT
932
		 * operators. The resulting string of SQL conditions replaces the
933
		 * ":cond" placeholder before the statement is sent to the database
934
		 * server.
935
		 *
936
		 * Both, the strings for ":joins" and for ":cond" are the same as for
937
		 * the "search" SQL statement.
938
		 *
939
		 * Contrary to the "search" statement, it doesn't return any records
940
		 * but instead the number of records that have been found. As counting
941
		 * thousands of records can be a long running task, the maximum number
942
		 * of counted records is limited for performance reasons.
943
		 *
944
		 * The SQL statement should conform to the ANSI standard to be
945
		 * compatible with most relational database systems. This also
946
		 * includes using double quotes for table and column names.
947
		 *
948
		 * @param string SQL statement for counting items
949
		 * @since 2014.03
950
		 * @see mshop/order/manager/insert/ansi
951
		 * @see mshop/order/manager/update/ansi
952
		 * @see mshop/order/manager/newid/ansi
953
		 * @see mshop/order/manager/delete/ansi
954
		 * @see mshop/order/manager/search/ansi
955
		 */
956
		$cfgPathCount = 'mshop/order/manager/count';
957
958
		$results = $this->searchItemsBase( $conn, $search, $cfgPathSearch, $cfgPathCount,
959
			$required, $total, $level );
960
961
		try
962
		{
963
			while( $row = $results->fetch() )
964
			{
965
				$row['.price'] = $priceManager->create( [
966
					'price.currencyid' => $row['order.currencyid'],
967
					'price.value' => $row['order.price'],
968
					'price.costs' => $row['order.costs'],
969
					'price.rebate' => $row['order.rebate'],
970
					'price.taxflag' => $row['order.taxflag'],
971
					'price.taxvalue' => $row['order.taxvalue'],
972
					'price.siteid' => $row['order.siteid'],
973
				] );
974
975
				// you may need the site object! take care!
976
				$row['.locale'] = $localeManager->create( [
977
					'locale.currencyid' => $row['order.currencyid'],
978
					'locale.languageid' => $row['order.languageid'],
979
					'locale.siteid' => $row['order.siteid'],
980
				] );
981
982
				$map[$row['order.id']] = $row;
983
			}
984
		}
985
		catch( \Exception $e )
986
		{
987
			$results->finish();
988
			throw $e;
989
		}
990
991
		return $this->buildItems( $map, $ref );
992
	}
993
994
995
	/**
996
	 * Returns a new manager for order extensions
997
	 *
998
	 * @param string $manager Name of the sub manager type in lower case
999
	 * @param string|null $name Name of the implementation, will be from configuration (or Default) if null
1000
	 * @return \Aimeos\MShop\Common\Manager\Iface Manager for different extensions, e.g base, etc.
1001
	 */
1002
	public function getSubManager( string $manager, string $name = null ) : \Aimeos\MShop\Common\Manager\Iface
1003
	{
1004
		return $this->getSubManagerBase( 'order', $manager, $name );
1005
	}
1006
1007
1008
	/**
1009
	 * Adds the new payment and delivery values to the order status log.
1010
	 *
1011
	 * @param \Aimeos\MShop\Order\Item\Iface $item Order item object
1012
	 */
1013
	protected function addStatus( \Aimeos\MShop\Order\Item\Iface $item )
1014
	{
1015
		$statusManager = \Aimeos\MShop::create( $this->context(), 'order/status' );
1016
1017
		$statusItem = $statusManager->create();
1018
		$statusItem->setParentId( $item->getId() );
1019
1020
		if( ( $status = $item->get( '.statuspayment' ) ) !== null && $status != $item->getStatusPayment() )
1021
		{
1022
			$statusItem->setId( null )->setValue( $item->getStatusPayment() )
1023
				->setType( \Aimeos\MShop\Order\Item\Status\Base::STATUS_PAYMENT );
1024
1025
			$statusManager->save( $statusItem, false );
1026
		}
1027
1028
		if( ( $status = $item->get( '.statusdelivery' ) ) !== null && $status != $item->getStatusDelivery() )
1029
		{
1030
			$statusItem->setId( null )->setValue( $item->getStatusDelivery() )
1031
				->setType( \Aimeos\MShop\Order\Item\Status\Base::STATUS_DELIVERY );
1032
1033
			$statusManager->save( $statusItem, false );
1034
		}
1035
	}
1036
1037
1038
	/**
1039
	 * Creates the order base item objects from the map and adds the referenced items
1040
	 *
1041
	 * @param array $map Associative list of order base IDs as keys and list of price/locale/row as values
1042
	 * @param string[] $ref Domain items that should be added as well, e.g.
1043
	 *	"order/address", "order/coupon", "order/product", "order/service"
1044
	 * @return \Aimeos\Map List of items implementing \Aimeos\MShop\Order\Item\Iface with IDs as keys
1045
	 */
1046
	protected function buildItems( array $map, array $ref ) : \Aimeos\Map
1047
	{
1048
		$ids = array_keys( $map );
1049
		$items = $addresses = $customers = $coupons = $products = $services = [];
1050
1051
		if( $this->hasRef( $ref, 'customer' ) && !( $cids = map( $map )->col( 'order.customerid' )->filter() )->empty() )
1052
		{
1053
			$manager = \Aimeos\MShop::create( $this->context(), 'customer' );
1054
			$search = $manager->filter()->slice( 0, 0x7fffffff )->add( ['customer.id' => $cids] );
1055
			$customers = $manager->search( $search, $ref );
1056
		}
1057
1058
		if( $this->hasRef( $ref, 'order/address' ) ) {
1059
			$addresses = $this->getAddresses( $ids, $ref );
1060
		}
1061
1062
		if( $this->hasRef( $ref, 'order/product' ) || $this->hasRef( $ref, 'order/coupon' ) ) {
1063
			$products = $this->getProducts( $ids, $ref );
1064
		}
1065
1066
		if( $this->hasRef( $ref, 'order/coupon' ) ) {
1067
			$coupons = $this->getCoupons( $ids, $ref );
1068
		}
1069
1070
		if( $this->hasRef( $ref, 'order/service' ) ) {
1071
			$services = $this->getServices( $ids, $ref );
1072
		}
1073
1074
		foreach( $map as $id => $row )
1075
		{
1076
			$row['.customer'] = $customers[$row['order.customerid']] ?? null;
1077
			$row['.addresses'] = $addresses[$id] ?? [];
1078
			$row['.coupons'] = $coupons[$id] ?? [];
1079
			$row['.products'] = $products[$id] ?? [];
1080
			$row['.services'] = $services[$id] ?? [];
1081
1082
			if( $item = $this->applyFilter( $item = $this->create( $row ) ) ) {
1083
				$items[$id] = $item;
1084
			}
1085
		}
1086
1087
		return map( $items );
1088
	}
1089
1090
1091
	/**
1092
	 * Creates a new invoice number for the passed order and site.
1093
	 *
1094
	 * @param \Aimeos\MShop\Order\Item\Iface $item Order item with necessary values
1095
	 * @return string Unique invoice number for the current site
1096
	 */
1097
	public function createInvoiceNumber( \Aimeos\MShop\Order\Item\Iface $item ) : string
1098
	{
1099
		$context = $this->context();
1100
		$siteId = $context->locale()->getSiteId();
1101
		$conn = $context->db( 'db-locale', true );
1102
1103
		try
1104
		{
1105
			$conn->query( 'SET TRANSACTION ISOLATION LEVEL SERIALIZABLE' )->finish();
1106
			$conn->query( 'START TRANSACTION' )->finish();
1107
1108
			$result = $conn->query( 'SELECT "invoiceno" FROM "mshop_locale_site" where "siteid" = ?', [$siteId] );
1109
			$row = $result->fetch();
1110
			$result->finish();
1111
1112
			$conn->create( 'UPDATE "mshop_locale_site" SET "invoiceno" = "invoiceno" + 1 WHERE "siteid" = ?' )
1113
				->bind( 1, $siteId )->execute()->finish();
1114
1115
			$conn->query( 'COMMIT' )->finish();
1116
		}
1117
		catch( \Exception $e )
1118
		{
1119
			$conn->close();
1120
			throw $e;
1121
		}
1122
1123
		return $row['invoiceno'] ?? '';
1124
	}
1125
1126
1127
	/**
1128
	 * Saves the modified basket content
1129
	 *
1130
	 * @param \Aimeos\MShop\Order\Item\Iface $basket Basket content
1131
	 * @return \Aimeos\MShop\Order\Item\Iface Saved basket content
1132
	 */
1133
	protected function saveBasket( \Aimeos\MShop\Order\Item\Iface $basket ) : \Aimeos\MShop\Order\Item\Iface
1134
	{
1135
		$this->saveAddresses( $basket );
1136
		$this->saveServices( $basket );
1137
		$this->saveProducts( $basket );
1138
		$this->saveCoupons( $basket );
1139
1140
		return $basket;
1141
	}
1142
}
1143