DB::searchItemsBase()   A
last analyzed

Complexity

Conditions 4
Paths 6

Size

Total Lines 32
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 1
Metric Value
eloc 17
dl 0
loc 32
rs 9.7
c 2
b 0
f 1
cc 4
nc 6
nop 8

How to fix   Many Parameters   

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

1
<?php
2
3
/**
4
 * @license LGPLv3, https://opensource.org/licenses/LGPL-3.0
5
 * @copyright Aimeos (aimeos.org), 2023
6
 * @package MShop
7
 * @subpackage Common
8
 */
9
10
11
namespace Aimeos\MShop\Common\Manager;
12
13
14
/**
15
 * Method trait for managers
16
 *
17
 * @package MShop
18
 * @subpackage Common
19
 */
20
trait DB
21
{
22
	private ?\Aimeos\Base\Criteria\Iface $search;
23
	private ?string $resourceName = null;
24
	private array $cachedStmts = [];
25
	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
	 * Returns the type of the mananger as separate parts
104
	 *
105
	 * @return string[] List of manager part names
106
	 */
107
	abstract public function type() : array;
108
109
110
	/**
111
	 * Adds additional column names to SQL statement
112
	 *
113
	 * @param string[] $columns List of column names
114
	 * @param string $sql Insert or update SQL statement
115
	 * @param bool $mode True for insert, false for update statement
116
	 * @return string Modified insert or update SQL statement
117
	 */
118
	protected function addSqlColumns( array $columns, string $sql, bool $mode = true ) : string
119
	{
120
		$names = $values = '';
121
122
		if( $mode )
123
		{
124
			foreach( $columns as $name ) {
125
				$names .= '"' . $name . '", '; $values .= '?, ';
126
			}
127
		}
128
		else
129
		{
130
			foreach( $columns as $name ) {
131
				$names .= '"' . $name . '" = ?, ';
132
			}
133
		}
134
135
		return str_replace( [':names', ':values'], [$names, $values], $sql );
136
	}
137
138
139
	/**
140
	 * Counts the number products that are available for the values of the given key.
141
	 *
142
	 * @param \Aimeos\Base\Criteria\Iface $search Search criteria
143
	 * @param array|string $keys Search key or list of keys for aggregation
144
	 * @param string $cfgPath Configuration key for the SQL statement
145
	 * @param string[] $required List of domain/sub-domain names like "catalog.index" that must be additionally joined
146
	 * @param string|null $value Search key for aggregating the value column
147
	 * @param string|null $type Type of aggregation, e.g.  "sum", "min", "max" or NULL for "count"
148
	 * @return \Aimeos\Map List of ID values as key and the number of counted products as value
149
	 */
150
	protected function aggregateBase( \Aimeos\Base\Criteria\Iface $search, $keys, string $cfgPath,
151
		array $required = [], ?string $value = null, ?string $type = null ) : \Aimeos\Map
152
	{
153
		/** mshop/common/manager/aggregate/limit
154
		 * Limits the number of records that are used when aggregating items
155
		 *
156
		 * As counting huge amount of records (several 10 000 records) takes a long time,
157
		 * the limit can cut down response times so the counts are available more quickly
158
		 * in the front-end and the server load is reduced.
159
		 *
160
		 * Using a low limit can lead to incorrect numbers if the amount of found items
161
		 * is very high. Approximate item counts are normally not a problem but it can
162
		 * lead to the situation that visitors see that no items are available despite
163
		 * the fact that there would be at least one.
164
		 *
165
		 * @param integer Number of records
166
		 * @since 2021.04
167
		 */
168
		$limit = $this->context()->config()->get( 'mshop/common/manager/aggregate/limit', 10000 );
169
170
		if( empty( $keys ) )
171
		{
172
			$msg = $this->context()->translate( 'mshop', 'At least one key is required for aggregation' );
173
			throw new \Aimeos\MShop\Exception( $msg );
174
		}
175
176
		$attrMap = array_column( array_filter( $this->object()->getSearchAttributes(), function( $item ) {
177
			return $item->isPublic() || strncmp( $item->getCode(), 'agg:', 4 ) === 0;
178
		} ), null, 'code' );
179
180
		if( $value === null && ( $value = key( $attrMap ) ) === null )
181
		{
182
			$msg = $this->context()->translate( 'mshop', 'No search keys available' );
183
			throw new \Aimeos\MShop\Exception( $msg );
184
		}
185
186
		if( ( $pos = strpos( $valkey = $value, '(' ) ) !== false ) {
187
			$value = substr( $value, 0, $pos ) . '()'; // remove parameters from search function
188
		}
189
190
		if( !isset( $attrMap[$value] ) )
191
		{
192
			$msg = $this->context()->translate( 'mshop', 'Unknown search key "%1$s"' );
193
			throw new \Aimeos\MShop\Exception( sprintf( $msg, $value ) );
194
		}
195
196
		$keys = (array) $keys;
197
		$acols = $cols = $expr = [];
198
		$search = (clone $search)->slice( $search->getOffset(), min( $search->getLimit(), $limit ) );
199
200
		foreach( $keys as $string )
201
		{
202
			if( ( $attrItem = $attrMap[$string] ?? null ) === null )
203
			{
204
				$msg = $this->context()->translate( 'mshop', 'Unknown search key "%1$s"' );
205
				throw new \Aimeos\MShop\Exception( sprintf( $msg, $string ) );
206
			}
207
208
			if( strpos( $attrItem->getInternalCode(), '"' ) === false ) {
209
				$prefixed = $this->alias( $attrItem->getCode() ) . '."' . $attrItem->getInternalCode() . '"';
210
			} else { // @todo: Remove in 2025.01
211
				$prefixed = $attrItem->getInternalCode();
212
			}
213
214
			$acols[] = $prefixed . ' AS "' . $string . '"';
215
			$cols[] = $prefixed;
216
217
			$expr[] = $search->compare( '!=', $string, null ); // required for the joins
218
		}
219
220
		$expr[] = $search->compare( '!=', $valkey, null );
221
		$search->add( $search->and( $expr ) );
222
223
		$val = $attrMap[$value]->getInternalCode();
224
225
		if( strpos( $val, '"' ) === false ) {
226
			$val = $this->alias( $attrMap[$value]->getCode() ) . '."' . $val . '"';
227
		}
228
229
		$sql = $this->getSqlConfig( $cfgPath );
230
		$sql = str_replace( ':cols', join( ', ', $cols ), $sql );
231
		$sql = str_replace( ':acols', join( ', ', $acols ), $sql );
232
		$sql = str_replace( ':keys', '"' . join( '", "', $keys ) . '"', $sql );
233
		$sql = str_replace( ':type', in_array( $type, ['avg', 'count', 'max', 'min', 'sum'] ) ? $type : 'count', $sql );
234
		$sql = str_replace( ':val', $val, $sql );
235
236
		return $this->aggregateResult( $search, $sql, $required );
237
	}
238
239
240
	/**
241
	 * Returns the aggregated values for the given SQL string and filter.
242
	 *
243
	 * @param \Aimeos\Base\Criteria\Iface $filter Filter object
244
	 * @param string $sql SQL statement
245
	 * @param string[] $required List of domain/sub-domain names like "catalog.index" that must be additionally joined
246
	 * @return \Aimeos\Map (Nested) list of aggregated values as key and the number of counted products as value
247
	 */
248
	protected function aggregateResult( \Aimeos\Base\Criteria\Iface $filter, string $sql, array $required ) : \Aimeos\Map
249
	{
250
		$map = [];
251
		$total = null;
252
		$level = \Aimeos\MShop\Locale\Manager\Base::SITE_ALL;
253
		$conn = $this->context()->db( $this->getResourceName() );
254
		$results = $this->searchItemsBase( $conn, $filter, $sql, '', $required, $total, $level );
255
256
		while( $row = $results->fetch() )
257
		{
258
			$row = $this->transform( $row );
259
260
			$temp = &$map;
261
			$last = array_pop( $row );
262
263
			foreach( $row as $val ) {
264
				$temp[$val] = $temp[$val] ?? [];
265
				$temp = &$temp[$val];
266
			}
267
			$temp = $last;
268
		}
269
270
		return map( $map );
271
	}
272
273
274
	/**
275
	 * Returns the table alias name.
276
	 *
277
	 * @param string|null $attrcode Search attribute code
278
	 * @return string Table alias name
279
	 */
280
	protected function alias( ?string $attrcode = null ) : string
281
	{
282
		if( $attrcode )
283
		{
284
			$parts = array_slice( explode( '.', $attrcode ), 0, -1 ) ?: $this->type();
285
			$str = 'm' . substr( (string) array_shift( $parts ), 0, 3 );
286
		}
287
		else
288
		{
289
			$parts = $this->type();
290
			$str = 'm' . substr( (string) array_shift( $parts ), 0, 3 );
291
		}
292
293
		foreach( $parts as $part ) {
294
			$str .= substr( $part, 0, 2 );
295
		}
296
297
		return $str;
298
	}
299
300
301
	/**
302
	 * Adds aliases for the columns
303
	 *
304
	 * @param array $map Associative list of search keys as keys and internal column names as values
305
	 * @return array Associative list of search keys as keys and aliased column names as values
306
	 */
307
	protected function aliasTranslations( array $map ) : array
308
	{
309
		foreach( $map as $key => $value )
310
		{
311
			if( strpos( $value, '"' ) === false ) {
312
				$map[$key] = $this->alias( $key ) . '."' . $value . '"';
313
			}
314
		}
315
316
		return $map;
317
	}
318
319
320
	/**
321
	 * Binds additional values to the statement before execution.
322
	 *
323
	 * @param \Aimeos\MShop\Common\Item\Iface $item Item object
324
	 * @param \Aimeos\Base\DB\Statement\Iface $stmt Database statement object
325
	 * @param int $idx Current bind index
326
	 * @return \Aimeos\Base\DB\Statement\Iface Database statement object with bound values
327
	 */
328
	protected function bind( \Aimeos\MShop\Common\Item\Iface $item, \Aimeos\Base\DB\Statement\Iface $stmt, int &$idx ) : \Aimeos\Base\DB\Statement\Iface
329
	{
330
		return $stmt;
331
	}
332
333
334
	/**
335
	 * Removes old entries from the storage.
336
	 *
337
	 * @param iterable $siteids List of IDs for sites whose entries should be deleted
338
	 * @param string $cfgpath Configuration key to the cleanup statement
339
	 * @return \Aimeos\MShop\Common\Manager\Iface Manager object for chaining method calls
340
	 */
341
	protected function clearBase( iterable $siteids, string $cfgpath ) : \Aimeos\MShop\Common\Manager\Iface
342
	{
343
		if( empty( $siteids ) ) {
344
			return $this;
345
		}
346
347
		$conn = $this->context()->db( $this->getResourceName() );
348
349
		$sql = $this->getSqlConfig( $cfgpath );
350
		$sql = str_replace( ':cond', '1=1', $sql );
351
352
		$stmt = $conn->create( $sql );
353
354
		foreach( $siteids as $siteid )
355
		{
356
			$stmt->bind( 1, $siteid );
357
			$stmt->execute()->finish();
358
		}
359
360
		return $this;
361
	}
362
363
364
	/**
365
	 * Deletes items.
366
	 *
367
	 * @param \Aimeos\MShop\Common\Item\Iface|\Aimeos\Map|array|string $items List of item objects or IDs of the items
368
	 * @param string $cfgpath Configuration path to the SQL statement
369
	 * @param bool $siteid If siteid should be used in the statement
370
	 * @param string $name Name of the ID column
371
	 * @return \Aimeos\MShop\Common\Manager\Iface Manager object for chaining method calls
372
	 */
373
	protected function deleteItemsBase( $items, string $cfgpath, bool $siteid = true,
374
		string $name = 'id' ) : \Aimeos\MShop\Common\Manager\Iface
375
	{
376
		if( map( $items )->isEmpty() ) {
377
			return $this;
378
		}
379
380
		$search = $this->object()->filter();
381
		$search->setConditions( $search->compare( '==', $name, $items ) );
382
383
		$types = array( $name => \Aimeos\Base\DB\Statement\Base::PARAM_STR );
384
		$translations = array( $name => '"' . $name . '"' );
385
386
		$cond = $search->getConditionSource( $types, $translations );
387
		$sql = str_replace( ':cond', $cond, $this->getSqlConfig( $cfgpath ) );
388
389
		$context = $this->context();
390
		$conn = $context->db( $this->getResourceName() );
391
392
		$stmt = $conn->create( $sql );
393
394
		if( $siteid ) {
395
			$stmt->bind( 1, $context->locale()->getSiteId() . '%' );
396
		}
397
398
		$stmt->execute()->finish();
399
400
		return $this;
401
	}
402
403
404
	/**
405
	 * Sets the base criteria "status".
406
	 * (setConditions overwrites the base criteria)
407
	 *
408
	 * @param string $domain Name of the domain/sub-domain like "product" or "product.list"
409
	 * @param bool|null $default TRUE for status=1, NULL for status>0, FALSE for no restriction
410
	 * @return \Aimeos\Base\Criteria\Iface Search critery object
411
	 */
412
	protected function filterBase( string $domain, ?bool $default = false ) : \Aimeos\Base\Criteria\Iface
413
	{
414
		$context = $this->context();
415
		$db = $this->getResourceName();
416
		$conn = $context->db( $db );
417
		$config = $context->config();
418
419
		if( ( $adapter = $config->get( 'resource/' . $db . '/adapter' ) ) === null ) {
420
			$adapter = $config->get( 'resource/db/adapter' );
421
		}
422
423
		switch( $adapter )
424
		{
425
			case 'pgsql':
426
				$filter = new \Aimeos\Base\Criteria\PgSQL( $conn ); break;
427
			default:
428
				$filter = new \Aimeos\Base\Criteria\SQL( $conn ); break;
429
		}
430
431
		if( $default !== false ) {
432
			$filter->add( $domain . '.status', $default ? '==' : '>=', 1 );
433
		}
434
435
		return $filter;
436
	}
437
438
439
	/**
440
	 * Returns the item for the given search key/value pairs.
441
	 *
442
	 * @param array $pairs Search key/value pairs for the item
443
	 * @param string[] $ref List of domains whose items should be fetched too
444
	 * @param bool|null $default Add default criteria or NULL for relaxed default criteria
445
	 * @return \Aimeos\MShop\Common\Item\Iface Requested item
446
	 * @throws \Aimeos\MShop\Exception if no item with the given ID found
447
	 */
448
	protected function findBase( array $pairs, array $ref, ?bool $default ) : \Aimeos\MShop\Common\Item\Iface
449
	{
450
		$expr = [];
451
		$criteria = $this->object()->filter( $default )->slice( 0, 1 );
452
453
		foreach( $pairs as $key => $value )
454
		{
455
			if( $value === null )
456
			{
457
				$msg = $this->context()->translate( 'mshop', 'Required value for "%1$s" is missing' );
458
				throw new \Aimeos\MShop\Exception( sprintf( $msg, $key ) );
459
			}
460
			$expr[] = $criteria->compare( '==', $key, $value );
461
		}
462
463
		$criteria->setConditions( $criteria->and( $expr ) );
464
465
		if( ( $item = $this->object()->search( $criteria, $ref )->first() ) ) {
466
			return $item;
467
		}
468
469
		$msg = $this->context()->translate( 'mshop', 'No item found for conditions: %1$s' );
470
		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

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

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

776
			/** @scrutinizer ignore-call */ 
777
   $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...
777
		}
778
779
		return $this->search;
780
	}
781
782
783
	/**
784
	 * Returns the string replacements for the SQL statements
785
	 *
786
	 * @param \Aimeos\Base\Criteria\Iface $search Search critera object
787
	 * @param \Aimeos\Base\Criteria\Attribute\Iface[] $attributes Associative list of search keys and criteria attribute items as values
788
	 * @param \Aimeos\Base\Criteria\Attribute\Iface[] $attributes Associative list of search keys and criteria attribute items as values for the base table
789
	 * @param \Aimeos\Base\Criteria\Plugin\Iface[] $plugins Associative list of search keys and criteria plugin items as values
790
	 * @param string[] $joins Associative list of SQL joins
791
	 * @param \Aimeos\Base\Criteria\Attribute\Iface[] $columns Additional columns to retrieve values from
792
	 * @return array Array of keys, find and replace arrays
793
	 */
794
	protected function getSQLReplacements( \Aimeos\Base\Criteria\Iface $search, array $attributes, array $attronly, array $plugins, array $joins ) : array
795
	{
796
		$types = $this->getSearchTypes( $attributes );
797
		$funcs = $this->getSearchFunctions( $attributes );
798
		$trans = $this->getSearchTranslations( $attributes );
799
		$trans = $this->aliasTranslations( $trans );
800
801
		if( empty( $search->getSortations() ) && ( $attribute = reset( $attronly ) ) !== false ) {
802
			$search = ( clone $search )->setSortations( [$search->sort( '+', $attribute->getCode() )] );
803
		}
804
		$sorts = $search->translate( $search->getSortations(), $trans, $funcs );
805
806
		$cols = $group = [];
807
		foreach( $attronly as $name => $entry )
808
		{
809
			if( str_contains( $name, ':' ) || empty( $entry->getInternalCode() ) ) {
810
				continue;
811
			}
812
813
			$icode = $entry->getInternalCode();
814
815
			if( !str_contains( $icode, '"' ) )
816
			{
817
				$alias = $this->alias( $entry->getCode() );
818
				$icode = $alias . '."' . $icode . '"';
819
			}
820
821
			$cols[] = $icode . ' AS "' . $entry->getCode() . '"';
822
			$group[] = $icode;
823
		}
824
825
		return [
826
			':columns' => join( ', ', $cols ),
827
			':joins' => join( "\n", array_unique( $joins ) ),
828
			':group' => join( ', ', array_unique( array_merge( $group, $sorts ) ) ),
829
			':cond' => $search->getConditionSource( $types, $trans, $plugins, $funcs ),
830
			':order' => $search->getSortationSource( $types, $trans, $funcs ),
831
			':start' => $search->getOffset(),
832
			':size' => $search->getLimit(),
833
		];
834
	}
835
836
837
	/**
838
	 * Returns the available sub-manager names
839
	 *
840
	 * @return array Sub-manager names, e.g. ['lists', 'property', 'type']
841
	 */
842
	protected function getSubManagers() : array
843
	{
844
		return $this->context()->config()->get( $this->getConfigKey( 'submanagers' ), [] );
845
	}
846
847
848
	/**
849
	 * Returns the name of the used table
850
	 *
851
	 * @return string Table name e.g. "mshop_product_property_type"
852
	 */
853
	protected function table() : string
854
	{
855
		return 'mshop_' . join( '_', $this->type() );
856
	}
857
858
859
	/**
860
	 * Checks if the item is modified
861
	 *
862
	 * @param \Aimeos\MShop\Common\Item\Iface $item Item object
863
	 * @return bool True if the item is modified, false if not
864
	 */
865
	protected function isModified( \Aimeos\MShop\Common\Item\Iface $item ) : bool
866
	{
867
		return $item->isModified();
868
	}
869
870
871
	/**
872
	 * Returns the newly created ID for the last record which was inserted.
873
	 *
874
	 * @param \Aimeos\Base\DB\Connection\Iface $conn Database connection used to insert the new record
875
	 * @param string $cfgpath Configuration path to the SQL statement for retrieving the new ID of the last inserted record
876
	 * @return string ID of the last record that was inserted by using the given connection
877
	 * @throws \Aimeos\MShop\Exception if there's no ID of the last record available
878
	 */
879
	protected function newId( \Aimeos\Base\DB\Connection\Iface $conn, string $cfgpath ) : string
880
	{
881
		$sql = $this->getSqlConfig( $cfgpath );
882
883
		$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

883
		$result = $conn->create( /** @scrutinizer ignore-type */ $sql )->execute();
Loading history...
884
885
		if( ( $row = $result->fetch( \Aimeos\Base\DB\Result\Base::FETCH_NUM ) ) === false )
886
		{
887
			$msg = $this->context()->translate( 'mshop', 'ID of last inserted database record not available' );
888
			throw new \Aimeos\MShop\Exception( $msg );
889
		}
890
		$result->finish();
891
892
		return $row[0];
893
	}
894
895
896
	/**
897
	 * Saves an attribute item to the storage.
898
	 *
899
	 * @param \Aimeos\MShop\Common\Item\Iface $item Item object
900
	 * @param bool $fetch True if the new ID should be returned in the item
901
	 * @return \Aimeos\MShop\Common\Item\Iface $item Updated item including the generated ID
902
	 */
903
	protected function saveBase( \Aimeos\MShop\Common\Item\Iface $item, bool $fetch = true ) : \Aimeos\MShop\Common\Item\Iface
904
	{
905
		if( !$this->isModified( $item ) ) {
906
			return $this->object()->saveRefs( $item );
907
		}
908
909
		$context = $this->context();
910
		$conn = $context->db( $this->getResourceName() );
911
912
		$id = $item->getId();
913
		$columns = array_column( $this->object()->getSaveAttributes(), null, 'internalcode' );
914
915
		if( $id === null )
916
		{
917
			/** mshop/common/manager/insert/mysql
918
			 * Inserts a new record into the database table
919
			 *
920
			 * @see mshop/common/manager/insert/ansi
921
			 */
922
923
			/** mshop/common/manager/insert/ansi
924
			 * Inserts a new record into the database table
925
			 *
926
			 * Items with no ID yet (i.e. the ID is NULL) will be created in
927
			 * the database and the newly created ID retrieved afterwards
928
			 * using the "newid" SQL statement.
929
			 *
930
			 * The SQL statement must be a string suitable for being used as
931
			 * prepared statement. It must include question marks for binding
932
			 * the values from the item to the statement before they are
933
			 * sent to the database server. The number of question marks must
934
			 * be the same as the number of columns listed in the INSERT
935
			 * statement. The order of the columns must correspond to the
936
			 * order in the save() method, so the correct values are
937
			 * bound to the columns.
938
			 *
939
			 * The SQL statement should conform to the ANSI standard to be
940
			 * compatible with most relational database systems. This also
941
			 * includes using double quotes for table and column names.
942
			 *
943
			 * @param string SQL statement for inserting records
944
			 * @since 2023.10
945
			 * @see mshop/common/manager/update/ansi
946
			 * @see mshop/common/manager/newid/ansi
947
			 * @see mshop/common/manager/delete/ansi
948
			 * @see mshop/common/manager/search/ansi
949
			 * @see mshop/common/manager/count/ansi
950
			 */
951
			$path = $this->getConfigKey( 'insert', 'mshop/common/manager/insert' );
952
			$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

952
			$sql = $this->addSqlColumns( array_keys( $columns ), /** @scrutinizer ignore-type */ $this->getSqlConfig( $path ) );
Loading history...
953
		}
954
		else
955
		{
956
			/** mshop/common/manager/update/mysql
957
			 * Updates an existing record in the database
958
			 *
959
			 * @see mshop/common/manager/update/ansi
960
			 */
961
962
			/** mshop/common/manager/update/ansi
963
			 * Updates an existing record in the database
964
			 *
965
			 * Items which already have an ID (i.e. the ID is not NULL) will
966
			 * be updated in the database.
967
			 *
968
			 * The SQL statement must be a string suitable for being used as
969
			 * prepared statement. It must include question marks for binding
970
			 * the values from the item to the statement before they are
971
			 * sent to the database server. The order of the columns must
972
			 * correspond to the order in the save() method, so the
973
			 * correct values are bound to the columns.
974
			 *
975
			 * The SQL statement should conform to the ANSI standard to be
976
			 * compatible with most relational database systems. This also
977
			 * includes using double quotes for table and column names.
978
			 *
979
			 * @param string SQL statement for updating records
980
			 * @since 2023.10
981
			 * @see mshop/common/manager/insert/ansi
982
			 * @see mshop/common/manager/newid/ansi
983
			 * @see mshop/common/manager/delete/ansi
984
			 * @see mshop/common/manager/search/ansi
985
			 * @see mshop/common/manager/count/ansi
986
			 */
987
			$path = $this->getConfigKey( 'update', 'mshop/common/manager/update' );
988
			$sql = $this->addSqlColumns( array_keys( $columns ), $this->getSqlConfig( $path ), false );
989
		}
990
991
		$idx = 1;
992
		$values = $item->toArray( true );
993
		$stmt = $this->getCachedStatement( $conn, $path, $sql );
994
995
		foreach( $columns as $entry )
996
		{
997
			$value = $values[$entry->getCode()] ?? null;
998
			$value = $entry->getType() === 'json' ? json_encode( $value, JSON_FORCE_OBJECT ) : $value;
999
			$stmt->bind( $idx++, $value, \Aimeos\Base\Criteria\SQL::type( $entry->getType() ) );
1000
		}
1001
1002
		$stmt = $this->bind( $item, $stmt, $idx );
1003
1004
		$stmt->bind( $idx++, $context->datetime() ); // mtime
1005
		$stmt->bind( $idx++, $context->editor() );
1006
1007
		if( $id !== null ) {
1008
			$stmt->bind( $idx++, $context->locale()->getSiteId() . '%' );
1009
			$stmt->bind( $idx++, $id, \Aimeos\Base\DB\Statement\Base::PARAM_INT );
1010
		} else {
1011
			$stmt->bind( $idx++, $this->siteId( $item->getSiteId(), \Aimeos\MShop\Locale\Manager\Base::SITE_SUBTREE ) );
1012
			$stmt->bind( $idx++, $item->getTimeCreated() ?: $context->datetime() ); // ctime
1013
		}
1014
1015
		$stmt->execute()->finish();
1016
1017
		if( $id === null )
1018
		{
1019
			/** mshop/common/manager/newid/mysql
1020
			 * Retrieves the ID generated by the database when inserting a new record
1021
			 *
1022
			 * @see mshop/common/manager/newid/ansi
1023
			 */
1024
1025
			/** mshop/common/manager/newid/ansi
1026
			 * Retrieves the ID generated by the database when inserting a new record
1027
			 *
1028
			 * As soon as a new record is inserted into the database table,
1029
			 * the database server generates a new and unique identifier for
1030
			 * that record. This ID can be used for retrieving, updating and
1031
			 * deleting that specific record from the table again.
1032
			 *
1033
			 * For MySQL:
1034
			 *  SELECT LAST_INSERT_ID()
1035
			 * For PostgreSQL:
1036
			 *  SELECT currval('seq_mcom_id')
1037
			 * For SQL Server:
1038
			 *  SELECT SCOPE_IDENTITY()
1039
			 * For Oracle:
1040
			 *  SELECT "seq_mcom_id".CURRVAL FROM DUAL
1041
			 *
1042
			 * There's no way to retrive the new ID by a SQL statements that
1043
			 * fits for most database servers as they implement their own
1044
			 * specific way.
1045
			 *
1046
			 * @param string SQL statement for retrieving the last inserted record ID
1047
			 * @since 2023.10
1048
			 * @see mshop/common/manager/insert/ansi
1049
			 * @see mshop/common/manager/update/ansi
1050
			 * @see mshop/common/manager/delete/ansi
1051
			 * @see mshop/common/manager/search/ansi
1052
			 * @see mshop/common/manager/count/ansi
1053
			 */
1054
			$id = $this->newId( $conn, $this->getConfigKey( 'newid', 'mshop/common/manager/newid' ) );
1055
		}
1056
1057
		return $this->object()->saveRefs( $item->setId( $id ) );
1058
	}
1059
1060
1061
	/**
1062
	 * Returns the search result of the statement combined with the given criteria.
1063
	 *
1064
	 * @param \Aimeos\Base\DB\Connection\Iface $conn Database connection
1065
	 * @param \Aimeos\Base\Criteria\Iface $search Search criteria object
1066
	 * @param string $cfgPathSearch Path to SQL statement in configuration for searching
1067
	 * @param string $cfgPathCount Path to SQL statement in configuration for counting
1068
	 * @param string[] $required Additional search keys to add conditions for even if no conditions are available
1069
	 * @param int|null $total Contains the number of all records matching the criteria if not null
1070
	 * @param int $sitelevel Constant from \Aimeos\MShop\Locale\Manager\Base for defining which site IDs should be used for searching
1071
	 * @param \Aimeos\Base\Criteria\Plugin\Iface[] $plugins Associative list of search keys and criteria plugin items as values
1072
	 * @return \Aimeos\Base\DB\Result\Iface SQL result object for accessing the found records
1073
	 * @throws \Aimeos\MShop\Exception if no number of all matching records is available
1074
	 */
1075
	protected function searchItemsBase( \Aimeos\Base\DB\Connection\Iface $conn, \Aimeos\Base\Criteria\Iface $search,
1076
		string $cfgPathSearch, string $cfgPathCount, array $required, ?int &$total = null,
1077
		int $sitelevel = \Aimeos\MShop\Locale\Manager\Base::SITE_ALL, array $plugins = [] ) : \Aimeos\Base\DB\Result\Iface
1078
	{
1079
		$attributes = $this->object()->getSearchAttributes();
1080
		$keys = $this->getCriteriaKeyList( $search, $required );
1081
		$joins = $this->getRequiredJoins( $attributes, $keys, array_shift( $required ) );
1082
1083
		if( !empty( $cond = $this->getSiteConditions( $keys, $attributes, $sitelevel ) ) ) {
1084
			$search = ( clone $search )->add( $search->and( $cond ) );
1085
		}
1086
1087
		$attronly = $this->object()->getSearchAttributes( false );
1088
		$replace = $this->getSQLReplacements( $search, $attributes, $attronly, $plugins, $joins );
1089
1090
		if( $total !== null )
1091
		{
1092
			$sql = $this->getSqlConfig( $cfgPathCount, $replace );
1093
			$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

1093
			$result = $this->getSearchResults( $conn, /** @scrutinizer ignore-type */ $sql );
Loading history...
1094
			$row = $result->fetch();
1095
			$result->finish();
1096
1097
			if( $row === null )
1098
			{
1099
				$msg = $this->context()->translate( 'mshop', 'Total results value not found' );
1100
				throw new \Aimeos\MShop\Exception( $msg );
1101
			}
1102
1103
			$total = (int) $row['count'];
1104
		}
1105
1106
		return $this->getSearchResults( $conn, $this->getSqlConfig( $cfgPathSearch, $replace ) );
1107
	}
1108
1109
1110
	/**
1111
	 * Sets the name of the database resource that should be used.
1112
	 *
1113
	 * @param string $name Name of the resource
1114
	 * @return \Aimeos\MShop\Common\Manager\Iface Manager object for chaining method calls
1115
	 */
1116
	protected function setResourceName( string $name ) : \Aimeos\MShop\Common\Manager\Iface
1117
	{
1118
		$config = $this->context()->config();
1119
1120
		if( $config->get( 'resource/' . $name ) === null ) {
1121
			$this->resourceName = $config->get( 'resource/default', 'db' );
1122
		} else {
1123
			$this->resourceName = $name;
1124
		}
1125
1126
		return $this;
1127
	}
1128
1129
1130
	/**
1131
	 * Replaces the given marker with an expression
1132
	 *
1133
	 * @param string $column Name (including alias) of the column
1134
	 * @param mixed $value Value used in the expression
1135
	 * @param string $op Operator used in the expression
1136
	 * @param int $type Type constant from \Aimeos\Base\DB\Statement\Base class
1137
	 * @return string Created expression
1138
	 */
1139
	protected function toExpression( string $column, $value, string $op = '==',
1140
		int $type = \Aimeos\Base\DB\Statement\Base::PARAM_STR ) : string
1141
	{
1142
		$types = ['marker' => $type];
1143
		$translations = ['marker' => $column];
1144
		$value = ( is_array( $value ) ? array_unique( $value ) : $value );
1145
1146
		return $this->getSearch()->compare( $op, 'marker', $value )->toSource( $types, $translations );
1147
	}
1148
1149
1150
	/**
1151
	 * Transforms the application specific values to Aimeos standard values.
1152
	 *
1153
	 * @param array $values Associative list of key/value pairs from the storage
1154
	 * @return array Associative list of key/value pairs with standard Aimeos values
1155
	 */
1156
	protected function transform( array $values ) : array
1157
	{
1158
		return $values;
1159
	}
1160
1161
1162
	/**
1163
	 * Cuts the last part separated by a dot repeatedly and returns the list of resulting string.
1164
	 *
1165
	 * @param string[] $prefix Required base prefixes of the search keys
1166
	 * @param string $string String containing parts separated by dots
1167
	 * @return array List of resulting strings
1168
	 */
1169
	private function cutNameTail( array $prefix, string $string ) : array
1170
	{
1171
		$result = [];
1172
		$noprefix = true;
1173
		$strlen = strlen( $string );
1174
1175
		foreach( $prefix as $key )
1176
		{
1177
			$len = strlen( $key );
1178
1179
			if( strncmp( $string, $key, $len ) === 0 )
1180
			{
1181
				if( $strlen > $len && ( $pos = strrpos( $string, '.' ) ) !== false )
1182
				{
1183
					$result[] = $string = substr( $string, 0, $pos );
1184
					$result = array_merge( $result, $this->cutNameTail( $prefix, $string ) );
1185
					$noprefix = false;
1186
				}
1187
1188
				break;
1189
			}
1190
		}
1191
1192
		if( $noprefix )
1193
		{
1194
			if( ( $pos = strrpos( $string, ':' ) ) !== false ) {
1195
				$result[] = substr( $string, 0, $pos );
1196
				$result[] = $string;
1197
			} elseif( ( $pos = strrpos( $string, '.' ) ) !== false ) {
1198
				$result[] = substr( $string, 0, $pos );
1199
			} else {
1200
				$result[] = $string;
1201
			}
1202
		}
1203
1204
		return $result;
1205
	}
1206
1207
1208
	/**
1209
	 * Returns a list of unique criteria names shortend by the last element after the ''
1210
	 *
1211
	 * @param string[] $prefix Required base prefixes of the search keys
1212
	 * @param \Aimeos\Base\Criteria\Expression\Iface|null $expr Criteria object
1213
	 * @return array List of shortend criteria names
1214
	 */
1215
	private function getCriteriaKeys( array $prefix, ?\Aimeos\Base\Criteria\Expression\Iface $expr = null ) : array
1216
	{
1217
		if( $expr === null ) { return []; }
1218
1219
		$result = [];
1220
1221
		foreach( $this->getCriteriaNames( $expr ) as $item )
1222
		{
1223
			if( strncmp( $item, 'sort:', 5 ) === 0 ) {
1224
				$item = substr( $item, 5 );
1225
			}
1226
1227
			if( ( $pos = strpos( $item, '(' ) ) !== false ) {
1228
				$item = substr( $item, 0, $pos );
1229
			}
1230
1231
			$result = array_merge( $result, $this->cutNameTail( $prefix, $item ) );
1232
		}
1233
1234
		return $result;
1235
	}
1236
1237
1238
	/**
1239
	 * Returns a list of criteria names from a expression and its sub-expressions.
1240
	 *
1241
	 * @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...
1242
	 * @return array List of criteria names
1243
	 */
1244
	private function getCriteriaNames( \Aimeos\Base\Criteria\Expression\Iface $expr ) : array
1245
	{
1246
		if( $expr instanceof \Aimeos\Base\Criteria\Expression\Compare\Iface ) {
1247
			return array( $expr->getName() );
1248
		}
1249
1250
		if( $expr instanceof \Aimeos\Base\Criteria\Expression\Combine\Iface )
1251
		{
1252
			$list = [];
1253
			foreach( $expr->getExpressions() as $item ) {
1254
				$list = array_merge( $list, $this->getCriteriaNames( $item ) );
1255
			}
1256
			return $list;
1257
		}
1258
1259
		if( $expr instanceof \Aimeos\Base\Criteria\Expression\Sort\Iface ) {
1260
			return array( $expr->getName() );
1261
		}
1262
1263
		return [];
1264
	}
1265
}
1266