Passed
Push — master ( 6aab01...0294bc )
by Aimeos
09:44
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
		if( $active && $siteItem->getStatus() < 1 ) {
128
			throw new \Aimeos\MShop\Locale\Exception( 'Site not found' );
129
		}
130
131
		$siteId = $siteItem->getSiteId();
132
		$sites = [Base::SITE_ONE => $siteId];
133
134
		return $this->bootstrapBase( $site, $lang, $currency, $active, $siteItem, $siteId, $sites, $bare );
135
	}
136
137
138
	/**
139
	 * Removes old entries from the storage.
140
	 *
141
	 * @param iterable $siteids List of IDs for sites whose entries should be deleted
142
	 * @return \Aimeos\MShop\Locale\Manager\Iface Manager object for chaining method calls
143
	 */
144
	public function clear( iterable $siteids ) : \Aimeos\MShop\Common\Manager\Iface
145
	{
146
		return $this->clearBase( $siteids, 'mshop/locale/manager/delete' );
147
	}
148
149
150
	/**
151
	 * Creates a new empty item instance
152
	 *
153
	 * @param array $values Values the item should be initialized with
154
	 * @return \Aimeos\MShop\Locale\Item\Iface New locale item object
155
	 */
156
	public function create( array $values = [] ) : \Aimeos\MShop\Common\Item\Iface
157
	{
158
		try
159
		{
160
			$values['locale.siteid'] = $this->context()->locale()->getSiteId();
161
			return $this->createItemBase( $values );
162
		}
163
		catch( \Exception $e )
164
		{
165
			return $this->createItemBase( $values );
166
		}
167
	}
168
169
170
	/**
171
	 * Creates a filter object.
172
	 *
173
	 * @param bool|null $default Add default criteria or NULL for relaxed default criteria
174
	 * @param bool $site TRUE for adding site criteria to limit items by the site of related items
175
	 * @return \Aimeos\Base\Criteria\Iface Returns the filter object
176
	 */
177
	public function filter( ?bool $default = false, bool $site = false ) : \Aimeos\Base\Criteria\Iface
178
	{
179
		return $this->filterBase( 'locale', $default );
180
	}
181
182
183
	/**
184
	 * Returns the item specified by its ID.
185
	 *
186
	 * @param string $id Unique ID of the locale item
187
	 * @param string[] $ref List of domains to fetch list items and referenced items for
188
	 * @param bool|null $default Add default criteria or NULL for relaxed default criteria
189
	 * @return \Aimeos\MShop\Locale\Item\Iface Returns the locale item of the given id
190
	 * @throws \Aimeos\MShop\Exception If item couldn't be found
191
	 */
192
	public function get( string $id, array $ref = [], ?bool $default = false ) : \Aimeos\MShop\Common\Item\Iface
193
	{
194
		return $this->getItemBase( 'locale.id', $id, $ref, $default );
195
	}
196
197
198
	/**
199
	 * Searches for all items matching the given critera.
200
	 *
201
	 * @param \Aimeos\Base\Criteria\Iface $search Criteria object with conditions, sortations, etc.
202
	 * @param string[] $ref List of domains to fetch list items and referenced items for
203
	 * @param int &$total Number of items that are available in total
204
	 * @return \Aimeos\Map List of items implementing \Aimeos\MShop\Locale\Item\Iface with ids as keys
205
	 */
206
	public function search( \Aimeos\Base\Criteria\Iface $search, array $ref = [], int &$total = null ) : \Aimeos\Map
207
	{
208
		$items = [];
209
		$level = \Aimeos\MShop\Locale\Manager\Base::SITE_PATH;
210
		$search = (clone $search)->add( $this->siteCondition( 'locale.siteid', $level ) );
211
212
		foreach( $this->searchEntries( $search, $ref, $total ) as $row )
213
		{
214
			if( $item = $this->applyFilter( $this->createItemBase( $row ) ) ) {
215
				$items[$row['locale.id']] = $item;
216
			}
217
		}
218
219
		return map( $items );
220
	}
221
222
223
	/**
224
	 * Removes multiple items.
225
	 *
226
	 * @param \Aimeos\MShop\Common\Item\Iface[]|string[] $itemIds List of item objects or IDs of the items
227
	 * @return \Aimeos\MShop\Locale\Manager\Iface Manager object for chaining method calls
228
	 */
229
	public function delete( $itemIds ) : \Aimeos\MShop\Common\Manager\Iface
230
	{
231
		/** mshop/locale/manager/delete/mysql
232
		 * Deletes the items matched by the given IDs from the database
233
		 *
234
		 * @see mshop/locale/manager/delete/ansi
235
		 */
236
237
		/** mshop/locale/manager/delete/ansi
238
		 * Deletes the items matched by the given IDs from the database
239
		 *
240
		 * Removes the records specified by the given IDs from the locale database.
241
		 * The records must be from the site that is configured via the
242
		 * context item.
243
		 *
244
		 * The ":cond" placeholder is replaced by the name of the ID column and
245
		 * the given ID or list of IDs while the site ID is bound to the question
246
		 * mark.
247
		 *
248
		 * The SQL statement should conform to the ANSI standard to be
249
		 * compatible with most relational database systems. This also
250
		 * includes using double quotes for table and column names.
251
		 *
252
		 * @param string SQL statement for deleting items
253
		 * @since 2014.03
254
		 * @category Developer
255
		 * @see mshop/locale/manager/insert/ansi
256
		 * @see mshop/locale/manager/update/ansi
257
		 * @see mshop/locale/manager/newid/ansi
258
		 * @see mshop/locale/manager/search/ansi
259
		 * @see mshop/locale/manager/count/ansi
260
		 */
261
		$path = 'mshop/locale/manager/delete';
262
263
		return $this->deleteItemsBase( $itemIds, $path );
264
	}
265
266
267
	/**
268
	 * Adds or updates an item object.
269
	 *
270
	 * @param \Aimeos\MShop\Locale\Item\Iface $item Item object whose data should be saved
271
	 * @param bool $fetch True if the new ID should be returned in the item
272
	 * @return \Aimeos\MShop\Locale\Item\Iface $item Updated item including the generated ID
273
	 */
274
	public function saveItem( \Aimeos\MShop\Locale\Item\Iface $item, bool $fetch = true ) : \Aimeos\MShop\Locale\Item\Iface
275
	{
276
		if( !$item->isModified() ) {
277
			return $item;
278
		}
279
280
		$context = $this->context();
281
		$conn = $context->db( $this->getResourceName() );
282
283
			$id = $item->getId();
284
			$date = date( 'Y-m-d H:i:s' );
285
			$columns = $this->object()->getSaveAttributes();
286
287
			if( $id === null )
288
			{
289
				/** mshop/locale/manager/insert/mysql
290
				 * Inserts a new locale record into the database table
291
				 *
292
				 * @see mshop/locale/manager/insert/ansi
293
				 */
294
295
				/** mshop/locale/manager/insert/ansi
296
				 * Inserts a new locale record into the database table
297
				 *
298
				 * Items with no ID yet (i.e. the ID is NULL) will be created in
299
				 * the database and the newly created ID retrieved afterwards
300
				 * using the "newid" SQL statement.
301
				 *
302
				 * The SQL statement must be a string suitable for being used as
303
				 * prepared statement. It must include question marks for binding
304
				 * the values from the locale item to the statement before they are
305
				 * sent to the database server. The number of question marks must
306
				 * be the same as the number of columns listed in the INSERT
307
				 * statement. The order of the columns must correspond to the
308
				 * order in the save() method, so the correct values are
309
				 * bound to the columns.
310
				 *
311
				 * The SQL statement should conform to the ANSI standard to be
312
				 * compatible with most relational database systems. This also
313
				 * includes using double quotes for table and column names.
314
				 *
315
				 * @param string SQL statement for inserting records
316
				 * @since 2014.03
317
				 * @category Developer
318
				 * @see mshop/locale/manager/update/ansi
319
				 * @see mshop/locale/manager/newid/ansi
320
				 * @see mshop/locale/manager/delete/ansi
321
				 * @see mshop/locale/manager/search/ansi
322
				 * @see mshop/locale/manager/count/ansi
323
				 */
324
				$path = 'mshop/locale/manager/insert';
325
				$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

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