Passed
Push — master ( 5fb848...fa0ea1 )
by Aimeos
05:25
created

DB::isModified()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 1
dl 0
loc 3
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 1
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
	private string $subpath;
26
	private string $domain;
27
28
29
	/**
30
	 * Returns the context object.
31
	 *
32
	 * @return \Aimeos\MShop\ContextIface Context object
33
	 */
34
	abstract protected function context() : \Aimeos\MShop\ContextIface;
35
36
37
	/**
38
	 * Creates the criteria attribute items from the list of entries
39
	 *
40
	 * @param array $list Associative array of code as key and array with properties as values
41
	 * @return \Aimeos\Base\Criteria\Attribute\Standard[] List of criteria attribute items
42
	 */
43
	abstract protected function createAttributes( array $list ) : array;
44
45
46
	/**
47
	 * Returns the attribute helper functions for searching defined by the manager.
48
	 *
49
	 * @param \Aimeos\Base\Criteria\Attribute\Iface[] $attributes List of search attribute items
50
	 * @return array Associative array of attribute code and helper function
51
	 */
52
	abstract protected function getSearchFunctions( array $attributes ) : array;
53
54
55
	/**
56
	 * Returns the attribute translations for searching defined by the manager.
57
	 *
58
	 * @param \Aimeos\Base\Criteria\Attribute\Iface[] $attributes List of search attribute items
59
	 * @return array Associative array of attribute code and internal attribute code
60
	 */
61
	abstract protected function getSearchTranslations( array $attributes ) : array;
62
63
64
	/**
65
	 * Returns the attribute types for searching defined by the manager.
66
	 *
67
	 * @param \Aimeos\Base\Criteria\Attribute\Iface[] $attributes List of search attribute items
68
	 * @return array Associative array of attribute code and internal attribute type
69
	 */
70
	abstract protected function getSearchTypes( array $attributes ) : array;
71
72
73
	/**
74
	 * Returns the outmost decorator of the decorator stack
75
	 *
76
	 * @return \Aimeos\MShop\Common\Manager\Iface Outmost decorator object
77
	 */
78
	abstract protected function object() : \Aimeos\MShop\Common\Manager\Iface;
79
80
81
	/**
82
	 * Returns the site expression for the given name
83
	 *
84
	 * @param string $name Name of the site condition
85
	 * @param int $sitelevel Site level constant from \Aimeos\MShop\Locale\Manager\Base
86
	 * @return \Aimeos\Base\Criteria\Expression\Iface Site search condition
87
	 */
88
	abstract protected function siteCondition( string $name, int $sitelevel ) : \Aimeos\Base\Criteria\Expression\Iface;
89
90
91
	/**
92
	 * Returns the site ID that should be used based on the site level
93
	 *
94
	 * @param string $siteId Site ID to check
95
	 * @param int $sitelevel Site level to check against
96
	 * @return string Site ID that should be use based on the site level
97
	 * @since 2022.04
98
	 */
99
	abstract protected function siteId( string $siteId, int $sitelevel ) : string;
100
101
102
	/**
103
	 * Adds additional column names to SQL statement
104
	 *
105
	 * @param string[] $columns List of column names
106
	 * @param string $sql Insert or update SQL statement
107
	 * @param bool $mode True for insert, false for update statement
108
	 * @return string Modified insert or update SQL statement
109
	 */
110
	protected function addSqlColumns( array $columns, string $sql, bool $mode = true ) : string
111
	{
112
		$names = $values = '';
113
114
		if( $mode )
115
		{
116
			foreach( $columns as $name ) {
117
				$names .= '"' . $name . '", '; $values .= '?, ';
118
			}
119
		}
120
		else
121
		{
122
			foreach( $columns as $name ) {
123
				$names .= '"' . $name . '" = ?, ';
124
			}
125
		}
126
127
		return str_replace( [':names', ':values'], [$names, $values], $sql );
128
	}
129
130
131
	/**
132
	 * Counts the number products that are available for the values of the given key.
133
	 *
134
	 * @param \Aimeos\Base\Criteria\Iface $search Search criteria
135
	 * @param array|string $keys Search key or list of keys for aggregation
136
	 * @param string $cfgPath Configuration key for the SQL statement
137
	 * @param string[] $required List of domain/sub-domain names like "catalog.index" that must be additionally joined
138
	 * @param string|null $value Search key for aggregating the value column
139
	 * @param string|null $type Type of aggregation, e.g.  "sum", "min", "max" or NULL for "count"
140
	 * @return \Aimeos\Map List of ID values as key and the number of counted products as value
141
	 * @todo 2018.01 Reorder Parameter list
142
	 */
143
	protected function aggregateBase( \Aimeos\Base\Criteria\Iface $search, $keys, string $cfgPath,
144
		array $required = [], string $value = null, string $type = null ) : \Aimeos\Map
145
	{
146
		/** mshop/common/manager/aggregate/limit
147
		 * Limits the number of records that are used when aggregating items
148
		 *
149
		 * As counting huge amount of records (several 10 000 records) takes a long time,
150
		 * the limit can cut down response times so the counts are available more quickly
151
		 * in the front-end and the server load is reduced.
152
		 *
153
		 * Using a low limit can lead to incorrect numbers if the amount of found items
154
		 * is very high. Approximate item counts are normally not a problem but it can
155
		 * lead to the situation that visitors see that no items are available despite
156
		 * the fact that there would be at least one.
157
		 *
158
		 * @param integer Number of records
159
		 * @since 2021.04
160
		 */
161
		$limit = $this->context()->config()->get( 'mshop/common/manager/aggregate/limit', 10000 );
162
163
		if( empty( $keys ) )
164
		{
165
			$msg = $this->context()->translate( 'mshop', 'At least one key is required for aggregation' );
166
			throw new \Aimeos\MShop\Exception( $msg );
167
		}
168
169
		$attrMap = array_column( array_filter( $this->object()->getSearchAttributes(), function( $item ) {
170
			return $item->isPublic() || strncmp( $item->getCode(), 'agg:', 4 ) === 0;
171
		} ), null, 'code' );
172
173
		if( $value === null && ( $value = key( $attrMap ) ) === null )
174
		{
175
			$msg = $this->context()->translate( 'mshop', 'No search keys available' );
176
			throw new \Aimeos\MShop\Exception( $msg );
177
		}
178
179
		if( ( $pos = strpos( $valkey = $value, '(' ) ) !== false ) {
180
			$value = substr( $value, 0, $pos ) . '()'; // remove parameters from search function
181
		}
182
183
		if( !isset( $attrMap[$value] ) )
184
		{
185
			$msg = $this->context()->translate( 'mshop', 'Unknown search key "%1$s"' );
186
			throw new \Aimeos\MShop\Exception( sprintf( $msg, $value ) );
187
		}
188
189
		$keys = (array) $keys;
190
		$acols = $cols = $expr = [];
191
		$search = (clone $search)->slice( $search->getOffset(), min( $search->getLimit(), $limit ) );
192
193
		foreach( $keys as $string )
194
		{
195
			if( ( $attrItem = $attrMap[$string] ?? null ) === null )
196
			{
197
				$msg = $this->context()->translate( 'mshop', 'Unknown search key "%1$s"' );
198
				throw new \Aimeos\MShop\Exception( sprintf( $msg, $string ) );
199
			}
200
201
			$cols[] = $attrItem->getInternalCode();
202
			$acols[] = $attrItem->getInternalCode() . ' AS "' . $string . '"';
203
204
			$expr[] = $search->compare( '!=', $string, null ); // required for the joins
205
		}
206
207
		$expr[] = $search->compare( '!=', $valkey, null );
208
		$search->add( $search->and( $expr ) );
209
210
		$sql = $this->getSqlConfig( $cfgPath );
211
		$sql = str_replace( ':cols', join( ', ', $cols ), $sql );
212
		$sql = str_replace( ':acols', join( ', ', $acols ), $sql );
213
		$sql = str_replace( ':keys', '"' . join( '", "', $keys ) . '"', $sql );
214
		$sql = str_replace( ':val', $attrMap[$value]->getInternalCode(), $sql );
215
		$sql = str_replace( ':type', in_array( $type, ['avg', 'count', 'max', 'min', 'sum'] ) ? $type : 'count', $sql );
216
217
		return $this->aggregateResult( $search, $sql, $required );
218
	}
219
220
221
	/**
222
	 * Returns the aggregated values for the given SQL string and filter.
223
	 *
224
	 * @param \Aimeos\Base\Criteria\Iface $filter Filter object
225
	 * @param string $sql SQL statement
226
	 * @param string[] $required List of domain/sub-domain names like "catalog.index" that must be additionally joined
227
	 * @return \Aimeos\Map (Nested) list of aggregated values as key and the number of counted products as value
228
	 */
229
	protected function aggregateResult( \Aimeos\Base\Criteria\Iface $filter, string $sql, array $required ) : \Aimeos\Map
230
	{
231
		$map = [];
232
		$total = null;
233
		$level = \Aimeos\MShop\Locale\Manager\Base::SITE_ALL;
234
		$conn = $this->context()->db( $this->getResourceName() );
235
		$results = $this->searchItemsBase( $conn, $filter, $sql, '', $required, $total, $level );
236
237
		while( $row = $results->fetch() )
238
		{
239
			$row = $this->transform( $row );
240
241
			$temp = &$map;
242
			$last = array_pop( $row );
243
244
			foreach( $row as $val ) {
245
				$temp[$val] = $temp[$val] ?? [];
246
				$temp = &$temp[$val];
247
			}
248
			$temp = $last;
249
		}
250
251
		return map( $map );
252
	}
253
254
255
	/**
256
	 * Returns the table alias name.
257
	 *
258
	 * @param string|null $attrcode Search attribute code
259
	 * @return string Table alias name
260
	 */
261
	protected function alias( string $attrcode = null ) : string
262
	{
263
		if( $attrcode )
264
		{
265
			$parts = array_slice( explode( '.', $attrcode ), 0, -1 );
266
			$str = 'm' . substr( array_shift( $parts ) ?: $this->getDomain(), 0, 3 );
267
		}
268
		else
269
		{
270
			$parts = explode( '/', $this->getSubPath() );
271
			$str = 'm' . substr( $this->getDomain(), 0, 3 );
272
		}
273
274
		foreach( $parts as $part ) {
275
			$str .= substr( $part, 0, 2 );
276
		}
277
278
		return $str;
279
	}
280
281
282
	/**
283
	 * Adds aliases for the columns
284
	 *
285
	 * @param array $map Associative list of search keys as keys and internal column names as values
286
	 * @return array Associative list of search keys as keys and aliased column names as values
287
	 */
288
	protected function aliasTranslations( array $map ) : array
289
	{
290
		foreach( $map as $key => $value )
291
		{
292
			if( strpos( $value, '"' ) === false ) {
293
				$map[$key] = $this->alias( $key ) . '."' . $value . '"';
294
			}
295
		}
296
297
		return $map;
298
	}
299
300
301
	/**
302
	 * Binds additional values to the statement before execution.
303
	 *
304
	 * @param \Aimeos\MShop\Common\Item\Iface $item Item object
305
	 * @param \Aimeos\Base\DB\Statement\Iface $stmt Database statement object
306
	 * @param int $idx Current bind index
307
	 * @return \Aimeos\Base\DB\Statement\Iface Database statement object with bound values
308
	 */
309
	protected function bind( \Aimeos\MShop\Common\Item\Iface $item, \Aimeos\Base\DB\Statement\Iface $stmt, int &$idx ) : \Aimeos\Base\DB\Statement\Iface
310
	{
311
		return $stmt;
312
	}
313
314
315
	/**
316
	 * Removes old entries from the storage.
317
	 *
318
	 * @param iterable $siteids List of IDs for sites whose entries should be deleted
319
	 * @param string $cfgpath Configuration key to the cleanup statement
320
	 * @return \Aimeos\MShop\Common\Manager\Iface Manager object for chaining method calls
321
	 */
322
	protected function clearBase( iterable $siteids, string $cfgpath ) : \Aimeos\MShop\Common\Manager\Iface
323
	{
324
		if( empty( $siteids ) ) {
325
			return $this;
326
		}
327
328
		$conn = $this->context()->db( $this->getResourceName() );
329
330
		$sql = $this->getSqlConfig( $cfgpath );
331
		$sql = str_replace( ':cond', '1=1', $sql );
332
333
		$stmt = $conn->create( $sql );
334
335
		foreach( $siteids as $siteid )
336
		{
337
			$stmt->bind( 1, $siteid );
338
			$stmt->execute()->finish();
339
		}
340
341
		return $this;
342
	}
343
344
345
	/**
346
	 * Deletes items.
347
	 *
348
	 * @param \Aimeos\MShop\Common\Item\Iface|\Aimeos\Map|array|string $items List of item objects or IDs of the items
349
	 * @param string $cfgpath Configuration path to the SQL statement
350
	 * @param bool $siteid If siteid should be used in the statement
351
	 * @param string $name Name of the ID column
352
	 * @return \Aimeos\MShop\Common\Manager\Iface Manager object for chaining method calls
353
	 */
354
	protected function deleteItemsBase( $items, string $cfgpath, bool $siteid = true,
355
		string $name = 'id' ) : \Aimeos\MShop\Common\Manager\Iface
356
	{
357
		if( map( $items )->isEmpty() ) {
358
			return $this;
359
		}
360
361
		$search = $this->object()->filter();
362
		$search->setConditions( $search->compare( '==', $name, $items ) );
363
364
		$types = array( $name => \Aimeos\Base\DB\Statement\Base::PARAM_STR );
365
		$translations = array( $name => '"' . $name . '"' );
366
367
		$cond = $search->getConditionSource( $types, $translations );
368
		$sql = str_replace( ':cond', $cond, $this->getSqlConfig( $cfgpath ) );
369
370
		$context = $this->context();
371
		$conn = $context->db( $this->getResourceName() );
372
373
		$stmt = $conn->create( $sql );
374
375
		if( $siteid ) {
376
			$stmt->bind( 1, $context->locale()->getSiteId() . '%' );
377
		}
378
379
		$stmt->execute()->finish();
380
381
		return $this;
382
	}
383
384
385
	/**
386
	 * Fetches the rows from the database statement and returns the list of items.
387
	 *
388
	 * @param \Aimeos\Base\DB\Result\Iface $stmt Database statement object
389
	 * @param array $ref List of domains whose items should be fetched too
390
	 * @param string $prefix Prefix for the property names
391
	 * @param array $attrs List of attributes that should be decoded
392
	 * @return \Aimeos\Map List of items implementing \Aimeos\MShop\Common\Item\Iface
393
	 */
394
	protected function fetch( \Aimeos\Base\DB\Result\Iface $results, array $ref, string $prefix = '', array $attrs = [] ) : \Aimeos\Map
395
	{
396
		$map = [];
397
398
		while( $row = $results->fetch() )
399
		{
400
			foreach( $attrs as $code => $attr ) {
401
				$row[$code] = json_decode( $row[$code], true );
402
			}
403
404
			if( $item = $this->applyFilter( $this->create( $row ) ) ) {
0 ignored issues
show
Bug introduced by
The method create() does not exist on Aimeos\MShop\Common\Manager\DB. Did you maybe mean createAttributes()? ( Ignorable by Annotation )

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

404
			if( $item = $this->applyFilter( $this->/** @scrutinizer ignore-call */ create( $row ) ) ) {

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...
Bug introduced by
It seems like applyFilter() 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

404
			if( $item = $this->/** @scrutinizer ignore-call */ applyFilter( $this->create( $row ) ) ) {
Loading history...
405
				$map[$row[$prefix . 'id']] = $item;
406
			}
407
		}
408
409
		return map( $map );
410
	}
411
412
413
	/**
414
	 * Sets the base criteria "status".
415
	 * (setConditions overwrites the base criteria)
416
	 *
417
	 * @param string $domain Name of the domain/sub-domain like "product" or "product.list"
418
	 * @param bool|null $default TRUE for status=1, NULL for status>0, FALSE for no restriction
419
	 * @return \Aimeos\Base\Criteria\Iface Search critery object
420
	 */
421
	protected function filterBase( string $domain, ?bool $default = false ) : \Aimeos\Base\Criteria\Iface
422
	{
423
		$context = $this->context();
424
		$db = $this->getResourceName();
425
		$conn = $context->db( $db );
426
		$config = $context->config();
427
428
		if( ( $adapter = $config->get( 'resource/' . $db . '/adapter' ) ) === null ) {
429
			$adapter = $config->get( 'resource/db/adapter' );
430
		}
431
432
		switch( $adapter )
433
		{
434
			case 'pgsql':
435
				$filter = new \Aimeos\Base\Criteria\PgSQL( $conn ); break;
436
			default:
437
				$filter = new \Aimeos\Base\Criteria\SQL( $conn ); break;
438
		}
439
440
		if( $default !== false ) {
441
			$filter->add( $domain . '.status', $default ? '==' : '>=', 1 );
442
		}
443
444
		return $filter;
445
	}
446
447
448
	/**
449
	 * Returns the item for the given search key/value pairs.
450
	 *
451
	 * @param array $pairs Search key/value pairs for the item
452
	 * @param string[] $ref List of domains whose items should be fetched too
453
	 * @param bool|null $default Add default criteria or NULL for relaxed default criteria
454
	 * @return \Aimeos\MShop\Common\Item\Iface Requested item
455
	 * @throws \Aimeos\MShop\Exception if no item with the given ID found
456
	 */
457
	protected function findBase( array $pairs, array $ref, ?bool $default ) : \Aimeos\MShop\Common\Item\Iface
458
	{
459
		$expr = [];
460
		$criteria = $this->object()->filter( $default )->slice( 0, 1 );
461
462
		foreach( $pairs as $key => $value )
463
		{
464
			if( $value === null )
465
			{
466
				$msg = $this->context()->translate( 'mshop', 'Required value for "%1$s" is missing' );
467
				throw new \Aimeos\MShop\Exception( sprintf( $msg, $key ) );
468
			}
469
			$expr[] = $criteria->compare( '==', $key, $value );
470
		}
471
472
		$criteria->setConditions( $criteria->and( $expr ) );
473
474
		if( ( $item = $this->object()->search( $criteria, $ref )->first() ) ) {
475
			return $item;
476
		}
477
478
		$msg = $this->context()->translate( 'mshop', 'No item found for conditions: %1$s' );
479
		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

479
		throw new \Aimeos\MShop\Exception( sprintf( $msg, /** @scrutinizer ignore-type */ print_r( $pairs, true ) ), 404 );
Loading history...
480
	}
481
482
483
	/**
484
	 * Returns the cached statement for the given key or creates a new prepared statement.
485
	 * If no SQL string is given, the key is used to retrieve the SQL string from the configuration.
486
	 *
487
	 * @param \Aimeos\Base\DB\Connection\Iface $conn Database connection
488
	 * @param string $cfgkey Unique key for the SQL
489
	 * @param string|null $sql SQL string if it shouldn't be retrieved from the configuration
490
	 * @return \Aimeos\Base\DB\Statement\Iface Database statement object
491
	 */
492
	protected function getCachedStatement( \Aimeos\Base\DB\Connection\Iface $conn, string $cfgkey,
493
		string $sql = null ) : \Aimeos\Base\DB\Statement\Iface
494
	{
495
		if( !isset( $this->cachedStmts['stmt'][$cfgkey] )
496
			|| !isset( $this->cachedStmts['conn'][$cfgkey] )
497
			|| $conn !== $this->cachedStmts['conn'][$cfgkey]
498
		) {
499
			if( $sql === null ) {
500
				$sql = $this->getSqlConfig( $cfgkey );
501
			}
502
503
			$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

503
			$this->cachedStmts['stmt'][$cfgkey] = $conn->create( /** @scrutinizer ignore-type */ $sql );
Loading history...
504
			$this->cachedStmts['conn'][$cfgkey] = $conn;
505
		}
506
507
		return $this->cachedStmts['stmt'][$cfgkey];
508
	}
509
510
511
	/**
512
	 * Returns the full configuration key for the passed last part
513
	 *
514
	 * @param string $name Configuration last part
515
	 * @return string Full configuration key
516
	 */
517
	protected function getConfigKey( string $name ) : string
518
	{
519
		$subPath = $this->getSubPath();
520
		return 'mshop/' . $this->getDomain() . '/manager/' . ( $subPath ? $subPath . '/' : '' ) . $name;
521
	}
522
523
524
	/**
525
	 * Returns a sorted list of required criteria keys.
526
	 *
527
	 * @param \Aimeos\Base\Criteria\Iface $criteria Search criteria object
528
	 * @param string[] $required List of prefixes of required search conditions
529
	 * @return string[] Sorted list of criteria keys
530
	 */
531
	protected function getCriteriaKeyList( \Aimeos\Base\Criteria\Iface $criteria, array $required ) : array
532
	{
533
		$keys = array_merge( $required, $this->getCriteriaKeys( $required, $criteria->getConditions() ) );
534
535
		foreach( $criteria->getSortations() as $sortation ) {
536
			$keys = array_merge( $keys, $this->getCriteriaKeys( $required, $sortation ) );
537
		}
538
539
		$keys = array_unique( array_merge( $required, $keys ) );
540
		sort( $keys );
541
542
		return $keys;
543
	}
544
545
546
	/**
547
	 * Returns the manager domain
548
	 *
549
	 * @return string Manager domain e.g. "product"
550
	 */
551
	protected function getDomain() : string
552
	{
553
		if( !isset( $this->domain ) ) {
554
			$this->initDb();
555
		}
556
557
		return $this->domain;
558
	}
559
560
561
	/**
562
	 * Returns the item for the given search key and ID.
563
	 *
564
	 * @param string $key Search key for the requested ID
565
	 * @param string $id Unique ID to search for
566
	 * @param string[] $ref List of domains whose items should be fetched too
567
	 * @param bool|null $default Add default criteria or NULL for relaxed default criteria
568
	 * @return \Aimeos\MShop\Common\Item\Iface Requested item
569
	 * @throws \Aimeos\MShop\Exception if no item with the given ID found
570
	 */
571
	protected function getItemBase( string $key, string $id, array $ref, ?bool $default ) : \Aimeos\MShop\Common\Item\Iface
572
	{
573
		$criteria = $this->object()->filter( $default )->add( [$key => $id] )->slice( 0, 1 );
574
575
		if( ( $item = $this->object()->search( $criteria, $ref )->first() ) ) {
576
			return $item;
577
		}
578
579
		$msg = $this->context()->translate( 'mshop', 'Item with ID "%2$s" in "%1$s" not found' );
580
		throw new \Aimeos\MShop\Exception( sprintf( $msg, $key, $id ), 404 );
581
	}
582
583
584
	/**
585
	 * Returns the SQL strings for joining dependent tables.
586
	 *
587
	 * @param \Aimeos\Base\Criteria\Attribute\Iface[] $attributes List of criteria attribute items
588
	 * @param string $prefix Search key prefix
589
	 * @return array List of JOIN SQL strings
590
	 */
591
	protected function getJoins( array $attributes, string $prefix ) : array
592
	{
593
		$iface = \Aimeos\Base\Criteria\Attribute\Iface::class;
594
		$name = $prefix . '.id';
595
596
		if( isset( $attributes[$prefix] ) && $attributes[$prefix] instanceof $iface ) {
597
			return $attributes[$prefix]->getInternalDeps();
598
		} elseif( isset( $attributes[$name] ) && $attributes[$name] instanceof $iface ) {
599
			return $attributes[$name]->getInternalDeps();
600
		} elseif( isset( $attributes['id'] ) && $attributes['id'] instanceof $iface ) {
601
			return $attributes['id']->getInternalDeps();
602
		}
603
604
		return [];
605
	}
606
607
608
	/**
609
	 * Returns the manager path
610
	 *
611
	 * @return string Manager path e.g. "product/lists/type"
612
	 */
613
	protected function getManagerPath() : string
614
	{
615
		$subPath = $this->getSubPath();
616
		return $this->getDomain() . ( $subPath ? '/' . $subPath : '' );
617
	}
618
619
620
	/**
621
	 * Returns the required SQL joins for the critera.
622
	 *
623
	 * @param \Aimeos\Base\Criteria\Attribute\Iface[] $attributes List of criteria attribute items
624
	 * @param string $prefix Search key prefix
625
	 * @return array|null List of JOIN SQL strings
626
	 */
627
	protected function getRequiredJoins( array $attributes, array $keys, string $basekey = null ) : array
628
	{
629
		$joins = [];
630
631
		foreach( $keys as $key )
632
		{
633
			if( $key !== $basekey ) {
634
				$joins = array_merge( $joins, $this->getJoins( $attributes, $key ) );
635
			}
636
		}
637
638
		return array_unique( $joins );
639
	}
640
641
642
	/**
643
	 * Returns the name of the resource.
644
	 *
645
	 * @return string Name of the resource, e.g. "db-product"
646
	 */
647
	protected function getResourceName() : string
648
	{
649
		if( $this->resourceName === null ) {
650
			$this->setResourceName( 'db-' . $this->getDomain() );
651
		}
652
653
		return $this->resourceName;
654
	}
655
656
657
	/**
658
	 * Returns the available manager types
659
	 *
660
	 * @param string $type Main manager type
661
	 * @param string $path Configuration path to the sub-domains
662
	 * @param string[] $default List of sub-domains if no others are configured
663
	 * @param bool $withsub Return also the resource type of sub-managers if true
664
	 * @return string[] Type of the manager and submanagers, subtypes are separated by slashes
665
	 */
666
	protected function getResourceTypeBase( string $type, string $path, array $default, bool $withsub ) : array
667
	{
668
		$list = [$type];
669
670
		if( $withsub )
671
		{
672
			foreach( $this->context()->config()->get( $path, $default ) as $domain ) {
673
				$list = array_merge( $list, $this->object()->getSubManager( $domain )->getResourceType( $withsub ) );
674
			}
675
		}
676
677
		return $list;
678
	}
679
680
681
	/**
682
	 * Returns the search attribute objects used for searching.
683
	 *
684
	 * @param array $list Associative list of search keys and the lists of search definitions
685
	 * @param string $path Configuration path to the sub-domains for fetching the search definitions
686
	 * @param string[] $default List of sub-domains if no others are configured
687
	 * @param bool $withsub True to include search definitions of sub-domains, false if not
688
	 * @return \Aimeos\Base\Criteria\Attribute\Iface[] Associative list of search keys and criteria attribute items as values
689
	 * @since 2014.09
690
	 */
691
	protected function getSearchAttributesBase( array $list, string $path, array $default, bool $withsub ) : array
692
	{
693
		$attr = $this->createAttributes( $list );
694
695
		if( $withsub === true )
696
		{
697
			$domains = $this->context()->config()->get( $path, $default );
698
699
			foreach( $domains as $domain ) {
700
				$attr += $this->object()->getSubManager( $domain )->getSearchAttributes( true );
701
			}
702
		}
703
704
		return $attr;
705
	}
706
707
708
	/**
709
	 * Returns the item search key for the passed name
710
	 *
711
	 * @return string Item prefix e.g. "product.lists.type.id"
712
	 */
713
	protected function getSearchKey( string $name = '' ) : string
714
	{
715
		$subPath = $this->getSubPath();
716
		return $this->getDomain() . ( $subPath ? '.' . $subPath : '' ) . ( $name ? '.' . $name : '' );
717
	}
718
719
720
	/**
721
	 * Returns the search results for the given SQL statement.
722
	 *
723
	 * @param \Aimeos\Base\DB\Connection\Iface $conn Database connection
724
	 * @param string $sql SQL statement
725
	 * @return \Aimeos\Base\DB\Result\Iface Search result object
726
	 */
727
	protected function getSearchResults( \Aimeos\Base\DB\Connection\Iface $conn, string $sql ) : \Aimeos\Base\DB\Result\Iface
728
	{
729
		$time = microtime( true );
730
731
		$stmt = $conn->create( $sql );
732
		$result = $stmt->execute();
733
734
		$level = \Aimeos\Base\Logger\Iface::DEBUG;
735
		$time = ( microtime( true ) - $time ) * 1000;
736
		$msg = 'Time: ' . $time . "ms\n"
737
			. 'Class: ' . get_class( $this ) . "\n"
738
			. str_replace( ["\t", "\n\n"], ['', "\n"], trim( (string) $stmt ) );
739
740
		if( $time > 1000.0 )
741
		{
742
			$level = \Aimeos\Base\Logger\Iface::NOTICE;
743
			$msg .= "\n" . ( new \Exception() )->getTraceAsString();
744
		}
745
746
		$this->context()->logger()->log( $msg, $level, 'core/sql' );
747
748
		return $result;
749
	}
750
751
752
	/**
753
	 * Returns the site coditions for the search request
754
	 *
755
	 * @param string[] $keys Sorted list of criteria keys
756
	 * @param \Aimeos\Base\Criteria\Attribute\Iface[] $attributes Associative list of search keys and criteria attribute items as values
757
	 * @param int $sitelevel Site level constant from \Aimeos\MShop\Locale\Manager\Base
758
	 * @return \Aimeos\Base\Criteria\Expression\Iface[] List of search conditions
759
	 * @since 2015.01
760
	 */
761
	protected function getSiteConditions( array $keys, array $attributes, int $sitelevel ) : array
762
	{
763
		$list = [];
764
		$entries = array_column( $attributes, null, 'code' );
765
766
		foreach( $keys as $key )
767
		{
768
			$name = $key . '.siteid';
769
770
			if( isset( $entries[$name] ) ) {
771
				$list[] = $this->siteCondition( $name, $sitelevel );
772
			} elseif( isset( $entries['siteid'] ) ) {
773
				$list[] = $this->siteCondition( 'siteid', $sitelevel );
774
			}
775
		}
776
777
		return $list;
778
	}
779
780
781
	/**
782
	 * Returns the SQL statement for the given config path
783
	 *
784
	 * If available, the database specific SQL statement is returned, otherwise
785
	 * the ANSI SQL statement. The database type is determined via the resource
786
	 * adapter.
787
	 *
788
	 * @param string $sql Configuration path to the SQL statement
789
	 * @param array $replace Associative list of keys with strings to replace by their values
790
	 * @return array|string ANSI or database specific SQL statement
791
	 */
792
	protected function getSqlConfig( string $sql, array $replace = [] )
793
	{
794
		if( preg_match( '#^[a-z0-9\-]+(/[a-z0-9\-]+)*$#', $sql ) === 1 )
795
		{
796
			$config = $this->context()->config();
797
			$adapter = $config->get( 'resource/' . $this->getResourceName() . '/adapter' );
798
799
			if( ( $str = $config->get( $sql . '/' . $adapter, $config->get( $sql . '/ansi' ) ) ) === null )
800
			{
801
				$parts = explode( '/', $sql );
802
				$cpath = 'mshop/common/manager/' . end( $parts );
803
				$str = $config->get( $cpath . '/' . $adapter, $config->get( $cpath . '/ansi', $sql ) );
804
			}
805
806
			$sql = $str;
807
		}
808
809
		foreach( $replace as $key => $value ) {
810
			$sql = str_replace( $key, $value, $sql );
811
		}
812
813
		return str_replace( [':alias', ':table'], [$this->alias(), $this->getTable()], $sql );
814
	}
815
816
817
	/**
818
	 * Returns a search object singleton
819
	 *
820
	 * @return \Aimeos\Base\Criteria\Iface Search object
821
	 */
822
	protected function getSearch() : \Aimeos\Base\Criteria\Iface
823
	{
824
		if( !isset( $this->search ) ) {
825
			$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

825
			/** @scrutinizer ignore-call */ 
826
   $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...
826
		}
827
828
		return $this->search;
829
	}
830
831
832
	/**
833
	 * Returns the string replacements for the SQL statements
834
	 *
835
	 * @param \Aimeos\Base\Criteria\Iface $search Search critera object
836
	 * @param \Aimeos\Base\Criteria\Attribute\Iface[] $attributes Associative list of search keys and criteria attribute items as values
837
	 * @param \Aimeos\Base\Criteria\Attribute\Iface[] $attributes Associative list of search keys and criteria attribute items as values for the base table
838
	 * @param \Aimeos\Base\Criteria\Plugin\Iface[] $plugins Associative list of search keys and criteria plugin items as values
839
	 * @param string[] $joins Associative list of SQL joins
840
	 * @param \Aimeos\Base\Criteria\Attribute\Iface[] $columns Additional columns to retrieve values from
841
	 * @return array Array of keys, find and replace arrays
842
	 */
843
	protected function getSQLReplacements( \Aimeos\Base\Criteria\Iface $search, array $attributes, array $attronly, array $plugins, array $joins ) : array
844
	{
845
		$types = $this->getSearchTypes( $attributes );
846
		$funcs = $this->getSearchFunctions( $attributes );
847
		$trans = $this->getSearchTranslations( $attributes );
848
		$trans = $this->aliasTranslations( $trans );
849
850
		if( empty( $search->getSortations() ) && ( $attribute = reset( $attronly ) ) !== false ) {
851
			$search = ( clone $search )->setSortations( [$search->sort( '+', $attribute->getCode() )] );
852
		}
853
		$sorts = $search->translate( $search->getSortations(), $trans, $funcs );
854
855
		$cols = $group = [];
856
		foreach( $attronly as $name => $entry )
857
		{
858
			if( str_contains( $name, ':' ) || empty( $entry->getInternalCode() ) ) {
859
				continue;
860
			}
861
862
			$icode = $entry->getInternalCode();
863
864
			if( !str_contains( $icode, '"' ) )
865
			{
866
				$alias = $this->alias( $entry->getCode() );
867
				$icode = $alias . '."' . $icode . '"';
868
			}
869
870
			$cols[] = $icode . ' AS "' . $entry->getCode() . '"';
871
			$group[] = $icode;
872
		}
873
874
		return [
875
			':columns' => join( ', ', $cols ),
876
			':joins' => join( "\n", array_unique( $joins ) ),
877
			':group' => join( ', ', array_unique( array_merge( $group, $sorts ) ) ),
878
			':cond' => $search->getConditionSource( $types, $trans, $plugins, $funcs ),
879
			':order' => $search->getSortationSource( $types, $trans, $funcs ),
880
			':start' => $search->getOffset(),
881
			':size' => $search->getLimit(),
882
		];
883
	}
884
885
886
	/**
887
	 * Returns the available sub-manager names
888
	 *
889
	 * @return array Sub-manager names, e.g. ['lists', 'property', 'type']
890
	 */
891
	protected function getSubManagers() : array
892
	{
893
		return $this->context()->config()->get( $this->getConfigKey( 'submanagers' ), [] );
894
	}
895
896
897
	/**
898
	 * Returns the manager domain sub-path
899
	 *
900
	 * @return string Manager domain sub-path e.g. "lists/type"
901
	 */
902
	protected function getSubPath() : string
903
	{
904
		if( !isset( $this->subpath ) ) {
905
			$this->initDb();
906
		}
907
908
		return $this->subpath;
909
	}
910
911
912
	/**
913
	 * Returns the name of the used table
914
	 *
915
	 * @return string Table name e.g. "mshop_product_lists_type"
916
	 */
917
	protected function getTable() : string
918
	{
919
		$subPath = $this->getSubPath();
920
		return 'mshop_' . $this->getDomain() . ( $subPath ? '_' . str_replace( '/', '_', $subPath ) : '' );
921
	}
922
923
924
	/**
925
	 * Initializes the trait
926
	 */
927
	protected function initDb()
928
	{
929
		$parts = array_slice( explode( '\\', strtolower( get_class( $this ) ) ), 2, -1 );
930
931
		$this->domain = array_shift( $parts ) ?: '';
932
		array_shift( $parts ); // remove "manager"
933
		$this->subpath = join( '/', $parts );
934
	}
935
936
937
	/**
938
	 * Checks if the item is modified
939
	 *
940
	 * @param \Aimeos\MShop\Common\Item\Iface $item Item object
941
	 * @return bool True if the item is modified, false if not
942
	 */
943
	protected function isModified( \Aimeos\MShop\Common\Item\Iface $item ) : bool
944
	{
945
		return $item->isModified();
946
	}
947
948
949
	/**
950
	 * Returns the newly created ID for the last record which was inserted.
951
	 *
952
	 * @param \Aimeos\Base\DB\Connection\Iface $conn Database connection used to insert the new record
953
	 * @param string $cfgpath Configuration path to the SQL statement for retrieving the new ID of the last inserted record
954
	 * @return string ID of the last record that was inserted by using the given connection
955
	 * @throws \Aimeos\MShop\Exception if there's no ID of the last record available
956
	 */
957
	protected function newId( \Aimeos\Base\DB\Connection\Iface $conn, string $cfgpath ) : string
958
	{
959
		$sql = $this->getSqlConfig( $cfgpath );
960
961
		$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

961
		$result = $conn->create( /** @scrutinizer ignore-type */ $sql )->execute();
Loading history...
962
963
		if( ( $row = $result->fetch( \Aimeos\Base\DB\Result\Base::FETCH_NUM ) ) === false )
964
		{
965
			$msg = $this->context()->translate( 'mshop', 'ID of last inserted database record not available' );
966
			throw new \Aimeos\MShop\Exception( $msg );
967
		}
968
		$result->finish();
969
970
		return $row[0];
971
	}
972
973
974
	/**
975
	 * Saves an attribute item to the storage.
976
	 *
977
	 * @param \Aimeos\MShop\Common\Item\Iface $item Item object
978
	 * @param bool $fetch True if the new ID should be returned in the item
979
	 * @return \Aimeos\MShop\Common\Item\Iface $item Updated item including the generated ID
980
	 */
981
	protected function saveBase( \Aimeos\MShop\Common\Item\Iface $item, bool $fetch = true ) : \Aimeos\MShop\Common\Item\Iface
982
	{
983
		if( !$this->isModified( $item ) ) {
984
			return $this->saveDeps( $item );
985
		}
986
987
		$context = $this->context();
988
		$conn = $context->db( $this->getResourceName() );
989
990
		$id = $item->getId();
991
		$columns = array_column( $this->object()->getSaveAttributes(), null, 'internalcode' );
992
993
		if( $id === null )
994
		{
995
			/** mshop/common/manager/insert/mysql
996
			 * Inserts a new record into the database table
997
			 *
998
			 * @see mshop/common/manager/insert/ansi
999
			 */
1000
1001
			/** mshop/common/manager/insert/ansi
1002
			 * Inserts a new record into the database table
1003
			 *
1004
			 * Items with no ID yet (i.e. the ID is NULL) will be created in
1005
			 * the database and the newly created ID retrieved afterwards
1006
			 * using the "newid" SQL statement.
1007
			 *
1008
			 * The SQL statement must be a string suitable for being used as
1009
			 * prepared statement. It must include question marks for binding
1010
			 * the values from the item to the statement before they are
1011
			 * sent to the database server. The number of question marks must
1012
			 * be the same as the number of columns listed in the INSERT
1013
			 * statement. The order of the columns must correspond to the
1014
			 * order in the save() method, so the correct values are
1015
			 * bound to the columns.
1016
			 *
1017
			 * The SQL statement should conform to the ANSI standard to be
1018
			 * compatible with most relational database systems. This also
1019
			 * includes using double quotes for table and column names.
1020
			 *
1021
			 * @param string SQL statement for inserting records
1022
			 * @since 2023.10
1023
			 * @see mshop/common/manager/update/ansi
1024
			 * @see mshop/common/manager/newid/ansi
1025
			 * @see mshop/common/manager/delete/ansi
1026
			 * @see mshop/common/manager/search/ansi
1027
			 * @see mshop/common/manager/count/ansi
1028
			 */
1029
			$path = $this->getConfigKey( 'insert' );
1030
			$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

1030
			$sql = $this->addSqlColumns( array_keys( $columns ), /** @scrutinizer ignore-type */ $this->getSqlConfig( $path ) );
Loading history...
1031
		}
1032
		else
1033
		{
1034
			/** mshop/common/manager/update/mysql
1035
			 * Updates an existing record in the database
1036
			 *
1037
			 * @see mshop/common/manager/update/ansi
1038
			 */
1039
1040
			/** mshop/common/manager/update/ansi
1041
			 * Updates an existing record in the database
1042
			 *
1043
			 * Items which already have an ID (i.e. the ID is not NULL) will
1044
			 * be updated in the database.
1045
			 *
1046
			 * The SQL statement must be a string suitable for being used as
1047
			 * prepared statement. It must include question marks for binding
1048
			 * the values from the item to the statement before they are
1049
			 * sent to the database server. The order of the columns must
1050
			 * correspond to the order in the save() method, so the
1051
			 * correct values are bound to the columns.
1052
			 *
1053
			 * The SQL statement should conform to the ANSI standard to be
1054
			 * compatible with most relational database systems. This also
1055
			 * includes using double quotes for table and column names.
1056
			 *
1057
			 * @param string SQL statement for updating records
1058
			 * @since 2023.10
1059
			 * @see mshop/common/manager/insert/ansi
1060
			 * @see mshop/common/manager/newid/ansi
1061
			 * @see mshop/common/manager/delete/ansi
1062
			 * @see mshop/common/manager/search/ansi
1063
			 * @see mshop/common/manager/count/ansi
1064
			 */
1065
			$path = $this->getConfigKey( 'update' );
1066
			$sql = $this->addSqlColumns( array_keys( $columns ), $this->getSqlConfig( $path ), false );
1067
		}
1068
1069
		$idx = 1;
1070
		$values = $item->toArray( true );
1071
		$stmt = $this->getCachedStatement( $conn, $path, $sql );
1072
1073
		foreach( $columns as $entry )
1074
		{
1075
			$value = $values[$entry->getCode()] ?? null;
1076
			$value = $entry->getType() === 'json' ? json_encode( $value, JSON_FORCE_OBJECT ) : $value;
1077
			$stmt->bind( $idx++, $value, \Aimeos\Base\Criteria\SQL::type( $entry->getType() ) );
1078
		}
1079
1080
		$stmt = $this->bind( $item, $stmt, $idx );
1081
1082
		$stmt->bind( $idx++, $context->datetime() ); // mtime
1083
		$stmt->bind( $idx++, $context->editor() );
1084
1085
		if( $id !== null ) {
1086
			$stmt->bind( $idx++, $context->locale()->getSiteId() . '%' );
1087
			$stmt->bind( $idx++, $id, \Aimeos\Base\DB\Statement\Base::PARAM_INT );
1088
		} else {
1089
			$stmt->bind( $idx++, $this->siteId( $item->getSiteId(), \Aimeos\MShop\Locale\Manager\Base::SITE_SUBTREE ) );
1090
			$stmt->bind( $idx++, $context->datetime() ); // ctime
1091
		}
1092
1093
		$stmt->execute()->finish();
1094
1095
		if( $id === null )
1096
		{
1097
			/** mshop/common/manager/newid/mysql
1098
			 * Retrieves the ID generated by the database when inserting a new record
1099
			 *
1100
			 * @see mshop/common/manager/newid/ansi
1101
			 */
1102
1103
			/** mshop/common/manager/newid/ansi
1104
			 * Retrieves the ID generated by the database when inserting a new record
1105
			 *
1106
			 * As soon as a new record is inserted into the database table,
1107
			 * the database server generates a new and unique identifier for
1108
			 * that record. This ID can be used for retrieving, updating and
1109
			 * deleting that specific record from the table again.
1110
			 *
1111
			 * For MySQL:
1112
			 *  SELECT LAST_INSERT_ID()
1113
			 * For PostgreSQL:
1114
			 *  SELECT currval('seq_matt_id')
1115
			 * For SQL Server:
1116
			 *  SELECT SCOPE_IDENTITY()
1117
			 * For Oracle:
1118
			 *  SELECT "seq_matt_id".CURRVAL FROM DUAL
1119
			 *
1120
			 * There's no way to retrive the new ID by a SQL statements that
1121
			 * fits for most database servers as they implement their own
1122
			 * specific way.
1123
			 *
1124
			 * @param string SQL statement for retrieving the last inserted record ID
1125
			 * @since 2023.10
1126
			 * @see mshop/common/manager/insert/ansi
1127
			 * @see mshop/common/manager/update/ansi
1128
			 * @see mshop/common/manager/delete/ansi
1129
			 * @see mshop/common/manager/search/ansi
1130
			 * @see mshop/common/manager/count/ansi
1131
			 */
1132
			$id = $this->newId( $conn, 'mshop/common/manager/newid' );
1133
		}
1134
1135
		return $this->saveDeps( $item->setId( $id ) );
1136
	}
1137
1138
1139
	/**
1140
	 * Saves the dependent items of the item
1141
	 *
1142
	 * @param \Aimeos\MShop\Common\Item\Iface $item Item object
1143
	 * @param bool $fetch True if the new ID should be returned in the item
1144
	 * @return \Aimeos\MShop\Common\Item\Iface Updated item
1145
	 */
1146
	protected function saveDeps( \Aimeos\MShop\Common\Item\Iface $item, bool $fetch = true ) : \Aimeos\MShop\Common\Item\Iface
1147
	{
1148
		return $item;
1149
	}
1150
1151
1152
	/**
1153
	 * Returns the search result of the statement combined with the given criteria.
1154
	 *
1155
	 * @param \Aimeos\Base\DB\Connection\Iface $conn Database connection
1156
	 * @param \Aimeos\Base\Criteria\Iface $search Search criteria object
1157
	 * @param string $cfgPathSearch Path to SQL statement in configuration for searching
1158
	 * @param string $cfgPathCount Path to SQL statement in configuration for counting
1159
	 * @param string[] $required Additional search keys to add conditions for even if no conditions are available
1160
	 * @param int|null $total Contains the number of all records matching the criteria if not null
1161
	 * @param int $sitelevel Constant from \Aimeos\MShop\Locale\Manager\Base for defining which site IDs should be used for searching
1162
	 * @param \Aimeos\Base\Criteria\Plugin\Iface[] $plugins Associative list of search keys and criteria plugin items as values
1163
	 * @return \Aimeos\Base\DB\Result\Iface SQL result object for accessing the found records
1164
	 * @throws \Aimeos\MShop\Exception if no number of all matching records is available
1165
	 */
1166
	protected function searchItemsBase( \Aimeos\Base\DB\Connection\Iface $conn, \Aimeos\Base\Criteria\Iface $search,
1167
		string $cfgPathSearch, string $cfgPathCount, array $required, int &$total = null,
1168
		int $sitelevel = \Aimeos\MShop\Locale\Manager\Base::SITE_ALL, array $plugins = [] ) : \Aimeos\Base\DB\Result\Iface
1169
	{
1170
		$attributes = $this->object()->getSearchAttributes();
1171
		$keys = $this->getCriteriaKeyList( $search, $required );
1172
		$joins = $this->getRequiredJoins( $attributes, $keys, array_shift( $required ) );
1173
1174
		if( !empty( $cond = $this->getSiteConditions( $keys, $attributes, $sitelevel ) ) ) {
1175
			$search = ( clone $search )->add( $search->and( $cond ) );
1176
		}
1177
1178
		$attronly = $this->object()->getSearchAttributes( false );
1179
		$replace = $this->getSQLReplacements( $search, $attributes, $attronly, $plugins, $joins );
1180
1181
		if( $total !== null )
1182
		{
1183
			$sql = $this->getSqlConfig( $cfgPathCount, $replace );
1184
			$result = $this->getSearchResults( $conn, $sql );
0 ignored issues
show
Bug introduced by
It seems like $sql can also be of type array; however, parameter $sql of Aimeos\MShop\Common\Manager\DB::getSearchResults() 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

1184
			$result = $this->getSearchResults( $conn, /** @scrutinizer ignore-type */ $sql );
Loading history...
1185
			$row = $result->fetch();
1186
			$result->finish();
1187
1188
			if( $row === null )
1189
			{
1190
				$msg = $this->context()->translate( 'mshop', 'Total results value not found' );
1191
				throw new \Aimeos\MShop\Exception( $msg );
1192
			}
1193
1194
			$total = (int) $row['count'];
1195
		}
1196
1197
		return $this->getSearchResults( $conn, $this->getSqlConfig( $cfgPathSearch, $replace ) );
1198
	}
1199
1200
1201
	/**
1202
	 * Sets the name of the database resource that should be used.
1203
	 *
1204
	 * @param string $name Name of the resource
1205
	 * @return \Aimeos\MShop\Common\Manager\Iface Manager object for chaining method calls
1206
	 */
1207
	protected function setResourceName( string $name ) : \Aimeos\MShop\Common\Manager\Iface
1208
	{
1209
		$config = $this->context()->config();
1210
1211
		if( $config->get( 'resource/' . $name ) === null ) {
1212
			$this->resourceName = $config->get( 'resource/default', 'db' );
1213
		} else {
1214
			$this->resourceName = $name;
1215
		}
1216
1217
		return $this;
1218
	}
1219
1220
1221
	/**
1222
	 * Replaces the given marker with an expression
1223
	 *
1224
	 * @param string $column Name (including alias) of the column
1225
	 * @param mixed $value Value used in the expression
1226
	 * @param string $op Operator used in the expression
1227
	 * @param int $type Type constant from \Aimeos\Base\DB\Statement\Base class
1228
	 * @return string Created expression
1229
	 */
1230
	protected function toExpression( string $column, $value, string $op = '==',
1231
		int $type = \Aimeos\Base\DB\Statement\Base::PARAM_STR ) : string
1232
	{
1233
		$types = ['marker' => $type];
1234
		$translations = ['marker' => $column];
1235
		$value = ( is_array( $value ) ? array_unique( $value ) : $value );
1236
1237
		return $this->getSearch()->compare( $op, 'marker', $value )->toSource( $types, $translations );
1238
	}
1239
1240
1241
	/**
1242
	 * Transforms the application specific values to Aimeos standard values.
1243
	 *
1244
	 * @param array $values Associative list of key/value pairs from the storage
1245
	 * @return array Associative list of key/value pairs with standard Aimeos values
1246
	 */
1247
	protected function transform( array $values ) : array
1248
	{
1249
		return $values;
1250
	}
1251
1252
1253
	/**
1254
	 * Cuts the last part separated by a dot repeatedly and returns the list of resulting string.
1255
	 *
1256
	 * @param string[] $prefix Required base prefixes of the search keys
1257
	 * @param string $string String containing parts separated by dots
1258
	 * @return array List of resulting strings
1259
	 */
1260
	private function cutNameTail( array $prefix, string $string ) : array
1261
	{
1262
		$result = [];
1263
		$noprefix = true;
1264
		$strlen = strlen( $string );
1265
1266
		foreach( $prefix as $key )
1267
		{
1268
			$len = strlen( $key );
1269
1270
			if( strncmp( $string, $key, $len ) === 0 )
1271
			{
1272
				if( $strlen > $len && ( $pos = strrpos( $string, '.' ) ) !== false )
1273
				{
1274
					$result[] = $string = substr( $string, 0, $pos );
1275
					$result = array_merge( $result, $this->cutNameTail( $prefix, $string ) );
1276
					$noprefix = false;
1277
				}
1278
1279
				break;
1280
			}
1281
		}
1282
1283
		if( $noprefix )
1284
		{
1285
			if( ( $pos = strrpos( $string, ':' ) ) !== false ) {
1286
				$result[] = substr( $string, 0, $pos );
1287
				$result[] = $string;
1288
			} elseif( ( $pos = strrpos( $string, '.' ) ) !== false ) {
1289
				$result[] = substr( $string, 0, $pos );
1290
			} else {
1291
				$result[] = $string;
1292
			}
1293
		}
1294
1295
		return $result;
1296
	}
1297
1298
1299
	/**
1300
	 * Returns a list of unique criteria names shortend by the last element after the ''
1301
	 *
1302
	 * @param string[] $prefix Required base prefixes of the search keys
1303
	 * @param \Aimeos\Base\Criteria\Expression\Iface|null $expr Criteria object
1304
	 * @return array List of shortend criteria names
1305
	 */
1306
	private function getCriteriaKeys( array $prefix, \Aimeos\Base\Criteria\Expression\Iface $expr = null ) : array
1307
	{
1308
		if( $expr === null ) { return []; }
1309
1310
		$result = [];
1311
1312
		foreach( $this->getCriteriaNames( $expr ) as $item )
1313
		{
1314
			if( strncmp( $item, 'sort:', 5 ) === 0 ) {
1315
				$item = substr( $item, 5 );
1316
			}
1317
1318
			if( ( $pos = strpos( $item, '(' ) ) !== false ) {
1319
				$item = substr( $item, 0, $pos );
1320
			}
1321
1322
			$result = array_merge( $result, $this->cutNameTail( $prefix, $item ) );
1323
		}
1324
1325
		return $result;
1326
	}
1327
1328
1329
	/**
1330
	 * Returns a list of criteria names from a expression and its sub-expressions.
1331
	 *
1332
	 * @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...
1333
	 * @return array List of criteria names
1334
	 */
1335
	private function getCriteriaNames( \Aimeos\Base\Criteria\Expression\Iface $expr ) : array
1336
	{
1337
		if( $expr instanceof \Aimeos\Base\Criteria\Expression\Compare\Iface ) {
1338
			return array( $expr->getName() );
1339
		}
1340
1341
		if( $expr instanceof \Aimeos\Base\Criteria\Expression\Combine\Iface )
1342
		{
1343
			$list = [];
1344
			foreach( $expr->getExpressions() as $item ) {
1345
				$list = array_merge( $list, $this->getCriteriaNames( $item ) );
1346
			}
1347
			return $list;
1348
		}
1349
1350
		if( $expr instanceof \Aimeos\Base\Criteria\Expression\Sort\Iface ) {
1351
			return array( $expr->getName() );
1352
		}
1353
1354
		return [];
1355
	}
1356
}
1357