Passed
Push — master ( 7ebf0b...8673f8 )
by Aimeos
04:20
created

DB::domain()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 3
c 0
b 0
f 0
dl 0
loc 7
rs 10
cc 2
nc 2
nop 0
1
<?php
2
3
/**
4
 * @license LGPLv3, https://opensource.org/licenses/LGPL-3.0
5
 * @copyright Aimeos (aimeos.org), 2023
6
 * @package MShop
7
 * @subpackage Common
8
 */
9
10
11
namespace Aimeos\MShop\Common\Manager;
12
13
14
/**
15
 * Method trait for managers
16
 *
17
 * @package MShop
18
 * @subpackage Common
19
 */
20
trait DB
21
{
22
	private ?\Aimeos\Base\Criteria\Iface $search;
23
	private ?string $resourceName = null;
24
	private array $cachedStmts = [];
25
	private string $subpath;
26
	private string $domain;
27
28
29
	/**
30
	 * Returns the context object.
31
	 *
32
	 * @return \Aimeos\MShop\ContextIface Context object
33
	 */
34
	abstract protected function context() : \Aimeos\MShop\ContextIface;
35
36
37
	/**
38
	 * Creates the criteria attribute items from the list of entries
39
	 *
40
	 * @param array $list Associative array of code as key and array with properties as values
41
	 * @return \Aimeos\Base\Criteria\Attribute\Standard[] List of criteria attribute items
42
	 */
43
	abstract protected function createAttributes( array $list ) : array;
44
45
46
	/**
47
	 * Returns the attribute helper functions for searching defined by the manager.
48
	 *
49
	 * @param \Aimeos\Base\Criteria\Attribute\Iface[] $attributes List of search attribute items
50
	 * @return array Associative array of attribute code and helper function
51
	 */
52
	abstract protected function getSearchFunctions( array $attributes ) : array;
53
54
55
	/**
56
	 * Returns the attribute translations for searching defined by the manager.
57
	 *
58
	 * @param \Aimeos\Base\Criteria\Attribute\Iface[] $attributes List of search attribute items
59
	 * @return array Associative array of attribute code and internal attribute code
60
	 */
61
	abstract protected function getSearchTranslations( array $attributes ) : array;
62
63
64
	/**
65
	 * Returns the attribute types for searching defined by the manager.
66
	 *
67
	 * @param \Aimeos\Base\Criteria\Attribute\Iface[] $attributes List of search attribute items
68
	 * @return array Associative array of attribute code and internal attribute type
69
	 */
70
	abstract protected function getSearchTypes( array $attributes ) : array;
71
72
73
	/**
74
	 * Returns the outmost decorator of the decorator stack
75
	 *
76
	 * @return \Aimeos\MShop\Common\Manager\Iface Outmost decorator object
77
	 */
78
	abstract protected function object() : \Aimeos\MShop\Common\Manager\Iface;
79
80
81
	/**
82
	 * Returns the site expression for the given name
83
	 *
84
	 * @param string $name Name of the site condition
85
	 * @param int $sitelevel Site level constant from \Aimeos\MShop\Locale\Manager\Base
86
	 * @return \Aimeos\Base\Criteria\Expression\Iface Site search condition
87
	 */
88
	abstract protected function siteCondition( string $name, int $sitelevel ) : \Aimeos\Base\Criteria\Expression\Iface;
89
90
91
	/**
92
	 * Returns the site ID that should be used based on the site level
93
	 *
94
	 * @param string $siteId Site ID to check
95
	 * @param int $sitelevel Site level to check against
96
	 * @return string Site ID that should be use based on the site level
97
	 * @since 2022.04
98
	 */
99
	abstract protected function siteId( string $siteId, int $sitelevel ) : string;
100
101
102
	/**
103
	 * Adds additional column names to SQL statement
104
	 *
105
	 * @param string[] $columns List of column names
106
	 * @param string $sql Insert or update SQL statement
107
	 * @param bool $mode True for insert, false for update statement
108
	 * @return string Modified insert or update SQL statement
109
	 */
110
	protected function addSqlColumns( array $columns, string $sql, bool $mode = true ) : string
111
	{
112
		$names = $values = '';
113
114
		if( $mode )
115
		{
116
			foreach( $columns as $name ) {
117
				$names .= '"' . $name . '", '; $values .= '?, ';
118
			}
119
		}
120
		else
121
		{
122
			foreach( $columns as $name ) {
123
				$names .= '"' . $name . '" = ?, ';
124
			}
125
		}
126
127
		return str_replace( [':names', ':values'], [$names, $values], $sql );
128
	}
129
130
131
	/**
132
	 * Counts the number products that are available for the values of the given key.
133
	 *
134
	 * @param \Aimeos\Base\Criteria\Iface $search Search criteria
135
	 * @param array|string $keys Search key or list of keys for aggregation
136
	 * @param string $cfgPath Configuration key for the SQL statement
137
	 * @param string[] $required List of domain/sub-domain names like "catalog.index" that must be additionally joined
138
	 * @param string|null $value Search key for aggregating the value column
139
	 * @param string|null $type Type of aggregation, e.g.  "sum", "min", "max" or NULL for "count"
140
	 * @return \Aimeos\Map List of ID values as key and the number of counted products as value
141
	 * @todo 2018.01 Reorder Parameter list
142
	 */
143
	protected function aggregateBase( \Aimeos\Base\Criteria\Iface $search, $keys, string $cfgPath,
144
		array $required = [], string $value = null, string $type = null ) : \Aimeos\Map
145
	{
146
		/** mshop/common/manager/aggregate/limit
147
		 * Limits the number of records that are used when aggregating items
148
		 *
149
		 * As counting huge amount of records (several 10 000 records) takes a long time,
150
		 * the limit can cut down response times so the counts are available more quickly
151
		 * in the front-end and the server load is reduced.
152
		 *
153
		 * Using a low limit can lead to incorrect numbers if the amount of found items
154
		 * is very high. Approximate item counts are normally not a problem but it can
155
		 * lead to the situation that visitors see that no items are available despite
156
		 * the fact that there would be at least one.
157
		 *
158
		 * @param integer Number of records
159
		 * @since 2021.04
160
		 */
161
		$limit = $this->context()->config()->get( 'mshop/common/manager/aggregate/limit', 10000 );
162
163
		if( empty( $keys ) )
164
		{
165
			$msg = $this->context()->translate( 'mshop', 'At least one key is required for aggregation' );
166
			throw new \Aimeos\MShop\Exception( $msg );
167
		}
168
169
		$attrMap = array_column( array_filter( $this->object()->getSearchAttributes(), function( $item ) {
170
			return $item->isPublic() || strncmp( $item->getCode(), 'agg:', 4 ) === 0;
171
		} ), null, 'code' );
172
173
		if( $value === null && ( $value = key( $attrMap ) ) === null )
174
		{
175
			$msg = $this->context()->translate( 'mshop', 'No search keys available' );
176
			throw new \Aimeos\MShop\Exception( $msg );
177
		}
178
179
		if( ( $pos = strpos( $valkey = $value, '(' ) ) !== false ) {
180
			$value = substr( $value, 0, $pos ) . '()'; // remove parameters from search function
181
		}
182
183
		if( !isset( $attrMap[$value] ) )
184
		{
185
			$msg = $this->context()->translate( 'mshop', 'Unknown search key "%1$s"' );
186
			throw new \Aimeos\MShop\Exception( sprintf( $msg, $value ) );
187
		}
188
189
		$keys = (array) $keys;
190
		$acols = $cols = $expr = [];
191
		$search = (clone $search)->slice( $search->getOffset(), min( $search->getLimit(), $limit ) );
192
193
		foreach( $keys as $string )
194
		{
195
			if( ( $attrItem = $attrMap[$string] ?? null ) === null )
196
			{
197
				$msg = $this->context()->translate( 'mshop', 'Unknown search key "%1$s"' );
198
				throw new \Aimeos\MShop\Exception( sprintf( $msg, $string ) );
199
			}
200
201
			if( strpos( $attrItem->getInternalCode(), '"' ) === false ) {
202
				$prefixed = $this->alias( $attrItem->getCode() ) . '."' . $attrItem->getInternalCode() . '"';
203
			} else { // @todo: Remove in 2025.01
204
				$prefixed = $attrItem->getInternalCode();
205
			}
206
207
			$acols[] = $prefixed . ' AS "' . $string . '"';
208
			$cols[] = $prefixed;
209
210
			$expr[] = $search->compare( '!=', $string, null ); // required for the joins
211
		}
212
213
		$expr[] = $search->compare( '!=', $valkey, null );
214
		$search->add( $search->and( $expr ) );
215
216
		$val = $attrMap[$value]->getInternalCode();
217
218
		if( strpos( $val, '"' ) === false ) {
219
			$val = $this->alias( $attrMap[$value]->getCode() ) . '."' . $val . '"';
220
		}
221
222
		$sql = $this->getSqlConfig( $cfgPath );
223
		$sql = str_replace( ':cols', join( ', ', $cols ), $sql );
224
		$sql = str_replace( ':acols', join( ', ', $acols ), $sql );
225
		$sql = str_replace( ':keys', '"' . join( '", "', $keys ) . '"', $sql );
226
		$sql = str_replace( ':type', in_array( $type, ['avg', 'count', 'max', 'min', 'sum'] ) ? $type : 'count', $sql );
227
		$sql = str_replace( ':val', $val, $sql );
228
229
		return $this->aggregateResult( $search, $sql, $required );
230
	}
231
232
233
	/**
234
	 * Returns the aggregated values for the given SQL string and filter.
235
	 *
236
	 * @param \Aimeos\Base\Criteria\Iface $filter Filter object
237
	 * @param string $sql SQL statement
238
	 * @param string[] $required List of domain/sub-domain names like "catalog.index" that must be additionally joined
239
	 * @return \Aimeos\Map (Nested) list of aggregated values as key and the number of counted products as value
240
	 */
241
	protected function aggregateResult( \Aimeos\Base\Criteria\Iface $filter, string $sql, array $required ) : \Aimeos\Map
242
	{
243
		$map = [];
244
		$total = null;
245
		$level = \Aimeos\MShop\Locale\Manager\Base::SITE_ALL;
246
		$conn = $this->context()->db( $this->getResourceName() );
247
		$results = $this->searchItemsBase( $conn, $filter, $sql, '', $required, $total, $level );
248
249
		while( $row = $results->fetch() )
250
		{
251
			$row = $this->transform( $row );
252
253
			$temp = &$map;
254
			$last = array_pop( $row );
255
256
			foreach( $row as $val ) {
257
				$temp[$val] = $temp[$val] ?? [];
258
				$temp = &$temp[$val];
259
			}
260
			$temp = $last;
261
		}
262
263
		return map( $map );
264
	}
265
266
267
	/**
268
	 * Returns the table alias name.
269
	 *
270
	 * @param string|null $attrcode Search attribute code
271
	 * @return string Table alias name
272
	 */
273
	protected function alias( string $attrcode = null ) : string
274
	{
275
		if( $attrcode )
276
		{
277
			$parts = array_slice( explode( '.', $attrcode ), 0, -1 );
278
			$str = 'm' . substr( array_shift( $parts ) ?: $this->domain(), 0, 3 );
279
		}
280
		else
281
		{
282
			$parts = explode( '/', $this->subpath() );
283
			$str = 'm' . substr( $this->domain(), 0, 3 );
284
		}
285
286
		foreach( $parts as $part ) {
287
			$str .= substr( $part, 0, 2 );
288
		}
289
290
		return $str;
291
	}
292
293
294
	/**
295
	 * Adds aliases for the columns
296
	 *
297
	 * @param array $map Associative list of search keys as keys and internal column names as values
298
	 * @return array Associative list of search keys as keys and aliased column names as values
299
	 */
300
	protected function aliasTranslations( array $map ) : array
301
	{
302
		foreach( $map as $key => $value )
303
		{
304
			if( strpos( $value, '"' ) === false ) {
305
				$map[$key] = $this->alias( $key ) . '."' . $value . '"';
306
			}
307
		}
308
309
		return $map;
310
	}
311
312
313
	/**
314
	 * Binds additional values to the statement before execution.
315
	 *
316
	 * @param \Aimeos\MShop\Common\Item\Iface $item Item object
317
	 * @param \Aimeos\Base\DB\Statement\Iface $stmt Database statement object
318
	 * @param int $idx Current bind index
319
	 * @return \Aimeos\Base\DB\Statement\Iface Database statement object with bound values
320
	 */
321
	protected function bind( \Aimeos\MShop\Common\Item\Iface $item, \Aimeos\Base\DB\Statement\Iface $stmt, int &$idx ) : \Aimeos\Base\DB\Statement\Iface
322
	{
323
		return $stmt;
324
	}
325
326
327
	/**
328
	 * Removes old entries from the storage.
329
	 *
330
	 * @param iterable $siteids List of IDs for sites whose entries should be deleted
331
	 * @param string $cfgpath Configuration key to the cleanup statement
332
	 * @return \Aimeos\MShop\Common\Manager\Iface Manager object for chaining method calls
333
	 */
334
	protected function clearBase( iterable $siteids, string $cfgpath ) : \Aimeos\MShop\Common\Manager\Iface
335
	{
336
		if( empty( $siteids ) ) {
337
			return $this;
338
		}
339
340
		$conn = $this->context()->db( $this->getResourceName() );
341
342
		$sql = $this->getSqlConfig( $cfgpath );
343
		$sql = str_replace( ':cond', '1=1', $sql );
344
345
		$stmt = $conn->create( $sql );
346
347
		foreach( $siteids as $siteid )
348
		{
349
			$stmt->bind( 1, $siteid );
350
			$stmt->execute()->finish();
351
		}
352
353
		return $this;
354
	}
355
356
357
	/**
358
	 * Deletes items.
359
	 *
360
	 * @param \Aimeos\MShop\Common\Item\Iface|\Aimeos\Map|array|string $items List of item objects or IDs of the items
361
	 * @param string $cfgpath Configuration path to the SQL statement
362
	 * @param bool $siteid If siteid should be used in the statement
363
	 * @param string $name Name of the ID column
364
	 * @return \Aimeos\MShop\Common\Manager\Iface Manager object for chaining method calls
365
	 */
366
	protected function deleteItemsBase( $items, string $cfgpath, bool $siteid = true,
367
		string $name = 'id' ) : \Aimeos\MShop\Common\Manager\Iface
368
	{
369
		if( map( $items )->isEmpty() ) {
370
			return $this;
371
		}
372
373
		$search = $this->object()->filter();
374
		$search->setConditions( $search->compare( '==', $name, $items ) );
375
376
		$types = array( $name => \Aimeos\Base\DB\Statement\Base::PARAM_STR );
377
		$translations = array( $name => '"' . $name . '"' );
378
379
		$cond = $search->getConditionSource( $types, $translations );
380
		$sql = str_replace( ':cond', $cond, $this->getSqlConfig( $cfgpath ) );
381
382
		$context = $this->context();
383
		$conn = $context->db( $this->getResourceName() );
384
385
		$stmt = $conn->create( $sql );
386
387
		if( $siteid ) {
388
			$stmt->bind( 1, $context->locale()->getSiteId() . '%' );
389
		}
390
391
		$stmt->execute()->finish();
392
393
		return $this;
394
	}
395
396
397
	/**
398
	 * Fetches the rows from the database statement and returns the list of items.
399
	 *
400
	 * @param \Aimeos\Base\DB\Result\Iface $stmt Database statement object
401
	 * @param array $ref List of domains whose items should be fetched too
402
	 * @param string $prefix Prefix for the property names
403
	 * @param array $attrs List of attributes that should be decoded
404
	 * @return \Aimeos\Map List of items implementing \Aimeos\MShop\Common\Item\Iface
405
	 */
406
	protected function fetch( \Aimeos\Base\DB\Result\Iface $results, array $ref, string $prefix = '', array $attrs = [] ) : \Aimeos\Map
407
	{
408
		$map = [];
409
410
		while( $row = $results->fetch() )
411
		{
412
			foreach( $attrs as $code => $attr ) {
413
				$row[$code] = json_decode( $row[$code], true );
414
			}
415
416
			if( $item = $this->applyFilter( $this->create( $row ) ) ) {
0 ignored issues
show
Bug introduced by
The method create() does not exist on Aimeos\MShop\Common\Manager\DB. Did you maybe mean createAttributes()? ( Ignorable by Annotation )

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

416
			if( $item = $this->applyFilter( $this->/** @scrutinizer ignore-call */ create( $row ) ) ) {

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
Bug introduced by
It seems like applyFilter() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

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

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

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

491
		throw new \Aimeos\MShop\Exception( sprintf( $msg, /** @scrutinizer ignore-type */ print_r( $pairs, true ) ), 404 );
Loading history...
492
	}
493
494
495
	/**
496
	 * Returns the cached statement for the given key or creates a new prepared statement.
497
	 * If no SQL string is given, the key is used to retrieve the SQL string from the configuration.
498
	 *
499
	 * @param \Aimeos\Base\DB\Connection\Iface $conn Database connection
500
	 * @param string $cfgkey Unique key for the SQL
501
	 * @param string|null $sql SQL string if it shouldn't be retrieved from the configuration
502
	 * @return \Aimeos\Base\DB\Statement\Iface Database statement object
503
	 */
504
	protected function getCachedStatement( \Aimeos\Base\DB\Connection\Iface $conn, string $cfgkey,
505
		string $sql = null ) : \Aimeos\Base\DB\Statement\Iface
506
	{
507
		if( !isset( $this->cachedStmts['stmt'][$cfgkey] )
508
			|| !isset( $this->cachedStmts['conn'][$cfgkey] )
509
			|| $conn !== $this->cachedStmts['conn'][$cfgkey]
510
		) {
511
			if( $sql === null ) {
512
				$sql = $this->getSqlConfig( $cfgkey );
513
			}
514
515
			$this->cachedStmts['stmt'][$cfgkey] = $conn->create( $sql );
0 ignored issues
show
Bug introduced by
It seems like $sql can also be of type array; however, parameter $sql of Aimeos\Base\DB\Connection\Iface::create() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

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

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

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

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

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