Passed
Push — master ( 0cda2e...f3fa1e )
by Aimeos
17:16 queued 07:34
created

Base::searchItemsBase()   B

Complexity

Conditions 6
Paths 18

Size

Total Lines 48
Code Lines 26

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 6
eloc 26
nc 18
nop 8
dl 0
loc 48
rs 8.8817
c 0
b 0
f 0

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-2021
7
 * @package MShop
8
 * @subpackage Common
9
 */
10
11
12
namespace Aimeos\MShop\Common\Manager;
13
14
use \Aimeos\MShop\Locale\Manager\Base as Locale;
15
16
17
/**
18
 * Provides common methods required by most of the manager classes.
19
 *
20
 * @package MShop
21
 * @subpackage Common
22
 */
23
abstract class Base
24
	extends \Aimeos\MW\Common\Manager\Base
25
	implements \Aimeos\MW\Macro\Iface
26
{
27
	use \Aimeos\MShop\Common\Manager\Sub\Traits;
28
	use \Aimeos\MW\Macro\Traits;
29
30
31
	private $context;
32
	private $object;
33
	private $resourceName;
34
	private $stmts = [];
35
	private $search;
36
37
38
	/**
39
	 * Initialization of class.
40
	 *
41
	 * @param \Aimeos\MShop\Context\Item\Iface $context Context object
42
	 */
43
	public function __construct( \Aimeos\MShop\Context\Item\Iface $context )
44
	{
45
		$this->context = $context;
46
	}
47
48
49
	/**
50
	 * Removes old entries from the storage.
51
	 *
52
	 * @param iterable $siteids List of IDs for sites whose entries should be deleted
53
	 * @return \Aimeos\MShop\Common\Manager\Iface Manager object for chaining method calls
54
	 */
55
	public function clear( iterable $siteids ) : \Aimeos\MShop\Common\Manager\Iface
56
	{
57
		return $this;
58
	}
59
60
61
	/**
62
	 * Creates a search critera object
63
	 *
64
	 * @param bool|null $default Add default criteria or NULL for relaxed default criteria
65
	 * @param bool $site TRUE for adding site criteria to limit items by the site of related items
66
	 * @return \Aimeos\MW\Criteria\Iface New search criteria object
67
	 */
68
	public function filter( ?bool $default = false, bool $site = false ) : \Aimeos\MW\Criteria\Iface
69
	{
70
		$db = $this->getResourceName();
71
		$config = $this->context->getConfig();
72
		$dbm = $this->context->getDatabaseManager();
73
74
		if( ( $adapter = $config->get( 'resource/' . $db . '/adapter' ) ) === null ) {
75
			$adapter = $config->get( 'resource/db/adapter' );
76
		}
77
78
		$conn = $dbm->acquire( $db );
79
80
		switch( $adapter )
81
		{
82
			case 'pgsql':
83
				$search = new \Aimeos\MW\Criteria\PgSQL( $conn ); break;
84
			default:
85
				$search = new \Aimeos\MW\Criteria\SQL( $conn ); break;
86
		}
87
88
		$dbm->release( $conn, $db );
89
90
		return $search;
91
	}
92
93
94
	/**
95
	 * Adds or updates an item object or a list of them.
96
	 *
97
	 * @param \Aimeos\Map|\Aimeos\MShop\Common\Item\Iface[]|\Aimeos\MShop\Common\Item\Iface $items Item or list of items whose data should be saved
98
	 * @param bool $fetch True if the new ID should be returned in the item
99
	 * @return \Aimeos\Map|\Aimeos\MShop\Common\Item\Iface Saved item or items
100
	 */
101
	public function save( $items, bool $fetch = true )
102
	{
103
		if( is_iterable( $items ) )
104
		{
105
			foreach( $items as $id => $item ) {
106
				$items[$id] = $this->object()->saveItem( $item, $fetch );
107
			}
108
			return map( $items );
109
		}
110
111
		return $this->object()->saveItem( $items, $fetch );
112
	}
113
114
115
	/**
116
	 * Searches for all items matching the given critera.
117
	 *
118
	 * @param \Aimeos\MW\Criteria\Iface $filter Criteria object with conditions, sortations, etc.
119
	 * @param string[] $ref List of domains to fetch list items and referenced items for
120
	 * @param int &$total Number of items that are available in total
121
	 * @return \Aimeos\Map List of items implementing \Aimeos\MShop\Common\Item\Iface with ids as keys
122
	 */
123
	public function search( \Aimeos\MW\Criteria\Iface $filter, array $ref = [], int &$total = null ) : \Aimeos\Map
124
	{
125
		return $this->object()->search( $filter, $ref, $total );
126
	}
127
128
129
	/**
130
	 * Starts a database transaction on the connection identified by the given name
131
	 *
132
	 * @return \Aimeos\MShop\Common\Manager\Iface Manager object for chaining method calls
133
	 */
134
	public function begin() : \Aimeos\MShop\Common\Manager\Iface
135
	{
136
		return $this->beginTransation( $this->getResourceName() );
137
	}
138
139
140
	/**
141
	 * Commits the running database transaction on the connection identified by the given name
142
	 *
143
	 * @return \Aimeos\MShop\Common\Manager\Iface Manager object for chaining method calls
144
	 */
145
	public function commit() : \Aimeos\MShop\Common\Manager\Iface
146
	{
147
		return $this->commitTransaction( $this->getResourceName() );
148
	}
149
150
151
	/**
152
	 * Rolls back the running database transaction on the connection identified by the given name
153
	 *
154
	 * @return \Aimeos\MShop\Common\Manager\Iface Manager object for chaining method calls
155
	 */
156
	public function rollback() : \Aimeos\MShop\Common\Manager\Iface
157
	{
158
		return $this->rollbackTransaction( $this->getResourceName() );
159
	}
160
161
162
	/**
163
	 * Returns the additional column/search definitions
164
	 *
165
	 * @return array Associative list of column names as keys and items implementing \Aimeos\MW\Criteria\Attribute\Iface
166
	 */
167
	public function getSaveAttributes() : array
168
	{
169
		return [];
170
	}
171
172
173
	/**
174
	 * Injects the reference of the outmost object
175
	 *
176
	 * @param \Aimeos\MShop\Common\Manager\Iface $object Reference to the outmost manager or decorator
177
	 * @return \Aimeos\MShop\Common\Manager\Iface Manager object for chaining method calls
178
	 */
179
	public function setObject( \Aimeos\MShop\Common\Manager\Iface $object ) : \Aimeos\MShop\Common\Manager\Iface
180
	{
181
		$this->object = $object;
182
		return $this;
183
	}
184
185
186
	/**
187
	 * Adds additional column names to SQL statement
188
	 *
189
	 * @param string[] $columns List of column names
190
	 * @param string $sql Insert or update SQL statement
191
	 * @param bool $mode True for insert, false for update statement
192
	 * @return string Modified insert or update SQL statement
193
	 */
194
	protected function addSqlColumns( array $columns, string $sql, bool $mode = true ) : string
195
	{
196
		$names = $values = '';
197
198
		if( $mode )
199
		{
200
			foreach( $columns as $name ) {
201
				$names .= '"' . $name . '", '; $values .= '?, ';
202
			}
203
		}
204
		else
205
		{
206
			foreach( $columns as $name ) {
207
				$names .= '"' . $name . '" = ?, ';
208
			}
209
		}
210
211
		return str_replace( [':names', ':values'], [$names, $values], $sql );
212
	}
213
214
215
	/**
216
	 * Counts the number products that are available for the values of the given key.
217
	 *
218
	 * @param \Aimeos\MW\Criteria\Iface $search Search criteria
219
	 * @param array|string $keys Search key or list of keys for aggregation
220
	 * @param string $cfgPath Configuration key for the SQL statement
221
	 * @param string[] $required List of domain/sub-domain names like "catalog.index" that must be additionally joined
222
	 * @param string|null $value Search key for aggregating the value column
223
	 * @param string|null $type Type of aggregation, e.g.  "sum", "min", "max" or NULL for "count"
224
	 * @return \Aimeos\Map List of ID values as key and the number of counted products as value
225
	 * @todo 2018.01 Reorder Parameter list
226
	 */
227
	protected function aggregateBase( \Aimeos\MW\Criteria\Iface $search, $keys, string $cfgPath,
228
		array $required = [], string $value = null, string $type = null ) : \Aimeos\Map
229
	{
230
		/** mshop/common/manager/aggregate/limit
231
		 * Limits the number of records that are used when aggregating items
232
		 *
233
		 * As counting huge amount of records (several 10 000 records) takes a long time,
234
		 * the limit can cut down response times so the counts are available more quickly
235
		 * in the front-end and the server load is reduced.
236
		 *
237
		 * Using a low limit can lead to incorrect numbers if the amount of found items
238
		 * is very high. Approximate item counts are normally not a problem but it can
239
		 * lead to the situation that visitors see that no items are available despite
240
		 * the fact that there would be at least one.
241
		 *
242
		 * @param integer Number of records
243
		 * @since 2021.04
244
		 */
245
		$limit = $this->context->getConfig()->get( 'mshop/common/manager/aggregate/limit', 10000 );
246
		$keys = (array) $keys;
247
248
		if( !count( $keys ) )
249
		{
250
			$msg = $this->getContext()->translate( 'mshop', 'At least one key is required for aggregation' );
251
			throw new \Aimeos\MShop\Exception( $msg );
252
		}
253
254
		$dbname = $this->getResourceName();
255
		$dbm = $this->getContext()->getDatabaseManager();
256
		$conn = $dbm->acquire( $dbname );
257
258
		try
259
		{
260
			$total = null;
261
			$cols = $map = [];
262
			$search = clone $search;
263
			$search->slice( $search->getOffset(), min( $search->getLimit(), $limit ) );
264
265
			$level = \Aimeos\MShop\Locale\Manager\Base::SITE_ALL;
266
			$attrList = $this->object()->getSearchAttributes();
267
268
			if( $value === null && ( $value = key( $attrList ) ) === null )
269
			{
270
				$msg = $this->getContext()->translate( 'mshop', 'No search keys available' );
271
				throw new \Aimeos\MShop\Exception( $msg );
272
			}
273
274
			if( ( $pos = strpos( $valkey = $value, '(' ) ) !== false ) {
275
				$value = substr( $value, 0, $pos );
276
			}
277
278
			if( !isset( $attrList[$value] ) )
279
			{
280
				$msg = $this->getContext()->translate( 'mshop', 'Unknown search key "%1$s"' );
281
				throw new \Aimeos\MShop\Exception( $msg );
282
			}
283
284
			foreach( $keys as $string )
285
			{
286
				if( !isset( $attrList[$string] ) )
287
				{
288
					$msg = $this->getContext()->translate( 'mshop', 'Unknown search key "%1$s"' );
289
					throw new \Aimeos\MShop\Exception( $msg );
290
				}
291
292
				$cols[] = $attrList[$string]->getInternalCode();
293
				$acols[] = $attrList[$string]->getInternalCode() . ' AS "' . $string . '"';
294
295
				/** @todo Required to get the joins, but there should be a better way */
296
				$search->add( [$string => null], '!=' );
297
			}
298
			$search->add( [$valkey => null], '!=' );
299
300
			$sql = $this->getSqlConfig( $cfgPath );
301
			$sql = str_replace( ':cols', join( ', ', $cols ), $sql );
302
			$sql = str_replace( ':acols', join( ', ', $acols ), $sql );
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $acols seems to be defined by a foreach iteration on line 284. Are you sure the iterator is never empty, otherwise this variable is not defined?
Loading history...
303
			$sql = str_replace( ':keys', '"' . join( '", "', $keys ) . '"', $sql );
304
			$sql = str_replace( ':val', $attrList[$value]->getInternalCode(), $sql );
305
			$sql = str_replace( ':type', $type ?: 'count', $sql );
306
307
			$results = $this->searchItemsBase( $conn, $search, $sql, '', $required, $total, $level );
308
309
			while( ( $row = $results->fetch() ) !== null )
310
			{
311
				$row = $this->transform( $row );
312
313
				$temp = &$map;
314
				$last = array_pop( $row );
315
316
				foreach( $row as $val ) {
317
					$temp[$val] = $temp[$val] ?? [];
318
					$temp = &$temp[$val];
319
				}
320
				$temp = $last;
321
			}
322
323
			$dbm->release( $conn, $dbname );
324
		}
325
		catch( \Exception $e )
326
		{
327
			$dbm->release( $conn, $dbname );
328
			throw $e;
329
		}
330
331
		return map( $map );
332
	}
333
334
335
	/**
336
	 * Returns the newly created ID for the last record which was inserted.
337
	 *
338
	 * @param \Aimeos\MW\DB\Connection\Iface $conn Database connection used to insert the new record
339
	 * @param string $cfgpath Configuration path to the SQL statement for retrieving the new ID of the last inserted record
340
	 * @return string ID of the last record that was inserted by using the given connection
341
	 * @throws \Aimeos\MShop\Exception if there's no ID of the last record available
342
	 */
343
	protected function newId( \Aimeos\MW\DB\Connection\Iface $conn, string $cfgpath ) : string
344
	{
345
		$result = $conn->create( $this->getSqlConfig( $cfgpath ) )->execute();
0 ignored issues
show
Bug introduced by
It seems like $this->getSqlConfig($cfgpath) can also be of type array; however, parameter $sql of Aimeos\MW\DB\Connection\Iface::create() 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

345
		$result = $conn->create( /** @scrutinizer ignore-type */ $this->getSqlConfig( $cfgpath ) )->execute();
Loading history...
346
347
		if( ( $row = $result->fetch( \Aimeos\MW\DB\Result\Base::FETCH_NUM ) ) === false )
348
		{
349
			$msg = $this->getContext()->translate( 'mshop', 'ID of last inserted database record not available' );
350
			throw new \Aimeos\MShop\Exception( $msg );
351
		}
352
		$result->finish();
353
354
		return $row[0];
355
	}
356
357
358
	/**
359
	 * Removes old entries from the storage.
360
	 *
361
	 * @param iterable $siteids List of IDs for sites whose entries should be deleted
362
	 * @param string $cfgpath Configuration key to the cleanup statement
363
	 * @return \Aimeos\MShop\Common\Manager\Iface Manager object for chaining method calls
364
	 */
365
	protected function clearBase( iterable $siteids, string $cfgpath ) : \Aimeos\MShop\Common\Manager\Iface
366
	{
367
		if( empty( $siteids ) ) {
368
			return $this;
369
		}
370
371
		$dbm = $this->context->getDatabaseManager();
372
		$dbname = $this->getResourceName();
373
		$conn = $dbm->acquire( $dbname );
374
375
		try
376
		{
377
			$sql = $this->getSqlConfig( $cfgpath );
378
			$sql = str_replace( ':cond', '1=1', $sql );
379
380
			$stmt = $conn->create( $sql );
381
382
			foreach( $siteids as $siteid )
383
			{
384
				$stmt->bind( 1, $siteid );
385
				$stmt->execute()->finish();
386
			}
387
388
			$dbm->release( $conn, $dbname );
389
		}
390
		catch( \Exception $e )
391
		{
392
			$dbm->release( $conn, $dbname );
393
			throw $e;
394
		}
395
396
		return $this;
397
	}
398
399
400
	/**
401
	 * Creates the criteria attribute items from the list of entries
402
	 *
403
	 * @param array $list Associative array of code as key and array with properties as values
404
	 * @return \Aimeos\MW\Criteria\Attribute\Standard[] List of criteria attribute items
405
	 */
406
	protected function createAttributes( array $list ) : array
407
	{
408
		$attr = [];
409
410
		foreach( $list as $key => $fields ) {
411
			$attr[$key] = new \Aimeos\MW\Criteria\Attribute\Standard( $fields );
412
		}
413
414
		return $attr;
415
	}
416
417
418
	/**
419
	 * Sets the base criteria "status".
420
	 * (setConditions overwrites the base criteria)
421
	 *
422
	 * @param string $domain Name of the domain/sub-domain like "product" or "product.list"
423
	 * @param bool|null $default TRUE for status=1, NULL for status>0, FALSE for no restriction
424
	 * @return \Aimeos\MW\Criteria\Iface Search critery object
425
	 */
426
	protected function filterBase( string $domain, ?bool $default = true ) : \Aimeos\MW\Criteria\Iface
427
	{
428
		$filter = self::filter();
0 ignored issues
show
Bug Best Practice introduced by
The method Aimeos\MShop\Common\Manager\Base::filter() is not static, but was called statically. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

428
		/** @scrutinizer ignore-call */ 
429
  $filter = self::filter();
Loading history...
429
430
		if( $default !== false ) {
431
			$filter->add( [$domain . '.status' => 1], $default ? '==' : '>=' );
432
		}
433
434
		return $filter;
435
	}
436
437
438
	/**
439
	 * Returns the context object.
440
	 *
441
	 * @return \Aimeos\MShop\Context\Item\Iface Context object
442
	 */
443
	protected function getContext() : \Aimeos\MShop\Context\Item\Iface
444
	{
445
		return $this->context;
446
	}
447
448
449
	/**
450
	 * Returns the outmost decorator of the decorator stack
451
	 *
452
	 * @return \Aimeos\MShop\Common\Manager\Iface Outmost decorator object
453
	 */
454
	protected function object() : \Aimeos\MShop\Common\Manager\Iface
455
	{
456
		if( $this->object !== null ) {
457
			return $this->object;
458
		}
459
460
		return $this;
461
	}
462
463
464
	/**
465
	 * Returns the search attribute objects used for searching.
466
	 *
467
	 * @param array $list Associative list of search keys and the lists of search definitions
468
	 * @param string $path Configuration path to the sub-domains for fetching the search definitions
469
	 * @param string[] $default List of sub-domains if no others are configured
470
	 * @param bool $withsub True to include search definitions of sub-domains, false if not
471
	 * @return \Aimeos\MW\Criteria\Attribute\Iface[] Associative list of search keys and criteria attribute items as values
472
	 * @since 2014.09
473
	 */
474
	protected function getSearchAttributesBase( array $list, string $path, array $default, bool $withsub ) : array
475
	{
476
		$attr = $this->createAttributes( $list );
477
478
		if( $withsub === true )
479
		{
480
			$domains = $this->context->getConfig()->get( $path, $default );
481
482
			foreach( $domains as $domain ) {
483
				$attr += $this->object()->getSubManager( $domain )->getSearchAttributes( true );
484
			}
485
		}
486
487
		return $attr;
488
	}
489
490
491
	/**
492
	 * Returns the search results for the given SQL statement.
493
	 *
494
	 * @param \Aimeos\MW\DB\Connection\Iface $conn Database connection
495
	 * @param string $sql SQL statement
496
	 * @return \Aimeos\MW\DB\Result\Iface Search result object
497
	 */
498
	protected function getSearchResults( \Aimeos\MW\DB\Connection\Iface $conn, string $sql ) : \Aimeos\MW\DB\Result\Iface
499
	{
500
		$time = microtime( true );
501
502
		$stmt = $conn->create( $sql );
503
		$result = $stmt->execute();
504
505
		$level = \Aimeos\MW\Logger\Base::DEBUG;
506
		$time = ( microtime( true ) - $time ) * 1000;
507
		$msg = 'Time: ' . $time . "ms\n"
508
			. 'Class: ' . get_class( $this ) . "\n"
509
			. str_replace( ["\t", "\n\n"], ['', "\n"], trim( (string) $stmt ) );
510
511
		if( $time > 1000.0 )
512
		{
513
			$level = \Aimeos\MW\Logger\Base::NOTICE;
514
			$msg .= "\n" . ( new \Exception() )->getTraceAsString();
515
		}
516
517
		$this->context->getLogger()->log( $msg, $level, 'core/sql' );
518
519
		return $result;
520
	}
521
522
523
	/**
524
	 * Returns the SQL statement for the given config path
525
	 *
526
	 * If available, the database specific SQL statement is returned, otherwise
527
	 * the ANSI SQL statement. The database type is determined via the resource
528
	 * adapter.
529
	 *
530
	 * @param string $path Configuration path to the SQL statement
531
	 * @return array|string ANSI or database specific SQL statement
532
	 */
533
	protected function getSqlConfig( string $path )
534
	{
535
		$config = $this->getContext()->getConfig();
536
		$adapter = $config->get( 'resource/' . $this->getResourceName() . '/adapter' );
537
538
		return $config->get( $path . '/' . $adapter, $config->get( $path . '/ansi', $path ) );
539
	}
540
541
542
	/**
543
	 * Returns the item for the given search key/value pairs.
544
	 *
545
	 * @param array $pairs Search key/value pairs for the item
546
	 * @param string[] $ref List of domains whose items should be fetched too
547
	 * @param bool|null $default Add default criteria or NULL for relaxed default criteria
548
	 * @return \Aimeos\MShop\Common\Item\Iface Requested item
549
	 * @throws \Aimeos\MShop\Exception if no item with the given ID found
550
	 */
551
	protected function findBase( array $pairs, array $ref, ?bool $default ) : \Aimeos\MShop\Common\Item\Iface
552
	{
553
		$expr = [];
554
		$criteria = $this->object()->filter( $default )->slice( 0, 1 );
555
556
		foreach( $pairs as $key => $value )
557
		{
558
			if( $value === null )
559
			{
560
				$msg = $this->getContext()->translate( 'mshop', 'Required value for "%1$s" is missing' );
561
				throw new \Aimeos\MShop\Exception( $msg );
562
			}
563
			$expr[] = $criteria->compare( '==', $key, $value );
564
		}
565
566
		$criteria->setConditions( $criteria->and( $expr ) );
567
568
		if( ( $item = $this->object()->search( $criteria, $ref )->first() ) ) {
569
			return $item;
570
		}
571
572
		$msg = $this->getContext()->translate( 'mshop', 'No item found for conditions: %1$s' );
573
		throw new \Aimeos\MShop\Exception( sprintf( $msg, print_r( $pairs, true ) ) );
0 ignored issues
show
Bug introduced by
It seems like print_r($pairs, true) can also be of type true; however, parameter $values of sprintf() does only seem to accept double|integer|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

573
		throw new \Aimeos\MShop\Exception( sprintf( $msg, /** @scrutinizer ignore-type */ print_r( $pairs, true ) ) );
Loading history...
574
	}
575
576
577
	/**
578
	 * Returns the cached statement for the given key or creates a new prepared statement.
579
	 * If no SQL string is given, the key is used to retrieve the SQL string from the configuration.
580
	 *
581
	 * @param \Aimeos\MW\DB\Connection\Iface $conn Database connection
582
	 * @param string $cfgkey Unique key for the SQL
583
	 * @param string|null $sql SQL string if it shouldn't be retrieved from the configuration
584
	 * @return \Aimeos\MW\DB\Statement\Iface Database statement object
585
	 */
586
	protected function getCachedStatement( \Aimeos\MW\DB\Connection\Iface $conn, string $cfgkey,
587
		string $sql = null ) : \Aimeos\MW\DB\Statement\Iface
588
	{
589
		if( !isset( $this->stmts['stmt'][$cfgkey] )
590
			|| !isset( $this->stmts['conn'][$cfgkey] )
591
			|| $conn !== $this->stmts['conn'][$cfgkey]
592
		) {
593
			if( $sql === null ) {
594
				$sql = $this->getSqlConfig( $cfgkey );
595
			}
596
597
			$this->stmts['stmt'][$cfgkey] = $conn->create( $sql );
0 ignored issues
show
Bug introduced by
It seems like $sql can also be of type array; however, parameter $sql of Aimeos\MW\DB\Connection\Iface::create() 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

597
			$this->stmts['stmt'][$cfgkey] = $conn->create( /** @scrutinizer ignore-type */ $sql );
Loading history...
598
			$this->stmts['conn'][$cfgkey] = $conn;
599
		}
600
601
		return $this->stmts['stmt'][$cfgkey];
602
	}
603
604
605
	/**
606
	 * Returns the item for the given search key and ID.
607
	 *
608
	 * @param string $key Search key for the requested ID
609
	 * @param string $id Unique ID to search for
610
	 * @param string[] $ref List of domains whose items should be fetched too
611
	 * @param bool|null $default Add default criteria or NULL for relaxed default criteria
612
	 * @return \Aimeos\MShop\Common\Item\Iface Requested item
613
	 * @throws \Aimeos\MShop\Exception if no item with the given ID found
614
	 */
615
	protected function getItemBase( string $key, string $id, array $ref, ?bool $default ) : \Aimeos\MShop\Common\Item\Iface
616
	{
617
		$criteria = $this->object()->filter( $default )->slice( 0, 1 );
618
		$expr = [
619
			$criteria->compare( '==', $key, $id ),
620
			$criteria->getConditions()
621
		];
622
		$criteria->setConditions( $criteria->and( $expr ) );
623
624
		if( ( $item = $this->object()->search( $criteria, $ref )->first() ) ) {
625
			return $item;
626
		}
627
628
		$msg = $this->getContext()->translate( 'mshop', 'Item with ID "%2$s" in "%1$s" not found' );
629
		throw new \Aimeos\MShop\Exception( sprintf( $msg, $key, $id ) );
630
	}
631
632
633
	/**
634
	 * Returns the SQL strings for joining dependent tables.
635
	 *
636
	 * @param \Aimeos\MW\Criteria\Attribute\Iface[] $attributes List of criteria attribute items
637
	 * @param string $prefix Search key prefix
638
	 * @return array List of JOIN SQL strings
639
	 */
640
	private function getJoins( array $attributes, string $prefix ) : array
641
	{
642
		$iface = \Aimeos\MW\Criteria\Attribute\Iface::class;
643
		$sep = $this->getKeySeparator();
644
		$name = $prefix . $sep . 'id';
645
646
		if( isset( $attributes[$prefix] ) && $attributes[$prefix] instanceof $iface ) {
647
			return $attributes[$prefix]->getInternalDeps();
648
		}
649
		elseif( isset( $attributes[$name] ) && $attributes[$name] instanceof $iface ) {
650
			return $attributes[$name]->getInternalDeps();
651
		}
652
		else if( isset( $attributes['id'] ) && $attributes['id'] instanceof $iface ) {
653
			return $attributes['id']->getInternalDeps();
654
		}
655
656
		return [];
657
	}
658
659
660
	/**
661
	 * Returns the available manager types
662
	 *
663
	 * @param string $type Main manager type
664
	 * @param string $path Configuration path to the sub-domains
665
	 * @param string[] $default List of sub-domains if no others are configured
666
	 * @param bool $withsub Return also the resource type of sub-managers if true
667
	 * @return string[] Type of the manager and submanagers, subtypes are separated by slashes
668
	 */
669
	protected function getResourceTypeBase( string $type, string $path, array $default, bool $withsub ) : array
670
	{
671
		$list = array( $type );
672
673
		foreach( $this->context->getConfig()->get( $path, $default ) as $domain ) {
674
			$list = array_merge( $list, $this->object()->getSubManager( $domain )->getResourceType( $withsub ) );
675
		}
676
677
		return $list;
678
	}
679
680
681
	/**
682
	 * Returns the name of the resource or of the default resource.
683
	 *
684
	 * @return string Name of the resource
685
	 */
686
	protected function getResourceName() : string
687
	{
688
		if( $this->resourceName === null ) {
689
			$this->resourceName = $this->context->getConfig()->get( 'resource/default', 'db' );
690
		}
691
692
		return $this->resourceName;
693
	}
694
695
696
	/**
697
	 * Sets the name of the database resource that should be used.
698
	 *
699
	 * @param string $name Name of the resource
700
	 * @return \Aimeos\MShop\Common\Manager\Iface Manager object for chaining method calls
701
	 */
702
	protected function setResourceName( string $name ) : \Aimeos\MShop\Common\Manager\Iface
703
	{
704
		$config = $this->context->getConfig();
705
706
		if( $config->get( 'resource/' . $name ) === null ) {
707
			$this->resourceName = $config->get( 'resource/default', 'db' );
708
		} else {
709
			$this->resourceName = $name;
710
		}
711
712
		return $this;
713
	}
714
715
716
	/**
717
	 * Returns a search object singleton
718
	 *
719
	 * @return \Aimeos\MW\Criteria\Iface Search object
720
	 */
721
	protected function getSearch() : \Aimeos\MW\Criteria\Iface
722
	{
723
		if( !isset( $this->search ) ) {
724
			$this->search = $this->filter();
725
		}
726
727
		return $this->search;
728
	}
729
730
731
	/**
732
	 * Replaces the given marker with an expression
733
	 *
734
	 * @param string $column Name (including alias) of the column
735
	 * @param mixed $value Value used in the expression
736
	 * @param string $op Operator used in the expression
737
	 * @param int $type Type constant from \Aimeos\MW\DB\Statement\Base class
738
	 * @return string Created expression
739
	 */
740
	protected function toExpression( string $column, $value, string $op = '==',
741
		int $type = \Aimeos\MW\DB\Statement\Base::PARAM_STR ) : string
742
	{
743
		$types = ['marker' => $type];
744
		$translations = ['marker' => $column];
745
		$value = ( is_array( $value ) ? array_unique( $value ) : $value );
746
747
		return $this->getSearch()->compare( $op, 'marker', $value )->toSource( $types, $translations );
748
	}
749
750
751
	/**
752
	 * Returns the site expression for the given name
753
	 *
754
	 * @param \Aimeos\MW\Criteria\Iface $search Search criteria object
755
	 * @param string $name Name of the site condition
756
	 * @param int $sitelevel Site level constant from \Aimeos\MShop\Locale\Manager\Base
757
	 * @return \Aimeos\MW\Criteria\Expression\Iface Site search condition
758
	 * @since 2020.01
759
	 */
760
	protected function getSiteCondition( \Aimeos\MW\Criteria\Iface $search, string $name,
761
		int $sitelevel ) : \Aimeos\MW\Criteria\Expression\Iface
762
	{
763
		$sites = $this->context->getLocale()->getSites();
764
		$cond = [$search->compare( '==', $name, '' )];
765
766
		if( isset( $sites[Locale::SITE_PATH] ) && $sitelevel & Locale::SITE_PATH ) {
767
			$cond[] = $search->compare( '==', $name, $sites[Locale::SITE_PATH] );
768
		} elseif( isset( $sites[Locale::SITE_ONE] ) ) {
769
			$cond[] = $search->compare( '==', $name, $sites[Locale::SITE_ONE] );
770
		}
771
772
		if( isset( $sites[Locale::SITE_SUBTREE] ) && $sitelevel & Locale::SITE_SUBTREE ) {
773
			$cond[] = $search->compare( '=~', $name, $sites[Locale::SITE_SUBTREE] );
774
		}
775
776
		return $search->or( $cond );
777
	}
778
779
780
	/**
781
	 * Returns the site coditions for the search request
782
	 *
783
	 * @param string[] $keys Sorted list of criteria keys
784
	 * @param \Aimeos\MW\Criteria\Attribute\Iface[] $attributes Associative list of search keys and criteria attribute items as values
785
	 * @param int $sitelevel Site level constant from \Aimeos\MShop\Locale\Manager\Base
786
	 * @return \Aimeos\MW\Criteria\Expression\Iface[] List of search conditions
787
	 * @since 2015.01
788
	 */
789
	protected function getSiteConditions( array $keys, array $attributes, int $sitelevel ) : array
790
	{
791
		$list = [];
792
		$sep = $this->getKeySeparator();
793
794
		foreach( $keys as $key )
795
		{
796
			$name = $key . $sep . 'siteid';
797
798
			if( isset( $attributes[$name] ) ) {
799
				$list[] = $this->getSiteCondition( $this->getSearch(), $name, $sitelevel );
800
			}
801
		}
802
803
		return $list;
804
	}
805
806
807
	/**
808
	 * Returns the site expression for the given name
809
	 *
810
	 * @param string $name SQL name for the site condition
811
	 * @param int $sitelevel Site level constant from \Aimeos\MShop\Locale\Manager\Base
812
	 * @return string Site search condition
813
	 * @since 2020.01
814
	 */
815
	protected function getSiteString( string $name, int $sitelevel ) : string
816
	{
817
		$translation = ['marker' => $name];
818
		$types = ['marker' => \Aimeos\MW\DB\Statement\Base::PARAM_STR];
819
820
		return $this->getSiteCondition( $this->getSearch(), 'marker', $sitelevel )->toSource( $types, $translation );
821
	}
822
823
824
	/**
825
	 * Returns the string replacements for the SQL statements
826
	 *
827
	 * @param \Aimeos\MW\Criteria\Iface $search Search critera object
828
	 * @param \Aimeos\MW\Criteria\Attribute\Iface[] $attributes Associative list of search keys and criteria attribute items as values
829
	 * @param \Aimeos\MW\Criteria\Plugin\Iface[] $plugins Associative list of search keys and criteria plugin items as values
830
	 * @param string[] $joins Associative list of SQL joins
831
	 * @param \Aimeos\MW\Criteria\Attribute\Iface[] $columns Additional columns to retrieve values from
832
	 * @return array Array of keys, find and replace arrays
833
	 */
834
	protected function getSQLReplacements( \Aimeos\MW\Criteria\Iface $search, array $attributes, array $plugins,
835
		array $joins, array $columns = [] ) : array
836
	{
837
		$types = $this->getSearchTypes( $attributes );
838
		$funcs = $this->getSearchFunctions( $attributes );
839
		$translations = $this->getSearchTranslations( $attributes );
840
841
		$colstring = '';
842
		foreach( $columns as $name => $entry ) {
843
			$colstring .= $entry->getInternalCode() . ', ';
844
		}
845
846
		$find = array( ':columns', ':joins', ':cond', ':start', ':size' );
847
		$replace = array(
848
			$colstring,
849
			implode( "\n", array_unique( $joins ) ),
850
			$search->getConditionSource( $types, $translations, $plugins, $funcs ),
851
			$search->getOffset(),
852
			$search->getLimit(),
853
		);
854
855
		if( empty( $search->getSortations() ) && ( $attribute = reset( $attributes ) ) !== false ) {
856
			$search = ( clone $search )->setSortations( [$search->sort( '+', $attribute->getCode() )] );
857
		}
858
859
		$find[] = ':order';
860
		$replace[] = $search->getSortationSource( $types, $translations, $funcs );
861
862
		$find[] = ':group';
863
		$replace[] = implode( ', ', $search->translate( $search->getSortations(), $translations, $funcs ) ) . ', ';
864
865
		return [$find, $replace];
866
	}
867
868
869
	/**
870
	 * Returns the search result of the statement combined with the given criteria.
871
	 *
872
	 * @param \Aimeos\MW\DB\Connection\Iface $conn Database connection
873
	 * @param \Aimeos\MW\Criteria\Iface $search Search criteria object
874
	 * @param string $cfgPathSearch Path to SQL statement in configuration for searching
875
	 * @param string $cfgPathCount Path to SQL statement in configuration for counting
876
	 * @param string[] $required Additional search keys to add conditions for even if no conditions are available
877
	 * @param int|null $total Contains the number of all records matching the criteria if not null
878
	 * @param int $sitelevel Constant from \Aimeos\MShop\Locale\Manager\Base for defining which site IDs should be used for searching
879
	 * @param \Aimeos\MW\Criteria\Plugin\Iface[] $plugins Associative list of search keys and criteria plugin items as values
880
	 * @return \Aimeos\MW\DB\Result\Iface SQL result object for accessing the found records
881
	 * @throws \Aimeos\MShop\Exception if no number of all matching records is available
882
	 */
883
	protected function searchItemsBase( \Aimeos\MW\DB\Connection\Iface $conn, \Aimeos\MW\Criteria\Iface $search,
884
		string $cfgPathSearch, string $cfgPathCount, array $required, int &$total = null,
885
		int $sitelevel = \Aimeos\MShop\Locale\Manager\Base::SITE_ALL, array $plugins = [] ) : \Aimeos\MW\DB\Result\Iface
886
	{
887
		$joins = [];
888
		$conditions = $search->getConditions();
889
		$columns = $this->object()->getSaveAttributes();
890
		$attributes = $this->object()->getSearchAttributes();
891
		$keys = $this->getCriteriaKeyList( $search, $required );
892
893
		$basekey = array_shift( $required );
894
895
		foreach( $keys as $key )
896
		{
897
			if( $key !== $basekey ) {
898
				$joins = array_merge( $joins, $this->getJoins( $attributes, $key ) );
899
			}
900
		}
901
902
		$joins = array_unique( $joins );
903
		$cond = $this->getSiteConditions( $keys, $attributes, $sitelevel );
904
905
		if( $conditions !== null ) {
906
			$cond[] = $conditions;
907
		}
908
909
		$search = clone $search;
910
		$search->setConditions( $search->and( $cond ) );
911
912
		list( $find, $replace ) = $this->getSQLReplacements( $search, $attributes, $plugins, $joins, $columns );
913
914
		if( $total !== null )
915
		{
916
			$sql = str_replace( $find, $replace, $this->getSqlConfig( $cfgPathCount ) );
917
			$result = $this->getSearchResults( $conn, $sql );
918
			$row = $result->fetch();
919
			$result->finish();
920
921
			if( $row === null )
922
			{
923
				$msg = $this->getContext()->translate( 'mshop', 'Total results value not found' );
924
				throw new \Aimeos\MShop\Exception( $msg );
925
			}
926
927
			$total = (int) $row['count'];
928
		}
929
930
		return $this->getSearchResults( $conn, str_replace( $find, $replace, $this->getSqlConfig( $cfgPathSearch ) ) );
931
	}
932
933
934
	/**
935
	 * Deletes items.
936
	 *
937
	 * @param \Aimeos\MShop\Common\Item\Iface|\Aimeos\Map|array|string $items List of item objects or IDs of the items
938
	 * @param string $cfgpath Configuration path to the SQL statement
939
	 * @param bool $siteid If siteid should be used in the statement
940
	 * @param string $name Name of the ID column
941
	 * @return \Aimeos\MShop\Common\Manager\Iface Manager object for chaining method calls
942
	 */
943
	protected function deleteItemsBase( $items, string $cfgpath, bool $siteid = true,
944
		string $name = 'id' ) : \Aimeos\MShop\Common\Manager\Iface
945
	{
946
		if( is_map( $items ) ) { $items = $items->toArray(); }
947
		if( !is_array( $items ) ) { $items = [$items]; }
948
		if( empty( $items ) ) { return $this; }
949
950
		$context = $this->getContext();
951
		$dbname = $this->getResourceName();
952
953
		$search = $this->object()->filter();
954
		$search->setConditions( $search->compare( '==', $name, $items ) );
955
956
		$types = array( $name => \Aimeos\MW\DB\Statement\Base::PARAM_STR );
957
		$translations = array( $name => '"' . $name . '"' );
958
959
		$cond = $search->getConditionSource( $types, $translations );
960
		$sql = str_replace( ':cond', $cond, $this->getSqlConfig( $cfgpath ) );
961
962
		$dbm = $context->getDatabaseManager();
963
		$conn = $dbm->acquire( $dbname );
964
965
		try
966
		{
967
			$stmt = $conn->create( $sql );
968
969
			if( $siteid ) {
970
				$stmt->bind( 1, $context->getLocale()->getSiteId() );
971
			}
972
973
			$stmt->execute()->finish();
974
975
			$dbm->release( $conn, $dbname );
976
		}
977
		catch( \Exception $e )
978
		{
979
			$dbm->release( $conn, $dbname );
980
			throw $e;
981
		}
982
983
		return $this;
984
	}
985
986
987
	/**
988
	 * Starts a database transaction on the connection identified by the given name.
989
	 *
990
	 * @param string $dbname Name of the database settings in the resource configuration
991
	 * @return \Aimeos\MShop\Common\Manager\Iface Manager object for chaining method calls
992
	 */
993
	protected function beginTransation( string $dbname = 'db' ) : \Aimeos\MShop\Common\Manager\Iface
994
	{
995
		$dbm = $this->context->getDatabaseManager();
996
997
		$conn = $dbm->acquire( $dbname );
998
		$conn->begin();
999
		$dbm->release( $conn, $dbname );
1000
1001
		return $this;
1002
	}
1003
1004
1005
	/**
1006
	 * Commits the running database transaction on the connection identified by the given name.
1007
	 *
1008
	 * @param string $dbname Name of the database settings in the resource configuration
1009
	 * @return \Aimeos\MShop\Common\Manager\Iface Manager object for chaining method calls
1010
	 */
1011
	protected function commitTransaction( string $dbname = 'db' ) : \Aimeos\MShop\Common\Manager\Iface
1012
	{
1013
		$dbm = $this->context->getDatabaseManager();
1014
1015
		$conn = $dbm->acquire( $dbname );
1016
		$conn->commit();
1017
		$dbm->release( $conn, $dbname );
1018
1019
		return $this;
1020
	}
1021
1022
1023
	/**
1024
	 * Rolls back the running database transaction on the connection identified by the given name.
1025
	 *
1026
	 * @param string $dbname Name of the database settings in the resource configuration
1027
	 * @return \Aimeos\MShop\Common\Manager\Iface Manager object for chaining method calls
1028
	 */
1029
	protected function rollbackTransaction( string $dbname = 'db' ) : \Aimeos\MShop\Common\Manager\Iface
1030
	{
1031
		$dbm = $this->context->getDatabaseManager();
1032
1033
		$conn = $dbm->acquire( $dbname );
1034
		$conn->rollback();
1035
		$dbm->release( $conn, $dbname );
1036
1037
		return $this;
1038
	}
1039
1040
1041
	/**
1042
	 * Transforms the application specific values to Aimeos standard values.
1043
	 *
1044
	 * @param array $values Associative list of key/value pairs from the storage
1045
	 * @return array Associative list of key/value pairs with standard Aimeos values
1046
	 */
1047
	protected function transform( array $values ) : array
1048
	{
1049
		return $values;
1050
	}
1051
}
1052