• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

aplus-framework / mvc / 16927505441

22 Jul 2025 07:08PM UTC coverage: 100.0%. Remained the same
16927505441

push

github

natanfelles
Add info about order in which commands are added to the console

1836 of 1836 relevant lines covered (100.0%)

10.61 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

100.0
/src/Entity.php
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 Exception;
16
use Framework\Date\Date;
17
use Framework\HTTP\URL;
18
use JsonException;
19
use OutOfBoundsException;
20
use ReflectionProperty;
21
use stdClass;
22

23
/**
24
 * Class Entity.
25
 *
26
 * @todo In PHP 8.4 add property hooks to validate config properties.
27
 *
28
 * @package mvc
29
 */
30
abstract class Entity implements \JsonSerializable, \Stringable
31
{
32
    /**
33
     * Sets the flags that will be used to encode/decode JSON in internal
34
     * methods of this Entity class.
35
     */
36
    public int $_jsonFlags = \JSON_UNESCAPED_SLASHES | \JSON_UNESCAPED_UNICODE
37
    | \JSON_PRESERVE_ZERO_FRACTION | \JSON_THROW_ON_ERROR;
38
    /**
39
     * Sets the name of the properties that will be visible when this Entity is
40
     * JSON encoded.
41
     *
42
     * @var array<string>
43
     */
44
    public array $_jsonVars = [];
45
    /**
46
     * This timezone is used to convert times in the {@see Entity::toModel()}
47
     * method.
48
     *
49
     * Note that it must be the same timezone as the database configurations.
50
     *
51
     * @see Model::timezone()
52
     */
53
    public string $_timezone = '+00:00';
54

55
    /**
56
     * @param array<string,mixed> $properties
57
     */
58
    public function __construct(array $properties)
59
    {
60
        $this->populate($properties);
27✔
61
        $this->init();
27✔
62
    }
63

64
    public function __isset(string $property) : bool
65
    {
66
        return isset($this->{$property});
1✔
67
    }
68

69
    public function __unset(string $property) : void
70
    {
71
        unset($this->{$property});
1✔
72
    }
73

74
    /**
75
     * @param string $property
76
     * @param mixed $value
77
     *
78
     * @throws OutOfBoundsException If property is not defined
79
     */
80
    public function __set(string $property, mixed $value) : void
81
    {
82
        $method = $this->renderMethodName('set', $property);
3✔
83
        if (\method_exists($this, $method)) {
3✔
84
            $this->{$method}($value);
1✔
85
            return;
1✔
86
        }
87
        if (\property_exists($this, $property)) {
2✔
88
            $this->{$property} = $value;
1✔
89
            return;
1✔
90
        }
91
        throw $this->propertyNotDefined($property);
1✔
92
    }
93

94
    /**
95
     * @param string $property
96
     *
97
     * @throws OutOfBoundsException If property is not defined
98
     *
99
     * @return mixed
100
     */
101
    public function __get(string $property) : mixed
102
    {
103
        $method = $this->renderMethodName('get', $property);
6✔
104
        if (\method_exists($this, $method)) {
6✔
105
            return $this->{$method}();
2✔
106
        }
107
        if (\property_exists($this, $property)) {
4✔
108
            return $this->{$property};
3✔
109
        }
110
        throw $this->propertyNotDefined($property);
1✔
111
    }
112

113
    /**
114
     * Converts the entity to a JSON string.
115
     * All properties will be included.
116
     * Please note that sensitive property data may be exposed!
117
     *
118
     * @return string
119
     */
120
    public function __toString() : string
121
    {
122
        $origin = $this->_jsonVars;
1✔
123
        $all = \array_keys($this->getObjectVars());
1✔
124
        $this->_jsonVars = $all;
1✔
125
        $json = \json_encode($this, $this->_jsonFlags);
1✔
126
        $this->_jsonVars = $origin;
1✔
127
        return $json; // @phpstan-ignore-line
1✔
128
    }
129

130
    protected function propertyNotDefined(string $property) : OutOfBoundsException
131
    {
132
        return new OutOfBoundsException('Property not defined: ' . $property);
4✔
133
    }
134

135
    /**
136
     * Used to initialize settings, set custom properties, etc.
137
     * Called in the constructor just after the properties be populated.
138
     */
139
    protected function init() : void
140
    {
141
    }
27✔
142

143
    /**
144
     * @param string $type get or set
145
     * @param string $property Property name
146
     *
147
     * @return string
148
     */
149
    protected function renderMethodName(string $type, string $property) : string
150
    {
151
        static $properties;
27✔
152
        if (isset($properties[$property])) {
27✔
153
            return $type . $properties[$property];
7✔
154
        }
155
        $name = \ucwords($property, '_');
27✔
156
        $name = \strtr($name, ['_' => '']);
27✔
157
        $properties[$property] = $name;
27✔
158
        return $type . $name;
27✔
159
    }
160

161
    /**
162
     * @param array<string,mixed> $properties
163
     */
164
    protected function populate(array $properties) : void
165
    {
166
        foreach ($properties as $property => $value) {
27✔
167
            $method = $this->renderMethodName('set', $property);
27✔
168
            if (\method_exists($this, $method)) {
27✔
169
                $this->{$method}($value);
4✔
170
                continue;
4✔
171
            }
172
            $this->setProperty($property, $value);
27✔
173
        }
174
    }
175

176
    protected function setProperty(string $name, mixed $value) : void
177
    {
178
        if (!\property_exists($this, $name)) {
27✔
179
            throw $this->propertyNotDefined($name);
2✔
180
        }
181
        if ($value !== null) {
27✔
182
            $rp = new ReflectionProperty($this, $name);
27✔
183
            $propertyType = $rp->getType()?->getName(); // @phpstan-ignore-line
27✔
184
            if ($propertyType !== null) {
27✔
185
                $value = $this->typeHint($propertyType, $value);
27✔
186
            }
187
        }
188
        $this->{$name} = $value;
27✔
189
    }
190

191
    /**
192
     * Tries to convert the value according to the property type.
193
     *
194
     * @param string $propertyType
195
     * @param mixed $value
196
     *
197
     * @return mixed
198
     */
199
    protected function typeHint(string $propertyType, mixed $value) : mixed
200
    {
201
        $valueType = \get_debug_type($value);
27✔
202
        $newValue = $this->typeHintCustom($propertyType, $valueType, $value);
27✔
203
        if ($newValue === null) {
27✔
204
            $newValue = $this->typeHintNative($propertyType, $valueType, $value);
27✔
205
        }
206
        if ($newValue === null) {
27✔
207
            $newValue = $this->typeHintAplus($propertyType, $valueType, $value);
17✔
208
        }
209
        return $newValue ?? $value;
27✔
210
    }
211

212
    /**
213
     * Override this method to set customizable property types.
214
     *
215
     * @param string $propertyType
216
     * @param string $valueType
217
     * @param mixed $value
218
     *
219
     * @return mixed
220
     */
221
    protected function typeHintCustom(string $propertyType, string $valueType, mixed $value) : mixed
222
    {
223
        return null;
27✔
224
    }
225

226
    /**
227
     * Tries to convert the property value to native PHP types.
228
     *
229
     * @param string $propertyType
230
     * @param string $valueType
231
     * @param mixed $value
232
     *
233
     * @return mixed
234
     */
235
    protected function typeHintNative(string $propertyType, string $valueType, mixed $value) : mixed
236
    {
237
        if ($propertyType === 'array') {
27✔
238
            return $valueType === 'string'
15✔
239
                ? \json_decode($value, true, flags: $this->_jsonFlags)
1✔
240
                : (array) $value;
15✔
241
        }
242
        if ($propertyType === 'bool') {
27✔
243
            return (bool) $value;
15✔
244
        }
245
        if ($propertyType === 'float') {
27✔
246
            return (float) $value;
15✔
247
        }
248
        if ($propertyType === 'int') {
27✔
249
            return (int) $value;
15✔
250
        }
251
        if ($propertyType === 'string') {
27✔
252
            return (string) $value;
27✔
253
        }
254
        if ($propertyType === stdClass::class) {
17✔
255
            return $valueType === 'string'
15✔
256
                ? (object) \json_decode($value, flags: $this->_jsonFlags)
1✔
257
                : (object) $value;
15✔
258
        }
259
        return null;
17✔
260
    }
261

262
    /**
263
     * Tries to convert the property value using Aplus Framework types.
264
     *
265
     * @param string $propertyType
266
     * @param string $valueType
267
     * @param mixed $value
268
     *
269
     * @throws Exception
270
     *
271
     * @return mixed
272
     */
273
    protected function typeHintAplus(string $propertyType, string $valueType, mixed $value) : mixed
274
    {
275
        if ($propertyType === Date::class) {
17✔
276
            return new Date((string) $value);
17✔
277
        }
278
        if ($propertyType === URL::class) {
15✔
279
            return new URL((string) $value);
15✔
280
        }
281
        return null;
15✔
282
    }
283

284
    /**
285
     * Convert the Entity to an associative array accepted by Model methods.
286
     *
287
     * @throws Exception in case of error creating DateTimeZone
288
     * @throws JsonException in case of error while encoding/decoding JSON
289
     *
290
     * @return array<string,scalar>
291
     */
292
    public function toModel() : array
293
    {
294
        $jsonVars = $this->_jsonVars;
11✔
295
        $this->_jsonVars = \array_keys($this->getObjectVars());
11✔
296
        // @phpstan-ignore-next-line
297
        $data = \json_decode(\json_encode($this, $this->_jsonFlags), true, 512, $this->_jsonFlags);
11✔
298
        foreach ($data as $property => &$value) {
11✔
299
            if (\is_array($value)) {
11✔
300
                $value = \json_encode($value, $this->_jsonFlags);
1✔
301
                continue;
1✔
302
            }
303
            $type = \get_debug_type($this->{$property});
11✔
304
            if (\is_subclass_of($type, DateTimeInterface::class)) {
11✔
305
                $datetime = DateTime::createFromFormat(DateTimeInterface::ATOM, $value);
1✔
306
                // @phpstan-ignore-next-line
307
                $datetime->setTimezone(new DateTimeZone($this->_timezone));
1✔
308
                $value = $datetime->format('Y-m-d H:i:s'); // @phpstan-ignore-line
1✔
309
            }
310
        }
311
        unset($value);
11✔
312
        $this->_jsonVars = $jsonVars;
11✔
313
        return $data;
11✔
314
    }
315

316
    public function jsonSerialize() : stdClass
317
    {
318
        if (!$this->_jsonVars) {
13✔
319
            return new stdClass();
1✔
320
        }
321
        $allowed = \array_flip($this->_jsonVars);
13✔
322
        $filtered = \array_intersect_key($this->getObjectVars(), $allowed);
13✔
323
        $allowed = \array_intersect_key($allowed, $filtered);
13✔
324
        $ordered = \array_replace($allowed, $filtered);
13✔
325
        return (object) $ordered;
13✔
326
    }
327

328
    /**
329
     * @return array<string,mixed>
330
     */
331
    protected function getObjectVars() : array
332
    {
333
        $result = [];
14✔
334
        foreach (\get_object_vars($this) as $key => $value) {
14✔
335
            if (!\str_starts_with($key, '_')) {
14✔
336
                $result[$key] = $value;
14✔
337
            }
338
        }
339
        return $result;
14✔
340
    }
341
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc