Passed
Push — master ( ac2e3e...212e15 )
by Aimeos
05:15
created

Base   F

Complexity

Total Complexity 108

Size/Duplication

Total Lines 904
Duplicated Lines 0 %

Importance

Changes 3
Bugs 0 Features 0
Metric Value
wmc 108
eloc 298
dl 0
loc 904
rs 2
c 3
b 0
f 0

30 Methods

Rating   Name   Duplication   Size   Complexity  
A addSqlColumns() 0 18 4
A __construct() 0 3 1
A getSearchFunctions() 0 17 4
A getSearchAttributesBase() 0 14 3
A getSiteConditions() 0 14 3
A getCriteriaKeys() 0 20 5
A getResourceTypeBase() 0 12 3
A clearBase() 0 20 3
A getCachedStatement() 0 16 5
A getSearchTypes() 0 17 4
A createAttributes() 0 9 2
A getSearch() 0 7 2
B searchItemsBase() 0 48 6
A toExpression() 0 8 2
A getItemBase() 0 10 2
A transform() 0 3 1
A filterBase() 0 9 3
A context() 0 3 1
A getSqlConfig() 0 6 1
B getJoins() 0 16 7
A getCriteriaKeyList() 0 12 2
B cutNameTail() 0 36 8
A getSearchResults() 0 22 2
C aggregateBase() 0 95 12
A getSearchTranslations() 0 17 4
A getCriteriaNames() 0 20 5
A newId() 0 12 2
A getSQLReplacements() 0 32 4
A findBase() 0 23 4
A deleteItemsBase() 0 28 3

How to fix   Complexity   

Complex Class

Complex classes like Base often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Base, and based on these observations, apply Extract Interface, too.

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

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

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

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

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