Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
100.00% |
103 / 103 |
|
100.00% |
21 / 21 |
CRAP | |
100.00% |
1 / 1 |
Entity | |
100.00% |
103 / 103 |
|
100.00% |
21 / 21 |
49 | |
100.00% |
1 / 1 |
__construct | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
__isset | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
__unset | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
__set | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
3 | |||
__get | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
3 | |||
propertyNotDefined | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
init | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
renderMethodName | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
2 | |||
populate | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
3 | |||
setProperty | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
4 | |||
typeHint | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
3 | |||
typeHintCustom | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
typeHintNative | |
100.00% |
17 / 17 |
|
100.00% |
1 / 1 |
9 | |||
typeHintAplus | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
3 | |||
jsonOptions | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
timezone | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
toModel | |
100.00% |
15 / 15 |
|
100.00% |
1 / 1 |
4 | |||
jsonSerialize | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
2 | |||
getObjectVars | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
3 | |||
getJsonVars | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
setJsonVars | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 |
1 | <?php declare(strict_types=1); |
2 | /* |
3 | * This file is part of Aplus Framework MVC Library. |
4 | * |
5 | * (c) Natan Felles <natanfelles@gmail.com> |
6 | * |
7 | * For the full copyright and license information, please view the LICENSE |
8 | * file that was distributed with this source code. |
9 | */ |
10 | namespace Framework\MVC; |
11 | |
12 | use DateTime; |
13 | use DateTimeInterface; |
14 | use DateTimeZone; |
15 | use Framework\Date\Date; |
16 | use Framework\HTTP\URL; |
17 | use JsonException; |
18 | use OutOfBoundsException; |
19 | use ReflectionProperty; |
20 | use stdClass; |
21 | |
22 | /** |
23 | * Class Entity. |
24 | * |
25 | * @package mvc |
26 | */ |
27 | abstract class Entity implements \JsonSerializable //, \Stringable |
28 | { |
29 | protected int $_jsonOptions = \JSON_UNESCAPED_SLASHES | \JSON_UNESCAPED_UNICODE |
30 | | \JSON_PRESERVE_ZERO_FRACTION | \JSON_THROW_ON_ERROR; |
31 | /** |
32 | * @var array<string> |
33 | */ |
34 | protected array $_jsonVars = []; |
35 | protected string $_timezone = '+00:00'; |
36 | |
37 | /** |
38 | * @param array<string,mixed> $properties |
39 | */ |
40 | public function __construct(array $properties) |
41 | { |
42 | $this->populate($properties); |
43 | $this->init(); |
44 | } |
45 | |
46 | public function __isset(string $property) : bool |
47 | { |
48 | return isset($this->{$property}); |
49 | } |
50 | |
51 | public function __unset(string $property) : void |
52 | { |
53 | unset($this->{$property}); |
54 | } |
55 | |
56 | /** |
57 | * @param string $property |
58 | * @param mixed $value |
59 | * |
60 | * @throws OutOfBoundsException If property is not defined |
61 | */ |
62 | public function __set(string $property, mixed $value) : void |
63 | { |
64 | $method = $this->renderMethodName('set', $property); |
65 | if (\method_exists($this, $method)) { |
66 | $this->{$method}($value); |
67 | return; |
68 | } |
69 | if (\property_exists($this, $property)) { |
70 | $this->{$property} = $value; |
71 | return; |
72 | } |
73 | throw $this->propertyNotDefined($property); |
74 | } |
75 | |
76 | /** |
77 | * @param string $property |
78 | * |
79 | * @throws OutOfBoundsException If property is not defined |
80 | * |
81 | * @return mixed |
82 | */ |
83 | public function __get(string $property) : mixed |
84 | { |
85 | $method = $this->renderMethodName('get', $property); |
86 | if (\method_exists($this, $method)) { |
87 | return $this->{$method}(); |
88 | } |
89 | if (\property_exists($this, $property)) { |
90 | return $this->{$property}; |
91 | } |
92 | throw $this->propertyNotDefined($property); |
93 | } |
94 | |
95 | protected function propertyNotDefined(string $property) : OutOfBoundsException |
96 | { |
97 | return new OutOfBoundsException('Property not defined: ' . $property); |
98 | } |
99 | |
100 | /** |
101 | * Used to initialize settings, set custom properties, etc. |
102 | * Called in the constructor just after the properties be populated. |
103 | */ |
104 | protected function init() : void |
105 | { |
106 | } |
107 | |
108 | /** |
109 | * @param string $type get or set |
110 | * @param string $property Property name |
111 | * |
112 | * @return string |
113 | */ |
114 | protected function renderMethodName(string $type, string $property) : string |
115 | { |
116 | static $properties; |
117 | if (isset($properties[$property])) { |
118 | return $type . $properties[$property]; |
119 | } |
120 | $name = \ucwords($property, '_'); |
121 | $name = \strtr($name, ['_' => '']); |
122 | $properties[$property] = $name; |
123 | return $type . $name; |
124 | } |
125 | |
126 | /** |
127 | * @param array<string,mixed> $properties |
128 | */ |
129 | protected function populate(array $properties) : void |
130 | { |
131 | foreach ($properties as $property => $value) { |
132 | $method = $this->renderMethodName('set', $property); |
133 | if (\method_exists($this, $method)) { |
134 | $this->{$method}($value); |
135 | continue; |
136 | } |
137 | $this->setProperty($property, $value); |
138 | } |
139 | } |
140 | |
141 | protected function setProperty(string $name, mixed $value) : void |
142 | { |
143 | if ( ! \property_exists($this, $name)) { |
144 | throw $this->propertyNotDefined($name); |
145 | } |
146 | if ($value !== null) { |
147 | $rp = new ReflectionProperty($this, $name); |
148 | $propertyType = $rp->getType()?->getName(); // @phpstan-ignore-line |
149 | if ($propertyType !== null) { |
150 | $value = $this->typeHint($propertyType, $value); |
151 | } |
152 | } |
153 | $this->{$name} = $value; |
154 | } |
155 | |
156 | protected function typeHint(string $propertyType, mixed $value) : mixed |
157 | { |
158 | $valueType = \get_debug_type($value); |
159 | $newValue = $this->typeHintCustom($propertyType, $valueType, $value); |
160 | if ($newValue === null) { |
161 | $newValue = $this->typeHintNative($propertyType, $valueType, $value); |
162 | } |
163 | if ($newValue === null) { |
164 | $newValue = $this->typeHintAplus($propertyType, $valueType, $value); |
165 | } |
166 | return $newValue ?? $value; |
167 | } |
168 | |
169 | protected function typeHintCustom(string $propertyType, string $valueType, mixed $value) : mixed |
170 | { |
171 | return null; |
172 | } |
173 | |
174 | protected function typeHintNative(string $propertyType, string $valueType, mixed $value) : mixed |
175 | { |
176 | if ($propertyType === 'array') { |
177 | return $valueType === 'string' |
178 | ? \json_decode($value, true, flags: $this->jsonOptions()) |
179 | : (array) $value; |
180 | } |
181 | if ($propertyType === 'bool') { |
182 | return (bool) $value; |
183 | } |
184 | if ($propertyType === 'float') { |
185 | return (float) $value; |
186 | } |
187 | if ($propertyType === 'int') { |
188 | return (int) $value; |
189 | } |
190 | if ($propertyType === 'string') { |
191 | return (string) $value; |
192 | } |
193 | if ($propertyType === stdClass::class) { |
194 | return $valueType === 'string' |
195 | ? (object) \json_decode($value, flags: $this->jsonOptions()) |
196 | : (object) $value; |
197 | } |
198 | return null; |
199 | } |
200 | |
201 | protected function typeHintAplus(string $propertyType, string $valueType, mixed $value) : mixed |
202 | { |
203 | if ($propertyType === Date::class) { |
204 | return new Date((string) $value); |
205 | } |
206 | if ($propertyType === URL::class) { |
207 | return new URL((string) $value); |
208 | } |
209 | return null; |
210 | } |
211 | |
212 | protected function jsonOptions() : int |
213 | { |
214 | return $this->_jsonOptions; |
215 | } |
216 | |
217 | protected function timezone() : DateTimeZone |
218 | { |
219 | return new DateTimeZone($this->_timezone); |
220 | } |
221 | |
222 | /** |
223 | * Convert the Entity to an associative array accepted by Model methods. |
224 | * |
225 | * @throws JsonException |
226 | * |
227 | * @return array<string,scalar> |
228 | */ |
229 | public function toModel() : array |
230 | { |
231 | $jsonVars = $this->getJsonVars(); |
232 | $this->setJsonVars(\array_keys($this->getObjectVars())); |
233 | // @phpstan-ignore-next-line |
234 | $data = \json_decode(\json_encode($this, $this->jsonOptions()), true, 512, $this->jsonOptions()); |
235 | foreach ($data as $property => &$value) { |
236 | if (\is_array($value)) { |
237 | $value = \json_encode($value, $this->jsonOptions()); |
238 | continue; |
239 | } |
240 | $type = \get_debug_type($this->{$property}); |
241 | if (\is_subclass_of($type, DateTimeInterface::class)) { |
242 | $datetime = DateTime::createFromFormat(DateTimeInterface::ATOM, $value); |
243 | $datetime->setTimezone($this->timezone()); // @phpstan-ignore-line |
244 | $value = $datetime->format('Y-m-d H:i:s'); // @phpstan-ignore-line |
245 | } |
246 | } |
247 | unset($value); |
248 | $this->setJsonVars($jsonVars); |
249 | return $data; |
250 | } |
251 | |
252 | public function jsonSerialize() : stdClass |
253 | { |
254 | if ( ! $this->getJsonVars()) { |
255 | return new stdClass(); |
256 | } |
257 | $allowed = \array_flip($this->getJsonVars()); |
258 | $filtered = \array_intersect_key($this->getObjectVars(), $allowed); |
259 | $allowed = \array_intersect_key($allowed, $filtered); |
260 | $ordered = \array_replace($allowed, $filtered); |
261 | return (object) $ordered; |
262 | } |
263 | |
264 | /** |
265 | * @return array<string,mixed> |
266 | */ |
267 | protected function getObjectVars() : array |
268 | { |
269 | $result = []; |
270 | foreach (\get_object_vars($this) as $key => $value) { |
271 | if ( ! \str_starts_with($key, '_')) { |
272 | $result[$key] = $value; |
273 | } |
274 | } |
275 | return $result; |
276 | } |
277 | |
278 | /** |
279 | * @return array<string> |
280 | */ |
281 | public function getJsonVars() : array |
282 | { |
283 | return $this->_jsonVars; |
284 | } |
285 | |
286 | /** |
287 | * @param array<string> $vars |
288 | */ |
289 | public function setJsonVars(array $vars) : static |
290 | { |
291 | $this->_jsonVars = $vars; |
292 | return $this; |
293 | } |
294 | } |