Passed
Push — master ( 5a0518...247ea4 )
by Aimeos
14:19
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 ?string $resourceName = null;
23
	private array $cachedStmts = [];
24
25
26
	/**
27
	 * Returns the context object.
28
	 *
29
	 * @return \Aimeos\MShop\ContextIface Context object
30
	 */
31
	abstract protected function context() : \Aimeos\MShop\ContextIface;
32
33
34
	/**
35
	 * Returns the attribute helper functions for searching defined by the manager.
36
	 *
37
	 * @param \Aimeos\Base\Criteria\Attribute\Iface[] $attributes List of search attribute items
38
	 * @return array Associative array of attribute code and helper function
39
	 */
40
	abstract protected function getSearchFunctions( array $attributes ) : array;
41
42
43
	/**
44
	 * Returns the attribute translations for searching defined by the manager.
45
	 *
46
	 * @param \Aimeos\Base\Criteria\Attribute\Iface[] $attributes List of search attribute items
47
	 * @return array Associative array of attribute code and internal attribute code
48
	 */
49
	abstract protected function getSearchTranslations( array $attributes ) : array;
50
51
52
	/**
53
	 * Returns the attribute types for searching defined by the manager.
54
	 *
55
	 * @param \Aimeos\Base\Criteria\Attribute\Iface[] $attributes List of search attribute items
56
	 * @return array Associative array of attribute code and internal attribute type
57
	 */
58
	abstract protected function getSearchTypes( array $attributes ) : array;
59
60
61
	/**
62
	 * Returns the name of the used table
63
	 *
64
	 * @return string Table name e.g. "mshop_product_lists_type"
65
	 */
66
	abstract protected function getTable() : string;
67
68
69
	/**
70
	 * Returns the outmost decorator of the decorator stack
71
	 *
72
	 * @return \Aimeos\MShop\Common\Manager\Iface Outmost decorator object
73
	 */
74
	abstract protected function object() : \Aimeos\MShop\Common\Manager\Iface;
75
76
77
	/**
78
	 * Returns the site expression for the given name
79
	 *
80
	 * @param string $name Name of the site condition
81
	 * @param int $sitelevel Site level constant from \Aimeos\MShop\Locale\Manager\Base
82
	 * @return \Aimeos\Base\Criteria\Expression\Iface Site search condition
83
	 */
84
	abstract protected function siteCondition( string $name, int $sitelevel ) : \Aimeos\Base\Criteria\Expression\Iface;
85
86
87
	/**
88
	 * Adds additional column names to SQL statement
89
	 *
90
	 * @param string[] $columns List of column names
91
	 * @param string $sql Insert or update SQL statement
92
	 * @param bool $mode True for insert, false for update statement
93
	 * @return string Modified insert or update SQL statement
94
	 */
95
	protected function addSqlColumns( array $columns, string $sql, bool $mode = true ) : string
96
	{
97
		$names = $values = '';
98
99
		if( $mode )
100
		{
101
			foreach( $columns as $name ) {
102
				$names .= '"' . $name . '", '; $values .= '?, ';
103
			}
104
		}
105
		else
106
		{
107
			foreach( $columns as $name ) {
108
				$names .= '"' . $name . '" = ?, ';
109
			}
110
		}
111
112
		return str_replace( [':names', ':values'], [$names, $values], $sql );
113
	}
114
115
116
	/**
117
	 * Counts the number products that are available for the values of the given key.
118
	 *
119
	 * @param \Aimeos\Base\Criteria\Iface $search Search criteria
120
	 * @param array|string $keys Search key or list of keys for aggregation
121
	 * @param string $cfgPath Configuration key for the SQL statement
122
	 * @param string[] $required List of domain/sub-domain names like "catalog.index" that must be additionally joined
123
	 * @param string|null $value Search key for aggregating the value column
124
	 * @param string|null $type Type of aggregation, e.g.  "sum", "min", "max" or NULL for "count"
125
	 * @return \Aimeos\Map List of ID values as key and the number of counted products as value
126
	 * @todo 2018.01 Reorder Parameter list
127
	 */
128
	protected function aggregateBase( \Aimeos\Base\Criteria\Iface $search, $keys, string $cfgPath,
129
		array $required = [], string $value = null, string $type = null ) : \Aimeos\Map
130
	{
131
		/** mshop/common/manager/aggregate/limit
132
		 * Limits the number of records that are used when aggregating items
133
		 *
134
		 * As counting huge amount of records (several 10 000 records) takes a long time,
135
		 * the limit can cut down response times so the counts are available more quickly
136
		 * in the front-end and the server load is reduced.
137
		 *
138
		 * Using a low limit can lead to incorrect numbers if the amount of found items
139
		 * is very high. Approximate item counts are normally not a problem but it can
140
		 * lead to the situation that visitors see that no items are available despite
141
		 * the fact that there would be at least one.
142
		 *
143
		 * @param integer Number of records
144
		 * @since 2021.04
145
		 */
146
		$limit = $this->context()->config()->get( 'mshop/common/manager/aggregate/limit', 10000 );
147
		$keys = (array) $keys;
148
149
		if( !count( $keys ) )
150
		{
151
			$msg = $this->context()->translate( 'mshop', 'At least one key is required for aggregation' );
152
			throw new \Aimeos\MShop\Exception( $msg );
153
		}
154
155
		$conn = $this->context()->db( $this->getResourceName() );
156
157
		$total = null;
158
		$cols = $map = [];
159
		$search = clone $search;
160
		$search->slice( $search->getOffset(), min( $search->getLimit(), $limit ) );
161
162
		$level = \Aimeos\MShop\Locale\Manager\Base::SITE_ALL;
163
		$attrList = array_filter( $this->object()->getSearchAttributes(), function( $item ) {
164
			return $item->isPublic() || strncmp( $item->getCode(), 'agg:', 4 ) === 0;
165
		} );
166
167
		if( $value === null && ( $value = key( $attrList ) ) === null )
168
		{
169
			$msg = $this->context()->translate( 'mshop', 'No search keys available' );
170
			throw new \Aimeos\MShop\Exception( $msg );
171
		}
172
173
		if( ( $pos = strpos( $valkey = $value, '(' ) ) !== false ) {
174
			$value = substr( $value, 0, $pos );
175
		}
176
177
		if( !isset( $attrList[$value] ) )
178
		{
179
			$msg = $this->context()->translate( 'mshop', 'Unknown search key "%1$s"' );
180
			throw new \Aimeos\MShop\Exception( sprintf( $msg, $value ) );
181
		}
182
183
		foreach( $keys as $string )
184
		{
185
			if( !isset( $attrList[$string] ) )
186
			{
187
				$msg = $this->context()->translate( 'mshop', 'Unknown search key "%1$s"' );
188
				throw new \Aimeos\MShop\Exception( sprintf( $msg, $string ) );
189
			}
190
191
			$cols[] = $attrList[$string]->getInternalCode();
192
			$acols[] = $attrList[$string]->getInternalCode() . ' AS "' . $string . '"';
193
194
			/** @todo Required to get the joins, but there should be a better way */
195
			$search->add( [$string => null], '!=' );
196
		}
197
		$search->add( [$valkey => null], '!=' );
198
199
		$sql = $this->getSqlConfig( $cfgPath );
200
		$sql = str_replace( ':cols', join( ', ', $cols ), $sql );
201
		$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 183. Are you sure the iterator is never empty, otherwise this variable is not defined?
Loading history...
202
		$sql = str_replace( ':keys', '"' . join( '", "', $keys ) . '"', $sql );
203
		$sql = str_replace( ':val', $attrList[$value]->getInternalCode(), $sql );
204
		$sql = str_replace( ':type', in_array( $type, ['avg', 'count', 'max', 'min', 'sum'] ) ? $type : 'count', $sql );
205
206
		$results = $this->searchItemsBase( $conn, $search, $sql, '', $required, $total, $level );
207
208
		while( ( $row = $results->fetch() ) !== null )
209
		{
210
			$row = $this->transform( $row );
211
212
			$temp = &$map;
213
			$last = array_pop( $row );
214
215
			foreach( $row as $val ) {
216
				$temp[$val] = $temp[$val] ?? [];
217
				$temp = &$temp[$val];
218
			}
219
			$temp = $last;
220
		}
221
222
		return map( $map );
223
	}
224
225
226
	/**
227
	 * Removes old entries from the storage.
228
	 *
229
	 * @param iterable $siteids List of IDs for sites whose entries should be deleted
230
	 * @param string $cfgpath Configuration key to the cleanup statement
231
	 * @return \Aimeos\MShop\Common\Manager\Iface Manager object for chaining method calls
232
	 */
233
	protected function clearBase( iterable $siteids, string $cfgpath ) : \Aimeos\MShop\Common\Manager\Iface
234
	{
235
		if( empty( $siteids ) ) {
236
			return $this;
237
		}
238
239
		$conn = $this->context()->db( $this->getResourceName() );
240
241
		$sql = $this->getSqlConfig( $cfgpath );
242
		$sql = str_replace( ':cond', '1=1', $sql );
243
244
		$stmt = $conn->create( $sql );
245
246
		foreach( $siteids as $siteid )
247
		{
248
			$stmt->bind( 1, $siteid );
249
			$stmt->execute()->finish();
250
		}
251
252
		return $this;
253
	}
254
255
256
	/**
257
	 * Deletes items.
258
	 *
259
	 * @param \Aimeos\MShop\Common\Item\Iface|\Aimeos\Map|array|string $items List of item objects or IDs of the items
260
	 * @param string $cfgpath Configuration path to the SQL statement
261
	 * @param bool $siteid If siteid should be used in the statement
262
	 * @param string $name Name of the ID column
263
	 * @return \Aimeos\MShop\Common\Manager\Iface Manager object for chaining method calls
264
	 */
265
	protected function deleteItemsBase( $items, string $cfgpath, bool $siteid = true,
266
		string $name = 'id' ) : \Aimeos\MShop\Common\Manager\Iface
267
	{
268
		if( map( $items )->isEmpty() ) {
269
			return $this;
270
		}
271
272
		$search = $this->object()->filter();
273
		$search->setConditions( $search->compare( '==', $name, $items ) );
274
275
		$types = array( $name => \Aimeos\Base\DB\Statement\Base::PARAM_STR );
276
		$translations = array( $name => '"' . $name . '"' );
277
278
		$cond = $search->getConditionSource( $types, $translations );
279
		$sql = str_replace( ':cond', $cond, $this->getSqlConfig( $cfgpath ) );
280
281
		$context = $this->context();
282
		$conn = $context->db( $this->getResourceName() );
283
284
		$stmt = $conn->create( $sql );
285
286
		if( $siteid ) {
287
			$stmt->bind( 1, $context->locale()->getSiteId() . '%' );
288
		}
289
290
		$stmt->execute()->finish();
291
292
		return $this;
293
	}
294
295
296
	/**
297
	 * Returns a sorted list of required criteria keys.
298
	 *
299
	 * @param \Aimeos\Base\Criteria\Iface $criteria Search criteria object
300
	 * @param string[] $required List of prefixes of required search conditions
301
	 * @return string[] Sorted list of criteria keys
302
	 */
303
	protected function getCriteriaKeyList( \Aimeos\Base\Criteria\Iface $criteria, array $required ) : array
304
	{
305
		$keys = array_merge( $required, $this->getCriteriaKeys( $required, $criteria->getConditions() ) );
306
307
		foreach( $criteria->getSortations() as $sortation ) {
308
			$keys = array_merge( $keys, $this->getCriteriaKeys( $required, $sortation ) );
309
		}
310
311
		$keys = array_unique( array_merge( $required, $keys ) );
312
		sort( $keys );
313
314
		return $keys;
315
	}
316
317
318
	/**
319
	 * Returns the name of the resource.
320
	 *
321
	 * @return string Name of the resource, e.g. "db-product"
322
	 */
323
	protected function getResourceName() : string
324
	{
325
		if( $this->resourceName === null ) {
326
			$this->setResourceName( 'db-' . $this->getDomain() );
0 ignored issues
show
Bug introduced by
It seems like getDomain() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

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

326
			$this->setResourceName( 'db-' . $this->/** @scrutinizer ignore-call */ getDomain() );
Loading history...
327
		}
328
329
		return $this->resourceName;
330
	}
331
332
333
	/**
334
	 * Sets the name of the database resource that should be used.
335
	 *
336
	 * @param string $name Name of the resource
337
	 * @return \Aimeos\MShop\Common\Manager\Iface Manager object for chaining method calls
338
	 */
339
	protected function setResourceName( string $name ) : \Aimeos\MShop\Common\Manager\Iface
340
	{
341
		$config = $this->context()->config();
342
343
		if( $config->get( 'resource/' . $name ) === null ) {
344
			$this->resourceName = $config->get( 'resource/default', 'db' );
345
		} else {
346
			$this->resourceName = $name;
347
		}
348
349
		return $this;
350
	}
351
352
353
	/**
354
	 * Returns the search attribute objects used for searching.
355
	 *
356
	 * @param array $list Associative list of search keys and the lists of search definitions
357
	 * @param string $path Configuration path to the sub-domains for fetching the search definitions
358
	 * @param string[] $default List of sub-domains if no others are configured
359
	 * @param bool $withsub True to include search definitions of sub-domains, false if not
360
	 * @return \Aimeos\Base\Criteria\Attribute\Iface[] Associative list of search keys and criteria attribute items as values
361
	 * @since 2014.09
362
	 */
363
	protected function getSearchAttributesBase( array $list, string $path, array $default, bool $withsub ) : array
364
	{
365
		$attr = $this->createAttributes( $list );
0 ignored issues
show
Bug introduced by
It seems like createAttributes() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

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

365
		/** @scrutinizer ignore-call */ 
366
  $attr = $this->createAttributes( $list );
Loading history...
366
367
		if( $withsub === true )
368
		{
369
			$domains = $this->context()->config()->get( $path, $default );
370
371
			foreach( $domains as $domain ) {
372
				$attr += $this->object()->getSubManager( $domain )->getSearchAttributes( true );
373
			}
374
		}
375
376
		return $attr;
377
	}
378
379
380
	/**
381
	 * Returns the search results for the given SQL statement.
382
	 *
383
	 * @param \Aimeos\Base\DB\Connection\Iface $conn Database connection
384
	 * @param string $sql SQL statement
385
	 * @return \Aimeos\Base\DB\Result\Iface Search result object
386
	 */
387
	protected function getSearchResults( \Aimeos\Base\DB\Connection\Iface $conn, string $sql ) : \Aimeos\Base\DB\Result\Iface
388
	{
389
		$time = microtime( true );
390
391
		$stmt = $conn->create( $sql );
392
		$result = $stmt->execute();
393
394
		$level = \Aimeos\Base\Logger\Iface::DEBUG;
395
		$time = ( microtime( true ) - $time ) * 1000;
396
		$msg = 'Time: ' . $time . "ms\n"
397
			. 'Class: ' . get_class( $this ) . "\n"
398
			. str_replace( ["\t", "\n\n"], ['', "\n"], trim( (string) $stmt ) );
399
400
		if( $time > 1000.0 )
401
		{
402
			$level = \Aimeos\Base\Logger\Iface::NOTICE;
403
			$msg .= "\n" . ( new \Exception() )->getTraceAsString();
404
		}
405
406
		$this->context()->logger()->log( $msg, $level, 'core/sql' );
407
408
		return $result;
409
	}
410
411
412
	/**
413
	 * Returns the site coditions for the search request
414
	 *
415
	 * @param string[] $keys Sorted list of criteria keys
416
	 * @param \Aimeos\Base\Criteria\Attribute\Iface[] $attributes Associative list of search keys and criteria attribute items as values
417
	 * @param int $sitelevel Site level constant from \Aimeos\MShop\Locale\Manager\Base
418
	 * @return \Aimeos\Base\Criteria\Expression\Iface[] List of search conditions
419
	 * @since 2015.01
420
	 */
421
	protected function getSiteConditions( array $keys, array $attributes, int $sitelevel ) : array
422
	{
423
		$list = [];
424
425
		foreach( $keys as $key )
426
		{
427
			$name = $key . '.siteid';
428
429
			if( isset( $attributes[$name] ) ) {
430
				$list[] = $this->siteCondition( $name, $sitelevel );
431
			}
432
		}
433
434
		return $list;
435
	}
436
437
438
	/**
439
	 * Returns the SQL statement for the given config path
440
	 *
441
	 * If available, the database specific SQL statement is returned, otherwise
442
	 * the ANSI SQL statement. The database type is determined via the resource
443
	 * adapter.
444
	 *
445
	 * @param string $path Configuration path to the SQL statement
446
	 * @return array|string ANSI or database specific SQL statement
447
	 */
448
	protected function getSqlConfig( string $path )
449
	{
450
		$config = $this->context()->config();
451
		$adapter = $config->get( 'resource/' . $this->getResourceName() . '/adapter' );
452
453
		if( $sql = $config->get( $path . '/' . $adapter, $config->get( $path . '/ansi' ) ) ) {
454
			return str_replace( ':table', $this->getTable(), $sql );
455
		}
456
457
		$parts = explode( '/', $path );
458
		$cpath = 'mshop/common/manager/' . end( $parts );
459
		$sql = $config->get( $cpath . '/' . $adapter, $config->get( $cpath . '/ansi', $path ) );
460
461
		return str_replace( ':table', $this->getTable(), $sql );
462
	}
463
464
465
	/**
466
	 * Returns the available sub-manager names
467
	 *
468
	 * @return array Sub-manager names, e.g. ['lists', 'property', 'type']
469
	 */
470
	protected function getSubManagers() : array
471
	{
472
		return $this->context()->config()->get( $this->getConfigKey( 'submanagers' ), [] );
0 ignored issues
show
Bug introduced by
It seems like getConfigKey() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

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

472
		return $this->context()->config()->get( $this->/** @scrutinizer ignore-call */ getConfigKey( 'submanagers' ), [] );
Loading history...
473
	}
474
475
476
	/**
477
	 * Sets the base criteria "status".
478
	 * (setConditions overwrites the base criteria)
479
	 *
480
	 * @param string $domain Name of the domain/sub-domain like "product" or "product.list"
481
	 * @param bool|null $default TRUE for status=1, NULL for status>0, FALSE for no restriction
482
	 * @return \Aimeos\Base\Criteria\Iface Search critery object
483
	 */
484
	protected function filterBase( string $domain, ?bool $default = false ) : \Aimeos\Base\Criteria\Iface
485
	{
486
		$filter = self::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

486
		/** @scrutinizer ignore-call */ 
487
  $filter = self::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...
487
488
		if( $default !== false ) {
489
			$filter->add( $domain . '.status', $default ? '==' : '>=', 1 );
490
		}
491
492
		return $filter;
493
	}
494
495
496
	/**
497
	 * Returns the item for the given search key/value pairs.
498
	 *
499
	 * @param array $pairs Search key/value pairs for the item
500
	 * @param string[] $ref List of domains whose items should be fetched too
501
	 * @param bool|null $default Add default criteria or NULL for relaxed default criteria
502
	 * @return \Aimeos\MShop\Common\Item\Iface Requested item
503
	 * @throws \Aimeos\MShop\Exception if no item with the given ID found
504
	 */
505
	protected function findBase( array $pairs, array $ref, ?bool $default ) : \Aimeos\MShop\Common\Item\Iface
506
	{
507
		$expr = [];
508
		$criteria = $this->object()->filter( $default )->slice( 0, 1 );
509
510
		foreach( $pairs as $key => $value )
511
		{
512
			if( $value === null )
513
			{
514
				$msg = $this->context()->translate( 'mshop', 'Required value for "%1$s" is missing' );
515
				throw new \Aimeos\MShop\Exception( sprintf( $msg, $key ) );
516
			}
517
			$expr[] = $criteria->compare( '==', $key, $value );
518
		}
519
520
		$criteria->setConditions( $criteria->and( $expr ) );
521
522
		if( ( $item = $this->object()->search( $criteria, $ref )->first() ) ) {
523
			return $item;
524
		}
525
526
		$msg = $this->context()->translate( 'mshop', 'No item found for conditions: %1$s' );
527
		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

527
		throw new \Aimeos\MShop\Exception( sprintf( $msg, /** @scrutinizer ignore-type */ print_r( $pairs, true ) ), 404 );
Loading history...
528
	}
529
530
531
	/**
532
	 * Returns the cached statement for the given key or creates a new prepared statement.
533
	 * If no SQL string is given, the key is used to retrieve the SQL string from the configuration.
534
	 *
535
	 * @param \Aimeos\Base\DB\Connection\Iface $conn Database connection
536
	 * @param string $cfgkey Unique key for the SQL
537
	 * @param string|null $sql SQL string if it shouldn't be retrieved from the configuration
538
	 * @return \Aimeos\Base\DB\Statement\Iface Database statement object
539
	 */
540
	protected function getCachedStatement( \Aimeos\Base\DB\Connection\Iface $conn, string $cfgkey,
541
		string $sql = null ) : \Aimeos\Base\DB\Statement\Iface
542
	{
543
		if( !isset( $this->cachedStmts['stmt'][$cfgkey] )
544
			|| !isset( $this->cachedStmts['conn'][$cfgkey] )
545
			|| $conn !== $this->cachedStmts['conn'][$cfgkey]
546
		) {
547
			if( $sql === null ) {
548
				$sql = $this->getSqlConfig( $cfgkey );
549
			}
550
551
			$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

551
			$this->cachedStmts['stmt'][$cfgkey] = $conn->create( /** @scrutinizer ignore-type */ $sql );
Loading history...
552
			$this->cachedStmts['conn'][$cfgkey] = $conn;
553
		}
554
555
		return $this->cachedStmts['stmt'][$cfgkey];
556
	}
557
558
559
	/**
560
	 * Returns the item for the given search key and ID.
561
	 *
562
	 * @param string $key Search key for the requested ID
563
	 * @param string $id Unique ID to search for
564
	 * @param string[] $ref List of domains whose items should be fetched too
565
	 * @param bool|null $default Add default criteria or NULL for relaxed default criteria
566
	 * @return \Aimeos\MShop\Common\Item\Iface Requested item
567
	 * @throws \Aimeos\MShop\Exception if no item with the given ID found
568
	 */
569
	protected function getItemBase( string $key, string $id, array $ref, ?bool $default ) : \Aimeos\MShop\Common\Item\Iface
570
	{
571
		$criteria = $this->object()->filter( $default )->add( [$key => $id] )->slice( 0, 1 );
572
573
		if( ( $item = $this->object()->search( $criteria, $ref )->first() ) ) {
574
			return $item;
575
		}
576
577
		$msg = $this->context()->translate( 'mshop', 'Item with ID "%2$s" in "%1$s" not found' );
578
		throw new \Aimeos\MShop\Exception( sprintf( $msg, $key, $id ), 404 );
579
	}
580
581
582
	/**
583
	 * Returns the SQL strings for joining dependent tables.
584
	 *
585
	 * @param \Aimeos\Base\Criteria\Attribute\Iface[] $attributes List of criteria attribute items
586
	 * @param string $prefix Search key prefix
587
	 * @return array List of JOIN SQL strings
588
	 */
589
	private function getJoins( array $attributes, string $prefix ) : array
590
	{
591
		$iface = \Aimeos\Base\Criteria\Attribute\Iface::class;
592
		$name = $prefix . '.id';
593
594
		if( isset( $attributes[$prefix] ) && $attributes[$prefix] instanceof $iface ) {
595
			return $attributes[$prefix]->getInternalDeps();
596
		}
597
		elseif( isset( $attributes[$name] ) && $attributes[$name] instanceof $iface ) {
598
			return $attributes[$name]->getInternalDeps();
599
		}
600
		else if( isset( $attributes['id'] ) && $attributes['id'] instanceof $iface ) {
601
			return $attributes['id']->getInternalDeps();
602
		}
603
604
		return [];
605
	}
606
607
608
	/**
609
	 * Returns the available manager types
610
	 *
611
	 * @param string $type Main manager type
612
	 * @param string $path Configuration path to the sub-domains
613
	 * @param string[] $default List of sub-domains if no others are configured
614
	 * @param bool $withsub Return also the resource type of sub-managers if true
615
	 * @return string[] Type of the manager and submanagers, subtypes are separated by slashes
616
	 */
617
	protected function getResourceTypeBase( string $type, string $path, array $default, bool $withsub ) : array
618
	{
619
		$list = [$type];
620
621
		if( $withsub )
622
		{
623
			foreach( $this->context()->config()->get( $path, $default ) as $domain ) {
624
				$list = array_merge( $list, $this->object()->getSubManager( $domain )->getResourceType( $withsub ) );
625
			}
626
		}
627
628
		return $list;
629
	}
630
631
632
	/**
633
	 * Returns the string replacements for the SQL statements
634
	 *
635
	 * @param \Aimeos\Base\Criteria\Iface $search Search critera object
636
	 * @param \Aimeos\Base\Criteria\Attribute\Iface[] $attributes Associative list of search keys and criteria attribute items as values
637
	 * @param \Aimeos\Base\Criteria\Plugin\Iface[] $plugins Associative list of search keys and criteria plugin items as values
638
	 * @param string[] $joins Associative list of SQL joins
639
	 * @param \Aimeos\Base\Criteria\Attribute\Iface[] $columns Additional columns to retrieve values from
640
	 * @return array Array of keys, find and replace arrays
641
	 */
642
	protected function getSQLReplacements( \Aimeos\Base\Criteria\Iface $search, array $attributes, array $plugins,
643
		array $joins, array $columns = [] ) : array
644
	{
645
		$types = $this->getSearchTypes( $attributes );
646
		$funcs = $this->getSearchFunctions( $attributes );
647
		$translations = $this->getSearchTranslations( $attributes );
648
649
		$colstring = '';
650
		foreach( $columns as $name => $entry ) {
651
			$colstring .= '"' . $entry->getInternalCode() . '", ';
652
		}
653
654
		$find = array( ':columns', ':joins', ':cond', ':start', ':size' );
655
		$replace = array(
656
			$colstring,
657
			implode( "\n", array_unique( $joins ) ),
658
			$search->getConditionSource( $types, $translations, $plugins, $funcs ),
659
			$search->getOffset(),
660
			$search->getLimit(),
661
		);
662
663
		if( empty( $search->getSortations() ) && ( $attribute = reset( $attributes ) ) !== false ) {
664
			$search = ( clone $search )->setSortations( [$search->sort( '+', $attribute->getCode() )] );
665
		}
666
667
		$find[] = ':order';
668
		$replace[] = $search->getSortationSource( $types, $translations, $funcs );
669
670
		$find[] = ':group';
671
		$replace[] = implode( ', ', $search->translate( $search->getSortations(), $translations, $funcs ) ) . ', ';
672
673
		return [$find, $replace];
674
	}
675
676
677
	/**
678
	 * Returns the newly created ID for the last record which was inserted.
679
	 *
680
	 * @param \Aimeos\Base\DB\Connection\Iface $conn Database connection used to insert the new record
681
	 * @param string $cfgpath Configuration path to the SQL statement for retrieving the new ID of the last inserted record
682
	 * @return string ID of the last record that was inserted by using the given connection
683
	 * @throws \Aimeos\MShop\Exception if there's no ID of the last record available
684
	 */
685
	protected function newId( \Aimeos\Base\DB\Connection\Iface $conn, string $cfgpath ) : string
686
	{
687
		$sql = $this->getSqlConfig( $cfgpath );
688
689
		$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

689
		$result = $conn->create( /** @scrutinizer ignore-type */ $sql )->execute();
Loading history...
690
691
		if( ( $row = $result->fetch( \Aimeos\Base\DB\Result\Base::FETCH_NUM ) ) === false )
692
		{
693
			$msg = $this->context()->translate( 'mshop', 'ID of last inserted database record not available' );
694
			throw new \Aimeos\MShop\Exception( $msg );
695
		}
696
		$result->finish();
697
698
		return $row[0];
699
	}
700
701
702
	/**
703
	 * Returns the search result of the statement combined with the given criteria.
704
	 *
705
	 * @param \Aimeos\Base\DB\Connection\Iface $conn Database connection
706
	 * @param \Aimeos\Base\Criteria\Iface $search Search criteria object
707
	 * @param string $cfgPathSearch Path to SQL statement in configuration for searching
708
	 * @param string $cfgPathCount Path to SQL statement in configuration for counting
709
	 * @param string[] $required Additional search keys to add conditions for even if no conditions are available
710
	 * @param int|null $total Contains the number of all records matching the criteria if not null
711
	 * @param int $sitelevel Constant from \Aimeos\MShop\Locale\Manager\Base for defining which site IDs should be used for searching
712
	 * @param \Aimeos\Base\Criteria\Plugin\Iface[] $plugins Associative list of search keys and criteria plugin items as values
713
	 * @return \Aimeos\Base\DB\Result\Iface SQL result object for accessing the found records
714
	 * @throws \Aimeos\MShop\Exception if no number of all matching records is available
715
	 */
716
	protected function searchItemsBase( \Aimeos\Base\DB\Connection\Iface $conn, \Aimeos\Base\Criteria\Iface $search,
717
		string $cfgPathSearch, string $cfgPathCount, array $required, int &$total = null,
718
		int $sitelevel = \Aimeos\MShop\Locale\Manager\Base::SITE_ALL, array $plugins = [] ) : \Aimeos\Base\DB\Result\Iface
719
	{
720
		$joins = [];
721
		$conditions = $search->getConditions();
722
		$columns = $this->object()->getSaveAttributes();
723
		$attributes = $this->object()->getSearchAttributes();
724
		$keys = $this->getCriteriaKeyList( $search, $required );
725
726
		$basekey = array_shift( $required );
727
728
		foreach( $keys as $key )
729
		{
730
			if( $key !== $basekey ) {
731
				$joins = array_merge( $joins, $this->getJoins( $attributes, $key ) );
732
			}
733
		}
734
735
		$joins = array_unique( $joins );
736
		$cond = $this->getSiteConditions( $keys, $attributes, $sitelevel );
737
738
		if( $conditions !== null ) {
739
			$cond[] = $conditions;
740
		}
741
742
		$search = clone $search;
743
		$search->setConditions( $search->and( $cond ) );
744
745
		list( $find, $replace ) = $this->getSQLReplacements( $search, $attributes, $plugins, $joins, $columns );
746
747
		if( $total !== null )
748
		{
749
			$sql = str_replace( $find, $replace, $this->getSqlConfig( $cfgPathCount ) );
750
			$result = $this->getSearchResults( $conn, $sql );
751
			$row = $result->fetch();
752
			$result->finish();
753
754
			if( $row === null )
755
			{
756
				$msg = $this->context()->translate( 'mshop', 'Total results value not found' );
757
				throw new \Aimeos\MShop\Exception( $msg );
758
			}
759
760
			$total = (int) $row['count'];
761
		}
762
763
		return $this->getSearchResults( $conn, str_replace( $find, $replace, $this->getSqlConfig( $cfgPathSearch ) ) );
764
	}
765
766
767
	/**
768
	 * Saves an attribute item to the storage.
769
	 *
770
	 * @param \Aimeos\MShop\Common\Item\Iface $item Item object
771
	 * @param bool $fetch True if the new ID should be returned in the item
772
	 * @return \Aimeos\MShop\Common\Item\Iface $item Updated item including the generated ID
773
	 */
774
	protected function saveBase( \Aimeos\MShop\Common\Item\Iface $item, bool $fetch = true ) : \Aimeos\MShop\Common\Item\Iface
775
	{
776
		if( !$item->isModified() ) {
777
			return $item;
778
		}
779
780
		$context = $this->context();
781
		$conn = $context->db( $this->getResourceName() );
782
783
		$id = $item->getId();
784
		$date = date( 'Y-m-d H:i:s' );
785
		$columns = $this->object()->getSaveAttributes();
786
787
		if( $id === null )
788
		{
789
			/** mshop/common/manager/insert/mysql
790
			 * Inserts a new record into the database table
791
			 *
792
			 * @see mshop/common/manager/insert/ansi
793
			 */
794
795
			/** mshop/common/manager/insert/ansi
796
			 * Inserts a new record into the database table
797
			 *
798
			 * Items with no ID yet (i.e. the ID is NULL) will be created in
799
			 * the database and the newly created ID retrieved afterwards
800
			 * using the "newid" SQL statement.
801
			 *
802
			 * The SQL statement must be a string suitable for being used as
803
			 * prepared statement. It must include question marks for binding
804
			 * the values from the item to the statement before they are
805
			 * sent to the database server. The number of question marks must
806
			 * be the same as the number of columns listed in the INSERT
807
			 * statement. The order of the columns must correspond to the
808
			 * order in the save() method, so the correct values are
809
			 * bound to the columns.
810
			 *
811
			 * The SQL statement should conform to the ANSI standard to be
812
			 * compatible with most relational database systems. This also
813
			 * includes using double quotes for table and column names.
814
			 *
815
			 * @param string SQL statement for inserting records
816
			 * @since 2023.10
817
			 * @category Developer
818
			 * @see mshop/common/manager/update/ansi
819
			 * @see mshop/common/manager/newid/ansi
820
			 * @see mshop/common/manager/delete/ansi
821
			 * @see mshop/common/manager/search/ansi
822
			 * @see mshop/common/manager/count/ansi
823
			 */
824
			$path = $this->getConfigKey( 'insert' );
825
			$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

825
			$sql = $this->addSqlColumns( array_keys( $columns ), /** @scrutinizer ignore-type */ $this->getSqlConfig( $path ) );
Loading history...
826
		}
827
		else
828
		{
829
			/** mshop/common/manager/update/mysql
830
			 * Updates an existing record in the database
831
			 *
832
			 * @see mshop/common/manager/update/ansi
833
			 */
834
835
			/** mshop/common/manager/update/ansi
836
			 * Updates an existing record in the database
837
			 *
838
			 * Items which already have an ID (i.e. the ID is not NULL) will
839
			 * be updated in the database.
840
			 *
841
			 * The SQL statement must be a string suitable for being used as
842
			 * prepared statement. It must include question marks for binding
843
			 * the values from the item to the statement before they are
844
			 * sent to the database server. The order of the columns must
845
			 * correspond to the order in the save() method, so the
846
			 * correct values are bound to the columns.
847
			 *
848
			 * The SQL statement should conform to the ANSI standard to be
849
			 * compatible with most relational database systems. This also
850
			 * includes using double quotes for table and column names.
851
			 *
852
			 * @param string SQL statement for updating records
853
			 * @since 2023.10
854
			 * @category Developer
855
			 * @see mshop/common/manager/insert/ansi
856
			 * @see mshop/common/manager/newid/ansi
857
			 * @see mshop/common/manager/delete/ansi
858
			 * @see mshop/common/manager/search/ansi
859
			 * @see mshop/common/manager/count/ansi
860
			 */
861
			$path = $this->getConfigKey( 'update' );
862
			$sql = $this->addSqlColumns( array_keys( $columns ), $this->getSqlConfig( $path ), false );
863
		}
864
865
		$idx = 1;
866
		$stmt = $this->getCachedStatement( $conn, $path, $sql );
867
868
		foreach( $columns as $entry ) {
869
			$stmt->bind( $idx++, $item->get( $entry->getCode() ), \Aimeos\Base\Criteria\SQL::type( $entry->getType() ) );
870
		}
871
872
		$stmt->bind( $idx++, $date ); // mtime
873
		$stmt->bind( $idx++, $context->editor() );
874
875
		if( $id !== null ) {
876
			$stmt->bind( $idx++, $context->locale()->getSiteId() . '%' );
877
			$stmt->bind( $idx++, $id, \Aimeos\Base\DB\Statement\Base::PARAM_INT );
878
		} else {
879
			$stmt->bind( $idx++, $this->siteId( $item->getSiteId(), \Aimeos\MShop\Locale\Manager\Base::SITE_SUBTREE ) );
0 ignored issues
show
Bug introduced by
It seems like siteId() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

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

879
			$stmt->bind( $idx++, $this->/** @scrutinizer ignore-call */ siteId( $item->getSiteId(), \Aimeos\MShop\Locale\Manager\Base::SITE_SUBTREE ) );
Loading history...
880
			$stmt->bind( $idx++, $date ); // ctime
881
		}
882
883
		$stmt->execute()->finish();
884
885
		if( $id === null )
886
		{
887
			/** mshop/common/manager/newid/mysql
888
			 * Retrieves the ID generated by the database when inserting a new record
889
			 *
890
			 * @see mshop/common/manager/newid/ansi
891
			 */
892
893
			/** mshop/common/manager/newid/ansi
894
			 * Retrieves the ID generated by the database when inserting a new record
895
			 *
896
			 * As soon as a new record is inserted into the database table,
897
			 * the database server generates a new and unique identifier for
898
			 * that record. This ID can be used for retrieving, updating and
899
			 * deleting that specific record from the table again.
900
			 *
901
			 * For MySQL:
902
			 *  SELECT LAST_INSERT_ID()
903
			 * For PostgreSQL:
904
			 *  SELECT currval('seq_matt_id')
905
			 * For SQL Server:
906
			 *  SELECT SCOPE_IDENTITY()
907
			 * For Oracle:
908
			 *  SELECT "seq_matt_id".CURRVAL FROM DUAL
909
			 *
910
			 * There's no way to retrive the new ID by a SQL statements that
911
			 * fits for most database servers as they implement their own
912
			 * specific way.
913
			 *
914
			 * @param string SQL statement for retrieving the last inserted record ID
915
			 * @since 2023.10
916
			 * @category Developer
917
			 * @see mshop/common/manager/insert/ansi
918
			 * @see mshop/common/manager/update/ansi
919
			 * @see mshop/common/manager/delete/ansi
920
			 * @see mshop/common/manager/search/ansi
921
			 * @see mshop/common/manager/count/ansi
922
			 */
923
			$id = $this->newId( $conn, 'mshop/common/manager/newid' );
924
		}
925
926
		return $item->setId( $id );
927
	}
928
929
930
	/**
931
	 * Replaces the given marker with an expression
932
	 *
933
	 * @param string $column Name (including alias) of the column
934
	 * @param mixed $value Value used in the expression
935
	 * @param string $op Operator used in the expression
936
	 * @param int $type Type constant from \Aimeos\Base\DB\Statement\Base class
937
	 * @return string Created expression
938
	 */
939
	protected function toExpression( string $column, $value, string $op = '==',
940
		int $type = \Aimeos\Base\DB\Statement\Base::PARAM_STR ) : string
941
	{
942
		$types = ['marker' => $type];
943
		$translations = ['marker' => $column];
944
		$value = ( is_array( $value ) ? array_unique( $value ) : $value );
945
946
		return $this->getSearch()->compare( $op, 'marker', $value )->toSource( $types, $translations );
0 ignored issues
show
Bug introduced by
The method getSearch() does not exist on Aimeos\MShop\Common\Manager\DB. Did you maybe mean getSearchResults()? ( Ignorable by Annotation )

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

946
		return $this->/** @scrutinizer ignore-call */ getSearch()->compare( $op, 'marker', $value )->toSource( $types, $translations );

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...
947
	}
948
949
950
	/**
951
	 * Transforms the application specific values to Aimeos standard values.
952
	 *
953
	 * @param array $values Associative list of key/value pairs from the storage
954
	 * @return array Associative list of key/value pairs with standard Aimeos values
955
	 */
956
	protected function transform( array $values ) : array
957
	{
958
		return $values;
959
	}
960
961
962
	/**
963
	 * Cuts the last part separated by a dot repeatedly and returns the list of resulting string.
964
	 *
965
	 * @param string[] $prefix Required base prefixes of the search keys
966
	 * @param string $string String containing parts separated by dots
967
	 * @return array List of resulting strings
968
	 */
969
	private function cutNameTail( array $prefix, string $string ) : array
970
	{
971
		$result = [];
972
		$noprefix = true;
973
		$strlen = strlen( $string );
974
975
		foreach( $prefix as $key )
976
		{
977
			$len = strlen( $key );
978
979
			if( strncmp( $string, $key, $len ) === 0 )
980
			{
981
				if( $strlen > $len && ( $pos = strrpos( $string, '.' ) ) !== false )
982
				{
983
					$result[] = $string = substr( $string, 0, $pos );
984
					$result = array_merge( $result, $this->cutNameTail( $prefix, $string ) );
985
					$noprefix = false;
986
				}
987
988
				break;
989
			}
990
		}
991
992
		if( $noprefix )
993
		{
994
			if( ( $pos = strrpos( $string, ':' ) ) !== false ) {
995
				$result[] = substr( $string, 0, $pos );
996
				$result[] = $string;
997
			} elseif( ( $pos = strrpos( $string, '.' ) ) !== false ) {
998
				$result[] = substr( $string, 0, $pos );
999
			} else {
1000
				$result[] = $string;
1001
			}
1002
		}
1003
1004
		return $result;
1005
	}
1006
1007
1008
	/**
1009
	 * Returns a list of unique criteria names shortend by the last element after the ''
1010
	 *
1011
	 * @param string[] $prefix Required base prefixes of the search keys
1012
	 * @param \Aimeos\Base\Criteria\Expression\Iface|null $expr Criteria object
1013
	 * @return array List of shortend criteria names
1014
	 */
1015
	private function getCriteriaKeys( array $prefix, \Aimeos\Base\Criteria\Expression\Iface $expr = null ) : array
1016
	{
1017
		if( $expr === null ) { return []; }
1018
1019
		$result = [];
1020
1021
		foreach( $this->getCriteriaNames( $expr ) as $item )
1022
		{
1023
			if( strncmp( $item, 'sort:', 5 ) === 0 ) {
1024
				$item = substr( $item, 5 );
1025
			}
1026
1027
			if( ( $pos = strpos( $item, '(' ) ) !== false ) {
1028
				$item = substr( $item, 0, $pos );
1029
			}
1030
1031
			$result = array_merge( $result, $this->cutNameTail( $prefix, $item ) );
1032
		}
1033
1034
		return $result;
1035
	}
1036
1037
1038
	/**
1039
	 * Returns a list of criteria names from a expression and its sub-expressions.
1040
	 *
1041
	 * @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...
1042
	 * @return array List of criteria names
1043
	 */
1044
	private function getCriteriaNames( \Aimeos\Base\Criteria\Expression\Iface $expr ) : array
1045
	{
1046
		if( $expr instanceof \Aimeos\Base\Criteria\Expression\Compare\Iface ) {
1047
			return array( $expr->getName() );
1048
		}
1049
1050
		if( $expr instanceof \Aimeos\Base\Criteria\Expression\Combine\Iface )
1051
		{
1052
			$list = [];
1053
			foreach( $expr->getExpressions() as $item ) {
1054
				$list = array_merge( $list, $this->getCriteriaNames( $item ) );
1055
			}
1056
			return $list;
1057
		}
1058
1059
		if( $expr instanceof \Aimeos\Base\Criteria\Expression\Sort\Iface ) {
1060
			return array( $expr->getName() );
1061
		}
1062
1063
		return [];
1064
	}
1065
}
1066