Passed
Push — master ( 8a4484...3ac2eb )
by Aimeos
04:39
created

DB::getSearchAttributesBase()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 17
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 1
Metric Value
eloc 8
c 2
b 0
f 1
dl 0
loc 17
rs 10
cc 3
nc 2
nop 4
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
			if( strpos( $attrItem->getInternalCode(), '"' ) === false ) {
202
				$prefixed = $this->alias( $attrItem->getCode() ) . '."' . $attrItem->getInternalCode() . '"';
203
			} else { // @todo: Remove in 2025.01
204
				$prefixed = $attrItem->getInternalCode();
205
			}
206
207
			$acols[] = $prefixed . ' AS "' . $string . '"';
208
			$cols[] = $prefixed;
209
210
			$expr[] = $search->compare( '!=', $string, null ); // required for the joins
211
		}
212
213
		$expr[] = $search->compare( '!=', $valkey, null );
214
		$search->add( $search->and( $expr ) );
215
216
		$val = $attrMap[$value]->getInternalCode();
217
218
		if( strpos( $val, '"' ) === false ) {
219
			$val = $this->alias( $attrMap[$value]->getCode() ) . '."' . $val . '"';
220
		}
221
222
		$sql = $this->getSqlConfig( $cfgPath );
223
		$sql = str_replace( ':cols', join( ', ', $cols ), $sql );
224
		$sql = str_replace( ':acols', join( ', ', $acols ), $sql );
225
		$sql = str_replace( ':keys', '"' . join( '", "', $keys ) . '"', $sql );
226
		$sql = str_replace( ':type', in_array( $type, ['avg', 'count', 'max', 'min', 'sum'] ) ? $type : 'count', $sql );
227
		$sql = str_replace( ':val', $val, $sql );
228
229
		return $this->aggregateResult( $search, $sql, $required );
230
	}
231
232
233
	/**
234
	 * Returns the aggregated values for the given SQL string and filter.
235
	 *
236
	 * @param \Aimeos\Base\Criteria\Iface $filter Filter object
237
	 * @param string $sql SQL statement
238
	 * @param string[] $required List of domain/sub-domain names like "catalog.index" that must be additionally joined
239
	 * @return \Aimeos\Map (Nested) list of aggregated values as key and the number of counted products as value
240
	 */
241
	protected function aggregateResult( \Aimeos\Base\Criteria\Iface $filter, string $sql, array $required ) : \Aimeos\Map
242
	{
243
		$map = [];
244
		$total = null;
245
		$level = \Aimeos\MShop\Locale\Manager\Base::SITE_ALL;
246
		$conn = $this->context()->db( $this->getResourceName() );
247
		$results = $this->searchItemsBase( $conn, $filter, $sql, '', $required, $total, $level );
248
249
		while( $row = $results->fetch() )
250
		{
251
			$row = $this->transform( $row );
252
253
			$temp = &$map;
254
			$last = array_pop( $row );
255
256
			foreach( $row as $val ) {
257
				$temp[$val] = $temp[$val] ?? [];
258
				$temp = &$temp[$val];
259
			}
260
			$temp = $last;
261
		}
262
263
		return map( $map );
264
	}
265
266
267
	/**
268
	 * Returns the table alias name.
269
	 *
270
	 * @param string|null $attrcode Search attribute code
271
	 * @return string Table alias name
272
	 */
273
	protected function alias( string $attrcode = null ) : string
274
	{
275
		if( $attrcode )
276
		{
277
			$parts = array_slice( explode( '.', $attrcode ), 0, -1 );
278
			$str = 'm' . substr( array_shift( $parts ) ?: $this->getDomain(), 0, 3 );
279
		}
280
		else
281
		{
282
			$parts = explode( '/', $this->getSubPath() );
283
			$str = 'm' . substr( $this->getDomain(), 0, 3 );
284
		}
285
286
		foreach( $parts as $part ) {
287
			$str .= substr( $part, 0, 2 );
288
		}
289
290
		return $str;
291
	}
292
293
294
	/**
295
	 * Adds aliases for the columns
296
	 *
297
	 * @param array $map Associative list of search keys as keys and internal column names as values
298
	 * @return array Associative list of search keys as keys and aliased column names as values
299
	 */
300
	protected function aliasTranslations( array $map ) : array
301
	{
302
		foreach( $map as $key => $value )
303
		{
304
			if( strpos( $value, '"' ) === false ) {
305
				$map[$key] = $this->alias( $key ) . '."' . $value . '"';
306
			}
307
		}
308
309
		return $map;
310
	}
311
312
313
	/**
314
	 * Binds additional values to the statement before execution.
315
	 *
316
	 * @param \Aimeos\MShop\Common\Item\Iface $item Item object
317
	 * @param \Aimeos\Base\DB\Statement\Iface $stmt Database statement object
318
	 * @param int $idx Current bind index
319
	 * @return \Aimeos\Base\DB\Statement\Iface Database statement object with bound values
320
	 */
321
	protected function bind( \Aimeos\MShop\Common\Item\Iface $item, \Aimeos\Base\DB\Statement\Iface $stmt, int &$idx ) : \Aimeos\Base\DB\Statement\Iface
322
	{
323
		return $stmt;
324
	}
325
326
327
	/**
328
	 * Removes old entries from the storage.
329
	 *
330
	 * @param iterable $siteids List of IDs for sites whose entries should be deleted
331
	 * @param string $cfgpath Configuration key to the cleanup statement
332
	 * @return \Aimeos\MShop\Common\Manager\Iface Manager object for chaining method calls
333
	 */
334
	protected function clearBase( iterable $siteids, string $cfgpath ) : \Aimeos\MShop\Common\Manager\Iface
335
	{
336
		if( empty( $siteids ) ) {
337
			return $this;
338
		}
339
340
		$conn = $this->context()->db( $this->getResourceName() );
341
342
		$sql = $this->getSqlConfig( $cfgpath );
343
		$sql = str_replace( ':cond', '1=1', $sql );
344
345
		$stmt = $conn->create( $sql );
346
347
		foreach( $siteids as $siteid )
348
		{
349
			$stmt->bind( 1, $siteid );
350
			$stmt->execute()->finish();
351
		}
352
353
		return $this;
354
	}
355
356
357
	/**
358
	 * Deletes items.
359
	 *
360
	 * @param \Aimeos\MShop\Common\Item\Iface|\Aimeos\Map|array|string $items List of item objects or IDs of the items
361
	 * @param string $cfgpath Configuration path to the SQL statement
362
	 * @param bool $siteid If siteid should be used in the statement
363
	 * @param string $name Name of the ID column
364
	 * @return \Aimeos\MShop\Common\Manager\Iface Manager object for chaining method calls
365
	 */
366
	protected function deleteItemsBase( $items, string $cfgpath, bool $siteid = true,
367
		string $name = 'id' ) : \Aimeos\MShop\Common\Manager\Iface
368
	{
369
		if( map( $items )->isEmpty() ) {
370
			return $this;
371
		}
372
373
		$search = $this->object()->filter();
374
		$search->setConditions( $search->compare( '==', $name, $items ) );
375
376
		$types = array( $name => \Aimeos\Base\DB\Statement\Base::PARAM_STR );
377
		$translations = array( $name => '"' . $name . '"' );
378
379
		$cond = $search->getConditionSource( $types, $translations );
380
		$sql = str_replace( ':cond', $cond, $this->getSqlConfig( $cfgpath ) );
381
382
		$context = $this->context();
383
		$conn = $context->db( $this->getResourceName() );
384
385
		$stmt = $conn->create( $sql );
386
387
		if( $siteid ) {
388
			$stmt->bind( 1, $context->locale()->getSiteId() . '%' );
389
		}
390
391
		$stmt->execute()->finish();
392
393
		return $this;
394
	}
395
396
397
	/**
398
	 * Fetches the rows from the database statement and returns the list of items.
399
	 *
400
	 * @param \Aimeos\Base\DB\Result\Iface $stmt Database statement object
401
	 * @param array $ref List of domains whose items should be fetched too
402
	 * @param string $prefix Prefix for the property names
403
	 * @param array $attrs List of attributes that should be decoded
404
	 * @return \Aimeos\Map List of items implementing \Aimeos\MShop\Common\Item\Iface
405
	 */
406
	protected function fetch( \Aimeos\Base\DB\Result\Iface $results, array $ref, string $prefix = '', array $attrs = [] ) : \Aimeos\Map
407
	{
408
		$map = [];
409
410
		while( $row = $results->fetch() )
411
		{
412
			foreach( $attrs as $code => $attr ) {
413
				$row[$code] = json_decode( $row[$code], true );
414
			}
415
416
			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

416
			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

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

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

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

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

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

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

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