DB::getCriteriaNames()   A
last analyzed

Complexity

Conditions 5
Paths 5

Size

Total Lines 20
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 1
Metric Value
eloc 10
c 1
b 0
f 1
dl 0
loc 20
rs 9.6111
cc 5
nc 5
nop 1
1
<?php
2
3
/**
4
 * @license LGPLv3, https://opensource.org/licenses/LGPL-3.0
5
 * @copyright Aimeos (aimeos.org), 2023
6
 * @package MShop
7
 * @subpackage Common
8
 */
9
10
11
namespace Aimeos\MShop\Common\Manager;
12
13
14
/**
15
 * Method trait for managers
16
 *
17
 * @package MShop
18
 * @subpackage Common
19
 */
20
trait DB
21
{
22
	private ?\Aimeos\Base\Criteria\Iface $search;
23
	private ?string $resourceName = null;
24
	private array $cachedStmts = [];
25
	private string $subpath;
26
	private string $domain;
27
28
29
	/**
30
	 * Returns the context object.
31
	 *
32
	 * @return \Aimeos\MShop\ContextIface Context object
33
	 */
34
	abstract protected function context() : \Aimeos\MShop\ContextIface;
35
36
37
	/**
38
	 * Creates the criteria attribute items from the list of entries
39
	 *
40
	 * @param array $list Associative array of code as key and array with properties as values
41
	 * @return \Aimeos\Base\Criteria\Attribute\Standard[] List of criteria attribute items
42
	 */
43
	abstract protected function createAttributes( array $list ) : array;
44
45
46
	/**
47
	 * Returns the attribute helper functions for searching defined by the manager.
48
	 *
49
	 * @param \Aimeos\Base\Criteria\Attribute\Iface[] $attributes List of search attribute items
50
	 * @return array Associative array of attribute code and helper function
51
	 */
52
	abstract protected function getSearchFunctions( array $attributes ) : array;
53
54
55
	/**
56
	 * Returns the attribute translations for searching defined by the manager.
57
	 *
58
	 * @param \Aimeos\Base\Criteria\Attribute\Iface[] $attributes List of search attribute items
59
	 * @return array Associative array of attribute code and internal attribute code
60
	 */
61
	abstract protected function getSearchTranslations( array $attributes ) : array;
62
63
64
	/**
65
	 * Returns the attribute types for searching defined by the manager.
66
	 *
67
	 * @param \Aimeos\Base\Criteria\Attribute\Iface[] $attributes List of search attribute items
68
	 * @return array Associative array of attribute code and internal attribute type
69
	 */
70
	abstract protected function getSearchTypes( array $attributes ) : array;
71
72
73
	/**
74
	 * Returns the outmost decorator of the decorator stack
75
	 *
76
	 * @return \Aimeos\MShop\Common\Manager\Iface Outmost decorator object
77
	 */
78
	abstract protected function object() : \Aimeos\MShop\Common\Manager\Iface;
79
80
81
	/**
82
	 * Returns the site expression for the given name
83
	 *
84
	 * @param string $name Name of the site condition
85
	 * @param int $sitelevel Site level constant from \Aimeos\MShop\Locale\Manager\Base
86
	 * @return \Aimeos\Base\Criteria\Expression\Iface Site search condition
87
	 */
88
	abstract protected function siteCondition( string $name, int $sitelevel ) : \Aimeos\Base\Criteria\Expression\Iface;
89
90
91
	/**
92
	 * Returns the site ID that should be used based on the site level
93
	 *
94
	 * @param string $siteId Site ID to check
95
	 * @param int $sitelevel Site level to check against
96
	 * @return string Site ID that should be use based on the site level
97
	 * @since 2022.04
98
	 */
99
	abstract protected function siteId( string $siteId, int $sitelevel ) : string;
100
101
102
	/**
103
	 * 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