Passed
Push — master ( 4ef44e...1abade )
by Arthur
15:00
created

Property::setRequired()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 0
dl 0
loc 3
rs 10
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Larapie\DataTransferObject;
6
7
use ReflectionProperty;
8
use Larapie\DataTransferObject\Contracts\DtoContract;
9
use Larapie\DataTransferObject\Contracts\PropertyContract;
10
use Larapie\DataTransferObject\Exceptions\InvalidTypeDtoException;
11
12
class Property implements PropertyContract
13
{
14
    /** @var array */
15
    protected const TYPE_MAPPING = [
16
        'int' => 'integer',
17
        'bool' => 'boolean',
18
        'float' => 'double',
19
    ];
20
21
    /** @var bool */
22
    protected $hasTypeDeclaration = false;
23
24
    /** @var bool */
25
    protected $nullable = false;
26
27
    /** @var bool */
28
    protected $optional = false;
29
30
    /** @var bool */
31
    protected $initialised = false;
32
33
    /** @var bool */
34
    protected $immutable = false;
35
36
    /** @var bool */
37
    protected $visible = true;
38
39
    /** @var array */
40
    protected $types = [];
41
42
    /** @var array */
43
    protected $arrayTypes = [];
44
45
    /** @var mixed */
46
    protected $default;
47
48
    /** @var mixed */
49
    public $value;
50
51
    /** @var ReflectionProperty */
52
    protected $reflection;
53
54
    public function __construct(ReflectionProperty $reflectionProperty)
55
    {
56
        $this->reflection = $reflectionProperty;
57
58
        $this->resolveTypeDefinition();
59
    }
60
61
    protected function resolveTypeDefinition()
62
    {
63
        $docComment = $this->reflection->getDocComment();
64
65
        if (!$docComment) {
66
            $this->setNullable(true);
67
68
            return;
69
        }
70
71
        preg_match('/\@var ((?:(?:[\w|\\\\])+(?:\[\])?)+)/', $docComment, $matches);
0 ignored issues
show
Bug introduced by
It seems like $docComment can also be of type true; however, parameter $subject of preg_match() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

71
        preg_match('/\@var ((?:(?:[\w|\\\\])+(?:\[\])?)+)/', /** @scrutinizer ignore-type */ $docComment, $matches);
Loading history...
72
73
        if (!count($matches)) {
74
            $this->setNullable(true);
75
76
            return;
77
        }
78
79
        $varDocComment = end($matches);
80
81
        $this->types = explode('|', $varDocComment);
82
        $this->arrayTypes = str_replace('[]', '', $this->types);
83
84
        if (in_array('immutable', $this->types) || in_array('Immutable', $this->types)) {
85
            $this->setImmutable(true);
86
            unset($this->types['immutable'], $this->types['Immutable']);
87
88
            if (empty($this->types)) {
89
                return;
90
            }
91
        }
92
93
        if (in_array('optional', $this->types) || in_array('Optional', $this->types)) {
94
            $this->setOptional();
95
            $this->setInitialized(false);
96
            unset($this->types['optional'], $this->types['Optional']);
97
98
            if (empty($this->types)) {
99
                return;
100
            }
101
        }
102
103
        $this->hasTypeDeclaration = true;
104
105
        $this->setNullable(strpos($varDocComment, 'null') !== false);
106
    }
107
108
    protected function isValidType($value): bool
109
    {
110
        if (!$this->hasTypeDeclaration) {
111
            return true;
112
        }
113
114
        if ($this->nullable() && $value === null) {
115
            return true;
116
        }
117
118
        foreach ($this->types as $currentType) {
119
            $isValidType = $this->assertTypeEquals($currentType, $value);
120
121
            if ($isValidType) {
122
                return true;
123
            }
124
        }
125
126
        return false;
127
    }
128
129
    protected function cast($value)
130
    {
131
        $castTo = null;
132
133
        foreach ($this->types as $type) {
134
            if (!is_subclass_of($type, DtoContract::class)) {
135
                continue;
136
            }
137
138
            $castTo = $type;
139
140
            break;
141
        }
142
143
        if (!$castTo) {
144
            return $value;
145
        }
146
147
        return new $castTo($value);
148
    }
149
150
    protected function castCollection(array $values)
151
    {
152
        $castTo = null;
153
154
        foreach ($this->arrayTypes as $type) {
155
            if (!is_subclass_of($type, DtoContract::class)) {
156
                continue;
157
            }
158
159
            $castTo = $type;
160
161
            break;
162
        }
163
164
        if (!$castTo) {
165
            return $values;
166
        }
167
168
        $casts = [];
169
170
        foreach ($values as $value) {
171
            $casts[] = new $castTo($value);
172
        }
173
174
        return $casts;
175
    }
176
177
    protected function shouldBeCastToCollection(array $values): bool
178
    {
179
        if (empty($values)) {
180
            return false;
181
        }
182
183
        foreach ($values as $key => $value) {
184
            if (is_string($key)) {
185
                return false;
186
            }
187
188
            if (!is_array($value)) {
189
                return false;
190
            }
191
        }
192
193
        return true;
194
    }
195
196
    protected function assertTypeEquals(string $type, $value): bool
197
    {
198
        if (strpos($type, '[]') !== false) {
199
            return $this->isValidGenericCollection($type, $value);
200
        }
201
202
        if ($type === 'mixed' && $value !== null) {
203
            return true;
204
        }
205
206
        return $value instanceof $type
207
            || gettype($value) === (self::TYPE_MAPPING[$type] ?? $type);
208
    }
209
210
    protected function isValidGenericCollection(string $type, $collection): bool
211
    {
212
        if (!is_array($collection)) {
213
            return false;
214
        }
215
216
        $valueType = str_replace('[]', '', $type);
217
218
        foreach ($collection as $value) {
219
            if (!$this->assertTypeEquals($valueType, $value)) {
220
                return false;
221
            }
222
        }
223
224
        return true;
225
    }
226
227
    public function set($value): void
228
    {
229
        if (is_array($value)) {
230
            $value = $this->shouldBeCastToCollection($value) ? $this->castCollection($value) : $this->cast($value);
231
        }
232
233
        if (!$this->isValidType($value)) {
234
            throw new InvalidTypeDtoException($this, $value);
235
        }
236
237
        $this->setInitialized(true);
238
239
        $this->value = $value;
240
    }
241
242
    public function setInitialized(bool $bool): void
243
    {
244
        $this->initialised = $bool;
245
    }
246
247
    public function isInitialized(): bool
248
    {
249
        return $this->initialised;
250
    }
251
252
    public function getTypes(): array
253
    {
254
        return $this->types;
255
    }
256
257
    public function getFqn(): string
258
    {
259
        return "{$this->reflection->getDeclaringClass()->getName()}::{$this->reflection->getName()}";
260
    }
261
262
    public function nullable(): bool
263
    {
264
        return $this->nullable;
265
    }
266
267
    public function setNullable(bool $bool): void
268
    {
269
        $this->nullable = $bool;
270
    }
271
272
    public function immutable(): bool
273
    {
274
        return $this->immutable;
275
    }
276
277
    public function setImmutable(bool $immutable): void
278
    {
279
        $this->immutable = $immutable;
280
    }
281
282
    public function getDefault()
283
    {
284
        return $this->default;
285
    }
286
287
    public function setDefault($default): void
288
    {
289
        $this->default = $default;
290
    }
291
292
    public function isVisible(): bool
293
    {
294
        return $this->visible;
295
    }
296
297
    public function setVisible(bool $bool): bool
298
    {
299
        return $this->visible = $bool;
300
    }
301
302
    public function getValue()
303
    {
304
        if (!$this->nullable() && $this->value == null) {
305
            return $this->getDefault();
306
        }
307
308
        return $this->value;
309
    }
310
311
    public function getValueFromReflection($object)
312
    {
313
        return $this->reflection->getValue($object);
314
    }
315
316
    public function getName(): string
317
    {
318
        return $this->reflection->getName();
319
    }
320
321
    public function isOptional(): bool
322
    {
323
        return $this->optional;
324
    }
325
326
    public function setOptional(): bool
327
    {
328
        return $this->optional = true;
329
    }
330
331
    public function setRequired(): bool
332
    {
333
        return $this->optional = false;
334
    }
335
}
336