Passed
Push — master ( 9a12ca...ba8ea1 )
by Aimeos
07:55
created

Base::fromArray()   B

Complexity

Conditions 7
Paths 6

Size

Total Lines 17
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 7
eloc 7
nc 6
nop 2
dl 0
loc 17
rs 8.8333
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-2021
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
abstract class Base
22
	extends \Aimeos\MW\Common\Item\Base
23
	implements \Aimeos\MShop\Common\Item\Iface, \ArrayAccess
24
{
25
	private $prefix;
26
	private $available = true;
27
	private $modified = false;
28
29
	protected static $methods = [];
30
	protected $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
	 */
39
	public function __construct( string $prefix, array $values )
40
	{
41
		$this->prefix = (string) $prefix;
42
		$this->bdata = $values;
43
	}
44
45
46
	/**
47
	 * Handles dynamic calls to custom methods for the class.
48
	 *
49
	 * Calls a custom method added by Item::method(). The called method has
50
	 * access to the internal $this->bdata property and all other proteced
51
	 * properties.
52
	 *
53
	 * @param string $name Method name
54
	 * @param array $params List of parameters
55
	 * @return mixed Result from called function
56
	 * @throws \BadMethodCallException If the method hasn't been registered
57
	 */
58
	public function __call( string $name, array $params )
59
	{
60
		if( isset( static::$methods[$name] ) ) {
61
			return call_user_func_array( static::$methods[$name]->bindTo( $this, static::class ), $params );
62
		}
63
64
		$msg = sprintf( 'Called unknown method "%1$s" on class "%2$s"', $name, get_class( $this ) );
65
		throw new \BadMethodCallException( $msg );
66
	}
67
68
69
	/**
70
	 * Registers a custom method that has access to the class properties if called non-static.
71
	 *
72
	 * Examples:
73
	 *  Item::method( 'test', function( $arg1, $arg2 ) {
74
	 *      return  $arg1 + $arg2;
75
	 *  } );
76
	 *
77
	 * @param string $name Method name
78
	 * @param \Closure $function Anonymous method
79
	 */
80
	public static function method( string $name, \Closure $function )
81
	{
82
		static::$methods[$name] = $function;
83
	}
84
85
86
	/**
87
	 * Creates a deep clone of all objects
88
	 */
89
	public function __clone()
90
	{
91
	}
92
93
94
	/**
95
	 * Returns the item property for the given name
96
	 *
97
	 * @param string $name Name of the property
98
	 * @return mixed|null Property value or null if property is unknown
99
	 */
100
	public function __get( string $name )
101
	{
102
		return $this->get( $name );
103
	}
104
105
106
	/**
107
	 * Tests if the item property for the given name is available
108
	 *
109
	 * @param string $name Name of the property
110
	 * @return bool True if the property exists, false if not
111
	 */
112
	public function __isset( string $name ) : bool
113
	{
114
		return array_key_exists( $name, $this->bdata );
115
	}
116
117
118
	/**
119
	 * Sets the new item property for the given name
120
	 *
121
	 * @param string $name Name of the property
122
	 * @param mixed $value New property value
123
	 */
124
	public function __set( string $name, $value )
125
	{
126
		$this->set( $name, $value );
127
	}
128
129
130
	/**
131
	 * Tests if the item property for the given name is available
132
	 *
133
	 * @param string $name Name of the property
134
	 * @return bool True if the property exists, false if not
135
	 */
136
	public function offsetExists( $name )
137
	{
138
		return array_key_exists( $name, $this->bdata );
139
	}
140
141
142
	/**
143
	 * Returns the item property for the given name
144
	 *
145
	 * @param string $name Name of the property
146
	 * @return mixed|null Property value or null if property is unknown
147
	 */
148
	public function offsetGet( $name )
149
	{
150
		return $this->get( $name );
151
	}
152
153
154
	/**
155
	 * Sets the new item property for the given name
156
	 *
157
	 * @param string $name Name of the property
158
	 * @param mixed $value New property value
159
	 */
160
	public function offsetSet( $name, $value )
161
	{
162
		$this->set( $name, $value );
163
	}
164
165
166
	/**
167
	 * Removes an item property
168
	 * This is not supported by items
169
	 *
170
	 * @param string $name Name of the property
171
	 * @throws \LogicException Always thrown because this method isn't supported
172
	 */
173
	public function offsetUnset( $name )
174
	{
175
		throw new \LogicException( 'Not implemented' );
176
	}
177
178
179
	/**
180
	 * Returns the ID of the items
181
	 *
182
	 * @return string ID of the item or null
183
	 */
184
	public function __toString() : string
185
	{
186
		return (string) $this->getId();
187
	}
188
189
190
	/**
191
	 * Assigns multiple key/value pairs to the item
192
	 *
193
	 * @param iterable $pairs Associative list of key/value pairs
194
	 * @return \Aimeos\MShop\Common\Item\Iface Item for method chaining
195
	 */
196
	public function assign( iterable $pairs ) : \Aimeos\MShop\Common\Item\Iface
197
	{
198
		foreach( $pairs as $key => $value ) {
199
			$this->set( $key, $value );
200
		}
201
202
		return $this;
203
	}
204
205
206
	/**
207
	 * Returns the item property for the given name
208
	 *
209
	 * @param string $name Name of the property
210
	 * @param mixed $default Default value if property is unknown
211
	 * @return mixed|null Property value or default value if property is unknown
212
	 */
213
	public function get( string $name, $default = null )
214
	{
215
		if( array_key_exists( $name, $this->bdata ) ) {
216
			return $this->bdata[$name];
217
		}
218
219
		return $default;
220
	}
221
222
223
	/**
224
	 * Sets the new item property for the given name
225
	 *
226
	 * @param string $name Name of the property
227
	 * @param mixed $value New property value
228
	 * @return \Aimeos\MShop\Common\Item\Iface Item for method chaining
229
	 */
230
	public function set( string $name, $value ) : \Aimeos\MShop\Common\Item\Iface
231
	{
232
		// workaround for NULL values instead of empty strings and stringified integers from database
233
		if( !array_key_exists( $name, $this->bdata ) || $this->bdata[$name] != $value
234
			|| $value === null && $this->bdata[$name] !== null
235
			|| $value !== null && $this->bdata[$name] === null
236
		) {
237
			$this->bdata[$name] = $value;
238
			$this->setModified();
239
		}
240
241
		return $this;
242
	}
243
244
245
	/**
246
	 * Returns the ID of the item if available.
247
	 *
248
	 * @return string|null ID of the item
249
	 */
250
	public function getId() : ?string
251
	{
252
		$key = $this->prefix . 'id';
253
254
		if( isset( $this->bdata[$key] ) && $this->bdata[$key] != '' ) {
255
			return (string) $this->bdata[$key];
256
		}
257
258
		return null;
259
	}
260
261
262
	/**
263
	 * Sets the new ID of the item.
264
	 *
265
	 * @param string|null $id ID of the item
266
	 * @return \Aimeos\MShop\Common\Item\Iface Item for chaining method calls
267
	 */
268
	public function setId( ?string $id ) : \Aimeos\MShop\Common\Item\Iface
269
	{
270
		$key = $this->prefix . 'id';
271
272
		if( ( $this->bdata[$key] = $this->checkId( $this->getId(), $id ) ) === null ) {
273
			$this->modified = true;
274
		} else {
275
			$this->modified = false;
276
		}
277
278
		return $this;
279
	}
280
281
282
	/**
283
	 * Returns the site ID of the item.
284
	 *
285
	 * @return string Site ID or null if no site id is available
286
	 */
287
	public function getSiteId() : string
288
	{
289
		return $this->get( $this->prefix . 'siteid', $this->get( 'siteid', '' ) );
290
	}
291
292
293
	/**
294
	 * Returns modify date/time of the order coupon.
295
	 *
296
	 * @return string|null Modification time (YYYY-MM-DD HH:mm:ss)
297
	 */
298
	public function getTimeModified() : ?string
299
	{
300
		return $this->get( $this->prefix . 'mtime', $this->get( 'mtime' ) );
301
	}
302
303
304
	/**
305
	 * Returns the create date of the item.
306
	 *
307
	 * @return string|null ISO date in YYYY-MM-DD hh:mm:ss format
308
	 */
309
	public function getTimeCreated() : ?string
310
	{
311
		return $this->get( $this->prefix . 'ctime', $this->get( 'ctime' ) );
312
	}
313
314
315
	/**
316
	 * Returns the name of editor who created/modified the item at last.
317
	 *
318
	 * @return string Name of editor who created/modified the item at last
319
	 */
320
	public function getEditor() : string
321
	{
322
		return $this->get( $this->prefix . 'editor', $this->get( 'editor', '' ) );
323
	}
324
325
326
	/**
327
	 * Tests if the item is available based on status, time, language and currency
328
	 *
329
	 * @return bool True if available, false if not
330
	 */
331
	public function isAvailable() : bool
332
	{
333
		return $this->available;
334
	}
335
336
337
	/**
338
	 * Sets the general availability of the item
339
	 *
340
	 * @return bool $value True if available, false if not
341
	 * @return \Aimeos\MShop\Common\Item\Iface Item for chaining method calls
342
	 */
343
	public function setAvailable( bool $value ) : \Aimeos\MShop\Common\Item\Iface
344
	{
345
		$this->available = $value;
346
		return $this;
347
	}
348
349
350
	/**
351
	 * Tests if this Item object was modified.
352
	 *
353
	 * @return bool True if modified, false if not
354
	 */
355
	public function isModified() : bool
356
	{
357
		return $this->modified;
358
	}
359
360
361
	/**
362
	 * Sets the modified flag of the object.
363
	 *
364
	 * @return \Aimeos\MShop\Common\Item\Iface Item for chaining method calls
365
	 */
366
	public function setModified() : \Aimeos\MShop\Common\Item\Iface
367
	{
368
		$this->modified = true;
369
		return $this;
370
	}
371
372
373
	/**
374
	 * Sets the item values from the given array and removes that entries from the list
375
	 *
376
	 * @param array $list Associative list of item keys and their values
377
	 * @param bool True to set private properties too, false for public only
378
	 * @return \Aimeos\MShop\Common\Item\Iface Item for chaining method calls
379
	 */
380
	public function fromArray( array &$list, bool $private = false ) : \Aimeos\MShop\Common\Item\Iface
381
	{
382
		if( $private && array_key_exists( $this->prefix . 'id', $list ) )
383
		{
384
			$this->setId( $list[$this->prefix . 'id'] );
385
			unset( $list[$this->prefix . 'id'] );
386
		}
387
388
		// Add custom columns
389
		foreach( $list as $key => $value )
390
		{
391
			if( ( $value === null || is_scalar( $value ) ) && strpos( $key, '.' ) === false ) {
392
				$this->set( $key, $value );
393
			}
394
		}
395
396
		return $this;
397
	}
398
399
400
	/**
401
	 * Returns the item values as array.
402
	 *
403
	 * @param bool True to return private properties, false for public only
404
	 * @return array Associative list of item properties and their values
405
	 */
406
	public function toArray( bool $private = false ) : array
407
	{
408
		$list = [$this->prefix . 'id' => $this->getId()];
409
410
		if( $private === true )
411
		{
412
			$list[$this->prefix . 'siteid'] = $this->getSiteId();
413
			$list[$this->prefix . 'ctime'] = $this->getTimeCreated();
414
			$list[$this->prefix . 'mtime'] = $this->getTimeModified();
415
			$list[$this->prefix . 'editor'] = $this->getEditor();
416
		}
417
418
		foreach( $this->bdata as $key => $value )
419
		{
420
			if( strpos( $key, '.' ) === false ) {
421
				$list[$key] = $value;
422
			}
423
		}
424
425
		return $list;
426
	}
427
428
429
	/**
430
	 * Checks if the new ID is valid for the item.
431
	 *
432
	 * @param string|null $old Current ID of the item
433
	 * @param string|null $new New ID which should be set in the item
434
	 * @return string|null Value of the new ID
435
	 */
436
	public static function checkId( ?string $old, ?string $new ) : ?string
437
	{
438
		return ( $new !== null ? (string) $new : $new );
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
			{
457
				$msg = sprintf( 'Invalid characters in date "%1$s". ISO format "YYYY-MM-DD hh:mm:ss" expected.', $date );
458
				throw new \Aimeos\MShop\Exception( $msg );
459
			}
460
461
			if( strlen( $date ) === 16 ) {
462
				$date .= ':00';
463
			}
464
465
			return str_replace( 'T', ' ', (string) $date );
466
		}
467
468
		return null;
469
	}
470
471
472
	/**
473
	 * Tests if the date param represents an ISO format.
474
	 *
475
	 * @param string|null $date ISO date in YYYY-MM-DD format or null for no date
476
	 */
477
	protected function checkDateOnlyFormat( ?string $date ) : ?string
478
	{
479
		if( $date !== null && $date !== '' )
480
		{
481
			if( preg_match( '/^[0-9]{4}-[0-1][0-9]-[0-3][0-9]$/', (string) $date ) !== 1 ) {
482
				throw new \Aimeos\MShop\Exception( sprintf( 'Invalid characters in date "%1$s". ISO format "YYYY-MM-DD" expected.', $date ) );
483
			}
484
485
			return (string) $date;
486
		}
487
488
		return null;
489
	}
490
491
492
	/**
493
	 * Tests if the code is valid.
494
	 *
495
	 * @param string $code New code for an item
496
	 * @param int $length Number of allowed characters
497
	 * @return string Item code
498
	 * @throws \Aimeos\MShop\Exception If the code is invalid
499
	 */
500
	protected function checkCode( string $code, int $length = 64 ) : string
501
	{
502
		if( strlen( $code ) > $length )
503
		{
504
			$msg = sprintf( 'Code "%1$s" must not be longer than %2$d characters', $code, $length );
505
			throw new \Aimeos\MShop\Exception( $msg );
506
		}
507
508
		return (string) $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 "%1$s"', '<null>' ) );
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 "%1$s"', $countryid ) );
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 "%1$s"', '<null>' ) );
551
		}
552
553
		if( $currencyid != null )
554
		{
555
			if( preg_match( '/^[A-Z]{3}$/', $currencyid ) !== 1 ) {
556
				throw new \Aimeos\MShop\Exception( sprintf( 'Invalid ISO currency code "%1$s"', $currencyid ) );
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 "%1$s"', '<null>' ) );
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 "%1$s"', $langid ) );
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