Skip to content

Instantly share code, notes, and snippets.

@JarJak
Created June 17, 2024 07:56
Show Gist options
  • Save JarJak/b0f57b57483e65573d941f960339fd84 to your computer and use it in GitHub Desktop.
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
<?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