Passed
Push — master ( be1898...f642b9 )
by Aimeos
04:39
created

DB::searchItemsBase()   B

Complexity

Conditions 6
Paths 18

Size

Total Lines 48
Code Lines 26

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 1
Metric Value
eloc 26
c 1
b 0
f 1
dl 0
loc 48
rs 8.8817
cc 6
nc 18
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 Aimeos (aimeos.org), 2023
6
 * @package MShop
7
 * @subpackage Common
8
 */
9
10
11
namespace Aimeos\MShop\Common\Manager;
12
13
14
/**
15
 * Method trait for managers
16
 *
17
 * @package MShop
18
 * @subpackage Common
19
 */
20
trait DB
21
{
22
	private ?\Aimeos\Base\Criteria\Iface $search;
23
	private ?string $resourceName = null;
24
	private array $cachedStmts = [];
25
26
27
	/**
28
	 * Returns the context object.
29
	 *
30
	 * @return \Aimeos\MShop\ContextIface Context object
31
	 */
32
	abstract protected function context() : \Aimeos\MShop\ContextIface;
33
34
35
	/**
36
	 * Creates the criteria attribute items from the list of entries
37
	 *
38
	 * @param array $list Associative array of code as key and array with properties as values
39
	 * @return \Aimeos\Base\Criteria\Attribute\Standard[] List of criteria attribute items
40
	 */
41
	abstract protected function createAttributes( array $list ) : array;
42
43
44
	/**
45
	 * Returns the full configuration key for the passed last part
46
	 *
47
	 * @param string $name Configuration last part
48
	 * @return string Full configuration key
49
	 */
50
	abstract protected function getConfigKey( string $name ) : string;
51
52
53
	/**
54
	 * Returns the manager domain
55
	 *
56
	 * @return string Manager domain e.g. "product"
57
	 */
58
	abstract protected function getDomain() : string;
59
60
61
	/**
62
	 * Returns the attribute helper functions for searching defined by the manager.
63
	 *
64
	 * @param \Aimeos\Base\Criteria\Attribute\Iface[] $attributes List of search attribute items
65
	 * @return array Associative array of attribute code and helper function
66
	 */
67
	abstract protected function getSearchFunctions( array $attributes ) : array;
68
69
70
	/**
71
	 * Returns the attribute translations for searching defined by the manager.
72
	 *
73
	 * @param \Aimeos\Base\Criteria\Attribute\Iface[] $attributes List of search attribute items
74
	 * @return array Associative array of attribute code and internal attribute code
75
	 */
76
	abstract protected function getSearchTranslations( array $attributes ) : array;
77
78
79
	/**
80
	 * Returns the attribute types for searching defined by the manager.
81
	 *
82
	 * @param \Aimeos\Base\Criteria\Attribute\Iface[] $attributes List of search attribute items
83
	 * @return array Associative array of attribute code and internal attribute type
84
	 */
85
	abstract protected function getSearchTypes( array $attributes ) : array;
86
87
88
	/**
89
	 * Returns the name of the used table
90
	 *
91
	 * @return string Table name e.g. "mshop_product_lists_type"
92
	 */
93
	abstract protected function getTable() : string;
94
95
96
	/**
97
	 * Returns the outmost decorator of the decorator stack
98
	 *
99
	 * @return \Aimeos\MShop\Common\Manager\Iface Outmost decorator object
100
	 */
101
	abstract protected function object() : \Aimeos\MShop\Common\Manager\Iface;
102
103
104
	/**
105
	 * Returns the site expression for the given name
106
	 *
107
	 * @param string $name Name of the site condition
108
	 * @param int $sitelevel Site level constant from \Aimeos\MShop\Locale\Manager\Base
109
	 * @return \Aimeos\Base\Criteria\Expression\Iface Site search condition
110
	 */
111
	abstract protected function siteCondition( string $name, int $sitelevel ) : \Aimeos\Base\Criteria\Expression\Iface;
112
113
114
	/**
115
	 * Returns the site ID that should be used based on the site level
116
	 *
117
	 * @param string $siteId Site ID to check
118
	 * @param int $sitelevel Site level to check against
119
	 * @return string Site ID that should be use based on the site level
120
	 * @since 2022.04
121
	 */
122
	abstract protected function siteId( string $siteId, int $sitelevel ) : string;
123
124
125
	/**
126
	 * Adds additional column names to SQL statement
127
	 *
128
	 * @param string[] $columns List of column names
129
	 * @param string $sql Insert or update SQL statement
130
	 * @param bool $mode True for insert, false for update statement
131
	 * @return string Modified insert or update SQL statement
132
	 */
133
	protected function addSqlColumns( array $columns, string $sql, bool $mode = true ) : string
134
	{
135
		$names = $values = '';
136
137
		if( $mode )
138
		{
139
			foreach( $columns as $name ) {
140
				$names .= '"' . $name . '", '; $values .= '?, ';
141
			}
142
		}
143
		else
144
		{
145
			foreach( $columns as $name ) {
146
				$names .= '"' . $name . '" = ?, ';
147
			}
148
		}
149
150
		return str_replace( [':names', ':values'], [$names, $values], $sql );
151
	}
152
153
154
	/**
155
	 * Counts the number products that are available for the values of the given key.
156
	 *
157
	 * @param \Aimeos\Base\Criteria\Iface $search Search criteria
158
	 * @param array|string $keys Search key or list of keys for aggregation
159
	 * @param string $cfgPath Configuration key for the SQL statement
160
	 * @param string[] $required List of domain/sub-domain names like "catalog.index" that must be additionally joined
161
	 * @param string|null $value Search key for aggregating the value column
162
	 * @param string|null $type Type of aggregation, e.g.  "sum", "min", "max" or NULL for "count"
163
	 * @return \Aimeos\Map List of ID values as key and the number of counted products as value
164
	 * @todo 2018.01 Reorder Parameter list
165
	 */
166
	protected function aggregateBase( \Aimeos\Base\Criteria\Iface $search, $keys, string $cfgPath,
167
		array $required = [], string $value = null, string $type = null ) : \Aimeos\Map
168
	{
169
		/** mshop/common/manager/aggregate/limit
170
		 * Limits the number of records that are used when aggregating items
171
		 *
172
		 * As counting huge amount of records (several 10 000 records) takes a long time,
173
		 * the limit can cut down response times so the counts are available more quickly
174
		 * in the front-end and the server load is reduced.
175
		 *
176
		 * Using a low limit can lead to incorrect numbers if the amount of found items
177
		 * is very high. Approximate item counts are normally not a problem but it can
178
		 * lead to the situation that visitors see that no items are available despite
179
		 * the fact that there would be at least one.
180
		 *
181
		 * @param integer Number of records
182
		 * @since 2021.04
183
		 */
184
		$limit = $this->context()->config()->get( 'mshop/common/manager/aggregate/limit', 10000 );
185
		$keys = (array) $keys;
186
187
		if( !count( $keys ) )
188
		{
189
			$msg = $this->context()->translate( 'mshop', 'At least one key is required for aggregation' );
190
			throw new \Aimeos\MShop\Exception( $msg );
191
		}
192
193
		$conn = $this->context()->db( $this->getResourceName() );
194
195
		$total = null;
196
		$cols = $map = [];
197
		$search = clone $search;
198
		$search->slice( $search->getOffset(), min( $search->getLimit(), $limit ) );
199
200
		$level = \Aimeos\MShop\Locale\Manager\Base::SITE_ALL;
201
		$attrList = array_filter( $this->object()->getSearchAttributes(), function( $item ) {
202
			return $item->isPublic() || strncmp( $item->getCode(), 'agg:', 4 ) === 0;
203
		} );
204
205
		if( $value === null && ( $value = key( $attrList ) ) === null )
206
		{
207
			$msg = $this->context()->translate( 'mshop', 'No search keys available' );
208
			throw new \Aimeos\MShop\Exception( $msg );
209
		}
210
211
		if( ( $pos = strpos( $valkey = $value, '(' ) ) !== false ) {
212
			$value = substr( $value, 0, $pos );
213
		}
214
215
		if( !isset( $attrList[$value] ) )
216
		{
217
			$msg = $this->context()->translate( 'mshop', 'Unknown search key "%1$s"' );
218
			throw new \Aimeos\MShop\Exception( sprintf( $msg, $value ) );
219
		}
220
221
		foreach( $keys as $string )
222
		{
223
			if( !isset( $attrList[$string] ) )
224
			{
225
				$msg = $this->context()->translate( 'mshop', 'Unknown search key "%1$s"' );
226
				throw new \Aimeos\MShop\Exception( sprintf( $msg, $string ) );
227
			}
228
229
			$cols[] = $attrList[$string]->getInternalCode();
230
			$acols[] = $attrList[$string]->getInternalCode() . ' AS "' . $string . '"';
231
232
			/** @todo Required to get the joins, but there should be a better way */
233
			$search->add( [$string => null], '!=' );
234
		}
235
		$search->add( [$valkey => null], '!=' );
236
237
		$sql = $this->getSqlConfig( $cfgPath );
238
		$sql = str_replace( ':cols', join( ', ', $cols ), $sql );
239
		$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 221. Are you sure the iterator is never empty, otherwise this variable is not defined?
Loading history...
240
		$sql = str_replace( ':keys', '"' . join( '", "', $keys ) . '"', $sql );
241
		$sql = str_replace( ':val', $attrList[$value]->getInternalCode(), $sql );
242
		$sql = str_replace( ':type', in_array( $type, ['avg', 'count', 'max', 'min', 'sum'] ) ? $type : 'count', $sql );
243
244
		$results = $this->searchItemsBase( $conn, $search, $sql, '', $required, $total, $level );
245
246
		while( ( $row = $results->fetch() ) !== null )
247
		{
248
			$row = $this->transform( $row );
249
250
			$temp = &$map;
251
			$last = array_pop( $row );
252
253
			foreach( $row as $val ) {
254
				$temp[$val] = $temp[$val] ?? [];
255
				$temp = &$temp[$val];
256
			}
257
			$temp = $last;
258
		}
259
260
		return map( $map );
261
	}
262
263
264
	/**
265
	 * Removes old entries from the storage.
266
	 *
267
	 * @param iterable $siteids List of IDs for sites whose entries should be deleted
268
	 * @param string $cfgpath Configuration key to the cleanup statement
269
	 * @return \Aimeos\MShop\Common\Manager\Iface Manager object for chaining method calls
270
	 */
271
	protected function clearBase( iterable $siteids, string $cfgpath ) : \Aimeos\MShop\Common\Manager\Iface
272
	{
273
		if( empty( $siteids ) ) {
274
			return $this;
275
		}
276
277
		$conn = $this->context()->db( $this->getResourceName() );
278
279
		$sql = $this->getSqlConfig( $cfgpath );
280
		$sql = str_replace( ':cond', '1=1', $sql );
281
282
		$stmt = $conn->create( $sql );
283
284
		foreach( $siteids as $siteid )
285
		{
286
			$stmt->bind( 1, $siteid );
287
			$stmt->execute()->finish();
288
		}
289
290
		return $this;
291
	}
292
293
294
	/**
295
	 * Deletes items.
296
	 *
297
	 * @param \Aimeos\MShop\Common\Item\Iface|\Aimeos\Map|array|string $items List of item objects or IDs of the items
298
	 * @param string $cfgpath Configuration path to the SQL statement
299
	 * @param bool $siteid If siteid should be used in the statement
300
	 * @param string $name Name of the ID column
301
	 * @return \Aimeos\MShop\Common\Manager\Iface Manager object for chaining method calls
302
	 */
303
	protected function deleteItemsBase( $items, string $cfgpath, bool $siteid = true,
304
		string $name = 'id' ) : \Aimeos\MShop\Common\Manager\Iface
305
	{
306
		if( map( $items )->isEmpty() ) {
307
			return $this;
308
		}
309
310
		$search = $this->object()->filter();
311
		$search->setConditions( $search->compare( '==', $name, $items ) );
312
313
		$types = array( $name => \Aimeos\Base\DB\Statement\Base::PARAM_STR );
314
		$translations = array( $name => '"' . $name . '"' );
315
316
		$cond = $search->getConditionSource( $types, $translations );
317
		$sql = str_replace( ':cond', $cond, $this->getSqlConfig( $cfgpath ) );
318
319
		$context = $this->context();
320
		$conn = $context->db( $this->getResourceName() );
321
322
		$stmt = $conn->create( $sql );
323
324
		if( $siteid ) {
325
			$stmt->bind( 1, $context->locale()->getSiteId() . '%' );
326
		}
327
328
		$stmt->execute()->finish();
329
330
		return $this;
331
	}
332
333
334
	/**
335
	 * Returns a sorted list of required criteria keys.
336
	 *
337
	 * @param \Aimeos\Base\Criteria\Iface $criteria Search criteria object
338
	 * @param string[] $required List of prefixes of required search conditions
339
	 * @return string[] Sorted list of criteria keys
340
	 */
341
	protected function getCriteriaKeyList( \Aimeos\Base\Criteria\Iface $criteria, array $required ) : array
342
	{
343
		$keys = array_merge( $required, $this->getCriteriaKeys( $required, $criteria->getConditions() ) );
344
345
		foreach( $criteria->getSortations() as $sortation ) {
346
			$keys = array_merge( $keys, $this->getCriteriaKeys( $required, $sortation ) );
347
		}
348
349
		$keys = array_unique( array_merge( $required, $keys ) );
350
		sort( $keys );
351
352
		return $keys;
353
	}
354
355
356
	/**
357
	 * Returns the name of the resource.
358
	 *
359
	 * @return string Name of the resource, e.g. "db-product"
360
	 */
361
	protected function getResourceName() : string
362
	{
363
		if( $this->resourceName === null ) {
364
			$this->setResourceName( 'db-' . $this->getDomain() );
365
		}
366
367
		return $this->resourceName;
368
	}
369
370
371
	/**
372
	 * Sets the name of the database resource that should be used.
373
	 *
374
	 * @param string $name Name of the resource
375
	 * @return \Aimeos\MShop\Common\Manager\Iface Manager object for chaining method calls
376
	 */
377
	protected function setResourceName( string $name ) : \Aimeos\MShop\Common\Manager\Iface
378
	{
379
		$config = $this->context()->config();
380
381
		if( $config->get( 'resource/' . $name ) === null ) {
382
			$this->resourceName = $config->get( 'resource/default', 'db' );
383
		} else {
384
			$this->resourceName = $name;
385
		}
386
387
		return $this;
388
	}
389
390
391
	/**
392
	 * Returns the search attribute objects used for searching.
393
	 *
394
	 * @param array $list Associative list of search keys and the lists of search definitions
395
	 * @param string $path Configuration path to the sub-domains for fetching the search definitions
396
	 * @param string[] $default List of sub-domains if no others are configured
397
	 * @param bool $withsub True to include search definitions of sub-domains, false if not
398
	 * @return \Aimeos\Base\Criteria\Attribute\Iface[] Associative list of search keys and criteria attribute items as values
399
	 * @since 2014.09
400
	 */
401
	protected function getSearchAttributesBase( array $list, string $path, array $default, bool $withsub ) : array
402
	{
403
		$attr = $this->createAttributes( $list );
404
405
		if( $withsub === true )
406
		{
407
			$domains = $this->context()->config()->get( $path, $default );
408
409
			foreach( $domains as $domain ) {
410
				$attr += $this->object()->getSubManager( $domain )->getSearchAttributes( true );
411
			}
412
		}
413
414
		return $attr;
415
	}
416
417
418
	/**
419
	 * Returns the search results for the given SQL statement.
420
	 *
421
	 * @param \Aimeos\Base\DB\Connection\Iface $conn Database connection
422
	 * @param string $sql SQL statement
423
	 * @return \Aimeos\Base\DB\Result\Iface Search result object
424
	 */
425
	protected function getSearchResults( \Aimeos\Base\DB\Connection\Iface $conn, string $sql ) : \Aimeos\Base\DB\Result\Iface
426
	{
427
		$time = microtime( true );
428
429
		$stmt = $conn->create( $sql );
430
		$result = $stmt->execute();
431
432
		$level = \Aimeos\Base\Logger\Iface::DEBUG;
433
		$time = ( microtime( true ) - $time ) * 1000;
434
		$msg = 'Time: ' . $time . "ms\n"
435
			. 'Class: ' . get_class( $this ) . "\n"
436
			. str_replace( ["\t", "\n\n"], ['', "\n"], trim( (string) $stmt ) );
437
438
		if( $time > 1000.0 )
439
		{
440
			$level = \Aimeos\Base\Logger\Iface::NOTICE;
441
			$msg .= "\n" . ( new \Exception() )->getTraceAsString();
442
		}
443
444
		$this->context()->logger()->log( $msg, $level, 'core/sql' );
445
446
		return $result;
447
	}
448
449
450
	/**
451
	 * Returns the site coditions for the search request
452
	 *
453
	 * @param string[] $keys Sorted list of criteria keys
454
	 * @param \Aimeos\Base\Criteria\Attribute\Iface[] $attributes Associative list of search keys and criteria attribute items as values
455
	 * @param int $sitelevel Site level constant from \Aimeos\MShop\Locale\Manager\Base
456
	 * @return \Aimeos\Base\Criteria\Expression\Iface[] List of search conditions
457
	 * @since 2015.01
458
	 */
459
	protected function getSiteConditions( array $keys, array $attributes, int $sitelevel ) : array
460
	{
461
		$list = [];
462
463
		foreach( $keys as $key )
464
		{
465
			$name = $key . '.siteid';
466
467
			if( isset( $attributes[$name] ) ) {
468
				$list[] = $this->siteCondition( $name, $sitelevel );
469
			}
470
		}
471
472
		return $list;
473
	}
474
475
476
	/**
477
	 * Returns the SQL statement for the given config path
478
	 *
479
	 * If available, the database specific SQL statement is returned, otherwise
480
	 * the ANSI SQL statement. The database type is determined via the resource
481
	 * adapter.
482
	 *
483
	 * @param string $path Configuration path to the SQL statement
484
	 * @return array|string ANSI or database specific SQL statement
485
	 */
486
	protected function getSqlConfig( string $path )
487
	{
488
		$config = $this->context()->config();
489
		$adapter = $config->get( 'resource/' . $this->getResourceName() . '/adapter' );
490
491
		if( $sql = $config->get( $path . '/' . $adapter, $config->get( $path . '/ansi' ) ) ) {
492
			return str_replace( ':table', $this->getTable(), $sql );
493
		}
494
495
		$parts = explode( '/', $path );
496
		$cpath = 'mshop/common/manager/' . end( $parts );
497
		$sql = $config->get( $cpath . '/' . $adapter, $config->get( $cpath . '/ansi', $path ) );
498
499
		return str_replace( ':table', $this->getTable(), $sql );
500
	}
501
502
503
	/**
504
	 * Returns the available sub-manager names
505
	 *
506
	 * @return array Sub-manager names, e.g. ['lists', 'property', 'type']
507
	 */
508
	protected function getSubManagers() : array
509
	{
510
		return $this->context()->config()->get( $this->getConfigKey( 'submanagers' ), [] );
511
	}
512
513
514
	/**
515
	 * Sets the base criteria "status".
516
	 * (setConditions overwrites the base criteria)
517
	 *
518
	 * @param string $domain Name of the domain/sub-domain like "product" or "product.list"
519
	 * @param bool|null $default TRUE for status=1, NULL for status>0, FALSE for no restriction
520
	 * @return \Aimeos\Base\Criteria\Iface Search critery object
521
	 */
522
	protected function filterBase( string $domain, ?bool $default = false ) : \Aimeos\Base\Criteria\Iface
523
	{
524
		$context = $this->context();
525
		$db = $this->getResourceName();
526
		$conn = $context->db( $db );
527
		$config = $context->config();
528
529
		if( ( $adapter = $config->get( 'resource/' . $db . '/adapter' ) ) === null ) {
530
			$adapter = $config->get( 'resource/db/adapter' );
531
		}
532
533
		switch( $adapter )
534
		{
535
			case 'pgsql':
536
				$filter = new \Aimeos\Base\Criteria\PgSQL( $conn ); break;
537
			default:
538
				$filter = new \Aimeos\Base\Criteria\SQL( $conn ); break;
539
		}
540
541
		if( $default !== false ) {
542
			$filter->add( $domain . '.status', $default ? '==' : '>=', 1 );
543
		}
544
545
		return $filter;
546
	}
547
548
549
	/**
550
	 * Returns the item for the given search key/value pairs.
551
	 *
552
	 * @param array $pairs Search key/value pairs for the item
553
	 * @param string[] $ref List of domains whose items should be fetched too
554
	 * @param bool|null $default Add default criteria or NULL for relaxed default criteria
555
	 * @return \Aimeos\MShop\Common\Item\Iface Requested item
556
	 * @throws \Aimeos\MShop\Exception if no item with the given ID found
557
	 */
558
	protected function findBase( array $pairs, array $ref, ?bool $default ) : \Aimeos\MShop\Common\Item\Iface
559
	{
560
		$expr = [];
561
		$criteria = $this->object()->filter( $default )->slice( 0, 1 );
562
563
		foreach( $pairs as $key => $value )
564
		{
565
			if( $value === null )
566
			{
567
				$msg = $this->context()->translate( 'mshop', 'Required value for "%1$s" is missing' );
568
				throw new \Aimeos\MShop\Exception( sprintf( $msg, $key ) );
569
			}
570
			$expr[] = $criteria->compare( '==', $key, $value );
571
		}
572
573
		$criteria->setConditions( $criteria->and( $expr ) );
574
575
		if( ( $item = $this->object()->search( $criteria, $ref )->first() ) ) {
576
			return $item;
577
		}
578
579
		$msg = $this->context()->translate( 'mshop', 'No item found for conditions: %1$s' );
580
		throw new \Aimeos\MShop\Exception( sprintf( $msg, print_r( $pairs, true ) ), 404 );
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

580
		throw new \Aimeos\MShop\Exception( sprintf( $msg, /** @scrutinizer ignore-type */ print_r( $pairs, true ) ), 404 );
Loading history...
581
	}
582
583
584
	/**
585
	 * Returns the cached statement for the given key or creates a new prepared statement.
586
	 * If no SQL string is given, the key is used to retrieve the SQL string from the configuration.
587
	 *
588
	 * @param \Aimeos\Base\DB\Connection\Iface $conn Database connection
589
	 * @param string $cfgkey Unique key for the SQL
590
	 * @param string|null $sql SQL string if it shouldn't be retrieved from the configuration
591
	 * @return \Aimeos\Base\DB\Statement\Iface Database statement object
592
	 */
593
	protected function getCachedStatement( \Aimeos\Base\DB\Connection\Iface $conn, string $cfgkey,
594
		string $sql = null ) : \Aimeos\Base\DB\Statement\Iface
595
	{
596
		if( !isset( $this->cachedStmts['stmt'][$cfgkey] )
597
			|| !isset( $this->cachedStmts['conn'][$cfgkey] )
598
			|| $conn !== $this->cachedStmts['conn'][$cfgkey]
599
		) {
600
			if( $sql === null ) {
601
				$sql = $this->getSqlConfig( $cfgkey );
602
			}
603
604
			$this->cachedStmts['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\Base\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

604
			$this->cachedStmts['stmt'][$cfgkey] = $conn->create( /** @scrutinizer ignore-type */ $sql );
Loading history...
605
			$this->cachedStmts['conn'][$cfgkey] = $conn;
606
		}
607
608
		return $this->cachedStmts['stmt'][$cfgkey];
609
	}
610
611
612
	/**
613
	 * Returns the item for the given search key and ID.
614
	 *
615
	 * @param string $key Search key for the requested ID
616
	 * @param string $id Unique ID to search for
617
	 * @param string[] $ref List of domains whose items should be fetched too
618
	 * @param bool|null $default Add default criteria or NULL for relaxed default criteria
619
	 * @return \Aimeos\MShop\Common\Item\Iface Requested item
620
	 * @throws \Aimeos\MShop\Exception if no item with the given ID found
621
	 */
622
	protected function getItemBase( string $key, string $id, array $ref, ?bool $default ) : \Aimeos\MShop\Common\Item\Iface
623
	{
624
		$criteria = $this->object()->filter( $default )->add( [$key => $id] )->slice( 0, 1 );
625
626
		if( ( $item = $this->object()->search( $criteria, $ref )->first() ) ) {
627
			return $item;
628
		}
629
630
		$msg = $this->context()->translate( 'mshop', 'Item with ID "%2$s" in "%1$s" not found' );
631
		throw new \Aimeos\MShop\Exception( sprintf( $msg, $key, $id ), 404 );
632
	}
633
634
635
	/**
636
	 * Returns the SQL strings for joining dependent tables.
637
	 *
638
	 * @param \Aimeos\Base\Criteria\Attribute\Iface[] $attributes List of criteria attribute items
639
	 * @param string $prefix Search key prefix
640
	 * @return array List of JOIN SQL strings
641
	 */
642
	private function getJoins( array $attributes, string $prefix ) : array
643
	{
644
		$iface = \Aimeos\Base\Criteria\Attribute\Iface::class;
645
		$name = $prefix . '.id';
646
647
		if( isset( $attributes[$prefix] ) && $attributes[$prefix] instanceof $iface ) {
648
			return $attributes[$prefix]->getInternalDeps();
649
		}
650
		elseif( isset( $attributes[$name] ) && $attributes[$name] instanceof $iface ) {
651
			return $attributes[$name]->getInternalDeps();
652
		}
653
		else if( isset( $attributes['id'] ) && $attributes['id'] instanceof $iface ) {
654
			return $attributes['id']->getInternalDeps();
655
		}
656
657
		return [];
658
	}
659
660
661
	/**
662
	 * Returns the available manager types
663
	 *
664
	 * @param string $type Main manager type
665
	 * @param string $path Configuration path to the sub-domains
666
	 * @param string[] $default List of sub-domains if no others are configured
667
	 * @param bool $withsub Return also the resource type of sub-managers if true
668
	 * @return string[] Type of the manager and submanagers, subtypes are separated by slashes
669
	 */
670
	protected function getResourceTypeBase( string $type, string $path, array $default, bool $withsub ) : array
671
	{
672
		$list = [$type];
673
674
		if( $withsub )
675
		{
676
			foreach( $this->context()->config()->get( $path, $default ) as $domain ) {
677
				$list = array_merge( $list, $this->object()->getSubManager( $domain )->getResourceType( $withsub ) );
678
			}
679
		}
680
681
		return $list;
682
	}
683
684
685
	/**
686
	 * Returns a search object singleton
687
	 *
688
	 * @return \Aimeos\Base\Criteria\Iface Search object
689
	 */
690
	protected function getSearch() : \Aimeos\Base\Criteria\Iface
691
	{
692
		if( !isset( $this->search ) ) {
693
			$this->search = $this->filter();
0 ignored issues
show
Bug introduced by
The method filter() does not exist on Aimeos\MShop\Common\Manager\DB. Did you maybe mean filterBase()? ( Ignorable by Annotation )

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

693
			/** @scrutinizer ignore-call */ 
694
   $this->search = $this->filter();

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
694
		}
695
696
		return $this->search;
697
	}
698
699
700
	/**
701
	 * Returns the string replacements for the SQL statements
702
	 *
703
	 * @param \Aimeos\Base\Criteria\Iface $search Search critera object
704
	 * @param \Aimeos\Base\Criteria\Attribute\Iface[] $attributes Associative list of search keys and criteria attribute items as values
705
	 * @param \Aimeos\Base\Criteria\Plugin\Iface[] $plugins Associative list of search keys and criteria plugin items as values
706
	 * @param string[] $joins Associative list of SQL joins
707
	 * @param \Aimeos\Base\Criteria\Attribute\Iface[] $columns Additional columns to retrieve values from
708
	 * @return array Array of keys, find and replace arrays
709
	 */
710
	protected function getSQLReplacements( \Aimeos\Base\Criteria\Iface $search, array $attributes, array $plugins,
711
		array $joins, array $columns = [] ) : array
712
	{
713
		$types = $this->getSearchTypes( $attributes );
714
		$funcs = $this->getSearchFunctions( $attributes );
715
		$translations = $this->getSearchTranslations( $attributes );
716
717
		$colstring = '';
718
		foreach( $columns as $name => $entry ) {
719
			$colstring .= $entry->getInternalCode() . ', ';
720
		}
721
722
		$find = array( ':columns', ':joins', ':cond', ':start', ':size' );
723
		$replace = array(
724
			$colstring,
725
			implode( "\n", array_unique( $joins ) ),
726
			$search->getConditionSource( $types, $translations, $plugins, $funcs ),
727
			$search->getOffset(),
728
			$search->getLimit(),
729
		);
730
731
		if( empty( $search->getSortations() ) && ( $attribute = reset( $attributes ) ) !== false ) {
732
			$search = ( clone $search )->setSortations( [$search->sort( '+', $attribute->getCode() )] );
733
		}
734
735
		$find[] = ':order';
736
		$replace[] = $search->getSortationSource( $types, $translations, $funcs );
737
738
		$find[] = ':group';
739
		$replace[] = implode( ', ', $search->translate( $search->getSortations(), $translations, $funcs ) ) . ', ';
740
741
		return [$find, $replace];
742
	}
743
744
745
	/**
746
	 * Returns the newly created ID for the last record which was inserted.
747
	 *
748
	 * @param \Aimeos\Base\DB\Connection\Iface $conn Database connection used to insert the new record
749
	 * @param string $cfgpath Configuration path to the SQL statement for retrieving the new ID of the last inserted record
750
	 * @return string ID of the last record that was inserted by using the given connection
751
	 * @throws \Aimeos\MShop\Exception if there's no ID of the last record available
752
	 */
753
	protected function newId( \Aimeos\Base\DB\Connection\Iface $conn, string $cfgpath ) : string
754
	{
755
		$sql = $this->getSqlConfig( $cfgpath );
756
757
		$result = $conn->create( $sql )->execute();
0 ignored issues
show
Bug introduced by
It seems like $sql can also be of type array; however, parameter $sql of Aimeos\Base\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

757
		$result = $conn->create( /** @scrutinizer ignore-type */ $sql )->execute();
Loading history...
758
759
		if( ( $row = $result->fetch( \Aimeos\Base\DB\Result\Base::FETCH_NUM ) ) === false )
760
		{
761
			$msg = $this->context()->translate( 'mshop', 'ID of last inserted database record not available' );
762
			throw new \Aimeos\MShop\Exception( $msg );
763
		}
764
		$result->finish();
765
766
		return $row[0];
767
	}
768
769
770
	/**
771
	 * Returns the search result of the statement combined with the given criteria.
772
	 *
773
	 * @param \Aimeos\Base\DB\Connection\Iface $conn Database connection
774
	 * @param \Aimeos\Base\Criteria\Iface $search Search criteria object
775
	 * @param string $cfgPathSearch Path to SQL statement in configuration for searching
776
	 * @param string $cfgPathCount Path to SQL statement in configuration for counting
777
	 * @param string[] $required Additional search keys to add conditions for even if no conditions are available
778
	 * @param int|null $total Contains the number of all records matching the criteria if not null
779
	 * @param int $sitelevel Constant from \Aimeos\MShop\Locale\Manager\Base for defining which site IDs should be used for searching
780
	 * @param \Aimeos\Base\Criteria\Plugin\Iface[] $plugins Associative list of search keys and criteria plugin items as values
781
	 * @return \Aimeos\Base\DB\Result\Iface SQL result object for accessing the found records
782
	 * @throws \Aimeos\MShop\Exception if no number of all matching records is available
783
	 */
784
	protected function searchItemsBase( \Aimeos\Base\DB\Connection\Iface $conn, \Aimeos\Base\Criteria\Iface $search,
785
		string $cfgPathSearch, string $cfgPathCount, array $required, int &$total = null,
786
		int $sitelevel = \Aimeos\MShop\Locale\Manager\Base::SITE_ALL, array $plugins = [] ) : \Aimeos\Base\DB\Result\Iface
787
	{
788
		$joins = [];
789
		$conditions = $search->getConditions();
790
		$columns = $this->object()->getSaveAttributes();
791
		$attributes = $this->object()->getSearchAttributes();
792
		$keys = $this->getCriteriaKeyList( $search, $required );
793
794
		$basekey = array_shift( $required );
795
796
		foreach( $keys as $key )
797
		{
798
			if( $key !== $basekey ) {
799
				$joins = array_merge( $joins, $this->getJoins( $attributes, $key ) );
800
			}
801
		}
802
803
		$joins = array_unique( $joins );
804
		$cond = $this->getSiteConditions( $keys, $attributes, $sitelevel );
805
806
		if( $conditions !== null ) {
807
			$cond[] = $conditions;
808
		}
809
810
		$search = clone $search;
811
		$search->setConditions( $search->and( $cond ) );
812
813
		list( $find, $replace ) = $this->getSQLReplacements( $search, $attributes, $plugins, $joins, $columns );
814
815
		if( $total !== null )
816
		{
817
			$sql = str_replace( $find, $replace, $this->getSqlConfig( $cfgPathCount ) );
818
			$result = $this->getSearchResults( $conn, $sql );
819
			$row = $result->fetch();
820
			$result->finish();
821
822
			if( $row === null )
823
			{
824
				$msg = $this->context()->translate( 'mshop', 'Total results value not found' );
825
				throw new \Aimeos\MShop\Exception( $msg );
826
			}
827
828
			$total = (int) $row['count'];
829
		}
830
831
		return $this->getSearchResults( $conn, str_replace( $find, $replace, $this->getSqlConfig( $cfgPathSearch ) ) );
832
	}
833
834
835
	/**
836
	 * Saves an attribute item to the storage.
837
	 *
838
	 * @param \Aimeos\MShop\Common\Item\Iface $item Item object
839
	 * @param bool $fetch True if the new ID should be returned in the item
840
	 * @return \Aimeos\MShop\Common\Item\Iface $item Updated item including the generated ID
841
	 */
842
	protected function saveBase( \Aimeos\MShop\Common\Item\Iface $item, bool $fetch = true ) : \Aimeos\MShop\Common\Item\Iface
843
	{
844
		if( !$item->isModified() ) {
845
			return $item;
846
		}
847
848
		$context = $this->context();
849
		$conn = $context->db( $this->getResourceName() );
850
851
		$id = $item->getId();
852
		$date = date( 'Y-m-d H:i:s' );
853
		$columns = $this->object()->getSaveAttributes();
854
855
		if( $id === null )
856
		{
857
			/** mshop/common/manager/insert/mysql
858
			 * Inserts a new record into the database table
859
			 *
860
			 * @see mshop/common/manager/insert/ansi
861
			 */
862
863
			/** mshop/common/manager/insert/ansi
864
			 * Inserts a new record into the database table
865
			 *
866
			 * Items with no ID yet (i.e. the ID is NULL) will be created in
867
			 * the database and the newly created ID retrieved afterwards
868
			 * using the "newid" SQL statement.
869
			 *
870
			 * The SQL statement must be a string suitable for being used as
871
			 * prepared statement. It must include question marks for binding
872
			 * the values from the item to the statement before they are
873
			 * sent to the database server. The number of question marks must
874
			 * be the same as the number of columns listed in the INSERT
875
			 * statement. The order of the columns must correspond to the
876
			 * order in the save() method, so the correct values are
877
			 * bound to the columns.
878
			 *
879
			 * The SQL statement should conform to the ANSI standard to be
880
			 * compatible with most relational database systems. This also
881
			 * includes using double quotes for table and column names.
882
			 *
883
			 * @param string SQL statement for inserting records
884
			 * @since 2023.10
885
			 * @category Developer
886
			 * @see mshop/common/manager/update/ansi
887
			 * @see mshop/common/manager/newid/ansi
888
			 * @see mshop/common/manager/delete/ansi
889
			 * @see mshop/common/manager/search/ansi
890
			 * @see mshop/common/manager/count/ansi
891
			 */
892
			$path = $this->getConfigKey( 'insert' );
893
			$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\DB::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

893
			$sql = $this->addSqlColumns( array_keys( $columns ), /** @scrutinizer ignore-type */ $this->getSqlConfig( $path ) );
Loading history...
894
		}
895
		else
896
		{
897
			/** mshop/common/manager/update/mysql
898
			 * Updates an existing record in the database
899
			 *
900
			 * @see mshop/common/manager/update/ansi
901
			 */
902
903
			/** mshop/common/manager/update/ansi
904
			 * Updates an existing record in the database
905
			 *
906
			 * Items which already have an ID (i.e. the ID is not NULL) will
907
			 * be updated in the database.
908
			 *
909
			 * The SQL statement must be a string suitable for being used as
910
			 * prepared statement. It must include question marks for binding
911
			 * the values from the item to the statement before they are
912
			 * sent to the database server. The order of the columns must
913
			 * correspond to the order in the save() method, so the
914
			 * correct values are bound to the columns.
915
			 *
916
			 * The SQL statement should conform to the ANSI standard to be
917
			 * compatible with most relational database systems. This also
918
			 * includes using double quotes for table and column names.
919
			 *
920
			 * @param string SQL statement for updating records
921
			 * @since 2023.10
922
			 * @category Developer
923
			 * @see mshop/common/manager/insert/ansi
924
			 * @see mshop/common/manager/newid/ansi
925
			 * @see mshop/common/manager/delete/ansi
926
			 * @see mshop/common/manager/search/ansi
927
			 * @see mshop/common/manager/count/ansi
928
			 */
929
			$path = $this->getConfigKey( 'update' );
930
			$sql = $this->addSqlColumns( array_keys( $columns ), $this->getSqlConfig( $path ), false );
931
		}
932
933
		$idx = 1;
934
		$stmt = $this->getCachedStatement( $conn, $path, $sql );
935
936
		foreach( $columns as $entry ) {
937
			$stmt->bind( $idx++, $item->get( $entry->getCode() ), \Aimeos\Base\Criteria\SQL::type( $entry->getType() ) );
938
		}
939
940
		$stmt->bind( $idx++, $date ); // mtime
941
		$stmt->bind( $idx++, $context->editor() );
942
943
		if( $id !== null ) {
944
			$stmt->bind( $idx++, $context->locale()->getSiteId() . '%' );
945
			$stmt->bind( $idx++, $id, \Aimeos\Base\DB\Statement\Base::PARAM_INT );
946
		} else {
947
			$stmt->bind( $idx++, $this->siteId( $item->getSiteId(), \Aimeos\MShop\Locale\Manager\Base::SITE_SUBTREE ) );
948
			$stmt->bind( $idx++, $date ); // ctime
949
		}
950
951
		$stmt->execute()->finish();
952
953
		if( $id === null )
954
		{
955
			/** mshop/common/manager/newid/mysql
956
			 * Retrieves the ID generated by the database when inserting a new record
957
			 *
958
			 * @see mshop/common/manager/newid/ansi
959
			 */
960
961
			/** mshop/common/manager/newid/ansi
962
			 * Retrieves the ID generated by the database when inserting a new record
963
			 *
964
			 * As soon as a new record is inserted into the database table,
965
			 * the database server generates a new and unique identifier for
966
			 * that record. This ID can be used for retrieving, updating and
967
			 * deleting that specific record from the table again.
968
			 *
969
			 * For MySQL:
970
			 *  SELECT LAST_INSERT_ID()
971
			 * For PostgreSQL:
972
			 *  SELECT currval('seq_matt_id')
973
			 * For SQL Server:
974
			 *  SELECT SCOPE_IDENTITY()
975
			 * For Oracle:
976
			 *  SELECT "seq_matt_id".CURRVAL FROM DUAL
977
			 *
978
			 * There's no way to retrive the new ID by a SQL statements that
979
			 * fits for most database servers as they implement their own
980
			 * specific way.
981
			 *
982
			 * @param string SQL statement for retrieving the last inserted record ID
983
			 * @since 2023.10
984
			 * @category Developer
985
			 * @see mshop/common/manager/insert/ansi
986
			 * @see mshop/common/manager/update/ansi
987
			 * @see mshop/common/manager/delete/ansi
988
			 * @see mshop/common/manager/search/ansi
989
			 * @see mshop/common/manager/count/ansi
990
			 */
991
			$id = $this->newId( $conn, 'mshop/common/manager/newid' );
992
		}
993
994
		return $item->setId( $id );
995
	}
996
997
998
	/**
999
	 * Replaces the given marker with an expression
1000
	 *
1001
	 * @param string $column Name (including alias) of the column
1002
	 * @param mixed $value Value used in the expression
1003
	 * @param string $op Operator used in the expression
1004
	 * @param int $type Type constant from \Aimeos\Base\DB\Statement\Base class
1005
	 * @return string Created expression
1006
	 */
1007
	protected function toExpression( string $column, $value, string $op = '==',
1008
		int $type = \Aimeos\Base\DB\Statement\Base::PARAM_STR ) : string
1009
	{
1010
		$types = ['marker' => $type];
1011
		$translations = ['marker' => $column];
1012
		$value = ( is_array( $value ) ? array_unique( $value ) : $value );
1013
1014
		return $this->getSearch()->compare( $op, 'marker', $value )->toSource( $types, $translations );
1015
	}
1016
1017
1018
	/**
1019
	 * Transforms the application specific values to Aimeos standard values.
1020
	 *
1021
	 * @param array $values Associative list of key/value pairs from the storage
1022
	 * @return array Associative list of key/value pairs with standard Aimeos values
1023
	 */
1024
	protected function transform( array $values ) : array
1025
	{
1026
		return $values;
1027
	}
1028
1029
1030
	/**
1031
	 * Cuts the last part separated by a dot repeatedly and returns the list of resulting string.
1032
	 *
1033
	 * @param string[] $prefix Required base prefixes of the search keys
1034
	 * @param string $string String containing parts separated by dots
1035
	 * @return array List of resulting strings
1036
	 */
1037
	private function cutNameTail( array $prefix, string $string ) : array
1038
	{
1039
		$result = [];
1040
		$noprefix = true;
1041
		$strlen = strlen( $string );
1042
1043
		foreach( $prefix as $key )
1044
		{
1045
			$len = strlen( $key );
1046
1047
			if( strncmp( $string, $key, $len ) === 0 )
1048
			{
1049
				if( $strlen > $len && ( $pos = strrpos( $string, '.' ) ) !== false )
1050
				{
1051
					$result[] = $string = substr( $string, 0, $pos );
1052
					$result = array_merge( $result, $this->cutNameTail( $prefix, $string ) );
1053
					$noprefix = false;
1054
				}
1055
1056
				break;
1057
			}
1058
		}
1059
1060
		if( $noprefix )
1061
		{
1062
			if( ( $pos = strrpos( $string, ':' ) ) !== false ) {
1063
				$result[] = substr( $string, 0, $pos );
1064
				$result[] = $string;
1065
			} elseif( ( $pos = strrpos( $string, '.' ) ) !== false ) {
1066
				$result[] = substr( $string, 0, $pos );
1067
			} else {
1068
				$result[] = $string;
1069
			}
1070
		}
1071
1072
		return $result;
1073
	}
1074
1075
1076
	/**
1077
	 * Returns a list of unique criteria names shortend by the last element after the ''
1078
	 *
1079
	 * @param string[] $prefix Required base prefixes of the search keys
1080
	 * @param \Aimeos\Base\Criteria\Expression\Iface|null $expr Criteria object
1081
	 * @return array List of shortend criteria names
1082
	 */
1083
	private function getCriteriaKeys( array $prefix, \Aimeos\Base\Criteria\Expression\Iface $expr = null ) : array
1084
	{
1085
		if( $expr === null ) { return []; }
1086
1087
		$result = [];
1088
1089
		foreach( $this->getCriteriaNames( $expr ) as $item )
1090
		{
1091
			if( strncmp( $item, 'sort:', 5 ) === 0 ) {
1092
				$item = substr( $item, 5 );
1093
			}
1094
1095
			if( ( $pos = strpos( $item, '(' ) ) !== false ) {
1096
				$item = substr( $item, 0, $pos );
1097
			}
1098
1099
			$result = array_merge( $result, $this->cutNameTail( $prefix, $item ) );
1100
		}
1101
1102
		return $result;
1103
	}
1104
1105
1106
	/**
1107
	 * Returns a list of criteria names from a expression and its sub-expressions.
1108
	 *
1109
	 * @param \Aimeos\Base\Criteria\Expression\Iface Criteria object
0 ignored issues
show
Bug introduced by
The type Aimeos\MShop\Common\Manager\Criteria was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
1110
	 * @return array List of criteria names
1111
	 */
1112
	private function getCriteriaNames( \Aimeos\Base\Criteria\Expression\Iface $expr ) : array
1113
	{
1114
		if( $expr instanceof \Aimeos\Base\Criteria\Expression\Compare\Iface ) {
1115
			return array( $expr->getName() );
1116
		}
1117
1118
		if( $expr instanceof \Aimeos\Base\Criteria\Expression\Combine\Iface )
1119
		{
1120
			$list = [];
1121
			foreach( $expr->getExpressions() as $item ) {
1122
				$list = array_merge( $list, $this->getCriteriaNames( $item ) );
1123
			}
1124
			return $list;
1125
		}
1126
1127
		if( $expr instanceof \Aimeos\Base\Criteria\Expression\Sort\Iface ) {
1128
			return array( $expr->getName() );
1129
		}
1130
1131
		return [];
1132
	}
1133
}
1134