Passed
Push — master ( 44c1d8...6b9f69 )
by Aimeos
16:18
created

DB::getCriteriaNames()   A

Complexity

Conditions 5
Paths 5

Size

Total Lines 20
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

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

593
		throw new \Aimeos\MShop\Exception( sprintf( $msg, /** @scrutinizer ignore-type */ print_r( $pairs, true ) ), 404 );
Loading history...
594
	}
595
596
597
	/**
598
	 * Returns the cached statement for the given key or creates a new prepared statement.
599
	 * If no SQL string is given, the key is used to retrieve the SQL string from the configuration.
600
	 *
601
	 * @param \Aimeos\Base\DB\Connection\Iface $conn Database connection
602
	 * @param string $cfgkey Unique key for the SQL
603
	 * @param string|null $sql SQL string if it shouldn't be retrieved from the configuration
604
	 * @return \Aimeos\Base\DB\Statement\Iface Database statement object
605
	 */
606
	protected function getCachedStatement( \Aimeos\Base\DB\Connection\Iface $conn, string $cfgkey,
607
		string $sql = null ) : \Aimeos\Base\DB\Statement\Iface
608
	{
609
		if( !isset( $this->cachedStmts['stmt'][$cfgkey] )
610
			|| !isset( $this->cachedStmts['conn'][$cfgkey] )
611
			|| $conn !== $this->cachedStmts['conn'][$cfgkey]
612
		) {
613
			if( $sql === null ) {
614
				$sql = $this->getSqlConfig( $cfgkey );
615
			}
616
617
			$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

617
			$this->cachedStmts['stmt'][$cfgkey] = $conn->create( /** @scrutinizer ignore-type */ $sql );
Loading history...
618
			$this->cachedStmts['conn'][$cfgkey] = $conn;
619
		}
620
621
		return $this->cachedStmts['stmt'][$cfgkey];
622
	}
623
624
625
	/**
626
	 * Returns the item for the given search key and ID.
627
	 *
628
	 * @param string $key Search key for the requested ID
629
	 * @param string $id Unique ID to search for
630
	 * @param string[] $ref List of domains whose items should be fetched too
631
	 * @param bool|null $default Add default criteria or NULL for relaxed default criteria
632
	 * @return \Aimeos\MShop\Common\Item\Iface Requested item
633
	 * @throws \Aimeos\MShop\Exception if no item with the given ID found
634
	 */
635
	protected function getItemBase( string $key, string $id, array $ref, ?bool $default ) : \Aimeos\MShop\Common\Item\Iface
636
	{
637
		$criteria = $this->object()->filter( $default )->add( [$key => $id] )->slice( 0, 1 );
638
639
		if( ( $item = $this->object()->search( $criteria, $ref )->first() ) ) {
640
			return $item;
641
		}
642
643
		$msg = $this->context()->translate( 'mshop', 'Item with ID "%2$s" in "%1$s" not found' );
644
		throw new \Aimeos\MShop\Exception( sprintf( $msg, $key, $id ), 404 );
645
	}
646
647
648
	/**
649
	 * Returns the SQL strings for joining dependent tables.
650
	 *
651
	 * @param \Aimeos\Base\Criteria\Attribute\Iface[] $attributes List of criteria attribute items
652
	 * @param string $prefix Search key prefix
653
	 * @return array List of JOIN SQL strings
654
	 */
655
	private function getJoins( array $attributes, string $prefix ) : array
656
	{
657
		$iface = \Aimeos\Base\Criteria\Attribute\Iface::class;
658
		$name = $prefix . '.id';
659
660
		if( isset( $attributes[$prefix] ) && $attributes[$prefix] instanceof $iface ) {
661
			return $attributes[$prefix]->getInternalDeps();
662
		} elseif( isset( $attributes[$name] ) && $attributes[$name] instanceof $iface ) {
663
			return $attributes[$name]->getInternalDeps();
664
		} elseif( isset( $attributes['id'] ) && $attributes['id'] instanceof $iface ) {
665
			return $attributes['id']->getInternalDeps();
666
		}
667
668
		return [];
669
	}
670
671
672
	/**
673
	 * Returns the required SQL joins for the critera.
674
	 *
675
	 * @param \Aimeos\Base\Criteria\Attribute\Iface[] $attributes List of criteria attribute items
676
	 * @param string $prefix Search key prefix
677
	 * @return array|null List of JOIN SQL strings
678
	 */
679
	private function getRequiredJoins( array $attributes, array $keys, string $basekey = null ) : array
680
	{
681
		$joins = [];
682
683
		foreach( $keys as $key )
684
		{
685
			if( $key !== $basekey ) {
686
				$joins = array_merge( $joins, $this->getJoins( $attributes, $key ) );
687
			}
688
		}
689
690
		return array_unique( $joins );
691
	}
692
693
694
	/**
695
	 * Returns the available manager types
696
	 *
697
	 * @param string $type Main manager type
698
	 * @param string $path Configuration path to the sub-domains
699
	 * @param string[] $default List of sub-domains if no others are configured
700
	 * @param bool $withsub Return also the resource type of sub-managers if true
701
	 * @return string[] Type of the manager and submanagers, subtypes are separated by slashes
702
	 */
703
	protected function getResourceTypeBase( string $type, string $path, array $default, bool $withsub ) : array
704
	{
705
		$list = [$type];
706
707
		if( $withsub )
708
		{
709
			foreach( $this->context()->config()->get( $path, $default ) as $domain ) {
710
				$list = array_merge( $list, $this->object()->getSubManager( $domain )->getResourceType( $withsub ) );
711
			}
712
		}
713
714
		return $list;
715
	}
716
717
718
	/**
719
	 * Returns a search object singleton
720
	 *
721
	 * @return \Aimeos\Base\Criteria\Iface Search object
722
	 */
723
	protected function getSearch() : \Aimeos\Base\Criteria\Iface
724
	{
725
		if( !isset( $this->search ) ) {
726
			$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

726
			/** @scrutinizer ignore-call */ 
727
   $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...
727
		}
728
729
		return $this->search;
730
	}
731
732
733
	/**
734
	 * Returns the string replacements for the SQL statements
735
	 *
736
	 * @param \Aimeos\Base\Criteria\Iface $search Search critera object
737
	 * @param \Aimeos\Base\Criteria\Attribute\Iface[] $attributes Associative list of search keys and criteria attribute items as values
738
	 * @param \Aimeos\Base\Criteria\Attribute\Iface[] $attributes Associative list of search keys and criteria attribute items as values for the base table
739
	 * @param \Aimeos\Base\Criteria\Plugin\Iface[] $plugins Associative list of search keys and criteria plugin items as values
740
	 * @param string[] $joins Associative list of SQL joins
741
	 * @param \Aimeos\Base\Criteria\Attribute\Iface[] $columns Additional columns to retrieve values from
742
	 * @return array Array of keys, find and replace arrays
743
	 */
744
	protected function getSQLReplacements( \Aimeos\Base\Criteria\Iface $search, array $attributes, array $attronly, array $plugins, array $joins ) : array
745
	{
746
		$types = $this->getSearchTypes( $attributes );
747
		$funcs = $this->getSearchFunctions( $attributes );
748
		$translations = $this->getSearchTranslations( $attributes );
749
750
		if( empty( $search->getSortations() ) && ( $attribute = reset( $attronly ) ) !== false ) {
751
			$search = ( clone $search )->setSortations( [$search->sort( '+', $attribute->getCode() )] );
752
		}
753
		$sorts = $search->translate( $search->getSortations(), $translations, $funcs );
754
755
		$cols = $group = [];
756
		foreach( $attronly as $name => $entry )
757
		{
758
			if( str_contains( $name, ':' ) || empty( $entry->getInternalCode() ) ) {
759
				continue;
760
			}
761
762
			$icode = $entry->getInternalCode();
763
764
			if( !( str_contains( $icode, '"' ) || str_contains( $icode, '.' ) ) ) {
765
				$icode = '"' . $icode . '"';
766
			}
767
768
			$cols[] = $icode . ' AS "' . $entry->getCode()  . '"';
769
			$group[] = $icode;
770
		}
771
772
		return [
773
			':columns' => join( ', ', $cols ),
774
			':joins' => join( "\n", array_unique( $joins ) ),
775
			':group' => join( ', ', array_unique( array_merge( $group, $sorts ) ) ),
776
			':cond' => $search->getConditionSource( $types, $translations, $plugins, $funcs ),
777
			':order' => $search->getSortationSource( $types, $translations, $funcs ),
778
			':start' => $search->getOffset(),
779
			':size' => $search->getLimit(),
780
		];
781
	}
782
783
784
	/**
785
	 * Returns the newly created ID for the last record which was inserted.
786
	 *
787
	 * @param \Aimeos\Base\DB\Connection\Iface $conn Database connection used to insert the new record
788
	 * @param string $cfgpath Configuration path to the SQL statement for retrieving the new ID of the last inserted record
789
	 * @return string ID of the last record that was inserted by using the given connection
790
	 * @throws \Aimeos\MShop\Exception if there's no ID of the last record available
791
	 */
792
	protected function newId( \Aimeos\Base\DB\Connection\Iface $conn, string $cfgpath ) : string
793
	{
794
		$sql = $this->getSqlConfig( $cfgpath );
795
796
		$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

796
		$result = $conn->create( /** @scrutinizer ignore-type */ $sql )->execute();
Loading history...
797
798
		if( ( $row = $result->fetch( \Aimeos\Base\DB\Result\Base::FETCH_NUM ) ) === false )
799
		{
800
			$msg = $this->context()->translate( 'mshop', 'ID of last inserted database record not available' );
801
			throw new \Aimeos\MShop\Exception( $msg );
802
		}
803
		$result->finish();
804
805
		return $row[0];
806
	}
807
808
809
	/**
810
	 * Returns the search result of the statement combined with the given criteria.
811
	 *
812
	 * @param \Aimeos\Base\DB\Connection\Iface $conn Database connection
813
	 * @param \Aimeos\Base\Criteria\Iface $search Search criteria object
814
	 * @param string $cfgPathSearch Path to SQL statement in configuration for searching
815
	 * @param string $cfgPathCount Path to SQL statement in configuration for counting
816
	 * @param string[] $required Additional search keys to add conditions for even if no conditions are available
817
	 * @param int|null $total Contains the number of all records matching the criteria if not null
818
	 * @param int $sitelevel Constant from \Aimeos\MShop\Locale\Manager\Base for defining which site IDs should be used for searching
819
	 * @param \Aimeos\Base\Criteria\Plugin\Iface[] $plugins Associative list of search keys and criteria plugin items as values
820
	 * @return \Aimeos\Base\DB\Result\Iface SQL result object for accessing the found records
821
	 * @throws \Aimeos\MShop\Exception if no number of all matching records is available
822
	 */
823
	protected function searchItemsBase( \Aimeos\Base\DB\Connection\Iface $conn, \Aimeos\Base\Criteria\Iface $search,
824
		string $cfgPathSearch, string $cfgPathCount, array $required, int &$total = null,
825
		int $sitelevel = \Aimeos\MShop\Locale\Manager\Base::SITE_ALL, array $plugins = [] ) : \Aimeos\Base\DB\Result\Iface
826
	{
827
		$conditions = $search->getConditions();
828
		$attributes = $this->object()->getSearchAttributes();
829
830
		$keys = $this->getCriteriaKeyList( $search, $required );
831
		$joins = $this->getRequiredJoins( $attributes, $keys, array_shift( $required ) );
832
833
		$attronly = $this->object()->getSearchAttributes( false );
834
		$cond = $this->getSiteConditions( $keys, $attributes, $sitelevel );
835
836
		if( $conditions !== null ) {
837
			$cond[] = $conditions;
838
		}
839
840
		$search = clone $search;
841
		$search->setConditions( $search->and( $cond ) );
842
843
		$replace = $this->getSQLReplacements( $search, $attributes, $attronly, $plugins, $joins );
844
845
		if( $total !== null )
846
		{
847
			$sql = $this->getSqlConfig( $cfgPathCount, $replace );
848
			$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

848
			$result = $this->getSearchResults( $conn, /** @scrutinizer ignore-type */ $sql );
Loading history...
849
			$row = $result->fetch();
850
			$result->finish();
851
852
			if( $row === null )
853
			{
854
				$msg = $this->context()->translate( 'mshop', 'Total results value not found' );
855
				throw new \Aimeos\MShop\Exception( $msg );
856
			}
857
858
			$total = (int) $row['count'];
859
		}
860
861
		return $this->getSearchResults( $conn, $this->getSqlConfig( $cfgPathSearch, $replace ) );
862
	}
863
864
865
	/**
866
	 * Saves an attribute item to the storage.
867
	 *
868
	 * @param \Aimeos\MShop\Common\Item\Iface $item Item object
869
	 * @param bool $fetch True if the new ID should be returned in the item
870
	 * @return \Aimeos\MShop\Common\Item\Iface $item Updated item including the generated ID
871
	 */
872
	protected function saveBase( \Aimeos\MShop\Common\Item\Iface $item, bool $fetch = true ) : \Aimeos\MShop\Common\Item\Iface
873
	{
874
		if( !$item->isModified() ) {
875
			return $item;
876
		}
877
878
		$context = $this->context();
879
		$conn = $context->db( $this->getResourceName() );
880
881
		$id = $item->getId();
882
		$date = date( 'Y-m-d H:i:s' );
883
		$columns = $this->object()->getSaveAttributes();
884
885
		if( $id === null )
886
		{
887
			/** mshop/common/manager/insert/mysql
888
			 * Inserts a new record into the database table
889
			 *
890
			 * @see mshop/common/manager/insert/ansi
891
			 */
892
893
			/** mshop/common/manager/insert/ansi
894
			 * Inserts a new record into the database table
895
			 *
896
			 * Items with no ID yet (i.e. the ID is NULL) will be created in
897
			 * the database and the newly created ID retrieved afterwards
898
			 * using the "newid" SQL statement.
899
			 *
900
			 * The SQL statement must be a string suitable for being used as
901
			 * prepared statement. It must include question marks for binding
902
			 * the values from the item to the statement before they are
903
			 * sent to the database server. The number of question marks must
904
			 * be the same as the number of columns listed in the INSERT
905
			 * statement. The order of the columns must correspond to the
906
			 * order in the save() method, so the correct values are
907
			 * bound to the columns.
908
			 *
909
			 * The SQL statement should conform to the ANSI standard to be
910
			 * compatible with most relational database systems. This also
911
			 * includes using double quotes for table and column names.
912
			 *
913
			 * @param string SQL statement for inserting records
914
			 * @since 2023.10
915
			 * @category Developer
916
			 * @see mshop/common/manager/update/ansi
917
			 * @see mshop/common/manager/newid/ansi
918
			 * @see mshop/common/manager/delete/ansi
919
			 * @see mshop/common/manager/search/ansi
920
			 * @see mshop/common/manager/count/ansi
921
			 */
922
			$path = $this->getConfigKey( 'insert' );
923
			$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

923
			$sql = $this->addSqlColumns( array_keys( $columns ), /** @scrutinizer ignore-type */ $this->getSqlConfig( $path ) );
Loading history...
924
		}
925
		else
926
		{
927
			/** mshop/common/manager/update/mysql
928
			 * Updates an existing record in the database
929
			 *
930
			 * @see mshop/common/manager/update/ansi
931
			 */
932
933
			/** mshop/common/manager/update/ansi
934
			 * Updates an existing record in the database
935
			 *
936
			 * Items which already have an ID (i.e. the ID is not NULL) will
937
			 * be updated in the database.
938
			 *
939
			 * The SQL statement must be a string suitable for being used as
940
			 * prepared statement. It must include question marks for binding
941
			 * the values from the item to the statement before they are
942
			 * sent to the database server. The order of the columns must
943
			 * correspond to the order in the save() method, so the
944
			 * correct values are bound to the columns.
945
			 *
946
			 * The SQL statement should conform to the ANSI standard to be
947
			 * compatible with most relational database systems. This also
948
			 * includes using double quotes for table and column names.
949
			 *
950
			 * @param string SQL statement for updating records
951
			 * @since 2023.10
952
			 * @category Developer
953
			 * @see mshop/common/manager/insert/ansi
954
			 * @see mshop/common/manager/newid/ansi
955
			 * @see mshop/common/manager/delete/ansi
956
			 * @see mshop/common/manager/search/ansi
957
			 * @see mshop/common/manager/count/ansi
958
			 */
959
			$path = $this->getConfigKey( 'update' );
960
			$sql = $this->addSqlColumns( array_keys( $columns ), $this->getSqlConfig( $path ), false );
961
		}
962
963
		$idx = 1;
964
		$stmt = $this->getCachedStatement( $conn, $path, $sql );
965
966
		foreach( $columns as $entry ) {
967
			$stmt->bind( $idx++, $item->get( $entry->getCode() ), \Aimeos\Base\Criteria\SQL::type( $entry->getType() ) );
968
		}
969
970
		$stmt->bind( $idx++, $date ); // mtime
971
		$stmt->bind( $idx++, $context->editor() );
972
973
		if( $id !== null ) {
974
			$stmt->bind( $idx++, $context->locale()->getSiteId() . '%' );
975
			$stmt->bind( $idx++, $id, \Aimeos\Base\DB\Statement\Base::PARAM_INT );
976
		} else {
977
			$stmt->bind( $idx++, $this->siteId( $item->getSiteId(), \Aimeos\MShop\Locale\Manager\Base::SITE_SUBTREE ) );
978
			$stmt->bind( $idx++, $date ); // ctime
979
		}
980
981
		$stmt->execute()->finish();
982
983
		if( $id === null )
984
		{
985
			/** mshop/common/manager/newid/mysql
986
			 * Retrieves the ID generated by the database when inserting a new record
987
			 *
988
			 * @see mshop/common/manager/newid/ansi
989
			 */
990
991
			/** mshop/common/manager/newid/ansi
992
			 * Retrieves the ID generated by the database when inserting a new record
993
			 *
994
			 * As soon as a new record is inserted into the database table,
995
			 * the database server generates a new and unique identifier for
996
			 * that record. This ID can be used for retrieving, updating and
997
			 * deleting that specific record from the table again.
998
			 *
999
			 * For MySQL:
1000
			 *  SELECT LAST_INSERT_ID()
1001
			 * For PostgreSQL:
1002
			 *  SELECT currval('seq_matt_id')
1003
			 * For SQL Server:
1004
			 *  SELECT SCOPE_IDENTITY()
1005
			 * For Oracle:
1006
			 *  SELECT "seq_matt_id".CURRVAL FROM DUAL
1007
			 *
1008
			 * There's no way to retrive the new ID by a SQL statements that
1009
			 * fits for most database servers as they implement their own
1010
			 * specific way.
1011
			 *
1012
			 * @param string SQL statement for retrieving the last inserted record ID
1013
			 * @since 2023.10
1014
			 * @category Developer
1015
			 * @see mshop/common/manager/insert/ansi
1016
			 * @see mshop/common/manager/update/ansi
1017
			 * @see mshop/common/manager/delete/ansi
1018
			 * @see mshop/common/manager/search/ansi
1019
			 * @see mshop/common/manager/count/ansi
1020
			 */
1021
			$id = $this->newId( $conn, 'mshop/common/manager/newid' );
1022
		}
1023
1024
		return $item->setId( $id );
1025
	}
1026
1027
1028
	/**
1029
	 * Replaces the given marker with an expression
1030
	 *
1031
	 * @param string $column Name (including alias) of the column
1032
	 * @param mixed $value Value used in the expression
1033
	 * @param string $op Operator used in the expression
1034
	 * @param int $type Type constant from \Aimeos\Base\DB\Statement\Base class
1035
	 * @return string Created expression
1036
	 */
1037
	protected function toExpression( string $column, $value, string $op = '==',
1038
		int $type = \Aimeos\Base\DB\Statement\Base::PARAM_STR ) : string
1039
	{
1040
		$types = ['marker' => $type];
1041
		$translations = ['marker' => $column];
1042
		$value = ( is_array( $value ) ? array_unique( $value ) : $value );
1043
1044
		return $this->getSearch()->compare( $op, 'marker', $value )->toSource( $types, $translations );
1045
	}
1046
1047
1048
	/**
1049
	 * Transforms the application specific values to Aimeos standard values.
1050
	 *
1051
	 * @param array $values Associative list of key/value pairs from the storage
1052
	 * @return array Associative list of key/value pairs with standard Aimeos values
1053
	 */
1054
	protected function transform( array $values ) : array
1055
	{
1056
		return $values;
1057
	}
1058
1059
1060
	/**
1061
	 * Cuts the last part separated by a dot repeatedly and returns the list of resulting string.
1062
	 *
1063
	 * @param string[] $prefix Required base prefixes of the search keys
1064
	 * @param string $string String containing parts separated by dots
1065
	 * @return array List of resulting strings
1066
	 */
1067
	private function cutNameTail( array $prefix, string $string ) : array
1068
	{
1069
		$result = [];
1070
		$noprefix = true;
1071
		$strlen = strlen( $string );
1072
1073
		foreach( $prefix as $key )
1074
		{
1075
			$len = strlen( $key );
1076
1077
			if( strncmp( $string, $key, $len ) === 0 )
1078
			{
1079
				if( $strlen > $len && ( $pos = strrpos( $string, '.' ) ) !== false )
1080
				{
1081
					$result[] = $string = substr( $string, 0, $pos );
1082
					$result = array_merge( $result, $this->cutNameTail( $prefix, $string ) );
1083
					$noprefix = false;
1084
				}
1085
1086
				break;
1087
			}
1088
		}
1089
1090
		if( $noprefix )
1091
		{
1092
			if( ( $pos = strrpos( $string, ':' ) ) !== false ) {
1093
				$result[] = substr( $string, 0, $pos );
1094
				$result[] = $string;
1095
			} elseif( ( $pos = strrpos( $string, '.' ) ) !== false ) {
1096
				$result[] = substr( $string, 0, $pos );
1097
			} else {
1098
				$result[] = $string;
1099
			}
1100
		}
1101
1102
		return $result;
1103
	}
1104
1105
1106
	/**
1107
	 * Returns a list of unique criteria names shortend by the last element after the ''
1108
	 *
1109
	 * @param string[] $prefix Required base prefixes of the search keys
1110
	 * @param \Aimeos\Base\Criteria\Expression\Iface|null $expr Criteria object
1111
	 * @return array List of shortend criteria names
1112
	 */
1113
	private function getCriteriaKeys( array $prefix, \Aimeos\Base\Criteria\Expression\Iface $expr = null ) : array
1114
	{
1115
		if( $expr === null ) { return []; }
1116
1117
		$result = [];
1118
1119
		foreach( $this->getCriteriaNames( $expr ) as $item )
1120
		{
1121
			if( strncmp( $item, 'sort:', 5 ) === 0 ) {
1122
				$item = substr( $item, 5 );
1123
			}
1124
1125
			if( ( $pos = strpos( $item, '(' ) ) !== false ) {
1126
				$item = substr( $item, 0, $pos );
1127
			}
1128
1129
			$result = array_merge( $result, $this->cutNameTail( $prefix, $item ) );
1130
		}
1131
1132
		return $result;
1133
	}
1134
1135
1136
	/**
1137
	 * Returns a list of criteria names from a expression and its sub-expressions.
1138
	 *
1139
	 * @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...
1140
	 * @return array List of criteria names
1141
	 */
1142
	private function getCriteriaNames( \Aimeos\Base\Criteria\Expression\Iface $expr ) : array
1143
	{
1144
		if( $expr instanceof \Aimeos\Base\Criteria\Expression\Compare\Iface ) {
1145
			return array( $expr->getName() );
1146
		}
1147
1148
		if( $expr instanceof \Aimeos\Base\Criteria\Expression\Combine\Iface )
1149
		{
1150
			$list = [];
1151
			foreach( $expr->getExpressions() as $item ) {
1152
				$list = array_merge( $list, $this->getCriteriaNames( $item ) );
1153
			}
1154
			return $list;
1155
		}
1156
1157
		if( $expr instanceof \Aimeos\Base\Criteria\Expression\Sort\Iface ) {
1158
			return array( $expr->getName() );
1159
		}
1160
1161
		return [];
1162
	}
1163
}
1164