Passed
Push — master ( e78dfc...695473 )
by Aimeos
04:21
created

Base::classes()   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 0
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;
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 search critera object
75
	 *
76
	 * @param bool|null $default Add default criteria or NULL for relaxed default criteria
77
	 * @param bool $site TRUE for adding site criteria to limit items by the site of related items
78
	 * @return \Aimeos\Base\Criteria\Iface New search criteria object
79
	 */
80
	public function filter( ?bool $default = false, bool $site = false ) : \Aimeos\Base\Criteria\Iface
81
	{
82
		$db = $this->getResourceName();
83
		$conn = $this->context->db( $db );
84
		$config = $this->context->config();
85
86
		if( ( $adapter = $config->get( 'resource/' . $db . '/adapter' ) ) === null ) {
87
			$adapter = $config->get( 'resource/db/adapter' );
88
		}
89
90
		switch( $adapter )
91
		{
92
			case 'pgsql':
93
				$search = new \Aimeos\Base\Criteria\PgSQL( $conn ); break;
94
			default:
95
				$search = new \Aimeos\Base\Criteria\SQL( $conn ); break;
96
		}
97
98
		return $search;
99
	}
100
101
102
	/**
103
	 * Adds or updates an item object or a list of them.
104
	 *
105
	 * @param \Aimeos\Map|\Aimeos\MShop\Common\Item\Iface[]|\Aimeos\MShop\Common\Item\Iface $items Item or list of items whose data should be saved
106
	 * @param bool $fetch True if the new ID should be returned in the item
107
	 * @return \Aimeos\Map|\Aimeos\MShop\Common\Item\Iface Saved item or items
108
	 */
109
	public function save( $items, bool $fetch = true )
110
	{
111
		if( is_iterable( $items ) )
112
		{
113
			foreach( $items as $id => $item ) {
114
				$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

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

297
				$acols[] = /** @scrutinizer ignore-type */ $attrList[$string]->getInternalCode() . ' AS "' . $string . '"';
Loading history...
298
299
				/** @todo Required to get the joins, but there should be a better way */
300
				$search->add( [$string => null], '!=' );
301
			}
302
			$search->add( [$valkey => null], '!=' );
303
304
			$sql = $this->getSqlConfig( $cfgPath );
305
			$sql = str_replace( ':cols', join( ', ', $cols ), $sql );
306
			$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 288. Are you sure the iterator is never empty, otherwise this variable is not defined?
Loading history...
307
			$sql = str_replace( ':keys', '"' . join( '", "', $keys ) . '"', $sql );
308
			$sql = str_replace( ':val', $attrList[$value]->getInternalCode(), $sql );
309
			$sql = str_replace( ':type', $type ?: 'count', $sql );
310
311
			$results = $this->searchItemsBase( $conn, $search, $sql, '', $required, $total, $level );
312
313
			while( ( $row = $results->fetch() ) !== null )
314
			{
315
				$row = $this->transform( $row );
316
317
				$temp = &$map;
318
				$last = array_pop( $row );
319
320
				foreach( $row as $val ) {
321
					$temp[$val] = $temp[$val] ?? [];
322
					$temp = &$temp[$val];
323
				}
324
				$temp = $last;
325
			}
326
327
		return map( $map );
328
	}
329
330
331
	/**
332
	 * Returns the newly created ID for the last record which was inserted.
333
	 *
334
	 * @param \Aimeos\Base\DB\Connection\Iface $conn Database connection used to insert the new record
335
	 * @param string $cfgpath Configuration path to the SQL statement for retrieving the new ID of the last inserted record
336
	 * @return string ID of the last record that was inserted by using the given connection
337
	 * @throws \Aimeos\MShop\Exception if there's no ID of the last record available
338
	 */
339
	protected function newId( \Aimeos\Base\DB\Connection\Iface $conn, string $cfgpath ) : string
340
	{
341
		$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

341
		$result = $conn->create( /** @scrutinizer ignore-type */ $this->getSqlConfig( $cfgpath ) )->execute();
Loading history...
342
343
		if( ( $row = $result->fetch( \Aimeos\Base\DB\Result\Base::FETCH_NUM ) ) === false )
344
		{
345
			$msg = $this->context()->translate( 'mshop', 'ID of last inserted database record not available' );
346
			throw new \Aimeos\MShop\Exception( $msg );
347
		}
348
		$result->finish();
349
350
		return $row[0];
351
	}
352
353
354
	/**
355
	 * Removes old entries from the storage.
356
	 *
357
	 * @param iterable $siteids List of IDs for sites whose entries should be deleted
358
	 * @param string $cfgpath Configuration key to the cleanup statement
359
	 * @return \Aimeos\MShop\Common\Manager\Iface Manager object for chaining method calls
360
	 */
361
	protected function clearBase( iterable $siteids, string $cfgpath ) : \Aimeos\MShop\Common\Manager\Iface
362
	{
363
		if( empty( $siteids ) ) {
364
			return $this;
365
		}
366
367
		$conn = $this->context->db( $this->getResourceName() );
368
369
		$sql = $this->getSqlConfig( $cfgpath );
370
		$sql = str_replace( ':cond', '1=1', $sql );
371
372
		$stmt = $conn->create( $sql );
373
374
		foreach( $siteids as $siteid )
375
		{
376
			$stmt->bind( 1, $siteid );
377
			$stmt->execute()->finish();
378
		}
379
380
		return $this;
381
	}
382
383
384
	/**
385
	 * Creates the criteria attribute items from the list of entries
386
	 *
387
	 * @param array $list Associative array of code as key and array with properties as values
388
	 * @return \Aimeos\Base\Criteria\Attribute\Standard[] List of criteria attribute items
389
	 */
390
	protected function createAttributes( array $list ) : array
391
	{
392
		$attr = [];
393
394
		foreach( $list as $key => $fields ) {
395
			$attr[$key] = new \Aimeos\Base\Criteria\Attribute\Standard( $fields );
396
		}
397
398
		return $attr;
399
	}
400
401
402
	/**
403
	 * Sets the base criteria "status".
404
	 * (setConditions overwrites the base criteria)
405
	 *
406
	 * @param string $domain Name of the domain/sub-domain like "product" or "product.list"
407
	 * @param bool|null $default TRUE for status=1, NULL for status>0, FALSE for no restriction
408
	 * @return \Aimeos\Base\Criteria\Iface Search critery object
409
	 */
410
	protected function filterBase( string $domain, ?bool $default = true ) : \Aimeos\Base\Criteria\Iface
411
	{
412
		$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

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

557
		throw new \Aimeos\MShop\Exception( sprintf( $msg, /** @scrutinizer ignore-type */ print_r( $pairs, true ) ) );
Loading history...
558
	}
559
560
561
	/**
562
	 * Returns the cached statement for the given key or creates a new prepared statement.
563
	 * If no SQL string is given, the key is used to retrieve the SQL string from the configuration.
564
	 *
565
	 * @param \Aimeos\Base\DB\Connection\Iface $conn Database connection
566
	 * @param string $cfgkey Unique key for the SQL
567
	 * @param string|null $sql SQL string if it shouldn't be retrieved from the configuration
568
	 * @return \Aimeos\Base\DB\Statement\Iface Database statement object
569
	 */
570
	protected function getCachedStatement( \Aimeos\Base\DB\Connection\Iface $conn, string $cfgkey,
571
		string $sql = null ) : \Aimeos\Base\DB\Statement\Iface
572
	{
573
		if( !isset( $this->stmts['stmt'][$cfgkey] )
574
			|| !isset( $this->stmts['conn'][$cfgkey] )
575
			|| $conn !== $this->stmts['conn'][$cfgkey]
576
		) {
577
			if( $sql === null ) {
578
				$sql = $this->getSqlConfig( $cfgkey );
579
			}
580
581
			$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

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