Passed
Push — master ( 587f1d...8659e3 )
by Aimeos
05:21
created

DB::cutNameTail()   B

Complexity

Conditions 8
Paths 16

Size

Total Lines 36
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 1
Metric Value
eloc 20
c 1
b 0
f 1
dl 0
loc 36
rs 8.4444
cc 8
nc 16
nop 2
1
<?php
2
3
/**
4
 * @license LGPLv3, https://opensource.org/licenses/LGPL-3.0
5
 * @copyright Aimeos (aimeos.org), 2023
6
 * @package MShop
7
 * @subpackage Common
8
 */
9
10
11
namespace Aimeos\MShop\Common\Manager;
12
13
14
/**
15
 * Method trait for managers
16
 *
17
 * @package MShop
18
 * @subpackage Common
19
 */
20
trait DB
21
{
22
	private ?\Aimeos\Base\Criteria\Iface $search;
23
	private ?string $resourceName = null;
24
	private array $cachedStmts = [];
25
	private string $subpath;
26
	private string $domain;
27
28
29
	/**
30
	 * Returns the context object.
31
	 *
32
	 * @return \Aimeos\MShop\ContextIface Context object
33
	 */
34
	abstract protected function context() : \Aimeos\MShop\ContextIface;
35
36
37
	/**
38
	 * Creates the criteria attribute items from the list of entries
39
	 *
40
	 * @param array $list Associative array of code as key and array with properties as values
41
	 * @return \Aimeos\Base\Criteria\Attribute\Standard[] List of criteria attribute items
42
	 */
43
	abstract protected function createAttributes( array $list ) : array;
44
45
46
	/**
47
	 * Returns the attribute helper functions for searching defined by the manager.
48
	 *
49
	 * @param \Aimeos\Base\Criteria\Attribute\Iface[] $attributes List of search attribute items
50
	 * @return array Associative array of attribute code and helper function
51
	 */
52
	abstract protected function getSearchFunctions( array $attributes ) : array;
53
54
55
	/**
56
	 * Returns the attribute translations for searching defined by the manager.
57
	 *
58
	 * @param \Aimeos\Base\Criteria\Attribute\Iface[] $attributes List of search attribute items
59
	 * @return array Associative array of attribute code and internal attribute code
60
	 */
61
	abstract protected function getSearchTranslations( array $attributes ) : array;
62
63
64
	/**
65
	 * Returns the attribute types for searching defined by the manager.
66
	 *
67
	 * @param \Aimeos\Base\Criteria\Attribute\Iface[] $attributes List of search attribute items
68
	 * @return array Associative array of attribute code and internal attribute type
69
	 */
70
	abstract protected function getSearchTypes( array $attributes ) : array;
71
72
73
	/**
74
	 * Returns the outmost decorator of the decorator stack
75
	 *
76
	 * @return \Aimeos\MShop\Common\Manager\Iface Outmost decorator object
77
	 */
78
	abstract protected function object() : \Aimeos\MShop\Common\Manager\Iface;
79
80
81
	/**
82
	 * Returns the site expression for the given name
83
	 *
84
	 * @param string $name Name of the site condition
85
	 * @param int $sitelevel Site level constant from \Aimeos\MShop\Locale\Manager\Base
86
	 * @return \Aimeos\Base\Criteria\Expression\Iface Site search condition
87
	 */
88
	abstract protected function siteCondition( string $name, int $sitelevel ) : \Aimeos\Base\Criteria\Expression\Iface;
89
90
91
	/**
92
	 * Returns the site ID that should be used based on the site level
93
	 *
94
	 * @param string $siteId Site ID to check
95
	 * @param int $sitelevel Site level to check against
96
	 * @return string Site ID that should be use based on the site level
97
	 * @since 2022.04
98
	 */
99
	abstract protected function siteId( string $siteId, int $sitelevel ) : string;
100
101
102
	/**
103
	 * Adds additional column names to SQL statement
104
	 *
105
	 * @param string[] $columns List of column names
106
	 * @param string $sql Insert or update SQL statement
107
	 * @param bool $mode True for insert, false for update statement
108
	 * @return string Modified insert or update SQL statement
109
	 */
110
	protected function addSqlColumns( array $columns, string $sql, bool $mode = true ) : string
111
	{
112
		$names = $values = '';
113
114
		if( $mode )
115
		{
116
			foreach( $columns as $name ) {
117
				$names .= '"' . $name . '", '; $values .= '?, ';
118
			}
119
		}
120
		else
121
		{
122
			foreach( $columns as $name ) {
123
				$names .= '"' . $name . '" = ?, ';
124
			}
125
		}
126
127
		return str_replace( [':names', ':values'], [$names, $values], $sql );
128
	}
129
130
131
	/**
132
	 * Counts the number products that are available for the values of the given key.
133
	 *
134
	 * @param \Aimeos\Base\Criteria\Iface $search Search criteria
135
	 * @param array|string $keys Search key or list of keys for aggregation
136
	 * @param string $cfgPath Configuration key for the SQL statement
137
	 * @param string[] $required List of domain/sub-domain names like "catalog.index" that must be additionally joined
138
	 * @param string|null $value Search key for aggregating the value column
139
	 * @param string|null $type Type of aggregation, e.g.  "sum", "min", "max" or NULL for "count"
140
	 * @return \Aimeos\Map List of ID values as key and the number of counted products as value
141
	 * @todo 2018.01 Reorder Parameter list
142
	 */
143
	protected function aggregateBase( \Aimeos\Base\Criteria\Iface $search, $keys, string $cfgPath,
144
		array $required = [], string $value = null, string $type = null ) : \Aimeos\Map
145
	{
146
		/** mshop/common/manager/aggregate/limit
147
		 * Limits the number of records that are used when aggregating items
148
		 *
149
		 * As counting huge amount of records (several 10 000 records) takes a long time,
150
		 * the limit can cut down response times so the counts are available more quickly
151
		 * in the front-end and the server load is reduced.
152
		 *
153
		 * Using a low limit can lead to incorrect numbers if the amount of found items
154
		 * is very high. Approximate item counts are normally not a problem but it can
155
		 * lead to the situation that visitors see that no items are available despite
156
		 * the fact that there would be at least one.
157
		 *
158
		 * @param integer Number of records
159
		 * @since 2021.04
160
		 */
161
		$limit = $this->context()->config()->get( 'mshop/common/manager/aggregate/limit', 10000 );
162
163
		if( empty( $keys ) )
164
		{
165
			$msg = $this->context()->translate( 'mshop', 'At least one key is required for aggregation' );
166
			throw new \Aimeos\MShop\Exception( $msg );
167
		}
168
169
		$attrMap = array_column( array_filter( $this->object()->getSearchAttributes(), function( $item ) {
170
			return $item->isPublic() || strncmp( $item->getCode(), 'agg:', 4 ) === 0;
171
		} ), null, 'code' );
172
173
		if( $value === null && ( $value = key( $attrMap ) ) === null )
174
		{
175
			$msg = $this->context()->translate( 'mshop', 'No search keys available' );
176
			throw new \Aimeos\MShop\Exception( $msg );
177
		}
178
179
		if( ( $pos = strpos( $valkey = $value, '(' ) ) !== false ) {
180
			$value = substr( $value, 0, $pos ) . '()'; // remove parameters from search function
181
		}
182
183
		if( !isset( $attrMap[$value] ) )
184
		{
185
			$msg = $this->context()->translate( 'mshop', 'Unknown search key "%1$s"' );
186
			throw new \Aimeos\MShop\Exception( sprintf( $msg, $value ) );
187
		}
188
189
		$keys = (array) $keys;
190
		$acols = $cols = $expr = [];
191
		$search = (clone $search)->slice( $search->getOffset(), min( $search->getLimit(), $limit ) );
192
193
		foreach( $keys as $string )
194
		{
195
			if( ( $attrItem = $attrMap[$string] ?? null ) === null )
196
			{
197
				$msg = $this->context()->translate( 'mshop', 'Unknown search key "%1$s"' );
198
				throw new \Aimeos\MShop\Exception( sprintf( $msg, $string ) );
199
			}
200
201
			if( strpos( $attrItem->getInternalCode(), '"' ) === false ) {
202
				$prefixed = $this->alias( $attrItem->getCode() ) . '."' . $attrItem->getInternalCode() . '"';
203
			} else { // @todo: Remove in 2025.01
204
				$prefixed = $attrItem->getInternalCode();
205
			}
206
207
			$acols[] = $prefixed . ' AS "' . $string . '"';
208
			$cols[] = $prefixed;
209
210
			$expr[] = $search->compare( '!=', $string, null ); // required for the joins
211
		}
212
213
		$expr[] = $search->compare( '!=', $valkey, null );
214
		$search->add( $search->and( $expr ) );
215
216
		$val = $attrMap[$value]->getInternalCode();
217
218
		if( strpos( $val, '"' ) === false ) {
219
			$val = $this->alias( $attrMap[$value]->getCode() ) . '."' . $val . '"';
220
		}
221
222
		$sql = $this->getSqlConfig( $cfgPath );
223
		$sql = str_replace( ':cols', join( ', ', $cols ), $sql );
224
		$sql = str_replace( ':acols', join( ', ', $acols ), $sql );
225
		$sql = str_replace( ':keys', '"' . join( '", "', $keys ) . '"', $sql );
226
		$sql = str_replace( ':type', in_array( $type, ['avg', 'count', 'max', 'min', 'sum'] ) ? $type : 'count', $sql );
227
		$sql = str_replace( ':val', $val, $sql );
228
229
		return $this->aggregateResult( $search, $sql, $required );
230
	}
231
232
233
	/**
234
	 * Returns the aggregated values for the given SQL string and filter.
235
	 *
236
	 * @param \Aimeos\Base\Criteria\Iface $filter Filter object
237
	 * @param string $sql SQL statement
238
	 * @param string[] $required List of domain/sub-domain names like "catalog.index" that must be additionally joined
239
	 * @return \Aimeos\Map (Nested) list of aggregated values as key and the number of counted products as value
240
	 */
241
	protected function aggregateResult( \Aimeos\Base\Criteria\Iface $filter, string $sql, array $required ) : \Aimeos\Map
242
	{
243
		$map = [];
244
		$total = null;
245
		$level = \Aimeos\MShop\Locale\Manager\Base::SITE_ALL;
246
		$conn = $this->context()->db( $this->getResourceName() );
247
		$results = $this->searchItemsBase( $conn, $filter, $sql, '', $required, $total, $level );
248
249
		while( $row = $results->fetch() )
250
		{
251
			$row = $this->transform( $row );
252
253
			$temp = &$map;
254
			$last = array_pop( $row );
255
256
			foreach( $row as $val ) {
257
				$temp[$val] = $temp[$val] ?? [];
258
				$temp = &$temp[$val];
259
			}
260
			$temp = $last;
261
		}
262
263
		return map( $map );
264
	}
265
266
267
	/**
268
	 * Returns the table alias name.
269
	 *
270
	 * @param string|null $attrcode Search attribute code
271
	 * @return string Table alias name
272
	 */
273
	protected function alias( string $attrcode = null ) : string
274
	{
275
		if( $attrcode )
276
		{
277
			$parts = array_slice( explode( '.', $attrcode ), 0, -1 );
278
			$str = 'm' . substr( array_shift( $parts ) ?: current( $this->type() ), 0, 3 );
0 ignored issues
show
Bug introduced by
It seems like type() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

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

278
			$str = 'm' . substr( array_shift( $parts ) ?: current( $this->/** @scrutinizer ignore-call */ type() ), 0, 3 );
Loading history...
279
		}
280
		else
281
		{
282
			$parts = $this->type();
283
			$str = 'm' . substr( array_shift( $parts ), 0, 3 );
284
		}
285
286
		foreach( $parts as $part ) {
287
			$str .= substr( $part, 0, 2 );
288
		}
289
290
		return $str;
291
	}
292
293
294
	/**
295
	 * Adds aliases for the columns
296
	 *
297
	 * @param array $map Associative list of search keys as keys and internal column names as values
298
	 * @return array Associative list of search keys as keys and aliased column names as values
299
	 */
300
	protected function aliasTranslations( array $map ) : array
301
	{
302
		foreach( $map as $key => $value )
303
		{
304
			if( strpos( $value, '"' ) === false ) {
305
				$map[$key] = $this->alias( $key ) . '."' . $value . '"';
306
			}
307
		}
308
309
		return $map;
310
	}
311
312
313
	/**
314
	 * Binds additional values to the statement before execution.
315
	 *
316
	 * @param \Aimeos\MShop\Common\Item\Iface $item Item object
317
	 * @param \Aimeos\Base\DB\Statement\Iface $stmt Database statement object
318
	 * @param int $idx Current bind index
319
	 * @return \Aimeos\Base\DB\Statement\Iface Database statement object with bound values
320
	 */
321
	protected function bind( \Aimeos\MShop\Common\Item\Iface $item, \Aimeos\Base\DB\Statement\Iface $stmt, int &$idx ) : \Aimeos\Base\DB\Statement\Iface
322
	{
323
		return $stmt;
324
	}
325
326
327
	/**
328
	 * Removes old entries from the storage.
329
	 *
330
	 * @param iterable $siteids List of IDs for sites whose entries should be deleted
331
	 * @param string $cfgpath Configuration key to the cleanup statement
332
	 * @return \Aimeos\MShop\Common\Manager\Iface Manager object for chaining method calls
333
	 */
334
	protected function clearBase( iterable $siteids, string $cfgpath ) : \Aimeos\MShop\Common\Manager\Iface
335
	{
336
		if( empty( $siteids ) ) {
337
			return $this;
338
		}
339
340
		$conn = $this->context()->db( $this->getResourceName() );
341
342
		$sql = $this->getSqlConfig( $cfgpath );
343
		$sql = str_replace( ':cond', '1=1', $sql );
344
345
		$stmt = $conn->create( $sql );
346
347
		foreach( $siteids as $siteid )
348
		{
349
			$stmt->bind( 1, $siteid );
350
			$stmt->execute()->finish();
351
		}
352
353
		return $this;
354
	}
355
356
357
	/**
358
	 * Deletes items.
359
	 *
360
	 * @param \Aimeos\MShop\Common\Item\Iface|\Aimeos\Map|array|string $items List of item objects or IDs of the items
361
	 * @param string $cfgpath Configuration path to the SQL statement
362
	 * @param bool $siteid If siteid should be used in the statement
363
	 * @param string $name Name of the ID column
364
	 * @return \Aimeos\MShop\Common\Manager\Iface Manager object for chaining method calls
365
	 */
366
	protected function deleteItemsBase( $items, string $cfgpath, bool $siteid = true,
367
		string $name = 'id' ) : \Aimeos\MShop\Common\Manager\Iface
368
	{
369
		if( map( $items )->isEmpty() ) {
370
			return $this;
371
		}
372
373
		$search = $this->object()->filter();
374
		$search->setConditions( $search->compare( '==', $name, $items ) );
375
376
		$types = array( $name => \Aimeos\Base\DB\Statement\Base::PARAM_STR );
377
		$translations = array( $name => '"' . $name . '"' );
378
379
		$cond = $search->getConditionSource( $types, $translations );
380
		$sql = str_replace( ':cond', $cond, $this->getSqlConfig( $cfgpath ) );
381
382
		$context = $this->context();
383
		$conn = $context->db( $this->getResourceName() );
384
385
		$stmt = $conn->create( $sql );
386
387
		if( $siteid ) {
388
			$stmt->bind( 1, $context->locale()->getSiteId() . '%' );
389
		}
390
391
		$stmt->execute()->finish();
392
393
		return $this;
394
	}
395
396
397
	/**
398
	 * Sets the base criteria "status".
399
	 * (setConditions overwrites the base criteria)
400
	 *
401
	 * @param string $domain Name of the domain/sub-domain like "product" or "product.list"
402
	 * @param bool|null $default TRUE for status=1, NULL for status>0, FALSE for no restriction
403
	 * @return \Aimeos\Base\Criteria\Iface Search critery object
404
	 */
405
	protected function filterBase( string $domain, ?bool $default = false ) : \Aimeos\Base\Criteria\Iface
406
	{
407
		$context = $this->context();
408
		$db = $this->getResourceName();
409
		$conn = $context->db( $db );
410
		$config = $context->config();
411
412
		if( ( $adapter = $config->get( 'resource/' . $db . '/adapter' ) ) === null ) {
413
			$adapter = $config->get( 'resource/db/adapter' );
414
		}
415
416
		switch( $adapter )
417
		{
418
			case 'pgsql':
419
				$filter = new \Aimeos\Base\Criteria\PgSQL( $conn ); break;
420
			default:
421
				$filter = new \Aimeos\Base\Criteria\SQL( $conn ); break;
422
		}
423
424
		if( $default !== false ) {
425
			$filter->add( $domain . '.status', $default ? '==' : '>=', 1 );
426
		}
427
428
		return $filter;
429
	}
430
431
432
	/**
433
	 * Returns the item for the given search key/value pairs.
434
	 *
435
	 * @param array $pairs Search key/value pairs for the item
436
	 * @param string[] $ref List of domains whose items should be fetched too
437
	 * @param bool|null $default Add default criteria or NULL for relaxed default criteria
438
	 * @return \Aimeos\MShop\Common\Item\Iface Requested item
439
	 * @throws \Aimeos\MShop\Exception if no item with the given ID found
440
	 */
441
	protected function findBase( array $pairs, array $ref, ?bool $default ) : \Aimeos\MShop\Common\Item\Iface
442
	{
443
		$expr = [];
444
		$criteria = $this->object()->filter( $default )->slice( 0, 1 );
445
446
		foreach( $pairs as $key => $value )
447
		{
448
			if( $value === null )
449
			{
450
				$msg = $this->context()->translate( 'mshop', 'Required value for "%1$s" is missing' );
451
				throw new \Aimeos\MShop\Exception( sprintf( $msg, $key ) );
452
			}
453
			$expr[] = $criteria->compare( '==', $key, $value );
454
		}
455
456
		$criteria->setConditions( $criteria->and( $expr ) );
457
458
		if( ( $item = $this->object()->search( $criteria, $ref )->first() ) ) {
459
			return $item;
460
		}
461
462
		$msg = $this->context()->translate( 'mshop', 'No item found for conditions: %1$s' );
463
		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

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

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

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

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

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

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