Passed
Push — master ( 2bb224...2eebaa )
by Aimeos
06:13
created

Base::call()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

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