Passed
Push — master ( ed0a8b...c8431f )
by Aimeos
05:25
created

Standard::bootstrapBase()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 17
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 8
nc 4
nop 8
dl 0
loc 17
rs 10
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 Locale
9
 */
10
11
12
namespace Aimeos\MShop\Locale\Manager;
13
14
15
/**
16
 * Default locale manager implementation.
17
 *
18
 * @package MShop
19
 * @subpackage Locale
20
 */
21
class Standard
22
	extends \Aimeos\MShop\Locale\Manager\Base
23
	implements \Aimeos\MShop\Locale\Manager\Iface, \Aimeos\MShop\Common\Manager\Factory\Iface
24
{
25
	private $searchConfig = array(
26
		'locale.id' => array(
27
			'code' => 'locale.id',
28
			'internalcode' => 'mloc."id"',
29
			'label' => 'ID',
30
			'type' => 'integer',
31
			'internaltype' => \Aimeos\Base\DB\Statement\Base::PARAM_INT,
32
			'public' => false,
33
		),
34
		'locale.siteid' => array(
35
			'code' => 'locale.siteid',
36
			'internalcode' => 'mloc."siteid"',
37
			'label' => 'Site ID',
38
			'type' => 'string',
39
			'internaltype' => \Aimeos\Base\DB\Statement\Base::PARAM_STR,
40
			'public' => false,
41
		),
42
		'locale.languageid' => array(
43
			'code' => 'locale.languageid',
44
			'internalcode' => 'mloc."langid"',
45
			'label' => 'Language ID',
46
			'type' => 'string',
47
			'internaltype' => \Aimeos\Base\DB\Statement\Base::PARAM_STR,
48
		),
49
		'locale.currencyid' => array(
50
			'code' => 'locale.currencyid',
51
			'internalcode' => 'mloc."currencyid"',
52
			'label' => 'Currency ID',
53
			'type' => 'string',
54
			'internaltype' => \Aimeos\Base\DB\Statement\Base::PARAM_STR,
55
		),
56
		'locale.status' => array(
57
			'code' => 'locale.status',
58
			'internalcode' => 'mloc."status"',
59
			'label' => 'Status',
60
			'type' => 'integer',
61
			'internaltype' => \Aimeos\Base\DB\Statement\Base::PARAM_INT,
62
		),
63
		'locale.position' => array(
64
			'code' => 'locale.position',
65
			'internalcode' => 'mloc."pos"',
66
			'label' => 'Position',
67
			'type' => 'integer',
68
			'internaltype' => \Aimeos\Base\DB\Statement\Base::PARAM_INT,
69
		),
70
		'locale.ctime' => array(
71
			'code' => 'locale.ctime',
72
			'internalcode' => 'mloc."ctime"',
73
			'label' => 'Create date/time',
74
			'type' => 'datetime',
75
			'internaltype' => \Aimeos\Base\DB\Statement\Base::PARAM_STR,
76
			'public' => false,
77
		),
78
		'locale.mtime' => array(
79
			'code' => 'locale.mtime',
80
			'internalcode' => 'mloc."mtime"',
81
			'label' => 'Modify date/time',
82
			'type' => 'datetime',
83
			'internaltype' => \Aimeos\Base\DB\Statement\Base::PARAM_STR,
84
			'public' => false,
85
		),
86
		'locale.editor' => array(
87
			'code' => 'locale.editor',
88
			'internalcode' => 'mloc."editor"',
89
			'label' => 'Editor',
90
			'type' => 'string',
91
			'internaltype' => \Aimeos\Base\DB\Statement\Base::PARAM_STR,
92
			'public' => false,
93
		),
94
	);
95
96
97
	/**
98
	 * Initializes the object.
99
	 *
100
	 * @param \Aimeos\MShop\ContextIface $context Context object
101
	 */
102
	public function __construct( \Aimeos\MShop\ContextIface $context )
103
	{
104
		parent::__construct( $context );
105
		$this->setResourceName( 'db-locale' );
106
	}
107
108
109
	/**
110
	 * Returns the locale item for the given site code, language code and currency code.
111
	 *
112
	 * @param string $site Site code
113
	 * @param string $lang Language code (optional)
114
	 * @param string $currency Currency code (optional)
115
	 * @param bool $active Flag to get only active items (optional)
116
	 * @param int|null $level Constant from abstract class which site ID levels should be available (optional),
117
	 * 	based on config or value for SITE_PATH if null
118
	 * @param bool $bare Allow locale items with sites only
119
	 * @return \Aimeos\MShop\Locale\Item\Iface Locale item for the given parameters
120
	 * @throws \Aimeos\MShop\Locale\Exception If no locale item is found
121
	 */
122
	public function bootstrap( string $site, string $lang = '', string $currency = '', bool $active = true, int $level = null,
123
		bool $bare = false ) : \Aimeos\MShop\Locale\Item\Iface
124
	{
125
		$siteItem = $this->object()->getSubManager( 'site' )->find( $site );
126
127
		// allow enabled sites and sites under review
128
		if( $active && $siteItem->getStatus() < 1 && $siteItem->getStatus() !== -1 ) {
129
			throw new \Aimeos\MShop\Locale\Exception( 'Site not found' );
130
		}
131
132
		$siteId = $siteItem->getSiteId();
133
		$sites = [Base::SITE_ONE => $siteId];
134
135
		return $this->bootstrapBase( $site, $lang, $currency, $active, $siteItem, $siteId, $sites, $bare );
136
	}
137
138
139
	/**
140
	 * Removes old entries from the storage.
141
	 *
142
	 * @param iterable $siteids List of IDs for sites whose entries should be deleted
143
	 * @return \Aimeos\MShop\Locale\Manager\Iface Manager object for chaining method calls
144
	 */
145
	public function clear( iterable $siteids ) : \Aimeos\MShop\Common\Manager\Iface
146
	{
147
		return $this->clearBase( $siteids, 'mshop/locale/manager/delete' );
148
	}
149
150
151
	/**
152
	 * Creates a new empty item instance
153
	 *
154
	 * @param array $values Values the item should be initialized with
155
	 * @return \Aimeos\MShop\Locale\Item\Iface New locale item object
156
	 */
157
	public function create( array $values = [] ) : \Aimeos\MShop\Common\Item\Iface
158
	{
159
		try
160
		{
161
			$values['locale.siteid'] = $this->context()->locale()->getSiteId();
162
			return $this->createItemBase( $values );
163
		}
164
		catch( \Exception $e )
165
		{
166
			return $this->createItemBase( $values );
167
		}
168
	}
169
170
171
	/**
172
	 * Creates a filter object.
173
	 *
174
	 * @param bool|null $default Add default criteria or NULL for relaxed default criteria
175
	 * @param bool $site TRUE for adding site criteria to limit items by the site of related items
176
	 * @return \Aimeos\Base\Criteria\Iface Returns the filter object
177
	 */
178
	public function filter( ?bool $default = false, bool $site = false ) : \Aimeos\Base\Criteria\Iface
179
	{
180
		return $this->filterBase( 'locale', $default );
181
	}
182
183
184
	/**
185
	 * Returns the item specified by its ID.
186
	 *
187
	 * @param string $id Unique ID of the locale item
188
	 * @param string[] $ref List of domains to fetch list items and referenced items for
189
	 * @param bool|null $default Add default criteria or NULL for relaxed default criteria
190
	 * @return \Aimeos\MShop\Locale\Item\Iface Returns the locale item of the given id
191
	 * @throws \Aimeos\MShop\Exception If item couldn't be found
192
	 */
193
	public function get( string $id, array $ref = [], ?bool $default = false ) : \Aimeos\MShop\Common\Item\Iface
194
	{
195
		return $this->getItemBase( 'locale.id', $id, $ref, $default );
196
	}
197
198
199
	/**
200
	 * Searches for all items matching the given critera.
201
	 *
202
	 * @param \Aimeos\Base\Criteria\Iface $search Criteria object with conditions, sortations, etc.
203
	 * @param string[] $ref List of domains to fetch list items and referenced items for
204
	 * @param int &$total Number of items that are available in total
205
	 * @return \Aimeos\Map List of items implementing \Aimeos\MShop\Locale\Item\Iface with ids as keys
206
	 */
207
	public function search( \Aimeos\Base\Criteria\Iface $search, array $ref = [], int &$total = null ) : \Aimeos\Map
208
	{
209
		$items = [];
210
		$level = \Aimeos\MShop\Locale\Manager\Base::SITE_PATH;
211
		$search = (clone $search)->add( $this->siteCondition( 'locale.siteid', $level ) );
212
213
		foreach( $this->searchEntries( $search, $ref, $total ) as $row )
214
		{
215
			if( $item = $this->applyFilter( $this->createItemBase( $row ) ) ) {
216
				$items[$row['locale.id']] = $item;
217
			}
218
		}
219
220
		return map( $items );
221
	}
222
223
224
	/**
225
	 * Removes multiple items.
226
	 *
227
	 * @param \Aimeos\MShop\Common\Item\Iface[]|string[] $itemIds List of item objects or IDs of the items
228
	 * @return \Aimeos\MShop\Locale\Manager\Iface Manager object for chaining method calls
229
	 */
230
	public function delete( $itemIds ) : \Aimeos\MShop\Common\Manager\Iface
231
	{
232
		/** mshop/locale/manager/delete/mysql
233
		 * Deletes the items matched by the given IDs from the database
234
		 *
235
		 * @see mshop/locale/manager/delete/ansi
236
		 */
237
238
		/** mshop/locale/manager/delete/ansi
239
		 * Deletes the items matched by the given IDs from the database
240
		 *
241
		 * Removes the records specified by the given IDs from the locale database.
242
		 * The records must be from the site that is configured via the
243
		 * context item.
244
		 *
245
		 * The ":cond" placeholder is replaced by the name of the ID column and
246
		 * the given ID or list of IDs while the site ID is bound to the question
247
		 * mark.
248
		 *
249
		 * The SQL statement should conform to the ANSI standard to be
250
		 * compatible with most relational database systems. This also
251
		 * includes using double quotes for table and column names.
252
		 *
253
		 * @param string SQL statement for deleting items
254
		 * @since 2014.03
255
		 * @category Developer
256
		 * @see mshop/locale/manager/insert/ansi
257
		 * @see mshop/locale/manager/update/ansi
258
		 * @see mshop/locale/manager/newid/ansi
259
		 * @see mshop/locale/manager/search/ansi
260
		 * @see mshop/locale/manager/count/ansi
261
		 */
262
		$path = 'mshop/locale/manager/delete';
263
264
		return $this->deleteItemsBase( $itemIds, $path );
265
	}
266
267
268
	/**
269
	 * Adds or updates an item object.
270
	 *
271
	 * @param \Aimeos\MShop\Locale\Item\Iface $item Item object whose data should be saved
272
	 * @param bool $fetch True if the new ID should be returned in the item
273
	 * @return \Aimeos\MShop\Locale\Item\Iface $item Updated item including the generated ID
274
	 */
275
	public function saveItem( \Aimeos\MShop\Locale\Item\Iface $item, bool $fetch = true ) : \Aimeos\MShop\Locale\Item\Iface
276
	{
277
		if( !$item->isModified() ) {
278
			return $item;
279
		}
280
281
		$context = $this->context();
282
		$conn = $context->db( $this->getResourceName() );
283
284
			$id = $item->getId();
285
			$date = date( 'Y-m-d H:i:s' );
286
			$columns = $this->object()->getSaveAttributes();
287
288
			if( $id === null )
289
			{
290
				/** mshop/locale/manager/insert/mysql
291
				 * Inserts a new locale record into the database table
292
				 *
293
				 * @see mshop/locale/manager/insert/ansi
294
				 */
295
296
				/** mshop/locale/manager/insert/ansi
297
				 * Inserts a new locale record into the database table
298
				 *
299
				 * Items with no ID yet (i.e. the ID is NULL) will be created in
300
				 * the database and the newly created ID retrieved afterwards
301
				 * using the "newid" SQL statement.
302
				 *
303
				 * The SQL statement must be a string suitable for being used as
304
				 * prepared statement. It must include question marks for binding
305
				 * the values from the locale item to the statement before they are
306
				 * sent to the database server. The number of question marks must
307
				 * be the same as the number of columns listed in the INSERT
308
				 * statement. The order of the columns must correspond to the
309
				 * order in the save() method, so the correct values are
310
				 * bound to the columns.
311
				 *
312
				 * The SQL statement should conform to the ANSI standard to be
313
				 * compatible with most relational database systems. This also
314
				 * includes using double quotes for table and column names.
315
				 *
316
				 * @param string SQL statement for inserting records
317
				 * @since 2014.03
318
				 * @category Developer
319
				 * @see mshop/locale/manager/update/ansi
320
				 * @see mshop/locale/manager/newid/ansi
321
				 * @see mshop/locale/manager/delete/ansi
322
				 * @see mshop/locale/manager/search/ansi
323
				 * @see mshop/locale/manager/count/ansi
324
				 */
325
				$path = 'mshop/locale/manager/insert';
326
				$sql = $this->addSqlColumns( array_keys( $columns ), $this->getSqlConfig( $path ) );
0 ignored issues
show
Bug introduced by
It seems like $this->getSqlConfig($path) can also be of type array; however, parameter $sql of Aimeos\MShop\Common\Manager\Base::addSqlColumns() 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

326
				$sql = $this->addSqlColumns( array_keys( $columns ), /** @scrutinizer ignore-type */ $this->getSqlConfig( $path ) );
Loading history...
327
			}
328
			else
329
			{
330
				/** mshop/locale/manager/update/mysql
331
				 * Updates an existing locale record in the database
332
				 *
333
				 * @see mshop/locale/manager/update/ansi
334
				 */
335
336
				/** mshop/locale/manager/update/ansi
337
				 * Updates an existing locale record in the database
338
				 *
339
				 * Items which already have an ID (i.e. the ID is not NULL) will
340
				 * be updated in the database.
341
				 *
342
				 * The SQL statement must be a string suitable for being used as
343
				 * prepared statement. It must include question marks for binding
344
				 * the values from the locale item to the statement before they are
345
				 * sent to the database server. The order of the columns must
346
				 * correspond to the order in the save() method, so the
347
				 * correct values are bound to the columns.
348
				 *
349
				 * The SQL statement should conform to the ANSI standard to be
350
				 * compatible with most relational database systems. This also
351
				 * includes using double quotes for table and column names.
352
				 *
353
				 * @param string SQL statement for updating records
354
				 * @since 2014.03
355
				 * @category Developer
356
				 * @see mshop/locale/manager/insert/ansi
357
				 * @see mshop/locale/manager/newid/ansi
358
				 * @see mshop/locale/manager/delete/ansi
359
				 * @see mshop/locale/manager/search/ansi
360
				 * @see mshop/locale/manager/count/ansi
361
				 */
362
				$path = 'mshop/locale/manager/update';
363
				$sql = $this->addSqlColumns( array_keys( $columns ), $this->getSqlConfig( $path ), false );
364
			}
365
366
			$idx = 1;
367
			$stmt = $this->getCachedStatement( $conn, $path, $sql );
368
369
			foreach( $columns as $name => $entry ) {
370
				$stmt->bind( $idx++, $item->get( $name ), $entry->getInternalType() );
371
			}
372
373
			$stmt->bind( $idx++, $item->getLanguageId() );
374
			$stmt->bind( $idx++, $item->getCurrencyId() );
375
			$stmt->bind( $idx++, $item->getPosition(), \Aimeos\Base\DB\Statement\Base::PARAM_INT );
376
			$stmt->bind( $idx++, $item->getStatus(), \Aimeos\Base\DB\Statement\Base::PARAM_INT );
377
			$stmt->bind( $idx++, $date ); // mtime
378
			$stmt->bind( $idx++, $context->editor() );
379
			$stmt->bind( $idx++, $item->get( 'site_id' ), \Aimeos\Base\DB\Statement\Base::PARAM_INT );
380
			$stmt->bind( $idx++, $item->getSiteId() );
381
382
			if( $id !== null ) {
383
				$stmt->bind( $idx++, $id, \Aimeos\Base\DB\Statement\Base::PARAM_INT );
384
			} else {
385
				$stmt->bind( $idx++, $date ); // ctime
386
			}
387
388
			$stmt->execute()->finish();
389
390
			if( $id === null && $fetch === true )
391
			{
392
				/** mshop/locale/manager/newid/mysql
393
				 * Retrieves the ID generated by the database when inserting a new record
394
				 *
395
				 * @see mshop/locale/manager/newid/ansi
396
				 */
397
398
				/** mshop/locale/manager/newid/ansi
399
				 * Retrieves the ID generated by the database when inserting a new record
400
				 *
401
				 * As soon as a new record is inserted into the database table,
402
				 * the database server generates a new and unique identifier for
403
				 * that record. This ID can be used for retrieving, updating and
404
				 * deleting that specific record from the table again.
405
				 *
406
				 * For MySQL:
407
				 *  SELECT LAST_INSERT_ID()
408
				 * For PostgreSQL:
409
				 *  SELECT currval('seq_mloc_id')
410
				 * For SQL Server:
411
				 *  SELECT SCOPE_IDENTITY()
412
				 * For Oracle:
413
				 *  SELECT "seq_mloc_id".CURRVAL FROM DUAL
414
				 *
415
				 * There's no way to retrive the new ID by a SQL statements that
416
				 * fits for most database servers as they implement their own
417
				 * specific way.
418
				 *
419
				 * @param string SQL statement for retrieving the last inserted record ID
420
				 * @since 2014.03
421
				 * @category Developer
422
				 * @see mshop/locale/manager/insert/ansi
423
				 * @see mshop/locale/manager/update/ansi
424
				 * @see mshop/locale/manager/delete/ansi
425
				 * @see mshop/locale/manager/search/ansi
426
				 * @see mshop/locale/manager/count/ansi
427
				 */
428
				$path = 'mshop/locale/manager/newid';
429
				$id = $this->newId( $conn, $path );
430
			}
431
432
			$item->setId( $id );
433
434
		return $item;
435
	}
436
437
438
	/**
439
	 * Returns a new manager for locale extensions
440
	 *
441
	 * @param string $manager Name of the sub manager type in lower case
442
	 * @param string|null $name Name of the implementation, will be from configuration (or Default) if null
443
	 * @return \Aimeos\MShop\Common\Manager\Iface Manager for different extensions, e.g site, language, currency.
444
	 */
445
	public function getSubManager( string $manager, string $name = null ) : \Aimeos\MShop\Common\Manager\Iface
446
	{
447
		return $this->getSubManagerBase( 'locale', $manager, $name );
448
	}
449
450
451
	/**
452
	 * Returns the available manager types
453
	 *
454
	 * @param bool $withsub Return also the resource type of sub-managers if true
455
	 * @return string[] Type of the manager and submanagers, subtypes are separated by slashes
456
	 */
457
	public function getResourceType( bool $withsub = true ) : array
458
	{
459
		$path = 'mshop/locale/manager/submanagers';
460
		return $this->getResourceTypeBase( 'locale', $path, array( 'currency', 'language', 'site' ), $withsub );
461
	}
462
463
464
	/**
465
	 * Returns the attributes that can be used for searching.
466
	 *
467
	 * @param bool $withsub Return also attributes of sub-managers if true
468
	 * @return \Aimeos\Base\Criteria\Attribute\Iface[] List of search attribute items
469
	 */
470
	public function getSearchAttributes( bool $withsub = true ) : array
471
	{
472
		/** mshop/locale/manager/submanagers
473
		 * List of manager names that can be instantiated by the locale manager
474
		 *
475
		 * Managers provide a generic interface to the underlying storage.
476
		 * Each manager has or can have sub-managers caring about particular
477
		 * aspects. Each of these sub-managers can be instantiated by its
478
		 * parent manager using the getSubManager() method.
479
		 *
480
		 * The search keys from sub-managers can be normally used in the
481
		 * manager as well. It allows you to search for items of the manager
482
		 * using the search keys of the sub-managers to further limit the
483
		 * retrieved list of items.
484
		 *
485
		 * @param array List of sub-manager names
486
		 * @since 2014.03
487
		 * @category Developer
488
		 */
489
		$path = 'mshop/locale/manager/submanagers';
490
		$default = array( 'language', 'currency', 'site' );
491
492
		return $this->getSearchAttributesBase( $this->searchConfig, $path, $default, $withsub );
493
	}
494
495
496
	/**
497
	 * Returns the locale item for the given site code, language code and currency code.
498
	 *
499
	 * If the locale item is inherited from a parent site, the site ID of this locale item
500
	 * is changed to the site ID of the actual site. This ensures that items assigned to
501
	 * the same site as the site item are still used.
502
	 *
503
	 * @param string $site Site code
504
	 * @param string $lang Language code
505
	 * @param string $currency Currency code
506
	 * @param bool $active Flag to get only active items
507
	 * @param \Aimeos\MShop\Locale\Item\Site\Iface $siteItem Site item
508
	 * @param string $siteId Site ID
509
	 * @param array $sites Associative list of site constant as key and sites as values
510
	 * @param bool $bare Allow locale items with sites only
511
	 * @return \Aimeos\MShop\Locale\Item\Iface Locale item for the given parameters
512
	 * @throws \Aimeos\MShop\Locale\Exception If no locale item is found
513
	 */
514
	protected function bootstrapBase( string $site, string $lang, string $currency, bool $active,
515
		\Aimeos\MShop\Locale\Item\Site\Iface $siteItem, string $siteId, array $sites, bool $bare ) : \Aimeos\MShop\Locale\Item\Iface
516
	{
517
		if( $result = $this->bootstrapMatch( $siteId, $lang, $currency, $active, $siteItem, $sites ) ) {
518
			return $result;
519
		}
520
521
		if( $result = $this->bootstrapClosest( $siteId, $lang, $active, $siteItem, $sites ) ) {
522
			return $result;
523
		}
524
525
		if( $bare === true ) {
526
			return $this->createItemBase( ['locale.siteid' => $siteId], $siteItem, $sites );
527
		}
528
529
		$msg = $this->context()->translate( 'mshop', 'Locale item for site "%1$s" not found' );
530
		throw new \Aimeos\MShop\Locale\Exception( sprintf( $msg, $site ) );
531
	}
532
533
534
	/**
535
	 * Returns the matching locale item for the given site code, language code and currency code.
536
	 *
537
	 * If the locale item is inherited from a parent site, the site ID of this locale item
538
	 * is changed to the site ID of the actual site. This ensures that items assigned to
539
	 * the same site as the site item are still used.
540
	 *
541
	 * @param string $siteId Site ID
542
	 * @param string $lang Language code
543
	 * @param string $currency Currency code
544
	 * @param bool $active Flag to get only active items
545
	 * @param \Aimeos\MShop\Locale\Item\Site\Iface $siteItem Site item
546
	 * @param array $sites Associative list of site constant as key and sites as values
547
	 * @return \Aimeos\MShop\Locale\Item\Iface|null Locale item for the given parameters or null if no item was found
548
	 */
549
	private function bootstrapMatch( string $siteId, string $lang, string $currency, bool $active,
550
		\Aimeos\MShop\Locale\Item\Site\Iface $siteItem, array $sites ) : ?\Aimeos\MShop\Locale\Item\Iface
551
	{
552
		// Try to find exact match
553
		$search = $this->object()->filter( $active );
554
555
		$expr = array( $search->compare( '==', 'locale.siteid', $sites[Base::SITE_PATH] ?? $sites[Base::SITE_ONE] ) );
556
557
		if( !empty( $lang ) )
558
		{
559
			$langIds = strlen( $lang ) > 2 ? [$lang, substr( $lang, 0, 2 )] : [$lang];
560
			$expr[] = $search->compare( '==', 'locale.languageid', $langIds );
561
		}
562
563
		if( !empty( $currency ) ) {
564
			$expr[] = $search->compare( '==', 'locale.currencyid', $currency );
565
		}
566
567
		$expr[] = $search->getConditions();
568
569
570
		if( $active === true )
571
		{
572
			$expr[] = $search->compare( '>', 'locale.currency.status', 0 );
573
			$expr[] = $search->compare( '>', 'locale.language.status', 0 );
574
			$expr[] = $search->compare( '>', 'locale.site.status', 0 );
575
		}
576
577
		$search->setConditions( $search->and( $expr ) );
578
		$search->setSortations( array( $search->sort( '+', 'locale.position' ) ) );
579
		$result = $this->searchEntries( $search );
580
581
		// Try to find first item where site matches
582
		foreach( $result as $row )
583
		{
584
			if( $row['locale.siteid'] === $siteId ) {
585
				return $this->createItemBase( $row, $siteItem, $sites );
586
			}
587
		}
588
589
		if( ( $row = reset( $result ) ) !== false )
590
		{
591
			$row['locale.siteid'] = $siteId;
592
			return $this->createItemBase( $row, $siteItem, $sites );
593
		}
594
595
		return null;
596
	}
597
598
599
	/**
600
	 * Returns the locale item for the given site code, language code and currency code.
601
	 *
602
	 * If the locale item is inherited from a parent site, the site ID of this locale item
603
	 * is changed to the site ID of the actual site. This ensures that items assigned to
604
	 * the same site as the site item are still used.
605
	 *
606
	 * @param string $siteId Site ID
607
	 * @param string $lang Language code
608
	 * @param bool $active Flag to get only active items
609
	 * @param \Aimeos\MShop\Locale\Item\Site\Iface $siteItem Site item
610
	 * @param array $sites Associative list of site constant as key and sites as values
611
	 * @return \Aimeos\MShop\Locale\Item\Iface|null Locale item for the given parameters or null if no item was found
612
	 */
613
	private function bootstrapClosest( string $siteId, string $lang, bool $active,
614
		\Aimeos\MShop\Locale\Item\Site\Iface $siteItem, array $sites ) : ?\Aimeos\MShop\Locale\Item\Iface
615
	{
616
		// Try to find the best matching locale
617
		$search = $this->object()->filter( $active );
618
619
		$expr = array(
620
			$search->compare( '==', 'locale.siteid', $sites[Base::SITE_PATH] ?? $sites[Base::SITE_ONE] ),
621
			$search->getConditions()
622
		);
623
624
		if( $active === true )
625
		{
626
			$expr[] = $search->compare( '>', 'locale.currency.status', 0 );
627
			$expr[] = $search->compare( '>', 'locale.language.status', 0 );
628
			$expr[] = $search->compare( '>', 'locale.site.status', 0 );
629
		}
630
631
		$search->setConditions( $search->and( $expr ) );
632
		$search->setSortations( array( $search->sort( '+', 'locale.position' ) ) );
633
		$result = $this->searchEntries( $search );
634
635
		$langIds = strlen( $lang ) > 2 ? [$lang, substr( $lang, 0, 2 )] : [$lang];
636
637
		// Try to find first item where site and language matches
638
		foreach( $result as $row )
639
		{
640
			if( $row['locale.siteid'] === $siteId && in_array( $row['locale.languageid'], $langIds, true ) ) {
641
				return $this->createItemBase( $row, $siteItem, $sites );
642
			}
643
		}
644
645
		// Try to find first item where language matches
646
		foreach( $result as $row )
647
		{
648
			if( in_array( $row['locale.languageid'], $langIds, true ) )
649
			{
650
				$row['locale.siteid'] = $siteId;
651
				return $this->createItemBase( $row, $siteItem, $sites );
652
			}
653
		}
654
655
		// Try to find first item where site matches
656
		foreach( $result as $row )
657
		{
658
			if( $row['locale.siteid'] === $siteId ) {
659
				return $this->createItemBase( $row, $siteItem, $sites );
660
			}
661
		}
662
663
		// Return first item (no other match found)
664
		if( ( $row = reset( $result ) ) !== false )
665
		{
666
			$row['locale.siteid'] = $siteId;
667
			return $this->createItemBase( $row, $siteItem, $sites );
668
		}
669
670
		return null;
671
	}
672
673
674
	/**
675
	 * Instances a new locale item object.
676
	 *
677
	 * @param array $values Parameter to initialise the item
678
	 * @param \Aimeos\MShop\Locale\Item\Site\Iface|null $site Site item
679
	 * @param array $sites Associative list of site constant as key and sites as values
680
	 * @return \Aimeos\MShop\Locale\Item\Iface Locale item
681
	 */
682
	protected function createItemBase( array $values = [], \Aimeos\MShop\Locale\Item\Site\Iface $site = null,
683
		array $sites = [] ) : \Aimeos\MShop\Locale\Item\Iface
684
	{
685
		return new \Aimeos\MShop\Locale\Item\Standard( $values, $site, $sites );
686
	}
687
688
689
	/**
690
	 * Returns the search results for the given SQL statement.
691
	 *
692
	 * @param \Aimeos\Base\DB\Connection\Iface $conn Database connection
693
	 * @param string $sql SQL statement
694
	 * @return \Aimeos\Base\DB\Result\Iface Search result object
695
	 */
696
	protected function getSearchResults( \Aimeos\Base\DB\Connection\Iface $conn, string $sql ) : \Aimeos\Base\DB\Result\Iface
697
	{
698
		$time = microtime( true );
699
700
		$stmt = $conn->create( $sql );
701
		$result = $stmt->execute();
702
703
		$msg = 'Time: ' . ( microtime( true ) - $time ) * 1000 . "ms\n"
704
			. 'Class: ' . get_class( $this ) . "\n"
705
			. str_replace( ["\t", "\n\n"], ['', "\n"], trim( (string) $stmt ) );
706
707
		$this->context()->logger()->debug( $msg, 'core/sql' );
708
709
		return $result;
710
	}
711
712
713
	/**
714
	 * Searches for all items matching the given critera.
715
	 *
716
	 * @param \Aimeos\Base\Criteria\Iface $search Criteria object with conditions, sortations, etc.
717
	 * @param string[] $ref List of domains to fetch list items and referenced items for
718
	 * @param int &$total Number of items that are available in total
719
	 * @return array Associative list of key/value pairs
720
	 */
721
	protected function searchEntries( \Aimeos\Base\Criteria\Iface $search, array $ref = [], int &$total = null ) : array
722
	{
723
		$map = [];
724
		$context = $this->context();
725
		$conn = $context->db( $this->getResourceName() );
726
727
			$attributes = $this->object()->getSearchAttributes();
728
			$translations = $this->getSearchTranslations( $attributes );
729
			$types = $this->getSearchTypes( $attributes );
730
			$columns = $this->object()->getSaveAttributes();
731
			$sortcols = $search->translate( $search->getSortations(), $translations );
732
733
			if( empty( $search->getSortations() ) && ( $attribute = reset( $attributes ) ) !== false ) {
734
				$search = ( clone $search )->setSortations( [$search->sort( '+', $attribute->getCode() )] );
735
			}
736
737
			$colstring = '';
738
			foreach( $columns as $name => $entry ) {
739
				$colstring .= $entry->getInternalCode() . ', ';
740
			}
741
742
			$find = array( ':columns', ':cond', ':order', ':group', ':start', ':size' );
743
			$replace = array(
744
				$colstring . ( $sortcols ? join( ', ', $sortcols ) . ', ' : '' ),
745
				$search->getConditionSource( $types, $translations ),
746
				$search->getSortationSource( $types, $translations ),
747
				implode( ', ', $search->translate( $search->getSortations(), $translations ) ) . ', ',
748
				$search->getOffset(),
749
				$search->getLimit(),
750
			);
751
752
			/** mshop/locale/manager/search/mysql
753
			 * Retrieves the records matched by the given criteria in the database
754
			 *
755
			 * @see mshop/locale/manager/search/ansi
756
			 */
757
758
			/** mshop/locale/manager/search/ansi
759
			 * Retrieves the records matched by the given criteria in the database
760
			 *
761
			 * Fetches the records matched by the given criteria from the locale
762
			 * database. The records must be from one of the sites that are
763
			 * configured via the context item. If the current site is part of
764
			 * a tree of sites, the SELECT statement can retrieve all records
765
			 * from the current site and the complete sub-tree of sites.
766
			 *
767
			 * To limit the records matched, conditions can be added to the given
768
			 * criteria object. It can contain comparisons like column names that
769
			 * must match specific values which can be combined by AND, OR or NOT
770
			 * operators. The resulting string of SQL conditions replaces the
771
			 * ":cond" placeholder before the statement is sent to the database
772
			 * server.
773
			 *
774
			 * If the records that are retrieved should be ordered by one or more
775
			 * columns, the generated string of column / sort direction pairs
776
			 * replaces the ":order" placeholder. In case no ordering is required,
777
			 * the complete ORDER BY part including the "\/*-orderby*\/...\/*orderby-*\/"
778
			 * markers is removed to speed up retrieving the records. Columns of
779
			 * sub-managers can also be used for ordering the result set but then
780
			 * no index can be used.
781
			 *
782
			 * The number of returned records can be limited and can start at any
783
			 * number between the begining and the end of the result set. For that
784
			 * the ":size" and ":start" placeholders are replaced by the
785
			 * corresponding values from the criteria object. The default values
786
			 * are 0 for the start and 100 for the size value.
787
			 *
788
			 * The SQL statement should conform to the ANSI standard to be
789
			 * compatible with most relational database systems. This also
790
			 * includes using double quotes for table and column names.
791
			 *
792
			 * @param string SQL statement for searching items
793
			 * @since 2014.03
794
			 * @category Developer
795
			 * @see mshop/locale/manager/insert/ansi
796
			 * @see mshop/locale/manager/update/ansi
797
			 * @see mshop/locale/manager/newid/ansi
798
			 * @see mshop/locale/manager/delete/ansi
799
			 * @see mshop/locale/manager/count/ansi
800
			 */
801
			$path = 'mshop/locale/manager/search';
802
803
			$sql = $this->getSqlConfig( $path );
804
			$results = $this->getSearchResults( $conn, str_replace( $find, $replace, $sql ) );
805
806
			try
807
			{
808
				while( ( $row = $results->fetch() ) !== null ) {
809
					$map[$row['locale.id']] = $row;
810
				}
811
			}
812
			catch( \Exception $e )
813
			{
814
				$results->finish();
815
				throw $e;
816
			}
817
818
			if( $total !== null )
819
			{
820
				/** mshop/locale/manager/count/mysql
821
				 * Counts the number of records matched by the given criteria in the database
822
				 *
823
				 * @see mshop/locale/manager/count/ansi
824
				 */
825
826
				/** mshop/locale/manager/count/ansi
827
				 * Counts the number of records matched by the given criteria in the database
828
				 *
829
				 * Counts all records matched by the given criteria from the locale
830
				 * database. The records must be from one of the sites that are
831
				 * configured via the context item. If the current site is part of
832
				 * a tree of sites, the statement can count all records from the
833
				 * current site and the complete sub-tree of sites.
834
				 *
835
				 * To limit the records matched, conditions can be added to the given
836
				 * criteria object. It can contain comparisons like column names that
837
				 * must match specific values which can be combined by AND, OR or NOT
838
				 * operators. The resulting string of SQL conditions replaces the
839
				 * ":cond" placeholder before the statement is sent to the database
840
				 * server.
841
				 *
842
				 * Both, the strings for ":joins" and for ":cond" are the same as for
843
				 * the "search" SQL statement.
844
				 *
845
				 * Contrary to the "search" statement, it doesn't return any records
846
				 * but instead the number of records that have been found. As counting
847
				 * thousands of records can be a long running task, the maximum number
848
				 * of counted records is limited for performance reasons.
849
				 *
850
				 * The SQL statement should conform to the ANSI standard to be
851
				 * compatible with most relational database systems. This also
852
				 * includes using double quotes for table and column names.
853
				 *
854
				 * @param string SQL statement for counting items
855
				 * @since 2014.03
856
				 * @category Developer
857
				 * @see mshop/locale/manager/insert/ansi
858
				 * @see mshop/locale/manager/update/ansi
859
				 * @see mshop/locale/manager/newid/ansi
860
				 * @see mshop/locale/manager/delete/ansi
861
				 * @see mshop/locale/manager/search/ansi
862
				 */
863
				$path = 'mshop/locale/manager/count';
864
865
				$sql = $this->getSqlConfig( $path );
866
				$results = $this->getSearchResults( $conn, str_replace( $find, $replace, $sql ) );
867
868
				$row = $results->fetch();
869
				$results->finish();
870
871
				if( $row === null )
872
				{
873
					$msg = $this->context()->translate( 'mshop', 'Total results value not found' );
874
					throw new \Aimeos\MShop\Locale\Exception( $msg );
875
				}
876
877
				$total = $row['count'];
878
			}
879
880
		return $map;
881
	}
882
}
883