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

Base::cursor()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 1
c 0
b 0
f 0
dl 0
loc 3
rs 10
cc 1
nc 1
nop 1
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