Passed
Push — master ( dbd3b4...b6e939 )
by Aimeos
04:19
created

Standard::create()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 4
dl 0
loc 7
rs 10
c 0
b 0
f 0
cc 2
nc 2
nop 1
1
<?php
2
3
/**
4
 * @license LGPLv3, https://opensource.org/licenses/LGPL-3.0
5
 * @copyright Metaways Infosystems GmbH, 2011
6
 * @copyright Aimeos (aimeos.org), 2015-2023
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
	/** mshop/locale/manager/name
26
	 * Class name of the used locale manager implementation
27
	 *
28
	 * Each default manager can be replace by an alternative imlementation.
29
	 * To use this implementation, you have to set the last part of the class
30
	 * name as configuration value so the manager factory knows which class it
31
	 * has to instantiate.
32
	 *
33
	 * For example, if the name of the default class is
34
	 *
35
	 *  \Aimeos\MShop\Locale\Manager\Standard
36
	 *
37
	 * and you want to replace it with your own version named
38
	 *
39
	 *  \Aimeos\MShop\Locale\Manager\Mymanager
40
	 *
41
	 * then you have to set the this configuration option:
42
	 *
43
	 *  mshop/locale/manager/name = Mymanager
44
	 *
45
	 * The value is the last part of your own class name and it's case sensitive,
46
	 * so take care that the configuration value is exactly named like the last
47
	 * part of the class name.
48
	 *
49
	 * The allowed characters of the class name are A-Z, a-z and 0-9. No other
50
	 * characters are possible! You should always start the last part of the class
51
	 * name with an upper case character and continue only with lower case characters
52
	 * or numbers. Avoid chamel case names like "MyManager"!
53
	 *
54
	 * @param string Last part of the class name
55
	 * @since 2014.03
56
	 * @category Developer
57
	 */
58
59
	/** mshop/locale/manager/decorators/excludes
60
	 * Excludes decorators added by the "common" option from the locale manager
61
	 *
62
	 * Decorators extend the functionality of a class by adding new aspects
63
	 * (e.g. log what is currently done), executing the methods of the underlying
64
	 * class only in certain conditions (e.g. only for logged in users) or
65
	 * modify what is returned to the caller.
66
	 *
67
	 * This option allows you to remove a decorator added via
68
	 * "mshop/common/manager/decorators/default" before they are wrapped
69
	 * around the locale manager.
70
	 *
71
	 *  mshop/locale/manager/decorators/excludes = array( 'decorator1' )
72
	 *
73
	 * This would remove the decorator named "decorator1" from the list of
74
	 * common decorators ("\Aimeos\MShop\Common\Manager\Decorator\*") added via
75
	 * "mshop/common/manager/decorators/default" for the locale manager.
76
	 *
77
	 * @param array List of decorator names
78
	 * @since 2014.03
79
	 * @category Developer
80
	 * @see mshop/common/manager/decorators/default
81
	 * @see mshop/locale/manager/decorators/global
82
	 * @see mshop/locale/manager/decorators/local
83
	 */
84
85
	/** mshop/locale/manager/decorators/global
86
	 * Adds a list of globally available decorators only to the locale manager
87
	 *
88
	 * Decorators extend the functionality of a class by adding new aspects
89
	 * (e.g. log what is currently done), executing the methods of the underlying
90
	 * class only in certain conditions (e.g. only for logged in users) or
91
	 * modify what is returned to the caller.
92
	 *
93
	 * This option allows you to wrap global decorators
94
	 * ("\Aimeos\MShop\Common\Manager\Decorator\*") around the locale manager.
95
	 *
96
	 *  mshop/locale/manager/decorators/global = array( 'decorator1' )
97
	 *
98
	 * This would add the decorator named "decorator1" defined by
99
	 * "\Aimeos\MShop\Common\Manager\Decorator\Decorator1" only to the locale
100
	 * manager.
101
	 *
102
	 * @param array List of decorator names
103
	 * @since 2014.03
104
	 * @category Developer
105
	 * @see mshop/common/manager/decorators/default
106
	 * @see mshop/locale/manager/decorators/excludes
107
	 * @see mshop/locale/manager/decorators/local
108
	 */
109
110
	/** mshop/locale/manager/decorators/local
111
	 * Adds a list of local decorators only to the locale manager
112
	 *
113
	 * Decorators extend the functionality of a class by adding new aspects
114
	 * (e.g. log what is currently done), executing the methods of the underlying
115
	 * class only in certain conditions (e.g. only for logged in users) or
116
	 * modify what is returned to the caller.
117
	 *
118
	 * This option allows you to wrap local decorators
119
	 * ("\Aimeos\MShop\Locale\Manager\Decorator\*") around the locale manager.
120
	 *
121
	 *  mshop/locale/manager/decorators/local = array( 'decorator2' )
122
	 *
123
	 * This would add the decorator named "decorator2" defined by
124
	 * "\Aimeos\MShop\Locale\Manager\Decorator\Decorator2" only to the locale
125
	 * manager.
126
	 *
127
	 * @param array List of decorator names
128
	 * @since 2014.03
129
	 * @category Developer
130
	 * @see mshop/common/manager/decorators/default
131
	 * @see mshop/locale/manager/decorators/excludes
132
	 * @see mshop/locale/manager/decorators/global
133
	 */
134
135
136
	private array $searchConfig = array(
137
		'locale.id' => array(
138
			'code' => 'locale.id',
139
			'internalcode' => 'mloc."id"',
140
			'label' => 'ID',
141
			'type' => 'integer',
142
			'internaltype' => \Aimeos\Base\DB\Statement\Base::PARAM_INT,
143
			'public' => false,
144
		),
145
		'locale.siteid' => array(
146
			'code' => 'locale.siteid',
147
			'internalcode' => 'mloc."siteid"',
148
			'label' => 'Site ID',
149
			'type' => 'string',
150
			'internaltype' => \Aimeos\Base\DB\Statement\Base::PARAM_STR,
151
			'public' => false,
152
		),
153
		'locale.languageid' => array(
154
			'code' => 'locale.languageid',
155
			'internalcode' => 'mloc."langid"',
156
			'label' => 'Language ID',
157
			'type' => 'string',
158
			'internaltype' => \Aimeos\Base\DB\Statement\Base::PARAM_STR,
159
		),
160
		'locale.currencyid' => array(
161
			'code' => 'locale.currencyid',
162
			'internalcode' => 'mloc."currencyid"',
163
			'label' => 'Currency ID',
164
			'type' => 'string',
165
			'internaltype' => \Aimeos\Base\DB\Statement\Base::PARAM_STR,
166
		),
167
		'locale.status' => array(
168
			'code' => 'locale.status',
169
			'internalcode' => 'mloc."status"',
170
			'label' => 'Status',
171
			'type' => 'integer',
172
			'internaltype' => \Aimeos\Base\DB\Statement\Base::PARAM_INT,
173
		),
174
		'locale.position' => array(
175
			'code' => 'locale.position',
176
			'internalcode' => 'mloc."pos"',
177
			'label' => 'Position',
178
			'type' => 'integer',
179
			'internaltype' => \Aimeos\Base\DB\Statement\Base::PARAM_INT,
180
		),
181
		'locale.ctime' => array(
182
			'code' => 'locale.ctime',
183
			'internalcode' => 'mloc."ctime"',
184
			'label' => 'Create date/time',
185
			'type' => 'datetime',
186
			'internaltype' => \Aimeos\Base\DB\Statement\Base::PARAM_STR,
187
			'public' => false,
188
		),
189
		'locale.mtime' => array(
190
			'code' => 'locale.mtime',
191
			'internalcode' => 'mloc."mtime"',
192
			'label' => 'Modify date/time',
193
			'type' => 'datetime',
194
			'internaltype' => \Aimeos\Base\DB\Statement\Base::PARAM_STR,
195
			'public' => false,
196
		),
197
		'locale.editor' => array(
198
			'code' => 'locale.editor',
199
			'internalcode' => 'mloc."editor"',
200
			'label' => 'Editor',
201
			'type' => 'string',
202
			'internaltype' => \Aimeos\Base\DB\Statement\Base::PARAM_STR,
203
			'public' => false,
204
		),
205
	);
206
207
208
	/**
209
	 * Initializes the object.
210
	 *
211
	 * @param \Aimeos\MShop\ContextIface $context Context object
212
	 */
213
	public function __construct( \Aimeos\MShop\ContextIface $context )
214
	{
215
		parent::__construct( $context );
216
217
		/** mshop/locale/manager/resource
218
		 * Name of the database connection resource to use
219
		 *
220
		 * You can configure a different database connection for each data domain
221
		 * and if no such connection name exists, the "db" connection will be used.
222
		 * It's also possible to use the same database connection for different
223
		 * data domains by configuring the same connection name using this setting.
224
		 *
225
		 * @param string Database connection name
226
		 * @since 2023.04
227
		 */
228
		$this->setResourceName( $context->config()->get( 'mshop/locale/manager/resource', 'db-locale' ) );
229
	}
230
231
232
	/**
233
	 * Returns the locale item for the given site code, language code and currency code.
234
	 *
235
	 * @param string $site Site code
236
	 * @param string $lang Language code (optional)
237
	 * @param string $currency Currency code (optional)
238
	 * @param bool $active Flag to get only active items (optional)
239
	 * @param int|null $level Constant from abstract class which site ID levels should be available (optional),
240
	 * 	based on config or value for SITE_PATH if null
241
	 * @param bool $bare Allow locale items with sites only
242
	 * @return \Aimeos\MShop\Locale\Item\Iface Locale item for the given parameters
243
	 * @throws \Aimeos\MShop\Locale\Exception If no locale item is found
244
	 */
245
	public function bootstrap( string $site, string $lang = '', string $currency = '', bool $active = true, int $level = null,
246
		bool $bare = false ) : \Aimeos\MShop\Locale\Item\Iface
247
	{
248
		$siteItem = $this->object()->getSubManager( 'site' )->find( $site );
249
250
		// allow enabled sites and sites under review
251
		if( $active && $siteItem->getStatus() < 1 && $siteItem->getStatus() !== -1 ) {
252
			throw new \Aimeos\MShop\Locale\Exception( 'Site not found' );
253
		}
254
255
		$siteId = $siteItem->getSiteId();
256
		$sites = [Base::SITE_ONE => $siteId];
257
258
		return $this->bootstrapBase( $site, $lang, $currency, $active, $siteItem, $siteId, $sites, $bare );
259
	}
260
261
262
	/**
263
	 * Removes old entries from the storage.
264
	 *
265
	 * @param iterable $siteids List of IDs for sites whose entries should be deleted
266
	 * @return \Aimeos\MShop\Locale\Manager\Iface Manager object for chaining method calls
267
	 */
268
	public function clear( iterable $siteids ) : \Aimeos\MShop\Common\Manager\Iface
269
	{
270
		return $this->clearBase( $siteids, 'mshop/locale/manager/delete' );
271
	}
272
273
274
	/**
275
	 * Creates a new empty item instance
276
	 *
277
	 * @param array $values Values the item should be initialized with
278
	 * @return \Aimeos\MShop\Locale\Item\Iface New locale item object
279
	 */
280
	public function create( array $values = [] ) : \Aimeos\MShop\Common\Item\Iface
281
	{
282
		try {
283
			$values['locale.siteid'] = $values['locale.siteid'] ?? $this->context()->locale()->getSiteId();
284
		} catch( \Exception $e ) {} // if no locale item is available
285
286
		return $this->createItemBase( $values );
287
	}
288
289
290
	/**
291
	 * Creates a filter object.
292
	 *
293
	 * @param bool|null $default Add default criteria or NULL for relaxed default criteria
294
	 * @param bool $site TRUE for adding site criteria to limit items by the site of related items
295
	 * @return \Aimeos\Base\Criteria\Iface Returns the filter object
296
	 */
297
	public function filter( ?bool $default = false, bool $site = false ) : \Aimeos\Base\Criteria\Iface
298
	{
299
		return $this->filterBase( 'locale', $default );
300
	}
301
302
303
	/**
304
	 * Returns the item specified by its ID.
305
	 *
306
	 * @param string $id Unique ID of the locale item
307
	 * @param string[] $ref List of domains to fetch list items and referenced items for
308
	 * @param bool|null $default Add default criteria or NULL for relaxed default criteria
309
	 * @return \Aimeos\MShop\Locale\Item\Iface Returns the locale item of the given id
310
	 * @throws \Aimeos\MShop\Exception If item couldn't be found
311
	 */
312
	public function get( string $id, array $ref = [], ?bool $default = false ) : \Aimeos\MShop\Common\Item\Iface
313
	{
314
		return $this->getItemBase( 'locale.id', $id, $ref, $default );
315
	}
316
317
318
	/**
319
	 * Searches for all items matching the given critera.
320
	 *
321
	 * @param \Aimeos\Base\Criteria\Iface $search Criteria object with conditions, sortations, etc.
322
	 * @param string[] $ref List of domains to fetch list items and referenced items for
323
	 * @param int &$total Number of items that are available in total
324
	 * @return \Aimeos\Map List of items implementing \Aimeos\MShop\Locale\Item\Iface with ids as keys
325
	 */
326
	public function search( \Aimeos\Base\Criteria\Iface $search, array $ref = [], int &$total = null ) : \Aimeos\Map
327
	{
328
		$items = [];
329
//		$level = \Aimeos\MShop\Locale\Manager\Base::SITE_PATH;
330
//		$search = (clone $search)->add( $this->siteCondition( 'locale.siteid', $level ) );
331
332
		foreach( $this->searchEntries( $search, $ref, $total ) as $row )
333
		{
334
			if( $item = $this->applyFilter( $this->createItemBase( $row ) ) ) {
335
				$items[$row['locale.id']] = $item;
336
			}
337
		}
338
339
		return map( $items );
340
	}
341
342
343
	/**
344
	 * Removes multiple items.
345
	 *
346
	 * @param \Aimeos\MShop\Common\Item\Iface[]|string[] $itemIds List of item objects or IDs of the items
347
	 * @return \Aimeos\MShop\Locale\Manager\Iface Manager object for chaining method calls
348
	 */
349
	public function delete( $itemIds ) : \Aimeos\MShop\Common\Manager\Iface
350
	{
351
		/** mshop/locale/manager/delete/mysql
352
		 * Deletes the items matched by the given IDs from the database
353
		 *
354
		 * @see mshop/locale/manager/delete/ansi
355
		 */
356
357
		/** mshop/locale/manager/delete/ansi
358
		 * Deletes the items matched by the given IDs from the database
359
		 *
360
		 * Removes the records specified by the given IDs from the locale database.
361
		 * The records must be from the site that is configured via the
362
		 * context item.
363
		 *
364
		 * The ":cond" placeholder is replaced by the name of the ID column and
365
		 * the given ID or list of IDs while the site ID is bound to the question
366
		 * mark.
367
		 *
368
		 * The SQL statement should conform to the ANSI standard to be
369
		 * compatible with most relational database systems. This also
370
		 * includes using double quotes for table and column names.
371
		 *
372
		 * @param string SQL statement for deleting items
373
		 * @since 2014.03
374
		 * @category Developer
375
		 * @see mshop/locale/manager/insert/ansi
376
		 * @see mshop/locale/manager/update/ansi
377
		 * @see mshop/locale/manager/newid/ansi
378
		 * @see mshop/locale/manager/search/ansi
379
		 * @see mshop/locale/manager/count/ansi
380
		 */
381
		$path = 'mshop/locale/manager/delete';
382
383
		return $this->deleteItemsBase( $itemIds, $path );
384
	}
385
386
387
	/**
388
	 * Adds or updates an item object.
389
	 *
390
	 * @param \Aimeos\MShop\Locale\Item\Iface $item Item object whose data should be saved
391
	 * @param bool $fetch True if the new ID should be returned in the item
392
	 * @return \Aimeos\MShop\Locale\Item\Iface $item Updated item including the generated ID
393
	 */
394
	protected function saveItem( \Aimeos\MShop\Locale\Item\Iface $item, bool $fetch = true ) : \Aimeos\MShop\Locale\Item\Iface
395
	{
396
		if( !$item->isModified() ) {
397
			return $item;
398
		}
399
400
		$context = $this->context();
401
		$conn = $context->db( $this->getResourceName() );
402
403
		$id = $item->getId();
404
		$date = date( 'Y-m-d H:i:s' );
405
		$columns = $this->object()->getSaveAttributes();
406
407
		if( $id === null )
408
		{
409
			/** mshop/locale/manager/insert/mysql
410
			 * Inserts a new locale record into the database table
411
			 *
412
			 * @see mshop/locale/manager/insert/ansi
413
			 */
414
415
			/** mshop/locale/manager/insert/ansi
416
			 * Inserts a new locale record into the database table
417
			 *
418
			 * Items with no ID yet (i.e. the ID is NULL) will be created in
419
			 * the database and the newly created ID retrieved afterwards
420
			 * using the "newid" SQL statement.
421
			 *
422
			 * The SQL statement must be a string suitable for being used as
423
			 * prepared statement. It must include question marks for binding
424
			 * the values from the locale item to the statement before they are
425
			 * sent to the database server. The number of question marks must
426
			 * be the same as the number of columns listed in the INSERT
427
			 * statement. The order of the columns must correspond to the
428
			 * order in the save() method, so the correct values are
429
			 * bound to the columns.
430
			 *
431
			 * The SQL statement should conform to the ANSI standard to be
432
			 * compatible with most relational database systems. This also
433
			 * includes using double quotes for table and column names.
434
			 *
435
			 * @param string SQL statement for inserting records
436
			 * @since 2014.03
437
			 * @category Developer
438
			 * @see mshop/locale/manager/update/ansi
439
			 * @see mshop/locale/manager/newid/ansi
440
			 * @see mshop/locale/manager/delete/ansi
441
			 * @see mshop/locale/manager/search/ansi
442
			 * @see mshop/locale/manager/count/ansi
443
			 */
444
			$path = 'mshop/locale/manager/insert';
445
			$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

445
			$sql = $this->addSqlColumns( array_keys( $columns ), /** @scrutinizer ignore-type */ $this->getSqlConfig( $path ) );
Loading history...
446
		}
447
		else
448
		{
449
			/** mshop/locale/manager/update/mysql
450
			 * Updates an existing locale record in the database
451
			 *
452
			 * @see mshop/locale/manager/update/ansi
453
			 */
454
455
			/** mshop/locale/manager/update/ansi
456
			 * Updates an existing locale record in the database
457
			 *
458
			 * Items which already have an ID (i.e. the ID is not NULL) will
459
			 * be updated in the database.
460
			 *
461
			 * The SQL statement must be a string suitable for being used as
462
			 * prepared statement. It must include question marks for binding
463
			 * the values from the locale item to the statement before they are
464
			 * sent to the database server. The order of the columns must
465
			 * correspond to the order in the save() method, so the
466
			 * correct values are bound to the columns.
467
			 *
468
			 * The SQL statement should conform to the ANSI standard to be
469
			 * compatible with most relational database systems. This also
470
			 * includes using double quotes for table and column names.
471
			 *
472
			 * @param string SQL statement for updating records
473
			 * @since 2014.03
474
			 * @category Developer
475
			 * @see mshop/locale/manager/insert/ansi
476
			 * @see mshop/locale/manager/newid/ansi
477
			 * @see mshop/locale/manager/delete/ansi
478
			 * @see mshop/locale/manager/search/ansi
479
			 * @see mshop/locale/manager/count/ansi
480
			 */
481
			$path = 'mshop/locale/manager/update';
482
			$sql = $this->addSqlColumns( array_keys( $columns ), $this->getSqlConfig( $path ), false );
483
		}
484
485
		$idx = 1;
486
		$stmt = $this->getCachedStatement( $conn, $path, $sql );
487
		$siteIds = explode( '.', trim( $item->getSiteId(), '.' ) );
488
489
		foreach( $columns as $name => $entry ) {
490
			$stmt->bind( $idx++, $item->get( $name ), $entry->getInternalType() );
491
		}
492
493
		$stmt->bind( $idx++, $item->getLanguageId() );
494
		$stmt->bind( $idx++, $item->getCurrencyId() );
495
		$stmt->bind( $idx++, $item->getPosition(), \Aimeos\Base\DB\Statement\Base::PARAM_INT );
496
		$stmt->bind( $idx++, $item->getStatus(), \Aimeos\Base\DB\Statement\Base::PARAM_INT );
497
		$stmt->bind( $idx++, $date ); // mtime
498
		$stmt->bind( $idx++, $context->editor() );
499
		$stmt->bind( $idx++, end( $siteIds ), \Aimeos\Base\DB\Statement\Base::PARAM_INT );
500
		$stmt->bind( $idx++, $item->getSiteId() );
501
502
		if( $id !== null ) {
503
			$stmt->bind( $idx++, $id, \Aimeos\Base\DB\Statement\Base::PARAM_INT );
504
		} else {
505
			$stmt->bind( $idx++, $date ); // ctime
506
		}
507
508
		$stmt->execute()->finish();
509
510
		if( $id === null && $fetch === true )
511
		{
512
			/** mshop/locale/manager/newid/mysql
513
			 * Retrieves the ID generated by the database when inserting a new record
514
			 *
515
			 * @see mshop/locale/manager/newid/ansi
516
			 */
517
518
			/** mshop/locale/manager/newid/ansi
519
			 * Retrieves the ID generated by the database when inserting a new record
520
			 *
521
			 * As soon as a new record is inserted into the database table,
522
			 * the database server generates a new and unique identifier for
523
			 * that record. This ID can be used for retrieving, updating and
524
			 * deleting that specific record from the table again.
525
			 *
526
			 * For MySQL:
527
			 *  SELECT LAST_INSERT_ID()
528
			 * For PostgreSQL:
529
			 *  SELECT currval('seq_mloc_id')
530
			 * For SQL Server:
531
			 *  SELECT SCOPE_IDENTITY()
532
			 * For Oracle:
533
			 *  SELECT "seq_mloc_id".CURRVAL FROM DUAL
534
			 *
535
			 * There's no way to retrive the new ID by a SQL statements that
536
			 * fits for most database servers as they implement their own
537
			 * specific way.
538
			 *
539
			 * @param string SQL statement for retrieving the last inserted record ID
540
			 * @since 2014.03
541
			 * @category Developer
542
			 * @see mshop/locale/manager/insert/ansi
543
			 * @see mshop/locale/manager/update/ansi
544
			 * @see mshop/locale/manager/delete/ansi
545
			 * @see mshop/locale/manager/search/ansi
546
			 * @see mshop/locale/manager/count/ansi
547
			 */
548
			$path = 'mshop/locale/manager/newid';
549
			$id = $this->newId( $conn, $path );
550
		}
551
552
		$item->setId( $id );
553
554
		return $item;
555
	}
556
557
558
	/**
559
	 * Returns a new manager for locale extensions
560
	 *
561
	 * @param string $manager Name of the sub manager type in lower case
562
	 * @param string|null $name Name of the implementation, will be from configuration (or Default) if null
563
	 * @return \Aimeos\MShop\Common\Manager\Iface Manager for different extensions, e.g site, language, currency.
564
	 */
565
	public function getSubManager( string $manager, string $name = null ) : \Aimeos\MShop\Common\Manager\Iface
566
	{
567
		return $this->getSubManagerBase( 'locale', $manager, $name );
568
	}
569
570
571
	/**
572
	 * Returns the available manager types
573
	 *
574
	 * @param bool $withsub Return also the resource type of sub-managers if true
575
	 * @return string[] Type of the manager and submanagers, subtypes are separated by slashes
576
	 */
577
	public function getResourceType( bool $withsub = true ) : array
578
	{
579
		$path = 'mshop/locale/manager/submanagers';
580
		return $this->getResourceTypeBase( 'locale', $path, array( 'currency', 'language', 'site' ), $withsub );
581
	}
582
583
584
	/**
585
	 * Returns the attributes that can be used for searching.
586
	 *
587
	 * @param bool $withsub Return also attributes of sub-managers if true
588
	 * @return \Aimeos\Base\Criteria\Attribute\Iface[] List of search attribute items
589
	 */
590
	public function getSearchAttributes( bool $withsub = true ) : array
591
	{
592
		/** mshop/locale/manager/submanagers
593
		 * List of manager names that can be instantiated by the locale manager
594
		 *
595
		 * Managers provide a generic interface to the underlying storage.
596
		 * Each manager has or can have sub-managers caring about particular
597
		 * aspects. Each of these sub-managers can be instantiated by its
598
		 * parent manager using the getSubManager() method.
599
		 *
600
		 * The search keys from sub-managers can be normally used in the
601
		 * manager as well. It allows you to search for items of the manager
602
		 * using the search keys of the sub-managers to further limit the
603
		 * retrieved list of items.
604
		 *
605
		 * @param array List of sub-manager names
606
		 * @since 2014.03
607
		 * @category Developer
608
		 */
609
		$path = 'mshop/locale/manager/submanagers';
610
		$default = array( 'language', 'currency', 'site' );
611
612
		return $this->getSearchAttributesBase( $this->searchConfig, $path, $default, $withsub );
613
	}
614
615
616
	/**
617
	 * Returns the locale item for the given site code, language code and currency code.
618
	 *
619
	 * If the locale item is inherited from a parent site, the site ID of this locale item
620
	 * is changed to the site ID of the actual site. This ensures that items assigned to
621
	 * the same site as the site item are still used.
622
	 *
623
	 * @param string $site Site code
624
	 * @param string $lang Language code
625
	 * @param string $currency Currency code
626
	 * @param bool $active Flag to get only active items
627
	 * @param \Aimeos\MShop\Locale\Item\Site\Iface $siteItem Site item
628
	 * @param string $siteId Site ID
629
	 * @param array $sites Associative list of site constant as key and sites as values
630
	 * @param bool $bare Allow locale items with sites only
631
	 * @return \Aimeos\MShop\Locale\Item\Iface Locale item for the given parameters
632
	 * @throws \Aimeos\MShop\Locale\Exception If no locale item is found
633
	 */
634
	protected function bootstrapBase( string $site, string $lang, string $currency, bool $active,
635
		\Aimeos\MShop\Locale\Item\Site\Iface $siteItem, string $siteId, array $sites, bool $bare ) : \Aimeos\MShop\Locale\Item\Iface
636
	{
637
		if( $result = $this->bootstrapMatch( $siteId, $lang, $currency, $active, $siteItem, $sites ) ) {
638
			return $result;
639
		}
640
641
		if( $result = $this->bootstrapClosest( $siteId, $lang, $active, $siteItem, $sites ) ) {
642
			return $result;
643
		}
644
645
		if( $bare === true ) {
646
			return $this->createItemBase( ['locale.siteid' => $siteId], $siteItem, $sites );
647
		}
648
649
		$msg = $this->context()->translate( 'mshop', 'Locale item for site "%1$s" not found' );
650
		throw new \Aimeos\MShop\Locale\Exception( sprintf( $msg, $site ) );
651
	}
652
653
654
	/**
655
	 * Returns the matching locale item for the given site code, language code and currency code.
656
	 *
657
	 * If the locale item is inherited from a parent site, the site ID of this locale item
658
	 * is changed to the site ID of the actual site. This ensures that items assigned to
659
	 * the same site as the site item are still used.
660
	 *
661
	 * @param string $siteId Site ID
662
	 * @param string $lang Language code
663
	 * @param string $currency Currency code
664
	 * @param bool $active Flag to get only active items
665
	 * @param \Aimeos\MShop\Locale\Item\Site\Iface $siteItem Site item
666
	 * @param array $sites Associative list of site constant as key and sites as values
667
	 * @return \Aimeos\MShop\Locale\Item\Iface|null Locale item for the given parameters or null if no item was found
668
	 */
669
	private function bootstrapMatch( string $siteId, string $lang, string $currency, bool $active,
670
		\Aimeos\MShop\Locale\Item\Site\Iface $siteItem, array $sites ) : ?\Aimeos\MShop\Locale\Item\Iface
671
	{
672
		// Try to find exact match
673
		$search = $this->object()->filter( $active );
674
675
		$expr = array( $search->compare( '==', 'locale.siteid', $sites[Base::SITE_PATH] ?? $sites[Base::SITE_ONE] ) );
676
677
		if( !empty( $lang ) )
678
		{
679
			$langIds = strlen( $lang ) > 2 ? [$lang, substr( $lang, 0, 2 )] : [$lang];
680
			$expr[] = $search->compare( '==', 'locale.languageid', $langIds );
681
		}
682
683
		if( !empty( $currency ) ) {
684
			$expr[] = $search->compare( '==', 'locale.currencyid', $currency );
685
		}
686
687
		$expr[] = $search->getConditions();
688
689
690
		if( $active === true )
691
		{
692
			$expr[] = $search->compare( '>', 'locale.currency.status', 0 );
693
			$expr[] = $search->compare( '>', 'locale.language.status', 0 );
694
			$expr[] = $search->compare( '>', 'locale.site.status', 0 );
695
		}
696
697
		$search->setConditions( $search->and( $expr ) );
698
		$search->setSortations( array( $search->sort( '+', 'locale.position' ) ) );
699
		$result = $this->searchEntries( $search );
700
701
		// Try to find first item where site matches
702
		foreach( $result as $row )
703
		{
704
			if( $row['locale.siteid'] === $siteId ) {
705
				return $this->createItemBase( $row, $siteItem, $sites );
706
			}
707
		}
708
709
		if( ( $row = reset( $result ) ) !== false )
710
		{
711
			$row['locale.siteid'] = $siteId;
712
			return $this->createItemBase( $row, $siteItem, $sites );
713
		}
714
715
		return null;
716
	}
717
718
719
	/**
720
	 * Returns the locale item for the given site code, language code and currency code.
721
	 *
722
	 * If the locale item is inherited from a parent site, the site ID of this locale item
723
	 * is changed to the site ID of the actual site. This ensures that items assigned to
724
	 * the same site as the site item are still used.
725
	 *
726
	 * @param string $siteId Site ID
727
	 * @param string $lang Language code
728
	 * @param bool $active Flag to get only active items
729
	 * @param \Aimeos\MShop\Locale\Item\Site\Iface $siteItem Site item
730
	 * @param array $sites Associative list of site constant as key and sites as values
731
	 * @return \Aimeos\MShop\Locale\Item\Iface|null Locale item for the given parameters or null if no item was found
732
	 */
733
	private function bootstrapClosest( string $siteId, string $lang, bool $active,
734
		\Aimeos\MShop\Locale\Item\Site\Iface $siteItem, array $sites ) : ?\Aimeos\MShop\Locale\Item\Iface
735
	{
736
		// Try to find the best matching locale
737
		$search = $this->object()->filter( $active );
738
739
		$expr = array(
740
			$search->compare( '==', 'locale.siteid', $sites[Base::SITE_PATH] ?? $sites[Base::SITE_ONE] ),
741
			$search->getConditions()
742
		);
743
744
		if( $active === true )
745
		{
746
			$expr[] = $search->compare( '>', 'locale.currency.status', 0 );
747
			$expr[] = $search->compare( '>', 'locale.language.status', 0 );
748
			$expr[] = $search->compare( '>', 'locale.site.status', 0 );
749
		}
750
751
		$search->setConditions( $search->and( $expr ) );
752
		$search->setSortations( array( $search->sort( '+', 'locale.position' ) ) );
753
		$result = $this->searchEntries( $search );
754
755
		// Try to find first item where site and language matches
756
		foreach( $result as $row )
757
		{
758
			if( $row['locale.siteid'] === $siteId && $row['locale.languageid'] === $lang ) {
759
				return $this->createItemBase( $row, $siteItem, $sites );
760
			}
761
		}
762
763
		$short = strlen( $lang ) > 2 ? substr( $lang, 0, 2 ) : null;
764
765
		// Try to find first item where site and language without country matches
766
		if( $short )
767
		{
768
			foreach( $result as $row )
769
			{
770
				if( $row['locale.siteid'] === $siteId && $row['locale.languageid'] === $short ) {
771
					return $this->createItemBase( $row, $siteItem, $sites );
772
				}
773
			}
774
		}
775
776
		// Try to find first item where language matches
777
		foreach( $result as $row )
778
		{
779
			if( $row['locale.languageid'] === $lang )
780
			{
781
				$row['locale.siteid'] = $siteId;
782
				return $this->createItemBase( $row, $siteItem, $sites );
783
			}
784
		}
785
786
		// Try to find first item where language without country matches
787
		if( $short )
788
		{
789
			foreach( $result as $row )
790
			{
791
				if( $row['locale.siteid'] === $siteId && $row['locale.languageid'] === $short ) {
792
					return $this->createItemBase( $row, $siteItem, $sites );
793
				}
794
			}
795
		}
796
797
		// Try to find first item where site matches
798
		foreach( $result as $row )
799
		{
800
			if( $row['locale.siteid'] === $siteId ) {
801
				return $this->createItemBase( $row, $siteItem, $sites );
802
			}
803
		}
804
805
		// Return first item (no other match found)
806
		if( ( $row = reset( $result ) ) !== false )
807
		{
808
			$row['locale.siteid'] = $siteId;
809
			return $this->createItemBase( $row, $siteItem, $sites );
810
		}
811
812
		return null;
813
	}
814
815
816
	/**
817
	 * Instances a new locale item object.
818
	 *
819
	 * @param array $values Parameter to initialise the item
820
	 * @param \Aimeos\MShop\Locale\Item\Site\Iface|null $site Site item
821
	 * @param array $sites Associative list of site constant as key and sites as values
822
	 * @return \Aimeos\MShop\Locale\Item\Iface Locale item
823
	 */
824
	protected function createItemBase( array $values = [], \Aimeos\MShop\Locale\Item\Site\Iface $site = null,
825
		array $sites = [] ) : \Aimeos\MShop\Locale\Item\Iface
826
	{
827
		return new \Aimeos\MShop\Locale\Item\Standard( $values, $site, $sites );
828
	}
829
830
831
	/**
832
	 * Returns the search results for the given SQL statement.
833
	 *
834
	 * @param \Aimeos\Base\DB\Connection\Iface $conn Database connection
835
	 * @param string $sql SQL statement
836
	 * @return \Aimeos\Base\DB\Result\Iface Search result object
837
	 */
838
	protected function getSearchResults( \Aimeos\Base\DB\Connection\Iface $conn, string $sql ) : \Aimeos\Base\DB\Result\Iface
839
	{
840
		$time = microtime( true );
841
842
		$stmt = $conn->create( $sql );
843
		$result = $stmt->execute();
844
845
		$msg = 'Time: ' . ( microtime( true ) - $time ) * 1000 . "ms\n"
846
			. 'Class: ' . get_class( $this ) . "\n"
847
			. str_replace( ["\t", "\n\n"], ['', "\n"], trim( (string) $stmt ) );
848
849
		$this->context()->logger()->debug( $msg, 'core/sql' );
850
851
		return $result;
852
	}
853
854
855
	/**
856
	 * Searches for all items matching the given critera.
857
	 *
858
	 * @param \Aimeos\Base\Criteria\Iface $search Criteria object with conditions, sortations, etc.
859
	 * @param string[] $ref List of domains to fetch list items and referenced items for
860
	 * @param int &$total Number of items that are available in total
861
	 * @return array Associative list of key/value pairs
862
	 */
863
	protected function searchEntries( \Aimeos\Base\Criteria\Iface $search, array $ref = [], int &$total = null ) : array
864
	{
865
		$map = [];
866
		$context = $this->context();
867
		$conn = $context->db( $this->getResourceName() );
868
869
			$attributes = $this->object()->getSearchAttributes();
870
			$translations = $this->getSearchTranslations( $attributes );
871
			$types = $this->getSearchTypes( $attributes );
872
			$columns = $this->object()->getSaveAttributes();
873
			$sortcols = $search->translate( $search->getSortations(), $translations );
874
875
			if( empty( $search->getSortations() ) && ( $attribute = reset( $attributes ) ) !== false ) {
876
				$search = ( clone $search )->setSortations( [$search->sort( '+', $attribute->getCode() )] );
877
			}
878
879
			$colstring = '';
880
			foreach( $columns as $name => $entry ) {
881
				$colstring .= $entry->getInternalCode() . ', ';
882
			}
883
884
			$find = array( ':columns', ':cond', ':order', ':group', ':start', ':size' );
885
			$replace = array(
886
				$colstring . ( $sortcols ? join( ', ', $sortcols ) . ', ' : '' ),
887
				$search->getConditionSource( $types, $translations ),
888
				$search->getSortationSource( $types, $translations ),
889
				implode( ', ', $search->translate( $search->getSortations(), $translations ) ) . ', ',
890
				$search->getOffset(),
891
				$search->getLimit(),
892
			);
893
894
			/** mshop/locale/manager/search/mysql
895
			 * Retrieves the records matched by the given criteria in the database
896
			 *
897
			 * @see mshop/locale/manager/search/ansi
898
			 */
899
900
			/** mshop/locale/manager/search/ansi
901
			 * Retrieves the records matched by the given criteria in the database
902
			 *
903
			 * Fetches the records matched by the given criteria from the locale
904
			 * database. The records must be from one of the sites that are
905
			 * configured via the context item. If the current site is part of
906
			 * a tree of sites, the SELECT statement can retrieve all records
907
			 * from the current site and the complete sub-tree of sites.
908
			 *
909
			 * To limit the records matched, conditions can be added to the given
910
			 * criteria object. It can contain comparisons like column names that
911
			 * must match specific values which can be combined by AND, OR or NOT
912
			 * operators. The resulting string of SQL conditions replaces the
913
			 * ":cond" placeholder before the statement is sent to the database
914
			 * server.
915
			 *
916
			 * If the records that are retrieved should be ordered by one or more
917
			 * columns, the generated string of column / sort direction pairs
918
			 * replaces the ":order" placeholder. In case no ordering is required,
919
			 * the complete ORDER BY part including the "\/*-orderby*\/...\/*orderby-*\/"
920
			 * markers is removed to speed up retrieving the records. Columns of
921
			 * sub-managers can also be used for ordering the result set but then
922
			 * no index can be used.
923
			 *
924
			 * The number of returned records can be limited and can start at any
925
			 * number between the begining and the end of the result set. For that
926
			 * the ":size" and ":start" placeholders are replaced by the
927
			 * corresponding values from the criteria object. The default values
928
			 * are 0 for the start and 100 for the size value.
929
			 *
930
			 * The SQL statement should conform to the ANSI standard to be
931
			 * compatible with most relational database systems. This also
932
			 * includes using double quotes for table and column names.
933
			 *
934
			 * @param string SQL statement for searching items
935
			 * @since 2014.03
936
			 * @category Developer
937
			 * @see mshop/locale/manager/insert/ansi
938
			 * @see mshop/locale/manager/update/ansi
939
			 * @see mshop/locale/manager/newid/ansi
940
			 * @see mshop/locale/manager/delete/ansi
941
			 * @see mshop/locale/manager/count/ansi
942
			 */
943
			$path = 'mshop/locale/manager/search';
944
945
			$sql = $this->getSqlConfig( $path );
946
			$results = $this->getSearchResults( $conn, str_replace( $find, $replace, $sql ) );
947
948
			try
949
			{
950
				while( ( $row = $results->fetch() ) !== null ) {
951
					$map[$row['locale.id']] = $row;
952
				}
953
			}
954
			catch( \Exception $e )
955
			{
956
				$results->finish();
957
				throw $e;
958
			}
959
960
			if( $total !== null )
961
			{
962
				/** mshop/locale/manager/count/mysql
963
				 * Counts the number of records matched by the given criteria in the database
964
				 *
965
				 * @see mshop/locale/manager/count/ansi
966
				 */
967
968
				/** mshop/locale/manager/count/ansi
969
				 * Counts the number of records matched by the given criteria in the database
970
				 *
971
				 * Counts all records matched by the given criteria from the locale
972
				 * database. The records must be from one of the sites that are
973
				 * configured via the context item. If the current site is part of
974
				 * a tree of sites, the statement can count all records from the
975
				 * current site and the complete sub-tree of sites.
976
				 *
977
				 * To limit the records matched, conditions can be added to the given
978
				 * criteria object. It can contain comparisons like column names that
979
				 * must match specific values which can be combined by AND, OR or NOT
980
				 * operators. The resulting string of SQL conditions replaces the
981
				 * ":cond" placeholder before the statement is sent to the database
982
				 * server.
983
				 *
984
				 * Both, the strings for ":joins" and for ":cond" are the same as for
985
				 * the "search" SQL statement.
986
				 *
987
				 * Contrary to the "search" statement, it doesn't return any records
988
				 * but instead the number of records that have been found. As counting
989
				 * thousands of records can be a long running task, the maximum number
990
				 * of counted records is limited for performance reasons.
991
				 *
992
				 * The SQL statement should conform to the ANSI standard to be
993
				 * compatible with most relational database systems. This also
994
				 * includes using double quotes for table and column names.
995
				 *
996
				 * @param string SQL statement for counting items
997
				 * @since 2014.03
998
				 * @category Developer
999
				 * @see mshop/locale/manager/insert/ansi
1000
				 * @see mshop/locale/manager/update/ansi
1001
				 * @see mshop/locale/manager/newid/ansi
1002
				 * @see mshop/locale/manager/delete/ansi
1003
				 * @see mshop/locale/manager/search/ansi
1004
				 */
1005
				$path = 'mshop/locale/manager/count';
1006
1007
				$sql = $this->getSqlConfig( $path );
1008
				$results = $this->getSearchResults( $conn, str_replace( $find, $replace, $sql ) );
1009
1010
				$row = $results->fetch();
1011
				$results->finish();
1012
1013
				if( $row === null )
1014
				{
1015
					$msg = $this->context()->translate( 'mshop', 'Total results value not found' );
1016
					throw new \Aimeos\MShop\Locale\Exception( $msg );
1017
				}
1018
1019
				$total = $row['count'];
1020
			}
1021
1022
		return $map;
1023
	}
1024
}
1025