Created
June 17, 2024 07:56
-
-
Save JarJak/b0f57b57483e65573d941f960339fd84 to your computer and use it in GitHub Desktop.
Optional properties php trait that distinguish from unset and null properties, useful tin DTOs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
trait OptionalProperties | |
{ | |
public static function fromArray(array $args = [], bool $strict = true): self | |
{ | |
$obj = (new ReflectionClass(self::class))->newInstanceWithoutConstructor(); | |
foreach ($args as $prop => $value) { | |
if (!property_exists($obj, $prop)) { | |
if ($strict) { | |
throw new InvalidArgumentException('Unknown property '.$prop); | |
} | |
} else { | |
if ($arrayableType = self::getArrayablePropType($prop)) { | |
self::setValue($obj, $prop, $arrayableType::fromArray($value)); | |
} else { | |
self::setValue($obj, $prop, $value); | |
} | |
} | |
} | |
return $obj; | |
} | |
/** | |
* Universal private setter to set also private properties | |
*/ | |
private static function setValue($obj, $prop, $value): void | |
{ | |
(new ReflectionProperty($obj, $prop))->setValue($obj, $value); | |
} | |
public function toArray(): array | |
{ | |
return array_filter(array_map( | |
fn ($val) => self::isArrayableVal($val) ? $val->toArray() : $val, | |
get_object_vars($this) | |
)); | |
} | |
/** | |
* isset($prop) && null !== $prop | |
*/ | |
public function isSet(string $prop): bool | |
{ | |
return (new ReflectionProperty($this, $prop))->isInitialized($this); | |
} | |
private static function getArrayablePropType(string $prop): ?string | |
{ | |
$type = (new ReflectionProperty(self::class, $prop))->getType(); | |
if (method_exists($type->getName(), 'fromArray')) { | |
return $type->getName(); | |
} | |
return null; | |
} | |
private static function isArrayableVal($val): bool | |
{ | |
return is_object($val) && method_exists($val, 'toArray'); | |
} | |
/** | |
* Makes all object props public readonly and default null | |
*/ | |
public function __get($prop) | |
{ | |
return $this->isSet($prop) ? $this->$prop : null; | |
} | |
} | |
class Address | |
{ | |
use OptionalProperties; | |
public function __construct( | |
private string $country, | |
private ?string $city, | |
private ?string $street, | |
private ?Address $nestedAddress | |
) {} | |
} | |
$addressData = [ | |
'country' => 'DE', | |
'street' => null, | |
'unknown' => 'value', | |
'nestedAddress' => [ | |
'country' => 'E', | |
] | |
]; | |
$address = Address::fromArray($addressData, false); | |
var_dump($address->toArray()); | |
var_dump($address->city); | |
var_dump($address->city ?? 'default city'); | |
var_dump($address->isSet('city')); | |
if ($address->isSet('city')) { | |
// new value provided, even if null | |
var_dump('new city value:', $address->city); | |
} else { | |
var_dump('city value not provided'); | |
} | |
var_dump($address->street ?? 'default street'); | |
var_dump($address->isSet('street')); | |
if ($address->isSet('street')) { | |
// new value provided, even if null | |
var_dump('new street value:', $address->street); | |
} else { | |
var_dump('street value not provided'); | |
} | |
var_dump($address); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment