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

DB::searchItemsBase()   A

Complexity

Conditions 4
Paths 6

Size

Total Lines 39
Code Lines 21

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 1
Metric Value
eloc 21
dl 0
loc 39
rs 9.584
c 1
b 0
f 1
cc 4
nc 6
nop 8

How to fix   Many Parameters   

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

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