Passed
Push — master ( 5a0518...247ea4 )
by Aimeos
14:19
created

DB::getJoins()   B

Complexity

Conditions 7
Paths 4

Size

Total Lines 16
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

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

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

326
			$this->setResourceName( 'db-' . $this->/** @scrutinizer ignore-call */ getDomain() );
Loading history...
327
		}
328
329
		return $this->resourceName;
330
	}
331
332
333
	/**
334
	 * Sets the name of the database resource that should be used.
335
	 *
336
	 * @param string $name Name of the resource
337
	 * @return \Aimeos\MShop\Common\Manager\Iface Manager object for chaining method calls
338
	 */
339
	protected function setResourceName( string $name ) : \Aimeos\MShop\Common\Manager\Iface
340
	{
341
		$config = $this->context()->config();
342
343
		if( $config->get( 'resource/' . $name ) === null ) {
344
			$this->resourceName = $config->get( 'resource/default', 'db' );
345
		} else {
346
			$this->resourceName = $name;
347
		}
348
349
		return $this;
350
	}
351
352
353
	/**
354
	 * Returns the search attribute objects used for searching.
355
	 *
356
	 * @param array $list Associative list of search keys and the lists of search definitions
357
	 * @param string $path Configuration path to the sub-domains for fetching the search definitions
358
	 * @param string[] $default List of sub-domains if no others are configured
359
	 * @param bool $withsub True to include search definitions of sub-domains, false if not
360
	 * @return \Aimeos\Base\Criteria\Attribute\Iface[] Associative list of search keys and criteria attribute items as values
361
	 * @since 2014.09
362
	 */
363
	protected function getSearchAttributesBase( array $list, string $path, array $default, bool $withsub ) : array
364
	{
365
		$attr = $this->createAttributes( $list );
0 ignored issues
show
Bug introduced by
It seems like createAttributes() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

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

365
		/** @scrutinizer ignore-call */ 
366
  $attr = $this->createAttributes( $list );
Loading history...
366
367
		if( $withsub === true )
368
		{
369
			$domains = $this->context()->config()->get( $path, $default );
370
371
			foreach( $domains as $domain ) {
372
				$attr += $this->object()->getSubManager( $domain )->getSearchAttributes( true );
373
			}
374
		}
375
376
		return $attr;
377
	}
378
379
380
	/**
381
	 * Returns the search results for the given SQL statement.
382
	 *
383
	 * @param \Aimeos\Base\DB\Connection\Iface $conn Database connection
384
	 * @param string $sql SQL statement
385
	 * @return \Aimeos\Base\DB\Result\Iface Search result object
386
	 */
387
	protected function getSearchResults( \Aimeos\Base\DB\Connection\Iface $conn, string $sql ) : \Aimeos\Base\DB\Result\Iface
388
	{
389
		$time = microtime( true );
390
391
		$stmt = $conn->create( $sql );
392
		$result = $stmt->execute();
393
394
		$level = \Aimeos\Base\Logger\Iface::DEBUG;
395
		$time = ( microtime( true ) - $time ) * 1000;
396
		$msg = 'Time: ' . $time . "ms\n"
397
			. 'Class: ' . get_class( $this ) . "\n"
398
			. str_replace( ["\t", "\n\n"], ['', "\n"], trim( (string) $stmt ) );
399
400
		if( $time > 1000.0 )
401
		{
402
			$level = \Aimeos\Base\Logger\Iface::NOTICE;
403
			$msg .= "\n" . ( new \Exception() )->getTraceAsString();
404
		}
405
406
		$this->context()->logger()->log( $msg, $level, 'core/sql' );
407
408
		return $result;
409
	}
410
411
412
	/**
413
	 * Returns the site coditions for the search request
414
	 *
415
	 * @param string[] $keys Sorted list of criteria keys
416
	 * @param \Aimeos\Base\Criteria\Attribute\Iface[] $attributes Associative list of search keys and criteria attribute items as values
417
	 * @param int $sitelevel Site level constant from \Aimeos\MShop\Locale\Manager\Base
418
	 * @return \Aimeos\Base\Criteria\Expression\Iface[] List of search conditions
419
	 * @since 2015.01
420
	 */
421
	protected function getSiteConditions( array $keys, array $attributes, int $sitelevel ) : array
422
	{
423
		$list = [];
424
425
		foreach( $keys as $key )
426
		{
427
			$name = $key . '.siteid';
428
429
			if( isset( $attributes[$name] ) ) {
430
				$list[] = $this->siteCondition( $name, $sitelevel );
431
			}
432
		}
433
434
		return $list;
435
	}
436
437
438
	/**
439
	 * Returns the SQL statement for the given config path
440
	 *
441
	 * If available, the database specific SQL statement is returned, otherwise
442
	 * the ANSI SQL statement. The database type is determined via the resource
443
	 * adapter.
444
	 *
445
	 * @param string $path Configuration path to the SQL statement
446
	 * @return array|string ANSI or database specific SQL statement
447
	 */
448
	protected function getSqlConfig( string $path )
449
	{
450
		$config = $this->context()->config();
451
		$adapter = $config->get( 'resource/' . $this->getResourceName() . '/adapter' );
452
453
		if( $sql = $config->get( $path . '/' . $adapter, $config->get( $path . '/ansi' ) ) ) {
454
			return str_replace( ':table', $this->getTable(), $sql );
455
		}
456
457
		$parts = explode( '/', $path );
458
		$cpath = 'mshop/common/manager/' . end( $parts );
459
		$sql = $config->get( $cpath . '/' . $adapter, $config->get( $cpath . '/ansi', $path ) );
460
461
		return str_replace( ':table', $this->getTable(), $sql );
462
	}
463
464
465
	/**
466
	 * Returns the available sub-manager names
467
	 *
468
	 * @return array Sub-manager names, e.g. ['lists', 'property', 'type']
469
	 */
470
	protected function getSubManagers() : array
471
	{
472
		return $this->context()->config()->get( $this->getConfigKey( 'submanagers' ), [] );
0 ignored issues
show
Bug introduced by
It seems like getConfigKey() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

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

472
		return $this->context()->config()->get( $this->/** @scrutinizer ignore-call */ getConfigKey( 'submanagers' ), [] );
Loading history...
473
	}
474
475
476
	/**
477
	 * Sets the base criteria "status".
478
	 * (setConditions overwrites the base criteria)
479
	 *
480
	 * @param string $domain Name of the domain/sub-domain like "product" or "product.list"
481
	 * @param bool|null $default TRUE for status=1, NULL for status>0, FALSE for no restriction
482
	 * @return \Aimeos\Base\Criteria\Iface Search critery object
483
	 */
484
	protected function filterBase( string $domain, ?bool $default = false ) : \Aimeos\Base\Criteria\Iface
485
	{
486
		$filter = self::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

486
		/** @scrutinizer ignore-call */ 
487
  $filter = self::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...
487
488
		if( $default !== false ) {
489
			$filter->add( $domain . '.status', $default ? '==' : '>=', 1 );
490
		}
491
492
		return $filter;
493
	}
494
495
496
	/**
497
	 * Returns the item for the given search key/value pairs.
498
	 *
499
	 * @param array $pairs Search key/value pairs for the item
500
	 * @param string[] $ref List of domains whose items should be fetched too
501
	 * @param bool|null $default Add default criteria or NULL for relaxed default criteria
502
	 * @return \Aimeos\MShop\Common\Item\Iface Requested item
503
	 * @throws \Aimeos\MShop\Exception if no item with the given ID found
504
	 */
505
	protected function findBase( array $pairs, array $ref, ?bool $default ) : \Aimeos\MShop\Common\Item\Iface
506
	{
507
		$expr = [];
508
		$criteria = $this->object()->filter( $default )->slice( 0, 1 );
509
510
		foreach( $pairs as $key => $value )
511
		{
512
			if( $value === null )
513
			{
514
				$msg = $this->context()->translate( 'mshop', 'Required value for "%1$s" is missing' );
515
				throw new \Aimeos\MShop\Exception( sprintf( $msg, $key ) );
516
			}
517
			$expr[] = $criteria->compare( '==', $key, $value );
518
		}
519
520
		$criteria->setConditions( $criteria->and( $expr ) );
521
522
		if( ( $item = $this->object()->search( $criteria, $ref )->first() ) ) {
523
			return $item;
524
		}
525
526
		$msg = $this->context()->translate( 'mshop', 'No item found for conditions: %1$s' );
527
		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

527
		throw new \Aimeos\MShop\Exception( sprintf( $msg, /** @scrutinizer ignore-type */ print_r( $pairs, true ) ), 404 );
Loading history...
528
	}
529
530
531
	/**
532
	 * Returns the cached statement for the given key or creates a new prepared statement.
533
	 * If no SQL string is given, the key is used to retrieve the SQL string from the configuration.
534
	 *
535
	 * @param \Aimeos\Base\DB\Connection\Iface $conn Database connection
536
	 * @param string $cfgkey Unique key for the SQL
537
	 * @param string|null $sql SQL string if it shouldn't be retrieved from the configuration
538
	 * @return \Aimeos\Base\DB\Statement\Iface Database statement object
539
	 */
540
	protected function getCachedStatement( \Aimeos\Base\DB\Connection\Iface $conn, string $cfgkey,
541
		string $sql = null ) : \Aimeos\Base\DB\Statement\Iface
542
	{
543
		if( !isset( $this->cachedStmts['stmt'][$cfgkey] )
544
			|| !isset( $this->cachedStmts['conn'][$cfgkey] )
545
			|| $conn !== $this->cachedStmts['conn'][$cfgkey]
546
		) {
547
			if( $sql === null ) {
548
				$sql = $this->getSqlConfig( $cfgkey );
549
			}
550
551
			$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

551
			$this->cachedStmts['stmt'][$cfgkey] = $conn->create( /** @scrutinizer ignore-type */ $sql );
Loading history...
552
			$this->cachedStmts['conn'][$cfgkey] = $conn;
553
		}
554
555
		return $this->cachedStmts['stmt'][$cfgkey];
556
	}
557
558
559
	/**
560
	 * Returns the item for the given search key and ID.
561
	 *
562
	 * @param string $key Search key for the requested ID
563
	 * @param string $id Unique ID to search for
564
	 * @param string[] $ref List of domains whose items should be fetched too
565
	 * @param bool|null $default Add default criteria or NULL for relaxed default criteria
566
	 * @return \Aimeos\MShop\Common\Item\Iface Requested item
567
	 * @throws \Aimeos\MShop\Exception if no item with the given ID found
568
	 */
569
	protected function getItemBase( string $key, string $id, array $ref, ?bool $default ) : \Aimeos\MShop\Common\Item\Iface
570
	{
571
		$criteria = $this->object()->filter( $default )->add( [$key => $id] )->slice( 0, 1 );
572
573
		if( ( $item = $this->object()->search( $criteria, $ref )->first() ) ) {
574
			return $item;
575
		}
576
577
		$msg = $this->context()->translate( 'mshop', 'Item with ID "%2$s" in "%1$s" not found' );
578
		throw new \Aimeos\MShop\Exception( sprintf( $msg, $key, $id ), 404 );
579
	}
580
581
582
	/**
583
	 * Returns the SQL strings for joining dependent tables.
584
	 *
585
	 * @param \Aimeos\Base\Criteria\Attribute\Iface[] $attributes List of criteria attribute items
586
	 * @param string $prefix Search key prefix
587
	 * @return array List of JOIN SQL strings
588
	 */
589
	private function getJoins( array $attributes, string $prefix ) : array
590
	{
591
		$iface = \Aimeos\Base\Criteria\Attribute\Iface::class;
592
		$name = $prefix . '.id';
593
594
		if( isset( $attributes[$prefix] ) && $attributes[$prefix] instanceof $iface ) {
595
			return $attributes[$prefix]->getInternalDeps();
596
		}
597
		elseif( isset( $attributes[$name] ) && $attributes[$name] instanceof $iface ) {
598
			return $attributes[$name]->getInternalDeps();
599
		}
600
		else if( isset( $attributes['id'] ) && $attributes['id'] instanceof $iface ) {
601
			return $attributes['id']->getInternalDeps();
602
		}
603
604
		return [];
605
	}
606
607
608
	/**
609
	 * Returns the available manager types
610
	 *
611
	 * @param string $type Main manager type
612
	 * @param string $path Configuration path to the sub-domains
613
	 * @param string[] $default List of sub-domains if no others are configured
614
	 * @param bool $withsub Return also the resource type of sub-managers if true
615
	 * @return string[] Type of the manager and submanagers, subtypes are separated by slashes
616
	 */
617
	protected function getResourceTypeBase( string $type, string $path, array $default, bool $withsub ) : array
618
	{
619
		$list = [$type];
620
621
		if( $withsub )
622
		{
623
			foreach( $this->context()->config()->get( $path, $default ) as $domain ) {
624
				$list = array_merge( $list, $this->object()->getSubManager( $domain )->getResourceType( $withsub ) );
625
			}
626
		}
627
628
		return $list;
629
	}
630
631
632
	/**
633
	 * Returns the string replacements for the SQL statements
634
	 *
635
	 * @param \Aimeos\Base\Criteria\Iface $search Search critera object
636
	 * @param \Aimeos\Base\Criteria\Attribute\Iface[] $attributes Associative list of search keys and criteria attribute items as values
637
	 * @param \Aimeos\Base\Criteria\Plugin\Iface[] $plugins Associative list of search keys and criteria plugin items as values
638
	 * @param string[] $joins Associative list of SQL joins
639
	 * @param \Aimeos\Base\Criteria\Attribute\Iface[] $columns Additional columns to retrieve values from
640
	 * @return array Array of keys, find and replace arrays
641
	 */
642
	protected function getSQLReplacements( \Aimeos\Base\Criteria\Iface $search, array $attributes, array $plugins,
643
		array $joins, array $columns = [] ) : array
644
	{
645
		$types = $this->getSearchTypes( $attributes );
646
		$funcs = $this->getSearchFunctions( $attributes );
647
		$translations = $this->getSearchTranslations( $attributes );
648
649
		$colstring = '';
650
		foreach( $columns as $name => $entry ) {
651
			$colstring .= '"' . $entry->getInternalCode() . '", ';
652
		}
653
654
		$find = array( ':columns', ':joins', ':cond', ':start', ':size' );
655
		$replace = array(
656
			$colstring,
657
			implode( "\n", array_unique( $joins ) ),
658
			$search->getConditionSource( $types, $translations, $plugins, $funcs ),
659
			$search->getOffset(),
660
			$search->getLimit(),
661
		);
662
663
		if( empty( $search->getSortations() ) && ( $attribute = reset( $attributes ) ) !== false ) {
664
			$search = ( clone $search )->setSortations( [$search->sort( '+', $attribute->getCode() )] );
665
		}
666
667
		$find[] = ':order';
668
		$replace[] = $search->getSortationSource( $types, $translations, $funcs );
669
670
		$find[] = ':group';
671
		$replace[] = implode( ', ', $search->translate( $search->getSortations(), $translations, $funcs ) ) . ', ';
672
673
		return [$find, $replace];
674
	}
675
676
677
	/**
678
	 * Returns the newly created ID for the last record which was inserted.
679
	 *
680
	 * @param \Aimeos\Base\DB\Connection\Iface $conn Database connection used to insert the new record
681
	 * @param string $cfgpath Configuration path to the SQL statement for retrieving the new ID of the last inserted record
682
	 * @return string ID of the last record that was inserted by using the given connection
683
	 * @throws \Aimeos\MShop\Exception if there's no ID of the last record available
684
	 */
685
	protected function newId( \Aimeos\Base\DB\Connection\Iface $conn, string $cfgpath ) : string
686
	{
687
		$sql = $this->getSqlConfig( $cfgpath );
688
689
		$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

689
		$result = $conn->create( /** @scrutinizer ignore-type */ $sql )->execute();
Loading history...
690
691
		if( ( $row = $result->fetch( \Aimeos\Base\DB\Result\Base::FETCH_NUM ) ) === false )
692
		{
693
			$msg = $this->context()->translate( 'mshop', 'ID of last inserted database record not available' );
694
			throw new \Aimeos\MShop\Exception( $msg );
695
		}
696
		$result->finish();
697
698
		return $row[0];
699
	}
700
701
702
	/**
703
	 * Returns the search result of the statement combined with the given criteria.
704
	 *
705
	 * @param \Aimeos\Base\DB\Connection\Iface $conn Database connection
706
	 * @param \Aimeos\Base\Criteria\Iface $search Search criteria object
707
	 * @param string $cfgPathSearch Path to SQL statement in configuration for searching
708
	 * @param string $cfgPathCount Path to SQL statement in configuration for counting
709
	 * @param string[] $required Additional search keys to add conditions for even if no conditions are available
710
	 * @param int|null $total Contains the number of all records matching the criteria if not null
711
	 * @param int $sitelevel Constant from \Aimeos\MShop\Locale\Manager\Base for defining which site IDs should be used for searching
712
	 * @param \Aimeos\Base\Criteria\Plugin\Iface[] $plugins Associative list of search keys and criteria plugin items as values
713
	 * @return \Aimeos\Base\DB\Result\Iface SQL result object for accessing the found records
714
	 * @throws \Aimeos\MShop\Exception if no number of all matching records is available
715
	 */
716
	protected function searchItemsBase( \Aimeos\Base\DB\Connection\Iface $conn, \Aimeos\Base\Criteria\Iface $search,
717
		string $cfgPathSearch, string $cfgPathCount, array $required, int &$total = null,
718
		int $sitelevel = \Aimeos\MShop\Locale\Manager\Base::SITE_ALL, array $plugins = [] ) : \Aimeos\Base\DB\Result\Iface
719
	{
720
		$joins = [];
721
		$conditions = $search->getConditions();
722
		$columns = $this->object()->getSaveAttributes();
723
		$attributes = $this->object()->getSearchAttributes();
724
		$keys = $this->getCriteriaKeyList( $search, $required );
725
726
		$basekey = array_shift( $required );
727
728
		foreach( $keys as $key )
729
		{
730
			if( $key !== $basekey ) {
731
				$joins = array_merge( $joins, $this->getJoins( $attributes, $key ) );
732
			}
733
		}
734
735
		$joins = array_unique( $joins );
736
		$cond = $this->getSiteConditions( $keys, $attributes, $sitelevel );
737
738
		if( $conditions !== null ) {
739
			$cond[] = $conditions;
740
		}
741
742
		$search = clone $search;
743
		$search->setConditions( $search->and( $cond ) );
744
745
		list( $find, $replace ) = $this->getSQLReplacements( $search, $attributes, $plugins, $joins, $columns );
746
747
		if( $total !== null )
748
		{
749
			$sql = str_replace( $find, $replace, $this->getSqlConfig( $cfgPathCount ) );
750
			$result = $this->getSearchResults( $conn, $sql );
751
			$row = $result->fetch();
752
			$result->finish();
753
754
			if( $row === null )
755
			{
756
				$msg = $this->context()->translate( 'mshop', 'Total results value not found' );
757
				throw new \Aimeos\MShop\Exception( $msg );
758
			}
759
760
			$total = (int) $row['count'];
761
		}
762
763
		return $this->getSearchResults( $conn, str_replace( $find, $replace, $this->getSqlConfig( $cfgPathSearch ) ) );
764
	}
765
766
767
	/**
768
	 * Saves an attribute item to the storage.
769
	 *
770
	 * @param \Aimeos\MShop\Common\Item\Iface $item Item object
771
	 * @param bool $fetch True if the new ID should be returned in the item
772
	 * @return \Aimeos\MShop\Common\Item\Iface $item Updated item including the generated ID
773
	 */
774
	protected function saveBase( \Aimeos\MShop\Common\Item\Iface $item, bool $fetch = true ) : \Aimeos\MShop\Common\Item\Iface
775
	{
776
		if( !$item->isModified() ) {
777
			return $item;
778
		}
779
780
		$context = $this->context();
781
		$conn = $context->db( $this->getResourceName() );
782
783
		$id = $item->getId();
784
		$date = date( 'Y-m-d H:i:s' );
785
		$columns = $this->object()->getSaveAttributes();
786
787
		if( $id === null )
788
		{
789
			/** mshop/common/manager/insert/mysql
790
			 * Inserts a new record into the database table
791
			 *
792
			 * @see mshop/common/manager/insert/ansi
793
			 */
794
795
			/** mshop/common/manager/insert/ansi
796
			 * Inserts a new record into the database table
797
			 *
798
			 * Items with no ID yet (i.e. the ID is NULL) will be created in
799
			 * the database and the newly created ID retrieved afterwards
800
			 * using the "newid" SQL statement.
801
			 *
802
			 * The SQL statement must be a string suitable for being used as
803
			 * prepared statement. It must include question marks for binding
804
			 * the values from the item to the statement before they are
805
			 * sent to the database server. The number of question marks must
806
			 * be the same as the number of columns listed in the INSERT
807
			 * statement. The order of the columns must correspond to the
808
			 * order in the save() method, so the correct values are
809
			 * bound to the columns.
810
			 *
811
			 * The SQL statement should conform to the ANSI standard to be
812
			 * compatible with most relational database systems. This also
813
			 * includes using double quotes for table and column names.
814
			 *
815
			 * @param string SQL statement for inserting records
816
			 * @since 2023.10
817
			 * @category Developer
818
			 * @see mshop/common/manager/update/ansi
819
			 * @see mshop/common/manager/newid/ansi
820
			 * @see mshop/common/manager/delete/ansi
821
			 * @see mshop/common/manager/search/ansi
822
			 * @see mshop/common/manager/count/ansi
823
			 */
824
			$path = $this->getConfigKey( 'insert' );
825
			$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

825
			$sql = $this->addSqlColumns( array_keys( $columns ), /** @scrutinizer ignore-type */ $this->getSqlConfig( $path ) );
Loading history...
826
		}
827
		else
828
		{
829
			/** mshop/common/manager/update/mysql
830
			 * Updates an existing record in the database
831
			 *
832
			 * @see mshop/common/manager/update/ansi
833
			 */
834
835
			/** mshop/common/manager/update/ansi
836
			 * Updates an existing record in the database
837
			 *
838
			 * Items which already have an ID (i.e. the ID is not NULL) will
839
			 * be updated in the database.
840
			 *
841
			 * The SQL statement must be a string suitable for being used as
842
			 * prepared statement. It must include question marks for binding
843
			 * the values from the item to the statement before they are
844
			 * sent to the database server. The order of the columns must
845
			 * correspond to the order in the save() method, so the
846
			 * correct values are bound to the columns.
847
			 *
848
			 * The SQL statement should conform to the ANSI standard to be
849
			 * compatible with most relational database systems. This also
850
			 * includes using double quotes for table and column names.
851
			 *
852
			 * @param string SQL statement for updating records
853
			 * @since 2023.10
854
			 * @category Developer
855
			 * @see mshop/common/manager/insert/ansi
856
			 * @see mshop/common/manager/newid/ansi
857
			 * @see mshop/common/manager/delete/ansi
858
			 * @see mshop/common/manager/search/ansi
859
			 * @see mshop/common/manager/count/ansi
860
			 */
861
			$path = $this->getConfigKey( 'update' );
862
			$sql = $this->addSqlColumns( array_keys( $columns ), $this->getSqlConfig( $path ), false );
863
		}
864
865
		$idx = 1;
866
		$stmt = $this->getCachedStatement( $conn, $path, $sql );
867
868
		foreach( $columns as $entry ) {
869
			$stmt->bind( $idx++, $item->get( $entry->getCode() ), \Aimeos\Base\Criteria\SQL::type( $entry->getType() ) );
870
		}
871
872
		$stmt->bind( $idx++, $date ); // mtime
873
		$stmt->bind( $idx++, $context->editor() );
874
875
		if( $id !== null ) {
876
			$stmt->bind( $idx++, $context->locale()->getSiteId() . '%' );
877
			$stmt->bind( $idx++, $id, \Aimeos\Base\DB\Statement\Base::PARAM_INT );
878
		} else {
879
			$stmt->bind( $idx++, $this->siteId( $item->getSiteId(), \Aimeos\MShop\Locale\Manager\Base::SITE_SUBTREE ) );
0 ignored issues
show
Bug introduced by
It seems like siteId() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

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

879
			$stmt->bind( $idx++, $this->/** @scrutinizer ignore-call */ siteId( $item->getSiteId(), \Aimeos\MShop\Locale\Manager\Base::SITE_SUBTREE ) );
Loading history...
880
			$stmt->bind( $idx++, $date ); // ctime
881
		}
882
883
		$stmt->execute()->finish();
884
885
		if( $id === null )
886
		{
887
			/** mshop/common/manager/newid/mysql
888
			 * Retrieves the ID generated by the database when inserting a new record
889
			 *
890
			 * @see mshop/common/manager/newid/ansi
891
			 */
892
893
			/** mshop/common/manager/newid/ansi
894
			 * Retrieves the ID generated by the database when inserting a new record
895
			 *
896
			 * As soon as a new record is inserted into the database table,
897
			 * the database server generates a new and unique identifier for
898
			 * that record. This ID can be used for retrieving, updating and
899
			 * deleting that specific record from the table again.
900
			 *
901
			 * For MySQL:
902
			 *  SELECT LAST_INSERT_ID()
903
			 * For PostgreSQL:
904
			 *  SELECT currval('seq_matt_id')
905
			 * For SQL Server:
906
			 *  SELECT SCOPE_IDENTITY()
907
			 * For Oracle:
908
			 *  SELECT "seq_matt_id".CURRVAL FROM DUAL
909
			 *
910
			 * There's no way to retrive the new ID by a SQL statements that
911
			 * fits for most database servers as they implement their own
912
			 * specific way.
913
			 *
914
			 * @param string SQL statement for retrieving the last inserted record ID
915
			 * @since 2023.10
916
			 * @category Developer
917
			 * @see mshop/common/manager/insert/ansi
918
			 * @see mshop/common/manager/update/ansi
919
			 * @see mshop/common/manager/delete/ansi
920
			 * @see mshop/common/manager/search/ansi
921
			 * @see mshop/common/manager/count/ansi
922
			 */
923
			$id = $this->newId( $conn, 'mshop/common/manager/newid' );
924
		}
925
926
		return $item->setId( $id );
927
	}
928
929
930
	/**
931
	 * Replaces the given marker with an expression
932
	 *
933
	 * @param string $column Name (including alias) of the column
934
	 * @param mixed $value Value used in the expression
935
	 * @param string $op Operator used in the expression
936
	 * @param int $type Type constant from \Aimeos\Base\DB\Statement\Base class
937
	 * @return string Created expression
938
	 */
939
	protected function toExpression( string $column, $value, string $op = '==',
940
		int $type = \Aimeos\Base\DB\Statement\Base::PARAM_STR ) : string
941
	{
942
		$types = ['marker' => $type];
943
		$translations = ['marker' => $column];
944
		$value = ( is_array( $value ) ? array_unique( $value ) : $value );
945
946
		return $this->getSearch()->compare( $op, 'marker', $value )->toSource( $types, $translations );
0 ignored issues
show
Bug introduced by
The method getSearch() does not exist on Aimeos\MShop\Common\Manager\DB. Did you maybe mean getSearchResults()? ( Ignorable by Annotation )

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

946
		return $this->/** @scrutinizer ignore-call */ getSearch()->compare( $op, 'marker', $value )->toSource( $types, $translations );

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...
947
	}
948
949
950
	/**
951
	 * Transforms the application specific values to Aimeos standard values.
952
	 *
953
	 * @param array $values Associative list of key/value pairs from the storage
954
	 * @return array Associative list of key/value pairs with standard Aimeos values
955
	 */
956
	protected function transform( array $values ) : array
957
	{
958
		return $values;
959
	}
960
961
962
	/**
963
	 * Cuts the last part separated by a dot repeatedly and returns the list of resulting string.
964
	 *
965
	 * @param string[] $prefix Required base prefixes of the search keys
966
	 * @param string $string String containing parts separated by dots
967
	 * @return array List of resulting strings
968
	 */
969
	private function cutNameTail( array $prefix, string $string ) : array
970
	{
971
		$result = [];
972
		$noprefix = true;
973
		$strlen = strlen( $string );
974
975
		foreach( $prefix as $key )
976
		{
977
			$len = strlen( $key );
978
979
			if( strncmp( $string, $key, $len ) === 0 )
980
			{
981
				if( $strlen > $len && ( $pos = strrpos( $string, '.' ) ) !== false )
982
				{
983
					$result[] = $string = substr( $string, 0, $pos );
984
					$result = array_merge( $result, $this->cutNameTail( $prefix, $string ) );
985
					$noprefix = false;
986
				}
987
988
				break;
989
			}
990
		}
991
992
		if( $noprefix )
993
		{
994
			if( ( $pos = strrpos( $string, ':' ) ) !== false ) {
995
				$result[] = substr( $string, 0, $pos );
996
				$result[] = $string;
997
			} elseif( ( $pos = strrpos( $string, '.' ) ) !== false ) {
998
				$result[] = substr( $string, 0, $pos );
999
			} else {
1000
				$result[] = $string;
1001
			}
1002
		}
1003
1004
		return $result;
1005
	}
1006
1007
1008
	/**
1009
	 * Returns a list of unique criteria names shortend by the last element after the ''
1010
	 *
1011
	 * @param string[] $prefix Required base prefixes of the search keys
1012
	 * @param \Aimeos\Base\Criteria\Expression\Iface|null $expr Criteria object
1013
	 * @return array List of shortend criteria names
1014
	 */
1015
	private function getCriteriaKeys( array $prefix, \Aimeos\Base\Criteria\Expression\Iface $expr = null ) : array
1016
	{
1017
		if( $expr === null ) { return []; }
1018
1019
		$result = [];
1020
1021
		foreach( $this->getCriteriaNames( $expr ) as $item )
1022
		{
1023
			if( strncmp( $item, 'sort:', 5 ) === 0 ) {
1024
				$item = substr( $item, 5 );
1025
			}
1026
1027
			if( ( $pos = strpos( $item, '(' ) ) !== false ) {
1028
				$item = substr( $item, 0, $pos );
1029
			}
1030
1031
			$result = array_merge( $result, $this->cutNameTail( $prefix, $item ) );
1032
		}
1033
1034
		return $result;
1035
	}
1036
1037
1038
	/**
1039
	 * Returns a list of criteria names from a expression and its sub-expressions.
1040
	 *
1041
	 * @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...
1042
	 * @return array List of criteria names
1043
	 */
1044
	private function getCriteriaNames( \Aimeos\Base\Criteria\Expression\Iface $expr ) : array
1045
	{
1046
		if( $expr instanceof \Aimeos\Base\Criteria\Expression\Compare\Iface ) {
1047
			return array( $expr->getName() );
1048
		}
1049
1050
		if( $expr instanceof \Aimeos\Base\Criteria\Expression\Combine\Iface )
1051
		{
1052
			$list = [];
1053
			foreach( $expr->getExpressions() as $item ) {
1054
				$list = array_merge( $list, $this->getCriteriaNames( $item ) );
1055
			}
1056
			return $list;
1057
		}
1058
1059
		if( $expr instanceof \Aimeos\Base\Criteria\Expression\Sort\Iface ) {
1060
			return array( $expr->getName() );
1061
		}
1062
1063
		return [];
1064
	}
1065
}
1066