Passed
Push — master ( 8659e3...021966 )
by Aimeos
14:38
created

Base::checkCountryId()   A

Complexity

Conditions 5
Paths 4

Size

Total Lines 16
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
eloc 7
nc 4
nop 2
dl 0
loc 16
rs 9.6111
c 0
b 0
f 0
1
<?php
2
3
/**
4
 * @license LGPLv3, https://opensource.org/licenses/LGPL-3.0
5
 * @copyright Metaways Infosystems GmbH, 2011
6
 * @copyright Aimeos (aimeos.org), 2015-2024
7
 * @package MShop
8
 * @subpackage Common
9
 */
10
11
12
namespace Aimeos\MShop\Common\Item;
13
14
15
/**
16
 * Common methods for all item objects.
17
 *
18
 * @package MShop
19
 * @subpackage Common
20
 */
21
class Base implements \Aimeos\MShop\Common\Item\Iface, \Aimeos\Macro\Iface, \ArrayAccess, \JsonSerializable
22
{
23
	use \Aimeos\Macro\Macroable;
24
25
	// protected due to PHP serialization
26
	protected bool $available = true;
27
	protected bool $modified = false;
28
	protected string $bprefix;
29
	protected ?string $type;
30
	protected array $bdata;
31
32
33
	/**
34
	 * Initializes the class properties.
35
	 *
36
	 * @param string $prefix Prefix for the keys returned by toArray()
37
	 * @param array $values Associative list of key/value pairs of the item properties
38
	 * @param string|null $type Item resource type
39
	 */
40
	public function __construct( string $prefix, array $values = [], string $type = null )
41
	{
42
		$this->bprefix = $prefix;
43
		$this->bdata = $values;
44
		$this->type = $type;
45
	}
46
47
48
	/**
49
	 * Creates a deep clone of all objects
50
	 */
51
	public function __clone()
52
	{
53
	}
54
55
56
	/**
57
	 * Returns the item property for the given name
58
	 *
59
	 * @param string $name Name of the property
60
	 * @return mixed|null Property value or null if property is unknown
61
	 */
62
	public function __get( string $name )
63
	{
64
		return $this->get( $name );
65
	}
66
67
68
	/**
69
	 * Tests if the item property for the given name is available
70
	 *
71
	 * @param string $name Name of the property
72
	 * @return bool True if the property exists, false if not
73
	 */
74
	public function __isset( string $name ) : bool
75
	{
76
		return array_key_exists( $name, $this->bdata );
77
	}
78
79
80
	/**
81
	 * Sets the new item property for the given name
82
	 *
83
	 * @param string $name Name of the property
84
	 * @param mixed $value New property value
85
	 */
86
	public function __set( string $name, $value )
87
	{
88
		$this->set( $name, $value );
89
	}
90
91
92
	/**
93
	 * Specifies the data which should be serialized to JSON by json_encode().
94
	 *
95
	 * @return array<string,mixed> Data to serialize to JSON
96
	 */
97
	#[\ReturnTypeWillChange]
98
	public function jsonSerialize()
99
	{
100
		return $this->bdata;
101
	}
102
103
104
	/**
105
	 * Tests if the item property for the given name is available
106
	 *
107
	 * @param string $name Name of the property
108
	 * @return bool True if the property exists, false if not
109
	 */
110
	public function offsetExists( $name ) : bool
111
	{
112
		return array_key_exists( $name, $this->bdata );
113
	}
114
115
116
	/**
117
	 * Returns the item property for the given name
118
	 *
119
	 * @param string $name Name of the property
120
	 * @return mixed|null Property value or null if property is unknown
121
	 */
122
	#[\ReturnTypeWillChange]
123
	public function offsetGet( $name )
124
	{
125
		return $this->get( $name );
126
	}
127
128
129
	/**
130
	 * Sets the new item property for the given name
131
	 *
132
	 * @param string $name Name of the property
133
	 * @param mixed $value New property value
134
	 */
135
	public function offsetSet( $name, $value ) : void
136
	{
137
		$this->set( $name, $value );
138
	}
139
140
141
	/**
142
	 * Removes an item property
143
	 * This is not supported by items
144
	 *
145
	 * @param string $name Name of the property
146
	 * @throws \LogicException Always thrown because this method isn't supported
147
	 */
148
	public function offsetUnset( $name ) : void
149
	{
150
		throw new \LogicException( 'Not implemented' );
151
	}
152
153
154
	/**
155
	 * Returns the ID of the items
156
	 *
157
	 * @return string ID of the item or an empty string
158
	 */
159
	public function __toString() : string
160
	{
161
		return (string) $this->getId();
162
	}
163
164
165
	/**
166
	 * Assigns multiple key/value pairs to the item
167
	 *
168
	 * @param iterable $pairs Associative list of key/value pairs
169
	 * @return \Aimeos\MShop\Common\Item\Iface Item for method chaining
170
	 */
171
	public function assign( iterable $pairs ) : \Aimeos\MShop\Common\Item\Iface
172
	{
173
		foreach( $pairs as $key => $value ) {
174
			$this->set( $key, $value );
175
		}
176
177
		return $this;
178
	}
179
180
181
	/**
182
	 * Returns the item property for the given name
183
	 *
184
	 * @param string $name Name of the property
185
	 * @param mixed $default Default value if property is unknown
186
	 * @return mixed|null Property value or default value if property is unknown
187
	 */
188
	public function get( string $name, $default = null )
189
	{
190
		if( array_key_exists( $name, $this->bdata ) ) {
191
			return $this->bdata[$name];
192
		}
193
194
		return $default;
195
	}
196
197
198
	/**
199
	 * Sets the new item property for the given name
200
	 *
201
	 * @param string $name Name of the property
202
	 * @param mixed $value New property value
203
	 * @return \Aimeos\MShop\Common\Item\Iface Item for method chaining
204
	 */
205
	public function set( string $name, $value ) : \Aimeos\MShop\Common\Item\Iface
206
	{
207
		// workaround for NULL values instead of empty strings and stringified integers from database
208
		if( !array_key_exists( $name, $this->bdata ) || $this->bdata[$name] != $value
209
			|| $value === null && $this->bdata[$name] !== null
210
			|| $value !== null && $this->bdata[$name] === null
211
		) {
212
			$this->bdata[$name] = $value;
213
			$this->setModified();
214
		}
215
216
		return $this;
217
	}
218
219
220
	/**
221
	 * Returns the ID of the item if available.
222
	 *
223
	 * @return string|null ID of the item
224
	 */
225
	public function getId() : ?string
226
	{
227
		$key = $this->bprefix . 'id';
228
229
		if( isset( $this->bdata[$key] ) && $this->bdata[$key] != '' ) {
230
			return (string) $this->bdata[$key];
231
		}
232
233
		return null;
234
	}
235
236
237
	/**
238
	 * Sets the new ID of the item.
239
	 *
240
	 * @param string|null $id ID of the item
241
	 * @return \Aimeos\MShop\Common\Item\Iface Item for chaining method calls
242
	 */
243
	public function setId( ?string $id ) : \Aimeos\MShop\Common\Item\Iface
244
	{
245
		$this->bdata[$this->bprefix . 'id'] = $id;
246
		$this->modified = ( $id === null );
247
248
		return $this;
249
	}
250
251
252
	/**
253
	 * Returns the site ID of the item.
254
	 *
255
	 * @return string Site ID or null if no site id is available
256
	 */
257
	public function getSiteId() : string
258
	{
259
		return $this->get( $this->bprefix . 'siteid', $this->get( 'siteid', '' ) );
260
	}
261
262
263
	/**
264
	 * Returns the list site IDs up to the root site item.
265
	 *
266
	 * @return array List of site IDs
267
	 */
268
	public function getSitePath() : array
269
	{
270
		$pos = 0;
271
		$list = [];
272
		$siteId = $this->getSiteId();
273
274
		while( ( $pos = strpos( $siteId, '.', $pos ) ) !== false ) {
275
			$list[] = substr( $siteId, 0, ++$pos );
276
		}
277
278
		return $list;
279
	}
280
281
282
	/**
283
	 * Returns modify date/time of the order coupon.
284
	 *
285
	 * @return string|null Modification time (YYYY-MM-DD HH:mm:ss)
286
	 */
287
	public function getTimeModified() : ?string
288
	{
289
		return $this->get( $this->bprefix . 'mtime', $this->get( 'mtime' ) );
290
	}
291
292
293
	/**
294
	 * Returns the create date of the item.
295
	 *
296
	 * @return string|null ISO date in YYYY-MM-DD hh:mm:ss format
297
	 */
298
	public function getTimeCreated() : ?string
299
	{
300
		return $this->get( $this->bprefix . 'ctime', $this->get( 'ctime' ) );
301
	}
302
303
304
	/**
305
	 * Returns the name of editor who created/modified the item at last.
306
	 *
307
	 * @return string Name of editor who created/modified the item at last
308
	 */
309
	public function editor() : string
310
	{
311
		return $this->get( $this->bprefix . 'editor', $this->get( 'editor', '' ) );
312
	}
313
314
315
	/**
316
	 * Tests if the item is available based on status, time, language and currency
317
	 *
318
	 * @return bool True if available, false if not
319
	 */
320
	public function isAvailable() : bool
321
	{
322
		return $this->available;
323
	}
324
325
326
	/**
327
	 * Sets the general availability of the item
328
	 *
329
	 * @return bool $value True if available, false if not
330
	 * @return \Aimeos\MShop\Common\Item\Iface Item for chaining method calls
331
	 */
332
	public function setAvailable( bool $value ) : \Aimeos\MShop\Common\Item\Iface
333
	{
334
		$this->available = $value;
335
		return $this;
336
	}
337
338
339
	/**
340
	 * Tests if this Item object was modified.
341
	 *
342
	 * @return bool True if modified, false if not
343
	 */
344
	public function isModified() : bool
345
	{
346
		return $this->modified;
347
	}
348
349
350
	/**
351
	 * Sets the modified flag of the object.
352
	 *
353
	 * @return \Aimeos\MShop\Common\Item\Iface Item for chaining method calls
354
	 */
355
	public function setModified() : \Aimeos\MShop\Common\Item\Iface
356
	{
357
		$this->modified = true;
358
		return $this;
359
	}
360
361
362
	/**
363
	 * Returns the item type
364
	 *
365
	 * @return string Item type, subtypes are separated by slashes
366
	 */
367
	public function getResourceType() : string
368
	{
369
		if( !$this->type )
370
		{
371
			$parts = explode( '\\', strtolower( get_class( $this ) ) );
372
			array_shift( $parts ); array_shift( $parts ); // remove "Aimeos\MShop"
373
			array_pop( $parts );
374
375
			$domain = array_shift( $parts ) ?: 'custom';
376
			array_shift( $parts ); // remove "item"
377
			array_unshift( $parts, $domain );
378
379
			$this->type = join( '/', $parts );
380
		}
381
382
		return $this->type;
383
	}
384
385
386
	/**
387
	 * Sets the item values from the given array and removes that entries from the list
388
	 *
389
	 * @param array $list Associative list of item keys and their values
390
	 * @param bool True to set private properties too, false for public only
391
	 * @return \Aimeos\MShop\Common\Item\Iface Item for chaining method calls
392
	 */
393
	public function fromArray( array &$list, bool $private = false ) : \Aimeos\MShop\Common\Item\Iface
394
	{
395
		if( $private && array_key_exists( $this->bprefix . 'id', $list ) )
396
		{
397
			$this->setId( $list[$this->bprefix . 'id'] );
398
			unset( $list[$this->bprefix . 'id'] );
399
		}
400
401
		// Add custom columns
402
		foreach( $list as $key => $value )
403
		{
404
			if( ( $value === null || is_scalar( $value ) ) && strpos( $key, '.' ) === false ) {
405
				$this->set( $key, $value );
406
			}
407
		}
408
409
		return $this;
410
	}
411
412
413
	/**
414
	 * Returns the item values as array.
415
	 *
416
	 * @param bool True to return private properties, false for public only
417
	 * @return array Associative list of item properties and their values
418
	 */
419
	public function toArray( bool $private = false ) : array
420
	{
421
		$list = [$this->bprefix . 'id' => $this->getId()];
422
423
		if( $private === true )
424
		{
425
			$list[$this->bprefix . 'siteid'] = $this->getSiteId();
426
			$list[$this->bprefix . 'ctime'] = $this->getTimeCreated();
427
			$list[$this->bprefix . 'mtime'] = $this->getTimeModified();
428
			$list[$this->bprefix . 'editor'] = $this->editor();
429
		}
430
431
		foreach( $this->bdata as $key => $value )
432
		{
433
			if( strpos( $key, '.' ) === false ) {
434
				$list[$key] = $value;
435
			}
436
		}
437
438
		return $list;
439
	}
440
441
442
	/**
443
	 * Tests if the date parameter represents an ISO format.
444
	 *
445
	 * @param string|null $date ISO date in yyyy-mm-dd HH:ii:ss format or null
446
	 * @return string|null Clean date or null for no date
447
	 * @throws \Aimeos\MShop\Exception If the date is invalid
448
	 */
449
	protected function checkDateFormat( ?string $date ) : ?string
450
	{
451
		$regex = '/^[0-9]{4}-[0-1][0-9]-[0-3][0-9](( |T)[0-2][0-9]:[0-5][0-9](:[0-5][0-9])?)?$/';
452
453
		if( $date != null )
454
		{
455
			if( preg_match( $regex, (string) $date ) !== 1 ) {
456
				throw new \Aimeos\MShop\Exception( sprintf( 'Invalid characters in date, ISO format "YYYY-MM-DD hh:mm:ss" expected' ) );
457
			}
458
459
			if( strlen( $date ) === 16 ) {
460
				$date .= ':00';
461
			}
462
463
			return str_replace( 'T', ' ', (string) $date );
464
		}
465
466
		return null;
467
	}
468
469
470
	/**
471
	 * Tests if the date param represents an ISO format.
472
	 *
473
	 * @param string|null $date ISO date in YYYY-MM-DD format or null for no date
474
	 */
475
	protected function checkDateOnlyFormat( ?string $date ) : ?string
476
	{
477
		if( $date !== null && $date !== '' )
478
		{
479
			if( preg_match( '/^[0-9]{4}-[0-1][0-9]-[0-3][0-9]$/', (string) $date ) !== 1 ) {
480
				throw new \Aimeos\MShop\Exception( sprintf( 'Invalid characters in date, ISO format "YYYY-MM-DD" expected' ) );
481
			}
482
483
			return (string) $date;
484
		}
485
486
		return null;
487
	}
488
489
490
	/**
491
	 * Tests if the code is valid.
492
	 *
493
	 * @param string $code New code for an item
494
	 * @param int $length Number of allowed characters
495
	 * @return string Item code
496
	 * @throws \Aimeos\MShop\Exception If the code is invalid
497
	 */
498
	protected function checkCode( string $code, int $length = 64 ) : string
499
	{
500
		if( strlen( $code ) > $length ) {
501
			throw new \Aimeos\MShop\Exception( sprintf( 'Code is too long' ) );
502
		}
503
504
		if( preg_match( '/[ \x{0000}\x{0009}\x{000A}\x{000C}\x{000D}\x{0085}]+/u', $code ) === 1 ) {
505
			throw new \Aimeos\MShop\Exception( sprintf( 'Code contains invalid characters: "%1$s"', $code ) );
506
		}
507
508
		return $code;
509
	}
510
511
512
	/**
513
	 * Tests if the country ID parameter represents an ISO country format.
514
	 *
515
	 * @param string|null $countryid Two letter ISO country format, e.g. DE
516
	 * @param bool $null True if null is allowed, false if not
517
	 * @return string|null Two letter ISO country ID or null for no country
518
	 * @throws \Aimeos\MShop\Exception If the country ID is invalid
519
	 */
520
	protected function checkCountryId( ?string $countryid, bool $null = true ) : ?string
521
	{
522
		if( $null === false && $countryid == null ) {
523
			throw new \Aimeos\MShop\Exception( sprintf( 'Invalid ISO country code' ) );
524
		}
525
526
		if( $countryid != null )
527
		{
528
			if( preg_match( '/^[A-Za-z]{2}$/', $countryid ) !== 1 ) {
529
				throw new \Aimeos\MShop\Exception( sprintf( 'Invalid ISO country code' ) );
530
			}
531
532
			return strtoupper( $countryid );
533
		}
534
535
		return null;
536
	}
537
538
539
	/**
540
	 * Tests if the currency ID parameter represents an ISO currency format.
541
	 *
542
	 * @param string|null $currencyid Three letter ISO currency format, e.g. EUR
543
	 * @param bool $null True if null is allowed, false if not
544
	 * @return string|null Three letter ISO currency ID or null for no currency
545
	 * @throws \Aimeos\MShop\Exception If the currency ID is invalid
546
	 */
547
	protected function checkCurrencyId( ?string $currencyid, bool $null = true ) : ?string
548
	{
549
		if( $null === false && $currencyid == null ) {
550
			throw new \Aimeos\MShop\Exception( sprintf( 'Invalid ISO currency code' ) );
551
		}
552
553
		if( $currencyid != null )
554
		{
555
			if( preg_match( '/^[A-Za-z]{3}$/', $currencyid ) !== 1 ) {
556
				throw new \Aimeos\MShop\Exception( sprintf( 'Invalid ISO currency code' ) );
557
			}
558
559
			return strtoupper( $currencyid );
560
		}
561
562
		return null;
563
	}
564
565
566
	/**
567
	 * Tests if the language ID parameter represents an ISO language format.
568
	 *
569
	 * @param string|null $langid ISO language format, e.g. de or de_DE
570
	 * @param bool $null True if null is allowed, false if not
571
	 * @return string|null ISO language ID or null for no language
572
	 * @throws \Aimeos\MShop\Exception If the language ID is invalid
573
	 */
574
	protected function checkLanguageId( ?string $langid, bool $null = true ) : ?string
575
	{
576
		if( $null === false && $langid == null ) {
577
			throw new \Aimeos\MShop\Exception( sprintf( 'Invalid ISO language code' ) );
578
		}
579
580
		if( $langid != null )
581
		{
582
			if( preg_match( '/^[a-zA-Z]{2}(_[a-zA-Z]{2})?$/', $langid ) !== 1 ) {
583
				throw new \Aimeos\MShop\Exception( sprintf( 'Invalid ISO language code' ) );
584
			}
585
586
			$parts = explode( '_', $langid );
587
			$parts[0] = strtolower( $parts[0] );
588
589
			if( isset( $parts[1] ) ) {
590
				$parts[1] = strtoupper( $parts[1] );
591
			}
592
593
			return implode( '_', $parts );
594
		}
595
596
		return null;
597
	}
598
599
600
	/**
601
	 * Returns the prefix for the item properties
602
	 *
603
	 * @return string Prefix for the item properties
604
	 */
605
	protected function prefix() : string
606
	{
607
		return $this->bprefix;
608
	}
609
}
610