Passed
Push — master ( be1898...f642b9 )
by Aimeos
04:39
created

DB::getSiteConditions()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 14
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 1
Metric Value
eloc 6
c 1
b 0
f 1
dl 0
loc 14
rs 10
cc 3
nc 3
nop 3
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
	 * @return array|string ANSI or database specific SQL statement
485
	 */
486
	protected function getSqlConfig( string $path )
487
	{
488
		$config = $this->context()->config();
489
		$adapter = $config->get( 'resource/' . $this->getResourceName() . '/adapter' );
490
491
		if( $sql = $config->get( $path . '/' . $adapter, $config->get( $path . '/ansi' ) ) ) {
492
			return str_replace( ':table', $this->getTable(), $sql );
493
		}
494
495
		$parts = explode( '/', $path );
496
		$cpath = 'mshop/common/manager/' . end( $parts );
497
		$sql = $config->get( $cpath . '/' . $adapter, $config->get( $cpath . '/ansi', $path ) );
498
499
		return str_replace( ':table', $this->getTable(), $sql );
500
	}
501
502
503
	/**
504
	 * Returns the available sub-manager names
505
	 *
506
	 * @return array Sub-manager names, e.g. ['lists', 'property', 'type']
507
	 */
508
	protected function getSubManagers() : array
509
	{
510
		return $this->context()->config()->get( $this->getConfigKey( 'submanagers' ), [] );
511
	}
512
513
514
	/**
515
	 * Sets the base criteria "status".
516
	 * (setConditions overwrites the base criteria)
517
	 *
518
	 * @param string $domain Name of the domain/sub-domain like "product" or "product.list"
519
	 * @param bool|null $default TRUE for status=1, NULL for status>0, FALSE for no restriction
520
	 * @return \Aimeos\Base\Criteria\Iface Search critery object
521
	 */
522
	protected function filterBase( string $domain, ?bool $default = false ) : \Aimeos\Base\Criteria\Iface
523
	{
524
		$context = $this->context();
525
		$db = $this->getResourceName();
526
		$conn = $context->db( $db );
527
		$config = $context->config();
528
529
		if( ( $adapter = $config->get( 'resource/' . $db . '/adapter' ) ) === null ) {
530
			$adapter = $config->get( 'resource/db/adapter' );
531
		}
532
533
		switch( $adapter )
534
		{
535
			case 'pgsql':
536
				$filter = new \Aimeos\Base\Criteria\PgSQL( $conn ); break;
537
			default:
538
				$filter = new \Aimeos\Base\Criteria\SQL( $conn ); break;
539
		}
540
541
		if( $default !== false ) {
542
			$filter->add( $domain . '.status', $default ? '==' : '>=', 1 );
543
		}
544
545
		return $filter;
546
	}
547
548
549
	/**
550
	 * Returns the item for the given search key/value pairs.
551
	 *
552
	 * @param array $pairs Search key/value pairs for the item
553
	 * @param string[] $ref List of domains whose items should be fetched too
554
	 * @param bool|null $default Add default criteria or NULL for relaxed default criteria
555
	 * @return \Aimeos\MShop\Common\Item\Iface Requested item
556
	 * @throws \Aimeos\MShop\Exception if no item with the given ID found
557
	 */
558
	protected function findBase( array $pairs, array $ref, ?bool $default ) : \Aimeos\MShop\Common\Item\Iface
559
	{
560
		$expr = [];
561
		$criteria = $this->object()->filter( $default )->slice( 0, 1 );
562
563
		foreach( $pairs as $key => $value )
564
		{
565
			if( $value === null )
566
			{
567
				$msg = $this->context()->translate( 'mshop', 'Required value for "%1$s" is missing' );
568
				throw new \Aimeos\MShop\Exception( sprintf( $msg, $key ) );
569
			}
570
			$expr[] = $criteria->compare( '==', $key, $value );
571
		}
572
573
		$criteria->setConditions( $criteria->and( $expr ) );
574
575
		if( ( $item = $this->object()->search( $criteria, $ref )->first() ) ) {
576
			return $item;
577
		}
578
579
		$msg = $this->context()->translate( 'mshop', 'No item found for conditions: %1$s' );
580
		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

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

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

693
			/** @scrutinizer ignore-call */ 
694
   $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...
694
		}
695
696
		return $this->search;
697
	}
698
699
700
	/**
701
	 * Returns the string replacements for the SQL statements
702
	 *
703
	 * @param \Aimeos\Base\Criteria\Iface $search Search critera object
704
	 * @param \Aimeos\Base\Criteria\Attribute\Iface[] $attributes Associative list of search keys and criteria attribute items as values
705
	 * @param \Aimeos\Base\Criteria\Plugin\Iface[] $plugins Associative list of search keys and criteria plugin items as values
706
	 * @param string[] $joins Associative list of SQL joins
707
	 * @param \Aimeos\Base\Criteria\Attribute\Iface[] $columns Additional columns to retrieve values from
708
	 * @return array Array of keys, find and replace arrays
709
	 */
710
	protected function getSQLReplacements( \Aimeos\Base\Criteria\Iface $search, array $attributes, array $plugins,
711
		array $joins, array $columns = [] ) : array
712
	{
713
		$types = $this->getSearchTypes( $attributes );
714
		$funcs = $this->getSearchFunctions( $attributes );
715
		$translations = $this->getSearchTranslations( $attributes );
716
717
		$colstring = '';
718
		foreach( $columns as $name => $entry ) {
719
			$colstring .= $entry->getInternalCode() . ', ';
720
		}
721
722
		$find = array( ':columns', ':joins', ':cond', ':start', ':size' );
723
		$replace = array(
724
			$colstring,
725
			implode( "\n", array_unique( $joins ) ),
726
			$search->getConditionSource( $types, $translations, $plugins, $funcs ),
727
			$search->getOffset(),
728
			$search->getLimit(),
729
		);
730
731
		if( empty( $search->getSortations() ) && ( $attribute = reset( $attributes ) ) !== false ) {
732
			$search = ( clone $search )->setSortations( [$search->sort( '+', $attribute->getCode() )] );
733
		}
734
735
		$find[] = ':order';
736
		$replace[] = $search->getSortationSource( $types, $translations, $funcs );
737
738
		$find[] = ':group';
739
		$replace[] = implode( ', ', $search->translate( $search->getSortations(), $translations, $funcs ) ) . ', ';
740
741
		return [$find, $replace];
742
	}
743
744
745
	/**
746
	 * Returns the newly created ID for the last record which was inserted.
747
	 *
748
	 * @param \Aimeos\Base\DB\Connection\Iface $conn Database connection used to insert the new record
749
	 * @param string $cfgpath Configuration path to the SQL statement for retrieving the new ID of the last inserted record
750
	 * @return string ID of the last record that was inserted by using the given connection
751
	 * @throws \Aimeos\MShop\Exception if there's no ID of the last record available
752
	 */
753
	protected function newId( \Aimeos\Base\DB\Connection\Iface $conn, string $cfgpath ) : string
754
	{
755
		$sql = $this->getSqlConfig( $cfgpath );
756
757
		$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

757
		$result = $conn->create( /** @scrutinizer ignore-type */ $sql )->execute();
Loading history...
758
759
		if( ( $row = $result->fetch( \Aimeos\Base\DB\Result\Base::FETCH_NUM ) ) === false )
760
		{
761
			$msg = $this->context()->translate( 'mshop', 'ID of last inserted database record not available' );
762
			throw new \Aimeos\MShop\Exception( $msg );
763
		}
764
		$result->finish();
765
766
		return $row[0];
767
	}
768
769
770
	/**
771
	 * Returns the search result of the statement combined with the given criteria.
772
	 *
773
	 * @param \Aimeos\Base\DB\Connection\Iface $conn Database connection
774
	 * @param \Aimeos\Base\Criteria\Iface $search Search criteria object
775
	 * @param string $cfgPathSearch Path to SQL statement in configuration for searching
776
	 * @param string $cfgPathCount Path to SQL statement in configuration for counting
777
	 * @param string[] $required Additional search keys to add conditions for even if no conditions are available
778
	 * @param int|null $total Contains the number of all records matching the criteria if not null
779
	 * @param int $sitelevel Constant from \Aimeos\MShop\Locale\Manager\Base for defining which site IDs should be used for searching
780
	 * @param \Aimeos\Base\Criteria\Plugin\Iface[] $plugins Associative list of search keys and criteria plugin items as values
781
	 * @return \Aimeos\Base\DB\Result\Iface SQL result object for accessing the found records
782
	 * @throws \Aimeos\MShop\Exception if no number of all matching records is available
783
	 */
784
	protected function searchItemsBase( \Aimeos\Base\DB\Connection\Iface $conn, \Aimeos\Base\Criteria\Iface $search,
785
		string $cfgPathSearch, string $cfgPathCount, array $required, int &$total = null,
786
		int $sitelevel = \Aimeos\MShop\Locale\Manager\Base::SITE_ALL, array $plugins = [] ) : \Aimeos\Base\DB\Result\Iface
787
	{
788
		$joins = [];
789
		$conditions = $search->getConditions();
790
		$columns = $this->object()->getSaveAttributes();
791
		$attributes = $this->object()->getSearchAttributes();
792
		$keys = $this->getCriteriaKeyList( $search, $required );
793
794
		$basekey = array_shift( $required );
795
796
		foreach( $keys as $key )
797
		{
798
			if( $key !== $basekey ) {
799
				$joins = array_merge( $joins, $this->getJoins( $attributes, $key ) );
800
			}
801
		}
802
803
		$joins = array_unique( $joins );
804
		$cond = $this->getSiteConditions( $keys, $attributes, $sitelevel );
805
806
		if( $conditions !== null ) {
807
			$cond[] = $conditions;
808
		}
809
810
		$search = clone $search;
811
		$search->setConditions( $search->and( $cond ) );
812
813
		list( $find, $replace ) = $this->getSQLReplacements( $search, $attributes, $plugins, $joins, $columns );
814
815
		if( $total !== null )
816
		{
817
			$sql = str_replace( $find, $replace, $this->getSqlConfig( $cfgPathCount ) );
818
			$result = $this->getSearchResults( $conn, $sql );
819
			$row = $result->fetch();
820
			$result->finish();
821
822
			if( $row === null )
823
			{
824
				$msg = $this->context()->translate( 'mshop', 'Total results value not found' );
825
				throw new \Aimeos\MShop\Exception( $msg );
826
			}
827
828
			$total = (int) $row['count'];
829
		}
830
831
		return $this->getSearchResults( $conn, str_replace( $find, $replace, $this->getSqlConfig( $cfgPathSearch ) ) );
832
	}
833
834
835
	/**
836
	 * Saves an attribute item to the storage.
837
	 *
838
	 * @param \Aimeos\MShop\Common\Item\Iface $item Item object
839
	 * @param bool $fetch True if the new ID should be returned in the item
840
	 * @return \Aimeos\MShop\Common\Item\Iface $item Updated item including the generated ID
841
	 */
842
	protected function saveBase( \Aimeos\MShop\Common\Item\Iface $item, bool $fetch = true ) : \Aimeos\MShop\Common\Item\Iface
843
	{
844
		if( !$item->isModified() ) {
845
			return $item;
846
		}
847
848
		$context = $this->context();
849
		$conn = $context->db( $this->getResourceName() );
850
851
		$id = $item->getId();
852
		$date = date( 'Y-m-d H:i:s' );
853
		$columns = $this->object()->getSaveAttributes();
854
855
		if( $id === null )
856
		{
857
			/** mshop/common/manager/insert/mysql
858
			 * Inserts a new record into the database table
859
			 *
860
			 * @see mshop/common/manager/insert/ansi
861
			 */
862
863
			/** mshop/common/manager/insert/ansi
864
			 * Inserts a new record into the database table
865
			 *
866
			 * Items with no ID yet (i.e. the ID is NULL) will be created in
867
			 * the database and the newly created ID retrieved afterwards
868
			 * using the "newid" SQL statement.
869
			 *
870
			 * The SQL statement must be a string suitable for being used as
871
			 * prepared statement. It must include question marks for binding
872
			 * the values from the item to the statement before they are
873
			 * sent to the database server. The number of question marks must
874
			 * be the same as the number of columns listed in the INSERT
875
			 * statement. The order of the columns must correspond to the
876
			 * order in the save() method, so the correct values are
877
			 * bound to the columns.
878
			 *
879
			 * The SQL statement should conform to the ANSI standard to be
880
			 * compatible with most relational database systems. This also
881
			 * includes using double quotes for table and column names.
882
			 *
883
			 * @param string SQL statement for inserting records
884
			 * @since 2023.10
885
			 * @category Developer
886
			 * @see mshop/common/manager/update/ansi
887
			 * @see mshop/common/manager/newid/ansi
888
			 * @see mshop/common/manager/delete/ansi
889
			 * @see mshop/common/manager/search/ansi
890
			 * @see mshop/common/manager/count/ansi
891
			 */
892
			$path = $this->getConfigKey( 'insert' );
893
			$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

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