Skip to content

Commit 0a6b059

Browse files
authored
Merge pull request #28 from hypervel/feature/support-data-object
feat: implement DataObject in support
2 parents 6938401 + 4b2aa06 commit 0a6b059

File tree

2 files changed

+497
-0
lines changed

2 files changed

+497
-0
lines changed

src/support/src/DataObject.php

Lines changed: 233 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,233 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Hypervel\Support;
6+
7+
use ArrayAccess;
8+
use JsonSerializable;
9+
use LogicException;
10+
use OutOfBoundsException;
11+
use ReflectionClass;
12+
use ReflectionNamedType;
13+
use ReflectionParameter;
14+
use ReflectionProperty;
15+
use RuntimeException;
16+
17+
abstract class DataObject implements ArrayAccess, JsonSerializable
18+
{
19+
/**
20+
* Property map cache (class name => [snake_case key => camelCase property]).
21+
*
22+
* @var array<string,string>>
23+
*/
24+
protected array $propertyMapCache = [];
25+
26+
/**
27+
* Cache for the array representation of the object.
28+
*/
29+
protected array $arrayCache = [];
30+
31+
/**
32+
* Flag to indicate if auto-casting is enabled.
33+
*/
34+
protected static bool $autoCasting = true;
35+
36+
/**
37+
* Create an instance of the class using the provided data array.
38+
*/
39+
public static function make(array $data): static
40+
{
41+
$reflection = new ReflectionClass(static::class);
42+
$constructor = $reflection->getConstructor();
43+
$parameters = $constructor->getParameters();
44+
$constructorArgs = [];
45+
46+
foreach ($parameters as $parameter) {
47+
$paramName = $parameter->getName();
48+
$dataKey = static::convertDataKeyToProperty($paramName);
49+
$dataValue = null;
50+
51+
// check if the data key exists in the array
52+
// and convert the value to the correct type automatically
53+
if (array_key_exists($dataKey, $data)) {
54+
$dataValue = $data[$dataKey];
55+
if (static::$autoCasting) {
56+
$dataValue = static::convertValueToType($dataValue, $parameter);
57+
}
58+
// use the default value if available
59+
} elseif ($parameter->isDefaultValueAvailable()) {
60+
$dataValue = $parameter->getDefaultValue();
61+
} else {
62+
$dataValue = static::getDefaultValueForType($parameter);
63+
}
64+
65+
$constructorArgs[$paramName] = $dataValue;
66+
}
67+
68+
return new static(...$constructorArgs);
69+
}
70+
71+
/**
72+
* Enable or disable auto-casting of data values.
73+
*/
74+
public static function enableAutoCasting(): void
75+
{
76+
static::$autoCasting = true;
77+
}
78+
79+
/**
80+
* Disable auto-casting of data values.
81+
*/
82+
public static function disableAutoCasting(): void
83+
{
84+
static::$autoCasting = false;
85+
}
86+
87+
/**
88+
* Convert the parameter name to the data key format.
89+
* It converts camelCase to snake_case by default.
90+
*/
91+
protected static function convertDataKeyToProperty(string $input): string
92+
{
93+
return Str::snake($input);
94+
}
95+
96+
/**
97+
* Convert the property name to the data key format.
98+
* It converts snake_case to camelCase by default.
99+
*/
100+
protected static function convertPropertyToDataKey(string $input): string
101+
{
102+
return Str::camel($input);
103+
}
104+
105+
/**
106+
* Convert the value to the correct type based on the parameter type.
107+
*/
108+
protected static function convertValueToType(mixed $value, ReflectionParameter $parameter): mixed
109+
{
110+
if (! $type = $parameter->getType()) {
111+
return $value;
112+
}
113+
114+
if ($type instanceof ReflectionNamedType) {
115+
return match ($type->getName()) {
116+
'int' => (int) $value,
117+
'float' => (float) $value,
118+
'string' => (string) $value,
119+
'bool' => (bool) $value,
120+
'array' => is_array($value) ? $value : [$value],
121+
default => $value,
122+
};
123+
}
124+
125+
return $value;
126+
}
127+
128+
/**
129+
* Get default value for the parameter type.
130+
*/
131+
protected static function getDefaultValueForType(ReflectionParameter $parameter): mixed
132+
{
133+
$type = $parameter->getType();
134+
if (! $type || $type->allowsNull()) {
135+
return null;
136+
}
137+
138+
throw new RuntimeException(
139+
"Missing required property `{$parameter->name}` in `" . static::class . '`'
140+
);
141+
}
142+
143+
/**
144+
* Get property map (snake_case key => camelCase property).
145+
*
146+
* @return array<string, string>
147+
*/
148+
protected function getPropertyMap(): array
149+
{
150+
if ($this->propertyMapCache) {
151+
return $this->propertyMapCache;
152+
}
153+
154+
$reflection = new ReflectionClass($this);
155+
$properties = $reflection->getProperties(ReflectionProperty::IS_PUBLIC);
156+
157+
foreach ($properties as $property) {
158+
$propName = $property->getName();
159+
$snakeKey = static::convertDataKeyToProperty($propName);
160+
$this->propertyMapCache[$snakeKey] = $propName;
161+
}
162+
163+
return $this->propertyMapCache;
164+
}
165+
166+
/**
167+
* Check if the offset exists.
168+
*/
169+
public function offsetExists(mixed $offset): bool
170+
{
171+
return array_key_exists($offset, $this->getPropertyMap());
172+
}
173+
174+
/**
175+
* Get the value at the specified offset.
176+
*/
177+
public function offsetGet(mixed $offset): mixed
178+
{
179+
return $this->toArray()[$offset]
180+
?? throw new OutOfBoundsException("Undefined offset: {$offset}");
181+
}
182+
183+
/**
184+
* Set the value at the specified offset.
185+
*/
186+
public function offsetSet(mixed $offset, mixed $value): void
187+
{
188+
throw new LogicException('Data object may not be mutated using array access.');
189+
}
190+
191+
/**
192+
* Unset the value at the specified offset.
193+
*/
194+
public function offsetUnset(mixed $offset): void
195+
{
196+
throw new LogicException('Data object may not be mutated using array access.');
197+
}
198+
199+
/**
200+
* Convert the object to an array representation.
201+
*/
202+
public function toArray(): array
203+
{
204+
if ($this->arrayCache) {
205+
return $this->arrayCache;
206+
}
207+
208+
$result = [];
209+
$map = $this->getPropertyMap();
210+
211+
foreach ($map as $snakeKey => $propName) {
212+
$value = $this->{$propName};
213+
// recursively convert nested objects to arrays
214+
if ($value instanceof self) {
215+
$value = $value->toArray();
216+
} elseif (is_object($value) && method_exists($value, 'toArray')) {
217+
$value = $value->toArray();
218+
}
219+
220+
$result[$snakeKey] = $value;
221+
}
222+
223+
return $this->arrayCache = $result;
224+
}
225+
226+
/**
227+
* JSON serialize the object.
228+
*/
229+
public function jsonSerialize(): array
230+
{
231+
return $this->toArray();
232+
}
233+
}

0 commit comments

Comments
 (0)