Passed
Push — master ( 494e53...aeeda4 )
by Aimeos
09:49
created

Base::searchItemsBase()   B

Complexity

Conditions 6
Paths 18

Size

Total Lines 48
Code Lines 26

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 6
eloc 26
nc 18
nop 8
dl 0
loc 48
rs 8.8817
c 0
b 0
f 0

How to fix   Many Parameters   

Many Parameters

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

There are several approaches to avoid long parameter lists:

1
<?php
2
3
/**
4
 * @license LGPLv3, https://opensource.org/licenses/LGPL-3.0
5
 * @copyright Metaways Infosystems GmbH, 2011
6
 * @copyright Aimeos (aimeos.org), 2015-2022
7
 * @package MShop
8
 * @subpackage Common
9
 */
10
11
12
namespace Aimeos\MShop\Common\Manager;
13
14
use \Aimeos\MShop\Locale\Manager\Base as Locale;
0 ignored issues
show
Bug introduced by
The type \Aimeos\MShop\Locale\Manager\Base 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...
15
16
17
/**
18
 * Provides common methods required by most of the manager classes.
19
 *
20
 * @package MShop
21
 * @subpackage Common
22
 */
23
abstract class Base
24
	extends \Aimeos\MW\Common\Manager\Base
25
	implements \Aimeos\Macro\Iface
26
{
27
	use \Aimeos\MShop\Common\Manager\Sub\Traits;
28
	use \Aimeos\Macro\Macroable;
29
	use Site;
30
31
32
	private $context;
33
	private $object;
34
	private $resourceName;
35
	private $stmts = [];
36
	private $search;
37
38
39
	/**
40
	 * Initialization of class.
41
	 *
42
	 * @param \Aimeos\MShop\ContextIface $context Context object
43
	 */
44
	public function __construct( \Aimeos\MShop\ContextIface $context )
45
	{
46
		$this->context = $context;
47
	}
48
49
50
	/**
51
	 * Returns the class names of the manager and used decorators.
52
	 *
53
	 * @return array List of class names
54
	 */
55
	public function classes() : array
56
	{
57
		return [get_class( $this )];
58
	}
59
60
61
	/**
62
	 * Removes old entries from the storage.
63
	 *
64
	 * @param iterable $siteids List of IDs for sites whose entries should be deleted
65
	 * @return \Aimeos\MShop\Common\Manager\Iface Manager object for chaining method calls
66
	 */
67
	public function clear( iterable $siteids ) : \Aimeos\MShop\Common\Manager\Iface
68
	{
69
		return $this;
70
	}
71
72
73
	/**
74
	 * Creates a new cursor based on the filter criteria
75
	 *
76
	 * @param \Aimeos\Base\Criteria\Iface $filter Criteria object with conditions, sortations, etc.
77
	 * @return \Aimeos\MShop\Common\Cursor\Iface Cursor object
78
	 */
79
	public function cursor( \Aimeos\Base\Criteria\Iface $filter ) : \Aimeos\MShop\Common\Cursor\Iface
80
	{
81
		return new \Aimeos\MShop\Common\Cursor\Standard( $filter );
82
	}
83
84
85
	/**
86
	 * Creates a search critera object
87
	 *
88
	 * @param bool|null $default Add default criteria or NULL for relaxed default criteria
89
	 * @param bool $site TRUE for adding site criteria to limit items by the site of related items
90
	 * @return \Aimeos\Base\Criteria\Iface New search criteria object
91
	 */
92
	public function filter( ?bool $default = false, bool $site = false ) : \Aimeos\Base\Criteria\Iface
93
	{
94
		$db = $this->getResourceName();
95
		$conn = $this->context->db( $db );
96
		$config = $this->context->config();
97
98
		if( ( $adapter = $config->get( 'resource/' . $db . '/adapter' ) ) === null ) {
99
			$adapter = $config->get( 'resource/db/adapter' );
100
		}
101
102
		switch( $adapter )
103
		{
104
			case 'pgsql':
105
				$search = new \Aimeos\Base\Criteria\PgSQL( $conn ); break;
106
			default:
107
				$search = new \Aimeos\Base\Criteria\SQL( $conn ); break;
108
		}
109
110
		return $search;
111
	}
112
113
114
	/**
115
	 * Iterates over all matched items and returns the found ones
116
	 *
117
	 * @param \Aimeos\MShop\Common\Cursor\Iface $cursor Cursor object with filter, domains and cursor
118
	 * @param string[] $ref List of domains whose items should be fetched too
119
	 * @return \Aimeos\Map|null List of items implementing \Aimeos\MShop\Common\Item\Iface with ids as keys
120
	 */
121
	public function iterate( \Aimeos\MShop\Common\Cursor\Iface $cursor, array $ref = [] ) : ?\Aimeos\Map
122
	{
123
		if( $cursor->value() === '' ) {
124
			return null;
125
		}
126
127
		$prefix = str_replace( '/', '.', (string) current( $this->getResourceType( false ) ) );
0 ignored issues
show
Bug introduced by
The method getResourceType() does not exist on Aimeos\MShop\Common\Manager\Base. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

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

127
		$prefix = str_replace( '/', '.', (string) current( $this->/** @scrutinizer ignore-call */ getResourceType( false ) ) );
Loading history...
128
		$filter = $cursor->filter()->add( $prefix . '.id', '>', (int) $cursor->value() )->order( $prefix . '.id' );
129
130
		$items = $this->search( $filter, $ref );
131
		$cursor->setValue( $items->lastKey() ?: '' );
132
133
		return !$items->isEmpty() ? $items : null;
134
	}
135
136
137
	/**
138
	 * Adds or updates an item object or a list of them.
139
	 *
140
	 * @param \Aimeos\Map|\Aimeos\MShop\Common\Item\Iface[]|\Aimeos\MShop\Common\Item\Iface $items Item or list of items whose data should be saved
141
	 * @param bool $fetch True if the new ID should be returned in the item
142
	 * @return \Aimeos\Map|\Aimeos\MShop\Common\Item\Iface Saved item or items
143
	 */
144
	public function save( $items, bool $fetch = true )
145
	{
146
		if( is_iterable( $items ) )
147
		{
148
			foreach( $items as $id => $item ) {
149
				$items[$id] = $this->saveItem( $item, $fetch );
0 ignored issues
show
Bug introduced by
The method saveItem() does not exist on Aimeos\MShop\Common\Manager\Base. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

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

149
				/** @scrutinizer ignore-call */ 
150
    $items[$id] = $this->saveItem( $item, $fetch );
Loading history...
150
			}
151
			return map( $items );
152
		}
153
154
		return $this->saveItem( $items, $fetch );
155
	}
156
157
158
	/**
159
	 * Searches for all items matching the given critera.
160
	 *
161
	 * @param \Aimeos\Base\Criteria\Iface $filter Criteria object with conditions, sortations, etc.
162
	 * @param string[] $ref List of domains to fetch list items and referenced items for
163
	 * @param int &$total Number of items that are available in total
164
	 * @return \Aimeos\Map List of items implementing \Aimeos\MShop\Common\Item\Iface with ids as keys
165
	 */
166
	public function search( \Aimeos\Base\Criteria\Iface $filter, array $ref = [], int &$total = null ) : \Aimeos\Map
167
	{
168
		return map();
169
	}
170
171
172
	/**
173
	 * Starts a database transaction on the connection identified by the given name
174
	 *
175
	 * @return \Aimeos\MShop\Common\Manager\Iface Manager object for chaining method calls
176
	 */
177
	public function begin() : \Aimeos\MShop\Common\Manager\Iface
178
	{
179
		return $this->beginTransation( $this->getResourceName() );
180
	}
181
182
183
	/**
184
	 * Commits the running database transaction on the connection identified by the given name
185
	 *
186
	 * @return \Aimeos\MShop\Common\Manager\Iface Manager object for chaining method calls
187
	 */
188
	public function commit() : \Aimeos\MShop\Common\Manager\Iface
189
	{
190
		return $this->commitTransaction( $this->getResourceName() );
191
	}
192
193
194
	/**
195
	 * Rolls back the running database transaction on the connection identified by the given name
196
	 *
197
	 * @return \Aimeos\MShop\Common\Manager\Iface Manager object for chaining method calls
198
	 */
199
	public function rollback() : \Aimeos\MShop\Common\Manager\Iface
200
	{
201
		return $this->rollbackTransaction( $this->getResourceName() );
202
	}
203
204
205
	/**
206
	 * Returns the additional column/search definitions
207
	 *
208
	 * @return array Associative list of column names as keys and items implementing \Aimeos\Base\Criteria\Attribute\Iface
209
	 */
210
	public function getSaveAttributes() : array
211
	{
212
		return [];
213
	}
214
215
216
	/**
217
	 * Injects the reference of the outmost object
218
	 *
219
	 * @param \Aimeos\MShop\Common\Manager\Iface $object Reference to the outmost manager or decorator
220
	 * @return \Aimeos\MShop\Common\Manager\Iface Manager object for chaining method calls
221
	 */
222
	public function setObject( \Aimeos\MShop\Common\Manager\Iface $object ) : \Aimeos\MShop\Common\Manager\Iface
223
	{
224
		$this->object = $object;
225
		return $this;
226
	}
227
228
229
	/**
230
	 * Adds additional column names to SQL statement
231
	 *
232
	 * @param string[] $columns List of column names
233
	 * @param string $sql Insert or update SQL statement
234
	 * @param bool $mode True for insert, false for update statement
235
	 * @return string Modified insert or update SQL statement
236
	 */
237
	protected function addSqlColumns( array $columns, string $sql, bool $mode = true ) : string
238
	{
239
		$names = $values = '';
240
241
		if( $mode )
242
		{
243
			foreach( $columns as $name ) {
244
				$names .= '"' . $name . '", '; $values .= '?, ';
245
			}
246
		}
247
		else
248
		{
249
			foreach( $columns as $name ) {
250
				$names .= '"' . $name . '" = ?, ';
251
			}
252
		}
253
254
		return str_replace( [':names', ':values'], [$names, $values], $sql );
255
	}
256
257
258
	/**
259
	 * Counts the number products that are available for the values of the given key.
260
	 *
261
	 * @param \Aimeos\Base\Criteria\Iface $search Search criteria
262
	 * @param array|string $keys Search key or list of keys for aggregation
263
	 * @param string $cfgPath Configuration key for the SQL statement
264
	 * @param string[] $required List of domain/sub-domain names like "catalog.index" that must be additionally joined
265
	 * @param string|null $value Search key for aggregating the value column
266
	 * @param string|null $type Type of aggregation, e.g.  "sum", "min", "max" or NULL for "count"
267
	 * @return \Aimeos\Map List of ID values as key and the number of counted products as value
268
	 * @todo 2018.01 Reorder Parameter list
269
	 */
270
	protected function aggregateBase( \Aimeos\Base\Criteria\Iface $search, $keys, string $cfgPath,
271
		array $required = [], string $value = null, string $type = null ) : \Aimeos\Map
272
	{
273
		/** mshop/common/manager/aggregate/limit
274
		 * Limits the number of records that are used when aggregating items
275
		 *
276
		 * As counting huge amount of records (several 10 000 records) takes a long time,
277
		 * the limit can cut down response times so the counts are available more quickly
278
		 * in the front-end and the server load is reduced.
279
		 *
280
		 * Using a low limit can lead to incorrect numbers if the amount of found items
281
		 * is very high. Approximate item counts are normally not a problem but it can
282
		 * lead to the situation that visitors see that no items are available despite
283
		 * the fact that there would be at least one.
284
		 *
285
		 * @param integer Number of records
286
		 * @since 2021.04
287
		 */
288
		$limit = $this->context->config()->get( 'mshop/common/manager/aggregate/limit', 10000 );
289
		$keys = (array) $keys;
290
291
		if( !count( $keys ) )
292
		{
293
			$msg = $this->context()->translate( 'mshop', 'At least one key is required for aggregation' );
294
			throw new \Aimeos\MShop\Exception( $msg );
295
		}
296
297
		$conn = $this->context->db( $this->getResourceName() );
298
299
		$total = null;
300
		$cols = $map = [];
301
		$search = clone $search;
302
		$search->slice( $search->getOffset(), min( $search->getLimit(), $limit ) );
303
304
		$level = \Aimeos\MShop\Locale\Manager\Base::SITE_ALL;
305
		$attrList = $this->object()->getSearchAttributes();
306
307
		if( $value === null && ( $value = key( $attrList ) ) === null )
308
		{
309
			$msg = $this->context()->translate( 'mshop', 'No search keys available' );
310
			throw new \Aimeos\MShop\Exception( $msg );
311
		}
312
313
		if( ( $pos = strpos( $valkey = $value, '(' ) ) !== false ) {
314
			$value = substr( $value, 0, $pos );
315
		}
316
317
		if( !isset( $attrList[$value] ) )
318
		{
319
			$msg = $this->context()->translate( 'mshop', 'Unknown search key "%1$s"' );
320
			throw new \Aimeos\MShop\Exception( sprintf( $msg, $value ) );
321
		}
322
323
		foreach( $keys as $string )
324
		{
325
			if( !isset( $attrList[$string] ) )
326
			{
327
				$msg = $this->context()->translate( 'mshop', 'Unknown search key "%1$s"' );
328
				throw new \Aimeos\MShop\Exception( sprintf( $msg, $string ) );
329
			}
330
331
			$cols[] = $attrList[$string]->getInternalCode();
332
			$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

332
			$acols[] = /** @scrutinizer ignore-type */ $attrList[$string]->getInternalCode() . ' AS "' . $string . '"';
Loading history...
333
334
			/** @todo Required to get the joins, but there should be a better way */
335
			$search->add( [$string => null], '!=' );
336
		}
337
		$search->add( [$valkey => null], '!=' );
338
339
		$sql = $this->getSqlConfig( $cfgPath );
340
		$sql = str_replace( ':cols', join( ', ', $cols ), $sql );
341
		$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 323. Are you sure the iterator is never empty, otherwise this variable is not defined?
Loading history...
342
		$sql = str_replace( ':keys', '"' . join( '", "', $keys ) . '"', $sql );
343
		$sql = str_replace( ':val', $attrList[$value]->getInternalCode(), $sql );
344
		$sql = str_replace( ':type', in_array( $type, ['avg', 'count', 'max', 'min', 'sum'] ) ? $type : 'count', $sql );
345
346
		$results = $this->searchItemsBase( $conn, $search, $sql, '', $required, $total, $level );
347
348
		while( ( $row = $results->fetch() ) !== null )
349
		{
350
			$row = $this->transform( $row );
351
352
			$temp = &$map;
353
			$last = array_pop( $row );
354
355
			foreach( $row as $val ) {
356
				$temp[$val] = $temp[$val] ?? [];
357
				$temp = &$temp[$val];
358
			}
359
			$temp = $last;
360
		}
361
362
		return map( $map );
363
	}
364
365
366
	/**
367
	 * Returns the newly created ID for the last record which was inserted.
368
	 *
369
	 * @param \Aimeos\Base\DB\Connection\Iface $conn Database connection used to insert the new record
370
	 * @param string $cfgpath Configuration path to the SQL statement for retrieving the new ID of the last inserted record
371
	 * @return string ID of the last record that was inserted by using the given connection
372
	 * @throws \Aimeos\MShop\Exception if there's no ID of the last record available
373
	 */
374
	protected function newId( \Aimeos\Base\DB\Connection\Iface $conn, string $cfgpath ) : string
375
	{
376
		$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

376
		$result = $conn->create( /** @scrutinizer ignore-type */ $this->getSqlConfig( $cfgpath ) )->execute();
Loading history...
377
378
		if( ( $row = $result->fetch( \Aimeos\Base\DB\Result\Base::FETCH_NUM ) ) === false )
379
		{
380
			$msg = $this->context()->translate( 'mshop', 'ID of last inserted database record not available' );
381
			throw new \Aimeos\MShop\Exception( $msg );
382
		}
383
		$result->finish();
384
385
		return $row[0];
386
	}
387
388
389
	/**
390
	 * Removes old entries from the storage.
391
	 *
392
	 * @param iterable $siteids List of IDs for sites whose entries should be deleted
393
	 * @param string $cfgpath Configuration key to the cleanup statement
394
	 * @return \Aimeos\MShop\Common\Manager\Iface Manager object for chaining method calls
395
	 */
396
	protected function clearBase( iterable $siteids, string $cfgpath ) : \Aimeos\MShop\Common\Manager\Iface
397
	{
398
		if( empty( $siteids ) ) {
399
			return $this;
400
		}
401
402
		$conn = $this->context->db( $this->getResourceName() );
403
404
		$sql = $this->getSqlConfig( $cfgpath );
405
		$sql = str_replace( ':cond', '1=1', $sql );
406
407
		$stmt = $conn->create( $sql );
408
409
		foreach( $siteids as $siteid )
410
		{
411
			$stmt->bind( 1, $siteid );
412
			$stmt->execute()->finish();
413
		}
414
415
		return $this;
416
	}
417
418
419
	/**
420
	 * Creates the criteria attribute items from the list of entries
421
	 *
422
	 * @param array $list Associative array of code as key and array with properties as values
423
	 * @return \Aimeos\Base\Criteria\Attribute\Standard[] List of criteria attribute items
424
	 */
425
	protected function createAttributes( array $list ) : array
426
	{
427
		$attr = [];
428
429
		foreach( $list as $key => $fields ) {
430
			$attr[$key] = new \Aimeos\Base\Criteria\Attribute\Standard( $fields );
431
		}
432
433
		return $attr;
434
	}
435
436
437
	/**
438
	 * Sets the base criteria "status".
439
	 * (setConditions overwrites the base criteria)
440
	 *
441
	 * @param string $domain Name of the domain/sub-domain like "product" or "product.list"
442
	 * @param bool|null $default TRUE for status=1, NULL for status>0, FALSE for no restriction
443
	 * @return \Aimeos\Base\Criteria\Iface Search critery object
444
	 */
445
	protected function filterBase( string $domain, ?bool $default = false ) : \Aimeos\Base\Criteria\Iface
446
	{
447
		$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

447
		/** @scrutinizer ignore-call */ 
448
  $filter = self::filter();
Loading history...
448
449
		if( $default !== false ) {
450
			$filter->add( [$domain . '.status' => 1], $default ? '==' : '>=' );
451
		}
452
453
		return $filter;
454
	}
455
456
457
	/**
458
	 * Returns the context object.
459
	 *
460
	 * @return \Aimeos\MShop\ContextIface Context object
461
	 */
462
	protected function context() : \Aimeos\MShop\ContextIface
463
	{
464
		return $this->context;
465
	}
466
467
468
	/**
469
	 * Returns the outmost decorator of the decorator stack
470
	 *
471
	 * @return \Aimeos\MShop\Common\Manager\Iface Outmost decorator object
472
	 */
473
	protected function object() : \Aimeos\MShop\Common\Manager\Iface
474
	{
475
		if( $this->object !== null ) {
476
			return $this->object;
477
		}
478
479
		return $this;
480
	}
481
482
483
	/**
484
	 * Returns the search attribute objects used for searching.
485
	 *
486
	 * @param array $list Associative list of search keys and the lists of search definitions
487
	 * @param string $path Configuration path to the sub-domains for fetching the search definitions
488
	 * @param string[] $default List of sub-domains if no others are configured
489
	 * @param bool $withsub True to include search definitions of sub-domains, false if not
490
	 * @return \Aimeos\Base\Criteria\Attribute\Iface[] Associative list of search keys and criteria attribute items as values
491
	 * @since 2014.09
492
	 */
493
	protected function getSearchAttributesBase( array $list, string $path, array $default, bool $withsub ) : array
494
	{
495
		$attr = $this->createAttributes( $list );
496
497
		if( $withsub === true )
498
		{
499
			$domains = $this->context->config()->get( $path, $default );
500
501
			foreach( $domains as $domain ) {
502
				$attr += $this->object()->getSubManager( $domain )->getSearchAttributes( true );
503
			}
504
		}
505
506
		return $attr;
507
	}
508
509
510
	/**
511
	 * Returns the search results for the given SQL statement.
512
	 *
513
	 * @param \Aimeos\Base\DB\Connection\Iface $conn Database connection
514
	 * @param string $sql SQL statement
515
	 * @return \Aimeos\Base\DB\Result\Iface Search result object
516
	 */
517
	protected function getSearchResults( \Aimeos\Base\DB\Connection\Iface $conn, string $sql ) : \Aimeos\Base\DB\Result\Iface
518
	{
519
		$time = microtime( true );
520
521
		$stmt = $conn->create( $sql );
522
		$result = $stmt->execute();
523
524
		$level = \Aimeos\Base\Logger\Iface::DEBUG;
525
		$time = ( microtime( true ) - $time ) * 1000;
526
		$msg = 'Time: ' . $time . "ms\n"
527
			. 'Class: ' . get_class( $this ) . "\n"
528
			. str_replace( ["\t", "\n\n"], ['', "\n"], trim( (string) $stmt ) );
529
530
		if( $time > 1000.0 )
531
		{
532
			$level = \Aimeos\Base\Logger\Iface::NOTICE;
533
			$msg .= "\n" . ( new \Exception() )->getTraceAsString();
534
		}
535
536
		$this->context->logger()->log( $msg, $level, 'core/sql' );
537
538
		return $result;
539
	}
540
541
542
	/**
543
	 * Returns the SQL statement for the given config path
544
	 *
545
	 * If available, the database specific SQL statement is returned, otherwise
546
	 * the ANSI SQL statement. The database type is determined via the resource
547
	 * adapter.
548
	 *
549
	 * @param string $path Configuration path to the SQL statement
550
	 * @return array|string ANSI or database specific SQL statement
551
	 */
552
	protected function getSqlConfig( string $path )
553
	{
554
		$config = $this->context()->config();
555
		$adapter = $config->get( 'resource/' . $this->getResourceName() . '/adapter' );
556
557
		return $config->get( $path . '/' . $adapter, $config->get( $path . '/ansi', $path ) );
558
	}
559
560
561
	/**
562
	 * Returns the item for the given search key/value pairs.
563
	 *
564
	 * @param array $pairs Search key/value pairs for the item
565
	 * @param string[] $ref List of domains whose items should be fetched too
566
	 * @param bool|null $default Add default criteria or NULL for relaxed default criteria
567
	 * @return \Aimeos\MShop\Common\Item\Iface Requested item
568
	 * @throws \Aimeos\MShop\Exception if no item with the given ID found
569
	 */
570
	protected function findBase( array $pairs, array $ref, ?bool $default ) : \Aimeos\MShop\Common\Item\Iface
571
	{
572
		$expr = [];
573
		$criteria = $this->object()->filter( $default )->slice( 0, 1 );
574
575
		foreach( $pairs as $key => $value )
576
		{
577
			if( $value === null )
578
			{
579
				$msg = $this->context()->translate( 'mshop', 'Required value for "%1$s" is missing' );
580
				throw new \Aimeos\MShop\Exception( $msg );
581
			}
582
			$expr[] = $criteria->compare( '==', $key, $value );
583
		}
584
585
		$criteria->setConditions( $criteria->and( $expr ) );
586
587
		if( ( $item = $this->object()->search( $criteria, $ref )->first() ) ) {
588
			return $item;
589
		}
590
591
		$msg = $this->context()->translate( 'mshop', 'No item found for conditions: %1$s' );
592
		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

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

616
			$this->stmts['stmt'][$cfgkey] = $conn->create( /** @scrutinizer ignore-type */ $sql );
Loading history...
617
			$this->stmts['conn'][$cfgkey] = $conn;
618
		}
619
620
		return $this->stmts['stmt'][$cfgkey];
621
	}
622
623
624
	/**
625
	 * Returns the item for the given search key and ID.
626
	 *
627
	 * @param string $key Search key for the requested ID
628
	 * @param string $id Unique ID to search for
629
	 * @param string[] $ref List of domains whose items should be fetched too
630
	 * @param bool|null $default Add default criteria or NULL for relaxed default criteria
631
	 * @return \Aimeos\MShop\Common\Item\Iface Requested item
632
	 * @throws \Aimeos\MShop\Exception if no item with the given ID found
633
	 */
634
	protected function getItemBase( string $key, string $id, array $ref, ?bool $default ) : \Aimeos\MShop\Common\Item\Iface
635
	{
636
		$criteria = $this->object()->filter( $default )->add( [$key => $id] )->slice( 0, 1 );
637
638
		if( ( $item = $this->object()->search( $criteria, $ref )->first() ) ) {
639
			return $item;
640
		}
641
642
		$msg = $this->context()->translate( 'mshop', 'Item with ID "%2$s" in "%1$s" not found' );
643
		throw new \Aimeos\MShop\Exception( sprintf( $msg, $key, $id ), 404 );
644
	}
645
646
647
	/**
648
	 * Returns the SQL strings for joining dependent tables.
649
	 *
650
	 * @param \Aimeos\Base\Criteria\Attribute\Iface[] $attributes List of criteria attribute items
651
	 * @param string $prefix Search key prefix
652
	 * @return array List of JOIN SQL strings
653
	 */
654
	private function getJoins( array $attributes, string $prefix ) : array
655
	{
656
		$iface = \Aimeos\Base\Criteria\Attribute\Iface::class;
657
		$sep = $this->getKeySeparator();
658
		$name = $prefix . $sep . 'id';
659
660
		if( isset( $attributes[$prefix] ) && $attributes[$prefix] instanceof $iface ) {
661
			return $attributes[$prefix]->getInternalDeps();
662
		}
663
		elseif( isset( $attributes[$name] ) && $attributes[$name] instanceof $iface ) {
664
			return $attributes[$name]->getInternalDeps();
665
		}
666
		else if( isset( $attributes['id'] ) && $attributes['id'] instanceof $iface ) {
667
			return $attributes['id']->getInternalDeps();
668
		}
669
670
		return [];
671
	}
672
673
674
	/**
675
	 * Returns the available manager types
676
	 *
677
	 * @param string $type Main manager type
678
	 * @param string $path Configuration path to the sub-domains
679
	 * @param string[] $default List of sub-domains if no others are configured
680
	 * @param bool $withsub Return also the resource type of sub-managers if true
681
	 * @return string[] Type of the manager and submanagers, subtypes are separated by slashes
682
	 */
683
	protected function getResourceTypeBase( string $type, string $path, array $default, bool $withsub ) : array
684
	{
685
		$list = [$type];
686
687
		if( $withsub )
688
		{
689
			foreach( $this->context->config()->get( $path, $default ) as $domain ) {
690
				$list = array_merge( $list, $this->object()->getSubManager( $domain )->getResourceType( $withsub ) );
691
			}
692
		}
693
694
		return $list;
695
	}
696
697
698
	/**
699
	 * Returns the name of the resource or of the default resource.
700
	 *
701
	 * @return string Name of the resource
702
	 */
703
	protected function getResourceName() : string
704
	{
705
		if( $this->resourceName === null ) {
706
			$this->resourceName = $this->context->config()->get( 'resource/default', 'db' );
707
		}
708
709
		return $this->resourceName;
710
	}
711
712
713
	/**
714
	 * Sets the name of the database resource that should be used.
715
	 *
716
	 * @param string $name Name of the resource
717
	 * @return \Aimeos\MShop\Common\Manager\Iface Manager object for chaining method calls
718
	 */
719
	protected function setResourceName( string $name ) : \Aimeos\MShop\Common\Manager\Iface
720
	{
721
		$config = $this->context->config();
722
723
		if( $config->get( 'resource/' . $name ) === null ) {
724
			$this->resourceName = $config->get( 'resource/default', 'db' );
725
		} else {
726
			$this->resourceName = $name;
727
		}
728
729
		return $this;
730
	}
731
732
733
	/**
734
	 * Returns a search object singleton
735
	 *
736
	 * @return \Aimeos\Base\Criteria\Iface Search object
737
	 */
738
	protected function getSearch() : \Aimeos\Base\Criteria\Iface
739
	{
740
		if( !isset( $this->search ) ) {
741
			$this->search = $this->filter();
742
		}
743
744
		return $this->search;
745
	}
746
747
748
	/**
749
	 * Replaces the given marker with an expression
750
	 *
751
	 * @param string $column Name (including alias) of the column
752
	 * @param mixed $value Value used in the expression
753
	 * @param string $op Operator used in the expression
754
	 * @param int $type Type constant from \Aimeos\Base\DB\Statement\Base class
755
	 * @return string Created expression
756
	 */
757
	protected function toExpression( string $column, $value, string $op = '==',
758
		int $type = \Aimeos\Base\DB\Statement\Base::PARAM_STR ) : string
759
	{
760
		$types = ['marker' => $type];
761
		$translations = ['marker' => $column];
762
		$value = ( is_array( $value ) ? array_unique( $value ) : $value );
763
764
		return $this->getSearch()->compare( $op, 'marker', $value )->toSource( $types, $translations );
765
	}
766
767
768
	/**
769
	 * Returns the site coditions for the search request
770
	 *
771
	 * @param string[] $keys Sorted list of criteria keys
772
	 * @param \Aimeos\Base\Criteria\Attribute\Iface[] $attributes Associative list of search keys and criteria attribute items as values
773
	 * @param int $sitelevel Site level constant from \Aimeos\MShop\Locale\Manager\Base
774
	 * @return \Aimeos\Base\Criteria\Expression\Iface[] List of search conditions
775
	 * @since 2015.01
776
	 */
777
	protected function getSiteConditions( array $keys, array $attributes, int $sitelevel ) : array
778
	{
779
		$list = [];
780
		$sep = $this->getKeySeparator();
781
782
		foreach( $keys as $key )
783
		{
784
			$name = $key . $sep . 'siteid';
785
786
			if( isset( $attributes[$name] ) ) {
787
				$list[] = $this->siteCondition( $name, $sitelevel );
788
			}
789
		}
790
791
		return $list;
792
	}
793
794
795
	/**
796
	 * Returns the string replacements for the SQL statements
797
	 *
798
	 * @param \Aimeos\Base\Criteria\Iface $search Search critera object
799
	 * @param \Aimeos\Base\Criteria\Attribute\Iface[] $attributes Associative list of search keys and criteria attribute items as values
800
	 * @param \Aimeos\Base\Criteria\Plugin\Iface[] $plugins Associative list of search keys and criteria plugin items as values
801
	 * @param string[] $joins Associative list of SQL joins
802
	 * @param \Aimeos\Base\Criteria\Attribute\Iface[] $columns Additional columns to retrieve values from
803
	 * @return array Array of keys, find and replace arrays
804
	 */
805
	protected function getSQLReplacements( \Aimeos\Base\Criteria\Iface $search, array $attributes, array $plugins,
806
		array $joins, array $columns = [] ) : array
807
	{
808
		$types = $this->getSearchTypes( $attributes );
809
		$funcs = $this->getSearchFunctions( $attributes );
810
		$translations = $this->getSearchTranslations( $attributes );
811
812
		$colstring = '';
813
		foreach( $columns as $name => $entry ) {
814
			$colstring .= $entry->getInternalCode() . ', ';
815
		}
816
817
		$find = array( ':columns', ':joins', ':cond', ':start', ':size' );
818
		$replace = array(
819
			$colstring,
820
			implode( "\n", array_unique( $joins ) ),
821
			$search->getConditionSource( $types, $translations, $plugins, $funcs ),
822
			$search->getOffset(),
823
			$search->getLimit(),
824
		);
825
826
		if( empty( $search->getSortations() ) && ( $attribute = reset( $attributes ) ) !== false ) {
827
			$search = ( clone $search )->setSortations( [$search->sort( '+', $attribute->getCode() )] );
828
		}
829
830
		$find[] = ':order';
831
		$replace[] = $search->getSortationSource( $types, $translations, $funcs );
832
833
		$find[] = ':group';
834
		$replace[] = implode( ', ', $search->translate( $search->getSortations(), $translations, $funcs ) ) . ', ';
835
836
		return [$find, $replace];
837
	}
838
839
840
	/**
841
	 * Returns the search result of the statement combined with the given criteria.
842
	 *
843
	 * @param \Aimeos\Base\DB\Connection\Iface $conn Database connection
844
	 * @param \Aimeos\Base\Criteria\Iface $search Search criteria object
845
	 * @param string $cfgPathSearch Path to SQL statement in configuration for searching
846
	 * @param string $cfgPathCount Path to SQL statement in configuration for counting
847
	 * @param string[] $required Additional search keys to add conditions for even if no conditions are available
848
	 * @param int|null $total Contains the number of all records matching the criteria if not null
849
	 * @param int $sitelevel Constant from \Aimeos\MShop\Locale\Manager\Base for defining which site IDs should be used for searching
850
	 * @param \Aimeos\Base\Criteria\Plugin\Iface[] $plugins Associative list of search keys and criteria plugin items as values
851
	 * @return \Aimeos\Base\DB\Result\Iface SQL result object for accessing the found records
852
	 * @throws \Aimeos\MShop\Exception if no number of all matching records is available
853
	 */
854
	protected function searchItemsBase( \Aimeos\Base\DB\Connection\Iface $conn, \Aimeos\Base\Criteria\Iface $search,
855
		string $cfgPathSearch, string $cfgPathCount, array $required, int &$total = null,
856
		int $sitelevel = \Aimeos\MShop\Locale\Manager\Base::SITE_ALL, array $plugins = [] ) : \Aimeos\Base\DB\Result\Iface
857
	{
858
		$joins = [];
859
		$conditions = $search->getConditions();
860
		$columns = $this->object()->getSaveAttributes();
861
		$attributes = $this->object()->getSearchAttributes();
862
		$keys = $this->getCriteriaKeyList( $search, $required );
863
864
		$basekey = array_shift( $required );
865
866
		foreach( $keys as $key )
867
		{
868
			if( $key !== $basekey ) {
869
				$joins = array_merge( $joins, $this->getJoins( $attributes, $key ) );
870
			}
871
		}
872
873
		$joins = array_unique( $joins );
874
		$cond = $this->getSiteConditions( $keys, $attributes, $sitelevel );
875
876
		if( $conditions !== null ) {
877
			$cond[] = $conditions;
878
		}
879
880
		$search = clone $search;
881
		$search->setConditions( $search->and( $cond ) );
882
883
		list( $find, $replace ) = $this->getSQLReplacements( $search, $attributes, $plugins, $joins, $columns );
884
885
		if( $total !== null )
886
		{
887
			$sql = str_replace( $find, $replace, $this->getSqlConfig( $cfgPathCount ) );
888
			$result = $this->getSearchResults( $conn, $sql );
889
			$row = $result->fetch();
890
			$result->finish();
891
892
			if( $row === null )
893
			{
894
				$msg = $this->context()->translate( 'mshop', 'Total results value not found' );
895
				throw new \Aimeos\MShop\Exception( $msg );
896
			}
897
898
			$total = (int) $row['count'];
899
		}
900
901
		return $this->getSearchResults( $conn, str_replace( $find, $replace, $this->getSqlConfig( $cfgPathSearch ) ) );
902
	}
903
904
905
	/**
906
	 * Deletes items.
907
	 *
908
	 * @param \Aimeos\MShop\Common\Item\Iface|\Aimeos\Map|array|string $items List of item objects or IDs of the items
909
	 * @param string $cfgpath Configuration path to the SQL statement
910
	 * @param bool $siteid If siteid should be used in the statement
911
	 * @param string $name Name of the ID column
912
	 * @return \Aimeos\MShop\Common\Manager\Iface Manager object for chaining method calls
913
	 */
914
	protected function deleteItemsBase( $items, string $cfgpath, bool $siteid = true,
915
		string $name = 'id' ) : \Aimeos\MShop\Common\Manager\Iface
916
	{
917
		if( map( $items )->isEmpty() ) {
918
			return $this;
919
		}
920
921
		$search = $this->object()->filter();
922
		$search->setConditions( $search->compare( '==', $name, $items ) );
923
924
		$types = array( $name => \Aimeos\Base\DB\Statement\Base::PARAM_STR );
925
		$translations = array( $name => '"' . $name . '"' );
926
927
		$cond = $search->getConditionSource( $types, $translations );
928
		$sql = str_replace( ':cond', $cond, $this->getSqlConfig( $cfgpath ) );
929
930
		$context = $this->context();
931
		$conn = $context->db( $this->getResourceName() );
932
933
		$stmt = $conn->create( $sql );
934
935
		if( $siteid ) {
936
			$stmt->bind( 1, $context->locale()->getSiteId() . '%' );
937
		}
938
939
		$stmt->execute()->finish();
940
941
		return $this;
942
	}
943
944
945
	/**
946
	 * Starts a database transaction on the connection identified by the given name.
947
	 *
948
	 * @param string $dbname Name of the database settings in the resource configuration
949
	 * @return \Aimeos\MShop\Common\Manager\Iface Manager object for chaining method calls
950
	 */
951
	protected function beginTransation( string $dbname = 'db' ) : \Aimeos\MShop\Common\Manager\Iface
952
	{
953
		$conn = $this->context->db( $dbname );
954
		$conn->begin();
955
956
		return $this;
957
	}
958
959
960
	/**
961
	 * Commits the running database transaction on the connection identified by the given name.
962
	 *
963
	 * @param string $dbname Name of the database settings in the resource configuration
964
	 * @return \Aimeos\MShop\Common\Manager\Iface Manager object for chaining method calls
965
	 */
966
	protected function commitTransaction( string $dbname = 'db' ) : \Aimeos\MShop\Common\Manager\Iface
967
	{
968
		$conn = $this->context->db( $dbname );
969
		$conn->commit();
970
971
		return $this;
972
	}
973
974
975
	/**
976
	 * Rolls back the running database transaction on the connection identified by the given name.
977
	 *
978
	 * @param string $dbname Name of the database settings in the resource configuration
979
	 * @return \Aimeos\MShop\Common\Manager\Iface Manager object for chaining method calls
980
	 */
981
	protected function rollbackTransaction( string $dbname = 'db' ) : \Aimeos\MShop\Common\Manager\Iface
982
	{
983
		$conn = $this->context->db( $dbname );
984
		$conn->rollback();
985
986
		return $this;
987
	}
988
989
990
	/**
991
	 * Transforms the application specific values to Aimeos standard values.
992
	 *
993
	 * @param array $values Associative list of key/value pairs from the storage
994
	 * @return array Associative list of key/value pairs with standard Aimeos values
995
	 */
996
	protected function transform( array $values ) : array
997
	{
998
		return $values;
999
	}
1000
}
1001