Passed
Push — master ( 9f1c00...0115e8 )
by Aimeos
05:45
created

DB::getJoins()   B

Complexity

Conditions 7
Paths 4

Size

Total Lines 14
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 1
Metric Value
eloc 9
dl 0
loc 14
rs 8.8333
c 1
b 0
f 1
cc 7
nc 4
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
26
27
	/**
28
	 * Returns the context object.
29
	 *
30
	 * @return \Aimeos\MShop\ContextIface Context object
31
	 */
32
	abstract protected function context() : \Aimeos\MShop\ContextIface;
33
34
35
	/**
36
	 * Creates the criteria attribute items from the list of entries
37
	 *
38
	 * @param array $list Associative array of code as key and array with properties as values
39
	 * @return \Aimeos\Base\Criteria\Attribute\Standard[] List of criteria attribute items
40
	 */
41
	abstract protected function createAttributes( array $list ) : array;
42
43
44
	/**
45
	 * Returns the attribute helper functions for searching defined by the manager.
46
	 *
47
	 * @param \Aimeos\Base\Criteria\Attribute\Iface[] $attributes List of search attribute items
48
	 * @return array Associative array of attribute code and helper function
49
	 */
50
	abstract protected function getSearchFunctions( array $attributes ) : array;
51
52
53
	/**
54
	 * Returns the attribute translations for searching defined by the manager.
55
	 *
56
	 * @param \Aimeos\Base\Criteria\Attribute\Iface[] $attributes List of search attribute items
57
	 * @return array Associative array of attribute code and internal attribute code
58
	 */
59
	abstract protected function getSearchTranslations( array $attributes ) : array;
60
61
62
	/**
63
	 * Returns the attribute types for searching defined by the manager.
64
	 *
65
	 * @param \Aimeos\Base\Criteria\Attribute\Iface[] $attributes List of search attribute items
66
	 * @return array Associative array of attribute code and internal attribute type
67
	 */
68
	abstract protected function getSearchTypes( array $attributes ) : array;
69
70
71
	/**
72
	 * Returns the outmost decorator of the decorator stack
73
	 *
74
	 * @return \Aimeos\MShop\Common\Manager\Iface Outmost decorator object
75
	 */
76
	abstract protected function object() : \Aimeos\MShop\Common\Manager\Iface;
77
78
79
	/**
80
	 * Returns the site expression for the given name
81
	 *
82
	 * @param string $name Name of the site condition
83
	 * @param int $sitelevel Site level constant from \Aimeos\MShop\Locale\Manager\Base
84
	 * @return \Aimeos\Base\Criteria\Expression\Iface Site search condition
85
	 */
86
	abstract protected function siteCondition( string $name, int $sitelevel ) : \Aimeos\Base\Criteria\Expression\Iface;
87
88
89
	/**
90
	 * Returns the site ID that should be used based on the site level
91
	 *
92
	 * @param string $siteId Site ID to check
93
	 * @param int $sitelevel Site level to check against
94
	 * @return string Site ID that should be use based on the site level
95
	 * @since 2022.04
96
	 */
97
	abstract protected function siteId( string $siteId, int $sitelevel ) : string;
98
99
100
	/**
101
	 * Adds additional column names to SQL statement
102
	 *
103
	 * @param string[] $columns List of column names
104
	 * @param string $sql Insert or update SQL statement
105
	 * @param bool $mode True for insert, false for update statement
106
	 * @return string Modified insert or update SQL statement
107
	 */
108
	protected function addSqlColumns( array $columns, string $sql, bool $mode = true ) : string
109
	{
110
		$names = $values = '';
111
112
		if( $mode )
113
		{
114
			foreach( $columns as $name ) {
115
				$names .= '"' . $name . '", '; $values .= '?, ';
116
			}
117
		}
118
		else
119
		{
120
			foreach( $columns as $name ) {
121
				$names .= '"' . $name . '" = ?, ';
122
			}
123
		}
124
125
		return str_replace( [':names', ':values'], [$names, $values], $sql );
126
	}
127
128
129
	/**
130
	 * Counts the number products that are available for the values of the given key.
131
	 *
132
	 * @param \Aimeos\Base\Criteria\Iface $search Search criteria
133
	 * @param array|string $keys Search key or list of keys for aggregation
134
	 * @param string $cfgPath Configuration key for the SQL statement
135
	 * @param string[] $required List of domain/sub-domain names like "catalog.index" that must be additionally joined
136
	 * @param string|null $value Search key for aggregating the value column
137
	 * @param string|null $type Type of aggregation, e.g.  "sum", "min", "max" or NULL for "count"
138
	 * @return \Aimeos\Map List of ID values as key and the number of counted products as value
139
	 * @todo 2018.01 Reorder Parameter list
140
	 */
141
	protected function aggregateBase( \Aimeos\Base\Criteria\Iface $search, $keys, string $cfgPath,
142
		array $required = [], string $value = null, string $type = null ) : \Aimeos\Map
143
	{
144
		/** mshop/common/manager/aggregate/limit
145
		 * Limits the number of records that are used when aggregating items
146
		 *
147
		 * As counting huge amount of records (several 10 000 records) takes a long time,
148
		 * the limit can cut down response times so the counts are available more quickly
149
		 * in the front-end and the server load is reduced.
150
		 *
151
		 * Using a low limit can lead to incorrect numbers if the amount of found items
152
		 * is very high. Approximate item counts are normally not a problem but it can
153
		 * lead to the situation that visitors see that no items are available despite
154
		 * the fact that there would be at least one.
155
		 *
156
		 * @param integer Number of records
157
		 * @since 2021.04
158
		 */
159
		$limit = $this->context()->config()->get( 'mshop/common/manager/aggregate/limit', 10000 );
160
161
		if( empty( $keys ) )
162
		{
163
			$msg = $this->context()->translate( 'mshop', 'At least one key is required for aggregation' );
164
			throw new \Aimeos\MShop\Exception( $msg );
165
		}
166
167
		$attrMap = array_column( array_filter( $this->object()->getSearchAttributes(), function( $item ) {
168
			return $item->isPublic() || strncmp( $item->getCode(), 'agg:', 4 ) === 0;
169
		} ), null, 'code' );
170
171
		if( $value === null && ( $value = key( $attrMap ) ) === null )
172
		{
173
			$msg = $this->context()->translate( 'mshop', 'No search keys available' );
174
			throw new \Aimeos\MShop\Exception( $msg );
175
		}
176
177
		if( ( $pos = strpos( $valkey = $value, '(' ) ) !== false ) {
178
			$value = substr( $value, 0, $pos ) . '()'; // remove parameters from search function
179
		}
180
181
		if( !isset( $attrMap[$value] ) )
182
		{
183
			$msg = $this->context()->translate( 'mshop', 'Unknown search key "%1$s"' );
184
			throw new \Aimeos\MShop\Exception( sprintf( $msg, $value ) );
185
		}
186
187
		$keys = (array) $keys;
188
		$acols = $cols = $expr = [];
189
		$search = (clone $search)->slice( $search->getOffset(), min( $search->getLimit(), $limit ) );
190
191
		foreach( $keys as $string )
192
		{
193
			if( ( $attrItem = $attrMap[$string] ?? null ) === null )
194
			{
195
				$msg = $this->context()->translate( 'mshop', 'Unknown search key "%1$s"' );
196
				throw new \Aimeos\MShop\Exception( sprintf( $msg, $string ) );
197
			}
198
199
			$cols[] = $attrItem->getInternalCode();
200
			$acols[] = $attrItem->getInternalCode() . ' AS "' . $string . '"';
201
202
			$expr[] = $search->compare( '!=', $string, null ); // required for the joins
203
		}
204
205
		$search->add( $search->and( $expr ) )->add( $valkey, '!=', null );
206
207
		$sql = $this->getSqlConfig( $cfgPath );
208
		$sql = str_replace( ':cols', join( ', ', $cols ), $sql );
209
		$sql = str_replace( ':acols', join( ', ', $acols ), $sql );
210
		$sql = str_replace( ':keys', '"' . join( '", "', $keys ) . '"', $sql );
211
		$sql = str_replace( ':val', $attrMap[$value]->getInternalCode(), $sql );
212
		$sql = str_replace( ':type', in_array( $type, ['avg', 'count', 'max', 'min', 'sum'] ) ? $type : 'count', $sql );
213
214
		return $this->aggregateResult( $search, $sql, $required );
215
	}
216
217
218
	/**
219
	 * Returns the aggregated values for the given SQL string and filter.
220
	 *
221
	 * @param \Aimeos\Base\Criteria\Iface $filter Filter object
222
	 * @param string $sql SQL statement
223
	 * @param string[] $required List of domain/sub-domain names like "catalog.index" that must be additionally joined
224
	 * @return \Aimeos\Map (Nested) list of aggregated values as key and the number of counted products as value
225
	 */
226
	protected function aggregateResult( \Aimeos\Base\Criteria\Iface $filter, string $sql, array $required ) : \Aimeos\Map
227
	{
228
		$map = [];
229
		$total = null;
230
		$level = \Aimeos\MShop\Locale\Manager\Base::SITE_ALL;
231
		$conn = $this->context()->db( $this->getResourceName() );
232
		$results = $this->searchItemsBase( $conn, $filter, $sql, '', $required, $total, $level );
233
234
		while( $row = $results->fetch() )
235
		{
236
			$row = $this->transform( $row );
237
238
			$temp = &$map;
239
			$last = array_pop( $row );
240
241
			foreach( $row as $val ) {
242
				$temp[$val] = $temp[$val] ?? [];
243
				$temp = &$temp[$val];
244
			}
245
			$temp = $last;
246
		}
247
248
		return map( $map );
249
	}
250
251
252
	/**
253
	 * Returns the table alias name.
254
	 *
255
	 * @param string|null $attrcode Search attribute code
256
	 * @return string Table alias name
257
	 */
258
	protected function alias( string $attrcode = null ) : string
259
	{
260
		if( $attrcode ) {
261
			$parts = array_slice( explode( '.', $attrcode ), 1, -1 );
262
		} else {
263
			$parts = explode( '/', $this->getSubPath() );
264
		}
265
266
		$str = 'm' . substr( $this->getDomain(), 0, 3 );
267
268
		foreach( $parts as $part ) {
269
			$str .= substr( $part, 0, 2 );
270
		}
271
272
		return $str;
273
	}
274
275
276
	/**
277
	 * Adds aliases for the columns
278
	 *
279
	 * @param array $map Associative list of search keys as keys and internal column names as values
280
	 * @return array Associative list of search keys as keys and aliased column names as values
281
	 */
282
	protected function aliasTranslations( array $map ) : array
283
	{
284
		foreach( $map as $key => $value )
285
		{
286
			if( ( $pos = strpos( $value, '"' ) ) === false ) {
0 ignored issues
show
Unused Code introduced by
The assignment to $pos is dead and can be removed.
Loading history...
287
				$map[$key] = $this->alias( $key ) . '."' . $value . '"';
288
			}
289
		}
290
291
		return $map;
292
	}
293
294
295
	/**
296
	 * Removes old entries from the storage.
297
	 *
298
	 * @param iterable $siteids List of IDs for sites whose entries should be deleted
299
	 * @param string $cfgpath Configuration key to the cleanup statement
300
	 * @return \Aimeos\MShop\Common\Manager\Iface Manager object for chaining method calls
301
	 */
302
	protected function clearBase( iterable $siteids, string $cfgpath ) : \Aimeos\MShop\Common\Manager\Iface
303
	{
304
		if( empty( $siteids ) ) {
305
			return $this;
306
		}
307
308
		$conn = $this->context()->db( $this->getResourceName() );
309
310
		$sql = $this->getSqlConfig( $cfgpath );
311
		$sql = str_replace( ':cond', '1=1', $sql );
312
313
		$stmt = $conn->create( $sql );
314
315
		foreach( $siteids as $siteid )
316
		{
317
			$stmt->bind( 1, $siteid );
318
			$stmt->execute()->finish();
319
		}
320
321
		return $this;
322
	}
323
324
325
	/**
326
	 * Deletes items.
327
	 *
328
	 * @param \Aimeos\MShop\Common\Item\Iface|\Aimeos\Map|array|string $items List of item objects or IDs of the items
329
	 * @param string $cfgpath Configuration path to the SQL statement
330
	 * @param bool $siteid If siteid should be used in the statement
331
	 * @param string $name Name of the ID column
332
	 * @return \Aimeos\MShop\Common\Manager\Iface Manager object for chaining method calls
333
	 */
334
	protected function deleteItemsBase( $items, string $cfgpath, bool $siteid = true,
335
		string $name = 'id' ) : \Aimeos\MShop\Common\Manager\Iface
336
	{
337
		if( map( $items )->isEmpty() ) {
338
			return $this;
339
		}
340
341
		$search = $this->object()->filter();
342
		$search->setConditions( $search->compare( '==', $name, $items ) );
343
344
		$types = array( $name => \Aimeos\Base\DB\Statement\Base::PARAM_STR );
345
		$translations = array( $name => '"' . $name . '"' );
346
347
		$cond = $search->getConditionSource( $types, $translations );
348
		$sql = str_replace( ':cond', $cond, $this->getSqlConfig( $cfgpath ) );
349
350
		$context = $this->context();
351
		$conn = $context->db( $this->getResourceName() );
352
353
		$stmt = $conn->create( $sql );
354
355
		if( $siteid ) {
356
			$stmt->bind( 1, $context->locale()->getSiteId() . '%' );
357
		}
358
359
		$stmt->execute()->finish();
360
361
		return $this;
362
	}
363
364
365
	/**
366
	 * Returns the full configuration key for the passed last part
367
	 *
368
	 * @param string $name Configuration last part
369
	 * @return string Full configuration key
370
	 */
371
	protected function getConfigKey( string $name ) : string
372
	{
373
		$subPath = $this->getSubPath();
374
		return 'mshop/' . $this->getDomain() . '/manager/' . ( $subPath ? $subPath . '/' : '' ) . $name;
375
	}
376
377
378
	/**
379
	 * Returns a sorted list of required criteria keys.
380
	 *
381
	 * @param \Aimeos\Base\Criteria\Iface $criteria Search criteria object
382
	 * @param string[] $required List of prefixes of required search conditions
383
	 * @return string[] Sorted list of criteria keys
384
	 */
385
	protected function getCriteriaKeyList( \Aimeos\Base\Criteria\Iface $criteria, array $required ) : array
386
	{
387
		$keys = array_merge( $required, $this->getCriteriaKeys( $required, $criteria->getConditions() ) );
388
389
		foreach( $criteria->getSortations() as $sortation ) {
390
			$keys = array_merge( $keys, $this->getCriteriaKeys( $required, $sortation ) );
391
		}
392
393
		$keys = array_unique( array_merge( $required, $keys ) );
394
		sort( $keys );
395
396
		return $keys;
397
	}
398
399
400
	/**
401
	 * Returns the manager domain
402
	 *
403
	 * @return string Manager domain e.g. "product"
404
	 */
405
	protected function getDomain() : string
406
	{
407
		if( !isset( $this->domain ) ) {
408
			$this->initDb();
409
		}
410
411
		return $this->domain;
412
	}
413
414
415
	/**
416
	 * Returns the manager path
417
	 *
418
	 * @return string Manager path e.g. "product/lists/type"
419
	 */
420
	protected function getManagerPath() : string
421
	{
422
		$subPath = $this->getSubPath();
423
		return $this->getDomain() . ( $subPath ? '/' . $subPath : '' );
424
	}
425
426
427
	/**
428
	 * Returns the name of the resource.
429
	 *
430
	 * @return string Name of the resource, e.g. "db-product"
431
	 */
432
	protected function getResourceName() : string
433
	{
434
		if( $this->resourceName === null ) {
435
			$this->setResourceName( 'db-' . $this->getDomain() );
436
		}
437
438
		return $this->resourceName;
439
	}
440
441
442
	/**
443
	 * Sets the name of the database resource that should be used.
444
	 *
445
	 * @param string $name Name of the resource
446
	 * @return \Aimeos\MShop\Common\Manager\Iface Manager object for chaining method calls
447
	 */
448
	protected function setResourceName( string $name ) : \Aimeos\MShop\Common\Manager\Iface
449
	{
450
		$config = $this->context()->config();
451
452
		if( $config->get( 'resource/' . $name ) === null ) {
453
			$this->resourceName = $config->get( 'resource/default', 'db' );
454
		} else {
455
			$this->resourceName = $name;
456
		}
457
458
		return $this;
459
	}
460
461
462
	/**
463
	 * Returns the search attribute objects used for searching.
464
	 *
465
	 * @param array $list Associative list of search keys and the lists of search definitions
466
	 * @param string $path Configuration path to the sub-domains for fetching the search definitions
467
	 * @param string[] $default List of sub-domains if no others are configured
468
	 * @param bool $withsub True to include search definitions of sub-domains, false if not
469
	 * @return \Aimeos\Base\Criteria\Attribute\Iface[] Associative list of search keys and criteria attribute items as values
470
	 * @since 2014.09
471
	 */
472
	protected function getSearchAttributesBase( array $list, string $path, array $default, bool $withsub ) : array
473
	{
474
		$attr = $this->createAttributes( $list );
475
476
		if( $withsub === true )
477
		{
478
			$domains = $this->context()->config()->get( $path, $default );
479
480
			foreach( $domains as $domain ) {
481
				$attr += $this->object()->getSubManager( $domain )->getSearchAttributes( true );
482
			}
483
		}
484
485
		return $attr;
486
	}
487
488
489
	/**
490
	 * Returns the item search key for the passed name
491
	 *
492
	 * @return string Item prefix e.g. "product.lists.type.id"
493
	 */
494
	protected function getSearchKey( string $name = '' ) : string
495
	{
496
		$subPath = $this->getSubPath();
497
		return $this->getDomain() . ( $subPath ? '.' . $subPath : '' ) . ( $name ? '.' . $name : '' );
498
	}
499
500
501
	/**
502
	 * Returns the search results for the given SQL statement.
503
	 *
504
	 * @param \Aimeos\Base\DB\Connection\Iface $conn Database connection
505
	 * @param string $sql SQL statement
506
	 * @return \Aimeos\Base\DB\Result\Iface Search result object
507
	 */
508
	protected function getSearchResults( \Aimeos\Base\DB\Connection\Iface $conn, string $sql ) : \Aimeos\Base\DB\Result\Iface
509
	{
510
		$time = microtime( true );
511
512
		$stmt = $conn->create( $sql );
513
		$result = $stmt->execute();
514
515
		$level = \Aimeos\Base\Logger\Iface::DEBUG;
516
		$time = ( microtime( true ) - $time ) * 1000;
517
		$msg = 'Time: ' . $time . "ms\n"
518
			. 'Class: ' . get_class( $this ) . "\n"
519
			. str_replace( ["\t", "\n\n"], ['', "\n"], trim( (string) $stmt ) );
520
521
		if( $time > 1000.0 )
522
		{
523
			$level = \Aimeos\Base\Logger\Iface::NOTICE;
524
			$msg .= "\n" . ( new \Exception() )->getTraceAsString();
525
		}
526
527
		$this->context()->logger()->log( $msg, $level, 'core/sql' );
528
529
		return $result;
530
	}
531
532
533
	/**
534
	 * Returns the site coditions for the search request
535
	 *
536
	 * @param string[] $keys Sorted list of criteria keys
537
	 * @param \Aimeos\Base\Criteria\Attribute\Iface[] $attributes Associative list of search keys and criteria attribute items as values
538
	 * @param int $sitelevel Site level constant from \Aimeos\MShop\Locale\Manager\Base
539
	 * @return \Aimeos\Base\Criteria\Expression\Iface[] List of search conditions
540
	 * @since 2015.01
541
	 */
542
	protected function getSiteConditions( array $keys, array $attributes, int $sitelevel ) : array
543
	{
544
		$list = [];
545
		$entries = array_column( $attributes, null, 'code' );
546
547
		foreach( $keys as $key )
548
		{
549
			$name = $key . '.siteid';
550
551
			if( isset( $entries[$name] ) ) {
552
				$list[] = $this->siteCondition( $name, $sitelevel );
553
			} elseif( isset( $entries['siteid'] ) ) {
554
				$list[] = $this->siteCondition( 'siteid', $sitelevel );
555
			}
556
		}
557
558
		return $list;
559
	}
560
561
562
	/**
563
	 * Returns the SQL statement for the given config path
564
	 *
565
	 * If available, the database specific SQL statement is returned, otherwise
566
	 * the ANSI SQL statement. The database type is determined via the resource
567
	 * adapter.
568
	 *
569
	 * @param string $sql Configuration path to the SQL statement
570
	 * @param array $replace Associative list of keys with strings to replace by their values
571
	 * @return array|string ANSI or database specific SQL statement
572
	 */
573
	protected function getSqlConfig( string $sql, array $replace = [] )
574
	{
575
		if( preg_match( '#^[a-z0-9\-]+(/[a-z0-9\-]+)*$#', $sql ) === 1 )
576
		{
577
			$config = $this->context()->config();
578
			$adapter = $config->get( 'resource/' . $this->getResourceName() . '/adapter' );
579
580
			if( ( $str = $config->get( $sql . '/' . $adapter, $config->get( $sql . '/ansi' ) ) ) === null )
581
			{
582
				$parts = explode( '/', $sql );
583
				$cpath = 'mshop/common/manager/' . end( $parts );
584
				$str = $config->get( $cpath . '/' . $adapter, $config->get( $cpath . '/ansi', $sql ) );
585
			}
586
587
			$sql = $str;
588
		}
589
590
		foreach( $replace as $key => $value ) {
591
			$sql = str_replace( $key, $value, $sql );
592
		}
593
594
		return str_replace( [':alias', ':table'], [$this->alias(), $this->getTable()], $sql );
595
	}
596
597
598
	/**
599
	 * Returns the available sub-manager names
600
	 *
601
	 * @return array Sub-manager names, e.g. ['lists', 'property', 'type']
602
	 */
603
	protected function getSubManagers() : array
604
	{
605
		return $this->context()->config()->get( $this->getConfigKey( 'submanagers' ), [] );
606
	}
607
608
609
	/**
610
	 * Returns the manager domain sub-path
611
	 *
612
	 * @return string Manager domain sub-path e.g. "lists/type"
613
	 */
614
	protected function getSubPath() : string
615
	{
616
		if( !isset( $this->subpath ) ) {
617
			$this->initDb();
618
		}
619
620
		return $this->subpath;
621
	}
622
623
624
	/**
625
	 * Sets the base criteria "status".
626
	 * (setConditions overwrites the base criteria)
627
	 *
628
	 * @param string $domain Name of the domain/sub-domain like "product" or "product.list"
629
	 * @param bool|null $default TRUE for status=1, NULL for status>0, FALSE for no restriction
630
	 * @return \Aimeos\Base\Criteria\Iface Search critery object
631
	 */
632
	protected function filterBase( string $domain, ?bool $default = false ) : \Aimeos\Base\Criteria\Iface
633
	{
634
		$context = $this->context();
635
		$db = $this->getResourceName();
636
		$conn = $context->db( $db );
637
		$config = $context->config();
638
639
		if( ( $adapter = $config->get( 'resource/' . $db . '/adapter' ) ) === null ) {
640
			$adapter = $config->get( 'resource/db/adapter' );
641
		}
642
643
		switch( $adapter )
644
		{
645
			case 'pgsql':
646
				$filter = new \Aimeos\Base\Criteria\PgSQL( $conn ); break;
647
			default:
648
				$filter = new \Aimeos\Base\Criteria\SQL( $conn ); break;
649
		}
650
651
		if( $default !== false ) {
652
			$filter->add( $domain . '.status', $default ? '==' : '>=', 1 );
653
		}
654
655
		return $filter;
656
	}
657
658
659
	/**
660
	 * Returns the item for the given search key/value pairs.
661
	 *
662
	 * @param array $pairs Search key/value pairs for the item
663
	 * @param string[] $ref List of domains whose items should be fetched too
664
	 * @param bool|null $default Add default criteria or NULL for relaxed default criteria
665
	 * @return \Aimeos\MShop\Common\Item\Iface Requested item
666
	 * @throws \Aimeos\MShop\Exception if no item with the given ID found
667
	 */
668
	protected function findBase( array $pairs, array $ref, ?bool $default ) : \Aimeos\MShop\Common\Item\Iface
669
	{
670
		$expr = [];
671
		$criteria = $this->object()->filter( $default )->slice( 0, 1 );
672
673
		foreach( $pairs as $key => $value )
674
		{
675
			if( $value === null )
676
			{
677
				$msg = $this->context()->translate( 'mshop', 'Required value for "%1$s" is missing' );
678
				throw new \Aimeos\MShop\Exception( sprintf( $msg, $key ) );
679
			}
680
			$expr[] = $criteria->compare( '==', $key, $value );
681
		}
682
683
		$criteria->setConditions( $criteria->and( $expr ) );
684
685
		if( ( $item = $this->object()->search( $criteria, $ref )->first() ) ) {
686
			return $item;
687
		}
688
689
		$msg = $this->context()->translate( 'mshop', 'No item found for conditions: %1$s' );
690
		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

690
		throw new \Aimeos\MShop\Exception( sprintf( $msg, /** @scrutinizer ignore-type */ print_r( $pairs, true ) ), 404 );
Loading history...
691
	}
692
693
694
	/**
695
	 * Returns the cached statement for the given key or creates a new prepared statement.
696
	 * If no SQL string is given, the key is used to retrieve the SQL string from the configuration.
697
	 *
698
	 * @param \Aimeos\Base\DB\Connection\Iface $conn Database connection
699
	 * @param string $cfgkey Unique key for the SQL
700
	 * @param string|null $sql SQL string if it shouldn't be retrieved from the configuration
701
	 * @return \Aimeos\Base\DB\Statement\Iface Database statement object
702
	 */
703
	protected function getCachedStatement( \Aimeos\Base\DB\Connection\Iface $conn, string $cfgkey,
704
		string $sql = null ) : \Aimeos\Base\DB\Statement\Iface
705
	{
706
		if( !isset( $this->cachedStmts['stmt'][$cfgkey] )
707
			|| !isset( $this->cachedStmts['conn'][$cfgkey] )
708
			|| $conn !== $this->cachedStmts['conn'][$cfgkey]
709
		) {
710
			if( $sql === null ) {
711
				$sql = $this->getSqlConfig( $cfgkey );
712
			}
713
714
			$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

714
			$this->cachedStmts['stmt'][$cfgkey] = $conn->create( /** @scrutinizer ignore-type */ $sql );
Loading history...
715
			$this->cachedStmts['conn'][$cfgkey] = $conn;
716
		}
717
718
		return $this->cachedStmts['stmt'][$cfgkey];
719
	}
720
721
722
	/**
723
	 * Returns the item for the given search key and ID.
724
	 *
725
	 * @param string $key Search key for the requested ID
726
	 * @param string $id Unique ID to search for
727
	 * @param string[] $ref List of domains whose items should be fetched too
728
	 * @param bool|null $default Add default criteria or NULL for relaxed default criteria
729
	 * @return \Aimeos\MShop\Common\Item\Iface Requested item
730
	 * @throws \Aimeos\MShop\Exception if no item with the given ID found
731
	 */
732
	protected function getItemBase( string $key, string $id, array $ref, ?bool $default ) : \Aimeos\MShop\Common\Item\Iface
733
	{
734
		$criteria = $this->object()->filter( $default )->add( [$key => $id] )->slice( 0, 1 );
735
736
		if( ( $item = $this->object()->search( $criteria, $ref )->first() ) ) {
737
			return $item;
738
		}
739
740
		$msg = $this->context()->translate( 'mshop', 'Item with ID "%2$s" in "%1$s" not found' );
741
		throw new \Aimeos\MShop\Exception( sprintf( $msg, $key, $id ), 404 );
742
	}
743
744
745
	/**
746
	 * Returns the SQL strings for joining dependent tables.
747
	 *
748
	 * @param \Aimeos\Base\Criteria\Attribute\Iface[] $attributes List of criteria attribute items
749
	 * @param string $prefix Search key prefix
750
	 * @return array List of JOIN SQL strings
751
	 */
752
	private function getJoins( array $attributes, string $prefix ) : array
753
	{
754
		$iface = \Aimeos\Base\Criteria\Attribute\Iface::class;
755
		$name = $prefix . '.id';
756
757
		if( isset( $attributes[$prefix] ) && $attributes[$prefix] instanceof $iface ) {
758
			return $attributes[$prefix]->getInternalDeps();
759
		} elseif( isset( $attributes[$name] ) && $attributes[$name] instanceof $iface ) {
760
			return $attributes[$name]->getInternalDeps();
761
		} elseif( isset( $attributes['id'] ) && $attributes['id'] instanceof $iface ) {
762
			return $attributes['id']->getInternalDeps();
763
		}
764
765
		return [];
766
	}
767
768
769
	/**
770
	 * Returns the required SQL joins for the critera.
771
	 *
772
	 * @param \Aimeos\Base\Criteria\Attribute\Iface[] $attributes List of criteria attribute items
773
	 * @param string $prefix Search key prefix
774
	 * @return array|null List of JOIN SQL strings
775
	 */
776
	private function getRequiredJoins( array $attributes, array $keys, string $basekey = null ) : array
777
	{
778
		$joins = [];
779
780
		foreach( $keys as $key )
781
		{
782
			if( $key !== $basekey ) {
783
				$joins = array_merge( $joins, $this->getJoins( $attributes, $key ) );
784
			}
785
		}
786
787
		return array_unique( $joins );
788
	}
789
790
791
	/**
792
	 * Returns the available manager types
793
	 *
794
	 * @param string $type Main manager type
795
	 * @param string $path Configuration path to the sub-domains
796
	 * @param string[] $default List of sub-domains if no others are configured
797
	 * @param bool $withsub Return also the resource type of sub-managers if true
798
	 * @return string[] Type of the manager and submanagers, subtypes are separated by slashes
799
	 */
800
	protected function getResourceTypeBase( string $type, string $path, array $default, bool $withsub ) : array
801
	{
802
		$list = [$type];
803
804
		if( $withsub )
805
		{
806
			foreach( $this->context()->config()->get( $path, $default ) as $domain ) {
807
				$list = array_merge( $list, $this->object()->getSubManager( $domain )->getResourceType( $withsub ) );
808
			}
809
		}
810
811
		return $list;
812
	}
813
814
815
	/**
816
	 * Returns a search object singleton
817
	 *
818
	 * @return \Aimeos\Base\Criteria\Iface Search object
819
	 */
820
	protected function getSearch() : \Aimeos\Base\Criteria\Iface
821
	{
822
		if( !isset( $this->search ) ) {
823
			$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

823
			/** @scrutinizer ignore-call */ 
824
   $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...
824
		}
825
826
		return $this->search;
827
	}
828
829
830
	/**
831
	 * Returns the string replacements for the SQL statements
832
	 *
833
	 * @param \Aimeos\Base\Criteria\Iface $search Search critera object
834
	 * @param \Aimeos\Base\Criteria\Attribute\Iface[] $attributes Associative list of search keys and criteria attribute items as values
835
	 * @param \Aimeos\Base\Criteria\Attribute\Iface[] $attributes Associative list of search keys and criteria attribute items as values for the base table
836
	 * @param \Aimeos\Base\Criteria\Plugin\Iface[] $plugins Associative list of search keys and criteria plugin items as values
837
	 * @param string[] $joins Associative list of SQL joins
838
	 * @param \Aimeos\Base\Criteria\Attribute\Iface[] $columns Additional columns to retrieve values from
839
	 * @return array Array of keys, find and replace arrays
840
	 */
841
	protected function getSQLReplacements( \Aimeos\Base\Criteria\Iface $search, array $attributes, array $attronly, array $plugins, array $joins ) : array
842
	{
843
		$types = $this->getSearchTypes( $attributes );
844
		$funcs = $this->getSearchFunctions( $attributes );
845
		$trans = $this->getSearchTranslations( $attributes );
846
		$trans = $this->aliasTranslations( $trans );
847
848
		if( empty( $search->getSortations() ) && ( $attribute = reset( $attronly ) ) !== false ) {
849
			$search = ( clone $search )->setSortations( [$search->sort( '+', $attribute->getCode() )] );
850
		}
851
		$sorts = $search->translate( $search->getSortations(), $trans, $funcs );
852
853
		$cols = $group = [];
854
		foreach( $attronly as $name => $entry )
855
		{
856
			if( str_contains( $name, ':' ) || empty( $entry->getInternalCode() ) ) {
857
				continue;
858
			}
859
860
			$icode = $entry->getInternalCode();
861
862
			if( !str_contains( $icode, '"' ) )
863
			{
864
				$alias = $this->alias( $entry->getCode() );
865
				$icode = $alias . '."' . $icode . '"';
866
			}
867
868
			$cols[] = $icode . ' AS "' . $entry->getCode() . '"';
869
			$group[] = $icode;
870
		}
871
872
		return [
873
			':columns' => join( ', ', $cols ),
874
			':joins' => join( "\n", array_unique( $joins ) ),
875
			':group' => join( ', ', array_unique( array_merge( $group, $sorts ) ) ),
876
			':cond' => $search->getConditionSource( $types, $trans, $plugins, $funcs ),
877
			':order' => $search->getSortationSource( $types, $trans, $funcs ),
878
			':start' => $search->getOffset(),
879
			':size' => $search->getLimit(),
880
		];
881
	}
882
883
884
	/**
885
	 * Returns the name of the used table
886
	 *
887
	 * @return string Table name e.g. "mshop_product_lists_type"
888
	 */
889
	protected function getTable() : string
890
	{
891
		$subPath = $this->getSubPath();
892
		return 'mshop_' . $this->getDomain() . ( $subPath ? '_' . str_replace( '/', '_', $subPath ) : '' );
893
	}
894
895
896
	/**
897
	 * Initializes the trait
898
	 */
899
	protected function initDb()
900
	{
901
		$parts = array_slice( explode( '\\', strtolower( current( $this->classes() ) ) ), 2, -1 );
0 ignored issues
show
Bug introduced by
It seems like classes() 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

901
		$parts = array_slice( explode( '\\', strtolower( current( $this->/** @scrutinizer ignore-call */ classes() ) ) ), 2, -1 );
Loading history...
902
903
		$this->domain = array_shift( $parts ) ?: '';
0 ignored issues
show
Bug Best Practice introduced by
The property domain does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
904
		array_shift( $parts ); // remove "manager"
905
		$this->subpath = join( '/', $parts );
0 ignored issues
show
Bug Best Practice introduced by
The property subpath does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
906
	}
907
908
909
	/**
910
	 * Returns the newly created ID for the last record which was inserted.
911
	 *
912
	 * @param \Aimeos\Base\DB\Connection\Iface $conn Database connection used to insert the new record
913
	 * @param string $cfgpath Configuration path to the SQL statement for retrieving the new ID of the last inserted record
914
	 * @return string ID of the last record that was inserted by using the given connection
915
	 * @throws \Aimeos\MShop\Exception if there's no ID of the last record available
916
	 */
917
	protected function newId( \Aimeos\Base\DB\Connection\Iface $conn, string $cfgpath ) : string
918
	{
919
		$sql = $this->getSqlConfig( $cfgpath );
920
921
		$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

921
		$result = $conn->create( /** @scrutinizer ignore-type */ $sql )->execute();
Loading history...
922
923
		if( ( $row = $result->fetch( \Aimeos\Base\DB\Result\Base::FETCH_NUM ) ) === false )
924
		{
925
			$msg = $this->context()->translate( 'mshop', 'ID of last inserted database record not available' );
926
			throw new \Aimeos\MShop\Exception( $msg );
927
		}
928
		$result->finish();
929
930
		return $row[0];
931
	}
932
933
934
	/**
935
	 * Returns the search result of the statement combined with the given criteria.
936
	 *
937
	 * @param \Aimeos\Base\DB\Connection\Iface $conn Database connection
938
	 * @param \Aimeos\Base\Criteria\Iface $search Search criteria object
939
	 * @param string $cfgPathSearch Path to SQL statement in configuration for searching
940
	 * @param string $cfgPathCount Path to SQL statement in configuration for counting
941
	 * @param string[] $required Additional search keys to add conditions for even if no conditions are available
942
	 * @param int|null $total Contains the number of all records matching the criteria if not null
943
	 * @param int $sitelevel Constant from \Aimeos\MShop\Locale\Manager\Base for defining which site IDs should be used for searching
944
	 * @param \Aimeos\Base\Criteria\Plugin\Iface[] $plugins Associative list of search keys and criteria plugin items as values
945
	 * @return \Aimeos\Base\DB\Result\Iface SQL result object for accessing the found records
946
	 * @throws \Aimeos\MShop\Exception if no number of all matching records is available
947
	 */
948
	protected function searchItemsBase( \Aimeos\Base\DB\Connection\Iface $conn, \Aimeos\Base\Criteria\Iface $search,
949
		string $cfgPathSearch, string $cfgPathCount, array $required, int &$total = null,
950
		int $sitelevel = \Aimeos\MShop\Locale\Manager\Base::SITE_ALL, array $plugins = [] ) : \Aimeos\Base\DB\Result\Iface
951
	{
952
		$conditions = $search->getConditions();
953
		$attributes = $this->object()->getSearchAttributes();
954
955
		$keys = $this->getCriteriaKeyList( $search, $required );
956
		$joins = $this->getRequiredJoins( $attributes, $keys, array_shift( $required ) );
957
958
		$attronly = $this->object()->getSearchAttributes( false );
959
		$cond = $this->getSiteConditions( $keys, $attributes, $sitelevel );
960
961
		if( $conditions !== null ) {
962
			$cond[] = $conditions;
963
		}
964
965
		$search = clone $search;
966
		$search->setConditions( $search->and( $cond ) );
967
968
		$replace = $this->getSQLReplacements( $search, $attributes, $attronly, $plugins, $joins );
969
970
		if( $total !== null )
971
		{
972
			$sql = $this->getSqlConfig( $cfgPathCount, $replace );
973
			$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

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

1046
			$sql = $this->addSqlColumns( array_keys( $columns ), /** @scrutinizer ignore-type */ $this->getSqlConfig( $path ) );
Loading history...
1047
		}
1048
		else
1049
		{
1050
			/** mshop/common/manager/update/mysql
1051
			 * Updates an existing record in the database
1052
			 *
1053
			 * @see mshop/common/manager/update/ansi
1054
			 */
1055
1056
			/** mshop/common/manager/update/ansi
1057
			 * Updates an existing record in the database
1058
			 *
1059
			 * Items which already have an ID (i.e. the ID is not NULL) will
1060
			 * be updated in the database.
1061
			 *
1062
			 * The SQL statement must be a string suitable for being used as
1063
			 * prepared statement. It must include question marks for binding
1064
			 * the values from the item to the statement before they are
1065
			 * sent to the database server. The order of the columns must
1066
			 * correspond to the order in the save() method, so the
1067
			 * correct values are bound to the columns.
1068
			 *
1069
			 * The SQL statement should conform to the ANSI standard to be
1070
			 * compatible with most relational database systems. This also
1071
			 * includes using double quotes for table and column names.
1072
			 *
1073
			 * @param string SQL statement for updating records
1074
			 * @since 2023.10
1075
			 * @see mshop/common/manager/insert/ansi
1076
			 * @see mshop/common/manager/newid/ansi
1077
			 * @see mshop/common/manager/delete/ansi
1078
			 * @see mshop/common/manager/search/ansi
1079
			 * @see mshop/common/manager/count/ansi
1080
			 */
1081
			$path = $this->getConfigKey( 'update' );
1082
			$sql = $this->addSqlColumns( array_keys( $columns ), $this->getSqlConfig( $path ), false );
1083
		}
1084
1085
		$idx = 1;
1086
		$values = $item->toArray( true );
1087
		$stmt = $this->getCachedStatement( $conn, $path, $sql );
1088
1089
		foreach( $columns as $entry )
1090
		{
1091
			$value = $values[$entry->getCode()] ?? null;
1092
			$value = $entry->getType() === 'json' ? json_encode( $value, JSON_FORCE_OBJECT ) : $value;
1093
			$stmt->bind( $idx++, $value, \Aimeos\Base\Criteria\SQL::type( $entry->getType() ) );
1094
		}
1095
1096
		$stmt->bind( $idx++, $context->datetime() ); // mtime
1097
		$stmt->bind( $idx++, $context->editor() );
1098
1099
		if( $id !== null ) {
1100
			$stmt->bind( $idx++, $context->locale()->getSiteId() . '%' );
1101
			$stmt->bind( $idx++, $id, \Aimeos\Base\DB\Statement\Base::PARAM_INT );
1102
		} else {
1103
			$stmt->bind( $idx++, $this->siteId( $item->getSiteId(), \Aimeos\MShop\Locale\Manager\Base::SITE_SUBTREE ) );
1104
			$stmt->bind( $idx++, $context->datetime() ); // ctime
1105
		}
1106
1107
		$stmt->execute()->finish();
1108
1109
		if( $id === null )
1110
		{
1111
			/** mshop/common/manager/newid/mysql
1112
			 * Retrieves the ID generated by the database when inserting a new record
1113
			 *
1114
			 * @see mshop/common/manager/newid/ansi
1115
			 */
1116
1117
			/** mshop/common/manager/newid/ansi
1118
			 * Retrieves the ID generated by the database when inserting a new record
1119
			 *
1120
			 * As soon as a new record is inserted into the database table,
1121
			 * the database server generates a new and unique identifier for
1122
			 * that record. This ID can be used for retrieving, updating and
1123
			 * deleting that specific record from the table again.
1124
			 *
1125
			 * For MySQL:
1126
			 *  SELECT LAST_INSERT_ID()
1127
			 * For PostgreSQL:
1128
			 *  SELECT currval('seq_matt_id')
1129
			 * For SQL Server:
1130
			 *  SELECT SCOPE_IDENTITY()
1131
			 * For Oracle:
1132
			 *  SELECT "seq_matt_id".CURRVAL FROM DUAL
1133
			 *
1134
			 * There's no way to retrive the new ID by a SQL statements that
1135
			 * fits for most database servers as they implement their own
1136
			 * specific way.
1137
			 *
1138
			 * @param string SQL statement for retrieving the last inserted record ID
1139
			 * @since 2023.10
1140
			 * @see mshop/common/manager/insert/ansi
1141
			 * @see mshop/common/manager/update/ansi
1142
			 * @see mshop/common/manager/delete/ansi
1143
			 * @see mshop/common/manager/search/ansi
1144
			 * @see mshop/common/manager/count/ansi
1145
			 */
1146
			$id = $this->newId( $conn, 'mshop/common/manager/newid' );
1147
		}
1148
1149
		return $item->setId( $id );
1150
	}
1151
1152
1153
	/**
1154
	 * Replaces the given marker with an expression
1155
	 *
1156
	 * @param string $column Name (including alias) of the column
1157
	 * @param mixed $value Value used in the expression
1158
	 * @param string $op Operator used in the expression
1159
	 * @param int $type Type constant from \Aimeos\Base\DB\Statement\Base class
1160
	 * @return string Created expression
1161
	 */
1162
	protected function toExpression( string $column, $value, string $op = '==',
1163
		int $type = \Aimeos\Base\DB\Statement\Base::PARAM_STR ) : string
1164
	{
1165
		$types = ['marker' => $type];
1166
		$translations = ['marker' => $column];
1167
		$value = ( is_array( $value ) ? array_unique( $value ) : $value );
1168
1169
		return $this->getSearch()->compare( $op, 'marker', $value )->toSource( $types, $translations );
1170
	}
1171
1172
1173
	/**
1174
	 * Transforms the application specific values to Aimeos standard values.
1175
	 *
1176
	 * @param array $values Associative list of key/value pairs from the storage
1177
	 * @return array Associative list of key/value pairs with standard Aimeos values
1178
	 */
1179
	protected function transform( array $values ) : array
1180
	{
1181
		return $values;
1182
	}
1183
1184
1185
	/**
1186
	 * Cuts the last part separated by a dot repeatedly and returns the list of resulting string.
1187
	 *
1188
	 * @param string[] $prefix Required base prefixes of the search keys
1189
	 * @param string $string String containing parts separated by dots
1190
	 * @return array List of resulting strings
1191
	 */
1192
	private function cutNameTail( array $prefix, string $string ) : array
1193
	{
1194
		$result = [];
1195
		$noprefix = true;
1196
		$strlen = strlen( $string );
1197
1198
		foreach( $prefix as $key )
1199
		{
1200
			$len = strlen( $key );
1201
1202
			if( strncmp( $string, $key, $len ) === 0 )
1203
			{
1204
				if( $strlen > $len && ( $pos = strrpos( $string, '.' ) ) !== false )
1205
				{
1206
					$result[] = $string = substr( $string, 0, $pos );
1207
					$result = array_merge( $result, $this->cutNameTail( $prefix, $string ) );
1208
					$noprefix = false;
1209
				}
1210
1211
				break;
1212
			}
1213
		}
1214
1215
		if( $noprefix )
1216
		{
1217
			if( ( $pos = strrpos( $string, ':' ) ) !== false ) {
1218
				$result[] = substr( $string, 0, $pos );
1219
				$result[] = $string;
1220
			} elseif( ( $pos = strrpos( $string, '.' ) ) !== false ) {
1221
				$result[] = substr( $string, 0, $pos );
1222
			} else {
1223
				$result[] = $string;
1224
			}
1225
		}
1226
1227
		return $result;
1228
	}
1229
1230
1231
	/**
1232
	 * Returns a list of unique criteria names shortend by the last element after the ''
1233
	 *
1234
	 * @param string[] $prefix Required base prefixes of the search keys
1235
	 * @param \Aimeos\Base\Criteria\Expression\Iface|null $expr Criteria object
1236
	 * @return array List of shortend criteria names
1237
	 */
1238
	private function getCriteriaKeys( array $prefix, \Aimeos\Base\Criteria\Expression\Iface $expr = null ) : array
1239
	{
1240
		if( $expr === null ) { return []; }
1241
1242
		$result = [];
1243
1244
		foreach( $this->getCriteriaNames( $expr ) as $item )
1245
		{
1246
			if( strncmp( $item, 'sort:', 5 ) === 0 ) {
1247
				$item = substr( $item, 5 );
1248
			}
1249
1250
			if( ( $pos = strpos( $item, '(' ) ) !== false ) {
1251
				$item = substr( $item, 0, $pos );
1252
			}
1253
1254
			$result = array_merge( $result, $this->cutNameTail( $prefix, $item ) );
1255
		}
1256
1257
		return $result;
1258
	}
1259
1260
1261
	/**
1262
	 * Returns a list of criteria names from a expression and its sub-expressions.
1263
	 *
1264
	 * @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...
1265
	 * @return array List of criteria names
1266
	 */
1267
	private function getCriteriaNames( \Aimeos\Base\Criteria\Expression\Iface $expr ) : array
1268
	{
1269
		if( $expr instanceof \Aimeos\Base\Criteria\Expression\Compare\Iface ) {
1270
			return array( $expr->getName() );
1271
		}
1272
1273
		if( $expr instanceof \Aimeos\Base\Criteria\Expression\Combine\Iface )
1274
		{
1275
			$list = [];
1276
			foreach( $expr->getExpressions() as $item ) {
1277
				$list = array_merge( $list, $this->getCriteriaNames( $item ) );
1278
			}
1279
			return $list;
1280
		}
1281
1282
		if( $expr instanceof \Aimeos\Base\Criteria\Expression\Sort\Iface ) {
1283
			return array( $expr->getName() );
1284
		}
1285
1286
		return [];
1287
	}
1288
}
1289