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 | } |