Passed
Push — master ( 04e202...9b7189 )
by Aimeos
05:11
created

Base::toExpression()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 4
nc 2
nop 4
dl 0
loc 8
rs 10
c 0
b 0
f 0
1
<?php
2
3
/**
4
 * @license LGPLv3, https://opensource.org/licenses/LGPL-3.0
5
 * @copyright Aimeos (aimeos.org), 2015-2023
6
 * @package MShop
7
 * @subpackage Common
8
 */
9
10
11
namespace Aimeos\MShop\Common\Manager;
12
13
14
/**
15
 * Provides common methods required by most of the manager classes.
16
 *
17
 * @package MShop
18
 * @subpackage Common
19
 */
20
abstract class Base implements \Aimeos\Macro\Iface
21
{
22
	use \Aimeos\Macro\Macroable;
23
	use Sub\Traits;
24
	use Methods;
25
	use Site;
26
27
28
	private $context;
29
	private $stmts = [];
30
	private $search;
31
32
33
	/**
34
	 * Initialization of class.
35
	 *
36
	 * @param \Aimeos\MShop\ContextIface $context Context object
37
	 */
38
	public function __construct( \Aimeos\MShop\ContextIface $context )
39
	{
40
		$this->context = $context;
41
	}
42
43
44
	/**
45
	 * Adds additional column names to SQL statement
46
	 *
47
	 * @param string[] $columns List of column names
48
	 * @param string $sql Insert or update SQL statement
49
	 * @param bool $mode True for insert, false for update statement
50
	 * @return string Modified insert or update SQL statement
51
	 */
52
	protected function addSqlColumns( array $columns, string $sql, bool $mode = true ) : string
53
	{
54
		$names = $values = '';
55
56
		if( $mode )
57
		{
58
			foreach( $columns as $name ) {
59
				$names .= '"' . $name . '", '; $values .= '?, ';
60
			}
61
		}
62
		else
63
		{
64
			foreach( $columns as $name ) {
65
				$names .= '"' . $name . '" = ?, ';
66
			}
67
		}
68
69
		return str_replace( [':names', ':values'], [$names, $values], $sql );
70
	}
71
72
73
	/**
74
	 * Counts the number products that are available for the values of the given key.
75
	 *
76
	 * @param \Aimeos\Base\Criteria\Iface $search Search criteria
77
	 * @param array|string $keys Search key or list of keys for aggregation
78
	 * @param string $cfgPath Configuration key for the SQL statement
79
	 * @param string[] $required List of domain/sub-domain names like "catalog.index" that must be additionally joined
80
	 * @param string|null $value Search key for aggregating the value column
81
	 * @param string|null $type Type of aggregation, e.g.  "sum", "min", "max" or NULL for "count"
82
	 * @return \Aimeos\Map List of ID values as key and the number of counted products as value
83
	 * @todo 2018.01 Reorder Parameter list
84
	 */
85
	protected function aggregateBase( \Aimeos\Base\Criteria\Iface $search, $keys, string $cfgPath,
86
		array $required = [], string $value = null, string $type = null ) : \Aimeos\Map
87
	{
88
		/** mshop/common/manager/aggregate/limit
89
		 * Limits the number of records that are used when aggregating items
90
		 *
91
		 * As counting huge amount of records (several 10 000 records) takes a long time,
92
		 * the limit can cut down response times so the counts are available more quickly
93
		 * in the front-end and the server load is reduced.
94
		 *
95
		 * Using a low limit can lead to incorrect numbers if the amount of found items
96
		 * is very high. Approximate item counts are normally not a problem but it can
97
		 * lead to the situation that visitors see that no items are available despite
98
		 * the fact that there would be at least one.
99
		 *
100
		 * @param integer Number of records
101
		 * @since 2021.04
102
		 */
103
		$limit = $this->context->config()->get( 'mshop/common/manager/aggregate/limit', 10000 );
104
		$keys = (array) $keys;
105
106
		if( !count( $keys ) )
107
		{
108
			$msg = $this->context()->translate( 'mshop', 'At least one key is required for aggregation' );
109
			throw new \Aimeos\MShop\Exception( $msg );
110
		}
111
112
		$conn = $this->context->db( $this->getResourceName() );
113
114
		$total = null;
115
		$cols = $map = [];
116
		$search = clone $search;
117
		$search->slice( $search->getOffset(), min( $search->getLimit(), $limit ) );
118
119
		$level = \Aimeos\MShop\Locale\Manager\Base::SITE_ALL;
120
		$attrList = $this->object()->getSearchAttributes();
121
122
		if( $value === null && ( $value = key( $attrList ) ) === null )
123
		{
124
			$msg = $this->context()->translate( 'mshop', 'No search keys available' );
125
			throw new \Aimeos\MShop\Exception( $msg );
126
		}
127
128
		if( ( $pos = strpos( $valkey = $value, '(' ) ) !== false ) {
129
			$value = substr( $value, 0, $pos );
130
		}
131
132
		if( !isset( $attrList[$value] ) )
133
		{
134
			$msg = $this->context()->translate( 'mshop', 'Unknown search key "%1$s"' );
135
			throw new \Aimeos\MShop\Exception( sprintf( $msg, $value ) );
136
		}
137
138
		foreach( $keys as $string )
139
		{
140
			if( !isset( $attrList[$string] ) )
141
			{
142
				$msg = $this->context()->translate( 'mshop', 'Unknown search key "%1$s"' );
143
				throw new \Aimeos\MShop\Exception( sprintf( $msg, $string ) );
144
			}
145
146
			$cols[] = $attrList[$string]->getInternalCode();
147
			$acols[] = $attrList[$string]->getInternalCode() . ' AS "' . $string . '"';
0 ignored issues
show
Bug introduced by
Are you sure $attrList[$string]->getInternalCode() of type array|string can be used in concatenation? ( Ignorable by Annotation )

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

147
			$acols[] = /** @scrutinizer ignore-type */ $attrList[$string]->getInternalCode() . ' AS "' . $string . '"';
Loading history...
148
149
			/** @todo Required to get the joins, but there should be a better way */
150
			$search->add( [$string => null], '!=' );
151
		}
152
		$search->add( [$valkey => null], '!=' );
153
154
		$sql = $this->getSqlConfig( $cfgPath );
155
		$sql = str_replace( ':cols', join( ', ', $cols ), $sql );
156
		$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 138. Are you sure the iterator is never empty, otherwise this variable is not defined?
Loading history...
157
		$sql = str_replace( ':keys', '"' . join( '", "', $keys ) . '"', $sql );
158
		$sql = str_replace( ':val', $attrList[$value]->getInternalCode(), $sql );
159
		$sql = str_replace( ':type', in_array( $type, ['avg', 'count', 'max', 'min', 'sum'] ) ? $type : 'count', $sql );
160
161
		$results = $this->searchItemsBase( $conn, $search, $sql, '', $required, $total, $level );
162
163
		while( ( $row = $results->fetch() ) !== null )
164
		{
165
			$row = $this->transform( $row );
166
167
			$temp = &$map;
168
			$last = array_pop( $row );
169
170
			foreach( $row as $val ) {
171
				$temp[$val] = $temp[$val] ?? [];
172
				$temp = &$temp[$val];
173
			}
174
			$temp = $last;
175
		}
176
177
		return map( $map );
178
	}
179
180
181
	/**
182
	 * Removes old entries from the storage.
183
	 *
184
	 * @param iterable $siteids List of IDs for sites whose entries should be deleted
185
	 * @param string $cfgpath Configuration key to the cleanup statement
186
	 * @return \Aimeos\MShop\Common\Manager\Iface Manager object for chaining method calls
187
	 */
188
	protected function clearBase( iterable $siteids, string $cfgpath ) : \Aimeos\MShop\Common\Manager\Iface
189
	{
190
		if( empty( $siteids ) ) {
191
			return $this;
192
		}
193
194
		$conn = $this->context->db( $this->getResourceName() );
195
196
		$sql = $this->getSqlConfig( $cfgpath );
197
		$sql = str_replace( ':cond', '1=1', $sql );
198
199
		$stmt = $conn->create( $sql );
200
201
		foreach( $siteids as $siteid )
202
		{
203
			$stmt->bind( 1, $siteid );
204
			$stmt->execute()->finish();
205
		}
206
207
		return $this;
208
	}
209
210
211
	/**
212
	 * Returns the context object.
213
	 *
214
	 * @return \Aimeos\MShop\ContextIface Context object
215
	 */
216
	protected function context() : \Aimeos\MShop\ContextIface
217
	{
218
		return $this->context;
219
	}
220
221
222
	/**
223
	 * Creates the criteria attribute items from the list of entries
224
	 *
225
	 * @param array $list Associative array of code as key and array with properties as values
226
	 * @return \Aimeos\Base\Criteria\Attribute\Standard[] List of criteria attribute items
227
	 */
228
	protected function createAttributes( array $list ) : array
229
	{
230
		$attr = [];
231
232
		foreach( $list as $key => $fields ) {
233
			$attr[$key] = new \Aimeos\Base\Criteria\Attribute\Standard( $fields );
234
		}
235
236
		return $attr;
237
	}
238
239
240
	/**
241
	 * Deletes items.
242
	 *
243
	 * @param \Aimeos\MShop\Common\Item\Iface|\Aimeos\Map|array|string $items List of item objects or IDs of the items
244
	 * @param string $cfgpath Configuration path to the SQL statement
245
	 * @param bool $siteid If siteid should be used in the statement
246
	 * @param string $name Name of the ID column
247
	 * @return \Aimeos\MShop\Common\Manager\Iface Manager object for chaining method calls
248
	 */
249
	protected function deleteItemsBase( $items, string $cfgpath, bool $siteid = true,
250
		string $name = 'id' ) : \Aimeos\MShop\Common\Manager\Iface
251
	{
252
		if( map( $items )->isEmpty() ) {
253
			return $this;
254
		}
255
256
		$search = $this->object()->filter();
257
		$search->setConditions( $search->compare( '==', $name, $items ) );
258
259
		$types = array( $name => \Aimeos\Base\DB\Statement\Base::PARAM_STR );
260
		$translations = array( $name => '"' . $name . '"' );
261
262
		$cond = $search->getConditionSource( $types, $translations );
263
		$sql = str_replace( ':cond', $cond, $this->getSqlConfig( $cfgpath ) );
264
265
		$context = $this->context();
266
		$conn = $context->db( $this->getResourceName() );
267
268
		$stmt = $conn->create( $sql );
269
270
		if( $siteid ) {
271
			$stmt->bind( 1, $context->locale()->getSiteId() . '%' );
272
		}
273
274
		$stmt->execute()->finish();
275
276
		return $this;
277
	}
278
279
280
	/**
281
	 * Returns a sorted list of required criteria keys.
282
	 *
283
	 * @param \Aimeos\Base\Criteria\Iface $criteria Search criteria object
284
	 * @param string[] $required List of prefixes of required search conditions
285
	 * @return string[] Sorted list of criteria keys
286
	 */
287
	protected function getCriteriaKeyList( \Aimeos\Base\Criteria\Iface $criteria, array $required ) : array
288
	{
289
		$keys = array_merge( $required, $this->getCriteriaKeys( $required, $criteria->getConditions() ) );
290
291
		foreach( $criteria->getSortations() as $sortation ) {
292
			$keys = array_merge( $keys, $this->getCriteriaKeys( $required, $sortation ) );
293
		}
294
295
		$keys = array_unique( array_merge( $required, $keys ) );
296
		sort( $keys );
297
298
		return $keys;
299
	}
300
301
302
	/**
303
	 * Returns the search attribute objects used for searching.
304
	 *
305
	 * @param array $list Associative list of search keys and the lists of search definitions
306
	 * @param string $path Configuration path to the sub-domains for fetching the search definitions
307
	 * @param string[] $default List of sub-domains if no others are configured
308
	 * @param bool $withsub True to include search definitions of sub-domains, false if not
309
	 * @return \Aimeos\Base\Criteria\Attribute\Iface[] Associative list of search keys and criteria attribute items as values
310
	 * @since 2014.09
311
	 */
312
	protected function getSearchAttributesBase( array $list, string $path, array $default, bool $withsub ) : array
313
	{
314
		$attr = $this->createAttributes( $list );
315
316
		if( $withsub === true )
317
		{
318
			$domains = $this->context->config()->get( $path, $default );
319
320
			foreach( $domains as $domain ) {
321
				$attr += $this->object()->getSubManager( $domain )->getSearchAttributes( true );
322
			}
323
		}
324
325
		return $attr;
326
	}
327
328
329
	/**
330
	 * Returns the attribute helper functions for searching defined by the manager.
331
	 *
332
	 * @param \Aimeos\Base\Criteria\Attribute\Iface[] $attributes List of search attribute items
333
	 * @return array Associative array of attribute code and helper function
334
	 */
335
	protected function getSearchFunctions( array $attributes ) : array
336
	{
337
		$list = [];
338
		$iface = \Aimeos\Base\Criteria\Attribute\Iface::class;
339
340
		foreach( $attributes as $key => $item )
341
		{
342
			if( $item instanceof $iface ) {
343
				$list[$item->getCode()] = $item->getFunction();
344
			} else if( isset( $item['code'] ) ) {
345
				$list[$item['code']] = $item['function'] ?? null;
346
			} else {
347
				throw new \Aimeos\MShop\Exception( sprintf( 'Invalid attribute at position "%1$d"', $key ) );
348
			}
349
		}
350
351
		return $list;
352
	}
353
354
355
	/**
356
	 * Returns the search results for the given SQL statement.
357
	 *
358
	 * @param \Aimeos\Base\DB\Connection\Iface $conn Database connection
359
	 * @param string $sql SQL statement
360
	 * @return \Aimeos\Base\DB\Result\Iface Search result object
361
	 */
362
	protected function getSearchResults( \Aimeos\Base\DB\Connection\Iface $conn, string $sql ) : \Aimeos\Base\DB\Result\Iface
363
	{
364
		$time = microtime( true );
365
366
		$stmt = $conn->create( $sql );
367
		$result = $stmt->execute();
368
369
		$level = \Aimeos\Base\Logger\Iface::DEBUG;
370
		$time = ( microtime( true ) - $time ) * 1000;
371
		$msg = 'Time: ' . $time . "ms\n"
372
			. 'Class: ' . get_class( $this ) . "\n"
373
			. str_replace( ["\t", "\n\n"], ['', "\n"], trim( (string) $stmt ) );
374
375
		if( $time > 1000.0 )
376
		{
377
			$level = \Aimeos\Base\Logger\Iface::NOTICE;
378
			$msg .= "\n" . ( new \Exception() )->getTraceAsString();
379
		}
380
381
		$this->context->logger()->log( $msg, $level, 'core/sql' );
382
383
		return $result;
384
	}
385
386
387
	/**
388
	 * Returns the attribute translations for searching defined by the manager.
389
	 *
390
	 * @param \Aimeos\Base\Criteria\Attribute\Iface[] $attributes List of search attribute items
391
	 * @return array Associative array of attribute code and internal attribute code
392
	 */
393
	protected function getSearchTranslations( array $attributes ) : array
394
	{
395
		$translations = [];
396
		$iface = \Aimeos\Base\Criteria\Attribute\Iface::class;
397
398
		foreach( $attributes as $key => $item )
399
		{
400
			if( $item instanceof $iface ) {
401
				$translations[$item->getCode()] = $item->getInternalCode();
402
			} else if( isset( $item['code'] ) ) {
403
				$translations[$item['code']] = $item['internalcode'];
404
			} else {
405
				throw new \Aimeos\MShop\Exception( sprintf( 'Invalid attribute at position "%1$d"', $key ) );
406
			}
407
		}
408
409
		return $translations;
410
	}
411
412
413
	/**
414
	 * Returns the attribute types for searching defined by the manager.
415
	 *
416
	 * @param \Aimeos\Base\Criteria\Attribute\Iface[] $attributes List of search attribute items
417
	 * @return array Associative array of attribute code and internal attribute type
418
	 */
419
	protected function getSearchTypes( array $attributes ) : array
420
	{
421
		$types = [];
422
		$iface = \Aimeos\Base\Criteria\Attribute\Iface::class;
423
424
		foreach( $attributes as $key => $item )
425
		{
426
			if( $item instanceof $iface ) {
427
				$types[$item->getCode()] = $item->getInternalType();
428
			} else if( isset( $item['code'] ) ) {
429
				$types[$item['code']] = $item['internaltype'];
430
			} else {
431
				throw new \Aimeos\MShop\Exception( sprintf( 'Invalid attribute at position "%1$d"', $key ) );
432
			}
433
		}
434
435
		return $types;
436
	}
437
438
439
	/**
440
	 * Returns the SQL statement for the given config path
441
	 *
442
	 * If available, the database specific SQL statement is returned, otherwise
443
	 * the ANSI SQL statement. The database type is determined via the resource
444
	 * adapter.
445
	 *
446
	 * @param string $path Configuration path to the SQL statement
447
	 * @return array|string ANSI or database specific SQL statement
448
	 */
449
	protected function getSqlConfig( string $path )
450
	{
451
		$config = $this->context()->config();
452
		$adapter = $config->get( 'resource/' . $this->getResourceName() . '/adapter' );
453
454
		return $config->get( $path . '/' . $adapter, $config->get( $path . '/ansi', $path ) );
455
	}
456
457
458
	/**
459
	 * Sets the base criteria "status".
460
	 * (setConditions overwrites the base criteria)
461
	 *
462
	 * @param string $domain Name of the domain/sub-domain like "product" or "product.list"
463
	 * @param bool|null $default TRUE for status=1, NULL for status>0, FALSE for no restriction
464
	 * @return \Aimeos\Base\Criteria\Iface Search critery object
465
	 */
466
	protected function filterBase( string $domain, ?bool $default = false ) : \Aimeos\Base\Criteria\Iface
467
	{
468
		$filter = self::filter();
0 ignored issues
show
Bug Best Practice introduced by
The method Aimeos\MShop\Common\Manager\Base::filter() is not static, but was called statically. ( Ignorable by Annotation )

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

468
		/** @scrutinizer ignore-call */ 
469
  $filter = self::filter();
Loading history...
469
470
		if( $default !== false ) {
471
			$filter->add( $domain . '.status', $default ? '==' : '>=', 1 );
472
		}
473
474
		return $filter;
475
	}
476
477
478
	/**
479
	 * Returns the item for the given search key/value pairs.
480
	 *
481
	 * @param array $pairs Search key/value pairs for the item
482
	 * @param string[] $ref List of domains whose items should be fetched too
483
	 * @param bool|null $default Add default criteria or NULL for relaxed default criteria
484
	 * @return \Aimeos\MShop\Common\Item\Iface Requested item
485
	 * @throws \Aimeos\MShop\Exception if no item with the given ID found
486
	 */
487
	protected function findBase( array $pairs, array $ref, ?bool $default ) : \Aimeos\MShop\Common\Item\Iface
488
	{
489
		$expr = [];
490
		$criteria = $this->object()->filter( $default )->slice( 0, 1 );
491
492
		foreach( $pairs as $key => $value )
493
		{
494
			if( $value === null )
495
			{
496
				$msg = $this->context()->translate( 'mshop', 'Required value for "%1$s" is missing' );
497
				throw new \Aimeos\MShop\Exception( sprintf( $msg, $key ) );
498
			}
499
			$expr[] = $criteria->compare( '==', $key, $value );
500
		}
501
502
		$criteria->setConditions( $criteria->and( $expr ) );
503
504
		if( ( $item = $this->object()->search( $criteria, $ref )->first() ) ) {
505
			return $item;
506
		}
507
508
		$msg = $this->context()->translate( 'mshop', 'No item found for conditions: %1$s' );
509
		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

509
		throw new \Aimeos\MShop\Exception( sprintf( $msg, /** @scrutinizer ignore-type */ print_r( $pairs, true ) ), 404 );
Loading history...
510
	}
511
512
513
	/**
514
	 * Returns the cached statement for the given key or creates a new prepared statement.
515
	 * If no SQL string is given, the key is used to retrieve the SQL string from the configuration.
516
	 *
517
	 * @param \Aimeos\Base\DB\Connection\Iface $conn Database connection
518
	 * @param string $cfgkey Unique key for the SQL
519
	 * @param string|null $sql SQL string if it shouldn't be retrieved from the configuration
520
	 * @return \Aimeos\Base\DB\Statement\Iface Database statement object
521
	 */
522
	protected function getCachedStatement( \Aimeos\Base\DB\Connection\Iface $conn, string $cfgkey,
523
		string $sql = null ) : \Aimeos\Base\DB\Statement\Iface
524
	{
525
		if( !isset( $this->stmts['stmt'][$cfgkey] )
526
			|| !isset( $this->stmts['conn'][$cfgkey] )
527
			|| $conn !== $this->stmts['conn'][$cfgkey]
528
		) {
529
			if( $sql === null ) {
530
				$sql = $this->getSqlConfig( $cfgkey );
531
			}
532
533
			$this->stmts['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

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

710
		$result = $conn->create( /** @scrutinizer ignore-type */ $this->getSqlConfig( $cfgpath ) )->execute();
Loading history...
711
712
		if( ( $row = $result->fetch( \Aimeos\Base\DB\Result\Base::FETCH_NUM ) ) === false )
713
		{
714
			$msg = $this->context()->translate( 'mshop', 'ID of last inserted database record not available' );
715
			throw new \Aimeos\MShop\Exception( $msg );
716
		}
717
		$result->finish();
718
719
		return $row[0];
720
	}
721
722
723
	/**
724
	 * Returns the search result of the statement combined with the given criteria.
725
	 *
726
	 * @param \Aimeos\Base\DB\Connection\Iface $conn Database connection
727
	 * @param \Aimeos\Base\Criteria\Iface $search Search criteria object
728
	 * @param string $cfgPathSearch Path to SQL statement in configuration for searching
729
	 * @param string $cfgPathCount Path to SQL statement in configuration for counting
730
	 * @param string[] $required Additional search keys to add conditions for even if no conditions are available
731
	 * @param int|null $total Contains the number of all records matching the criteria if not null
732
	 * @param int $sitelevel Constant from \Aimeos\MShop\Locale\Manager\Base for defining which site IDs should be used for searching
733
	 * @param \Aimeos\Base\Criteria\Plugin\Iface[] $plugins Associative list of search keys and criteria plugin items as values
734
	 * @return \Aimeos\Base\DB\Result\Iface SQL result object for accessing the found records
735
	 * @throws \Aimeos\MShop\Exception if no number of all matching records is available
736
	 */
737
	protected function searchItemsBase( \Aimeos\Base\DB\Connection\Iface $conn, \Aimeos\Base\Criteria\Iface $search,
738
		string $cfgPathSearch, string $cfgPathCount, array $required, int &$total = null,
739
		int $sitelevel = \Aimeos\MShop\Locale\Manager\Base::SITE_ALL, array $plugins = [] ) : \Aimeos\Base\DB\Result\Iface
740
	{
741
		$joins = [];
742
		$conditions = $search->getConditions();
743
		$columns = $this->object()->getSaveAttributes();
744
		$attributes = $this->object()->getSearchAttributes();
745
		$keys = $this->getCriteriaKeyList( $search, $required );
746
747
		$basekey = array_shift( $required );
748
749
		foreach( $keys as $key )
750
		{
751
			if( $key !== $basekey ) {
752
				$joins = array_merge( $joins, $this->getJoins( $attributes, $key ) );
753
			}
754
		}
755
756
		$joins = array_unique( $joins );
757
		$cond = $this->getSiteConditions( $keys, $attributes, $sitelevel );
758
759
		if( $conditions !== null ) {
760
			$cond[] = $conditions;
761
		}
762
763
		$search = clone $search;
764
		$search->setConditions( $search->and( $cond ) );
765
766
		list( $find, $replace ) = $this->getSQLReplacements( $search, $attributes, $plugins, $joins, $columns );
767
768
		if( $total !== null )
769
		{
770
			$sql = str_replace( $find, $replace, $this->getSqlConfig( $cfgPathCount ) );
771
			$result = $this->getSearchResults( $conn, $sql );
772
			$row = $result->fetch();
773
			$result->finish();
774
775
			if( $row === null )
776
			{
777
				$msg = $this->context()->translate( 'mshop', 'Total results value not found' );
778
				throw new \Aimeos\MShop\Exception( $msg );
779
			}
780
781
			$total = (int) $row['count'];
782
		}
783
784
		return $this->getSearchResults( $conn, str_replace( $find, $replace, $this->getSqlConfig( $cfgPathSearch ) ) );
785
	}
786
787
788
	/**
789
	 * Replaces the given marker with an expression
790
	 *
791
	 * @param string $column Name (including alias) of the column
792
	 * @param mixed $value Value used in the expression
793
	 * @param string $op Operator used in the expression
794
	 * @param int $type Type constant from \Aimeos\Base\DB\Statement\Base class
795
	 * @return string Created expression
796
	 */
797
	protected function toExpression( string $column, $value, string $op = '==',
798
		int $type = \Aimeos\Base\DB\Statement\Base::PARAM_STR ) : string
799
	{
800
		$types = ['marker' => $type];
801
		$translations = ['marker' => $column];
802
		$value = ( is_array( $value ) ? array_unique( $value ) : $value );
803
804
		return $this->getSearch()->compare( $op, 'marker', $value )->toSource( $types, $translations );
805
	}
806
807
808
	/**
809
	 * Transforms the application specific values to Aimeos standard values.
810
	 *
811
	 * @param array $values Associative list of key/value pairs from the storage
812
	 * @return array Associative list of key/value pairs with standard Aimeos values
813
	 */
814
	protected function transform( array $values ) : array
815
	{
816
		return $values;
817
	}
818
819
820
	/**
821
	 * Cuts the last part separated by a dot repeatedly and returns the list of resulting string.
822
	 *
823
	 * @param string[] $prefix Required base prefixes of the search keys
824
	 * @param string $string String containing parts separated by dots
825
	 * @return array List of resulting strings
826
	 */
827
	private function cutNameTail( array $prefix, string $string ) : array
828
	{
829
		$result = [];
830
		$noprefix = true;
831
		$strlen = strlen( $string );
832
833
		foreach( $prefix as $key )
834
		{
835
			$len = strlen( $key );
836
837
			if( strncmp( $string, $key, $len ) === 0 )
838
			{
839
				if( $strlen > $len && ( $pos = strrpos( $string, '.' ) ) !== false )
840
				{
841
					$result[] = $string = substr( $string, 0, $pos );
842
					$result = array_merge( $result, $this->cutNameTail( $prefix, $string ) );
843
					$noprefix = false;
844
				}
845
846
				break;
847
			}
848
		}
849
850
		if( $noprefix )
851
		{
852
			if( ( $pos = strrpos( $string, ':' ) ) !== false ) {
853
				$result[] = substr( $string, 0, $pos );
854
				$result[] = $string;
855
			} elseif( ( $pos = strrpos( $string, '.' ) ) !== false ) {
856
				$result[] = substr( $string, 0, $pos );
857
			} else {
858
				$result[] = $string;
859
			}
860
		}
861
862
		return $result;
863
	}
864
865
866
	/**
867
	 * Returns a list of unique criteria names shortend by the last element after the ''
868
	 *
869
	 * @param string[] $prefix Required base prefixes of the search keys
870
	 * @param \Aimeos\Base\Criteria\Expression\Iface|null 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...
871
	 * @return array List of shortend criteria names
872
	 */
873
	private function getCriteriaKeys( array $prefix, \Aimeos\Base\Criteria\Expression\Iface $expr = null ) : array
874
	{
875
		if( $expr === null ) { return []; }
876
877
		$result = [];
878
879
		foreach( $this->getCriteriaNames( $expr ) as $item )
880
		{
881
			if( strncmp( $item, 'sort:', 5 ) === 0 ) {
882
				$item = substr( $item, 5 );
883
			}
884
885
			if( ( $pos = strpos( $item, '(' ) ) !== false ) {
886
				$item = substr( $item, 0, $pos );
887
			}
888
889
			$result = array_merge( $result, $this->cutNameTail( $prefix, $item ) );
890
		}
891
892
		return $result;
893
	}
894
895
896
	/**
897
	 * Returns a list of criteria names from a expression and its sub-expressions.
898
	 *
899
	 * @param \Aimeos\Base\Criteria\Expression\Iface Criteria object
900
	 * @return array List of criteria names
901
	 */
902
	private function getCriteriaNames( \Aimeos\Base\Criteria\Expression\Iface $expr ) : array
903
	{
904
		if( $expr instanceof \Aimeos\Base\Criteria\Expression\Compare\Iface ) {
905
			return array( $expr->getName() );
906
		}
907
908
		if( $expr instanceof \Aimeos\Base\Criteria\Expression\Combine\Iface )
909
		{
910
			$list = [];
911
			foreach( $expr->getExpressions() as $item ) {
912
				$list = array_merge( $list, $this->getCriteriaNames( $item ) );
913
			}
914
			return $list;
915
		}
916
917
		if( $expr instanceof \Aimeos\Base\Criteria\Expression\Sort\Iface ) {
918
			return array( $expr->getName() );
919
		}
920
921
		return [];
922
	}
923
}
924