Passed
Push — master ( 245bf0...8f017c )
by Arthur
11:37
created

Property::validate()   A

Complexity

Conditions 5
Paths 9

Size

Total Lines 15
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
eloc 11
nc 9
nop 0
dl 0
loc 15
rs 9.6111
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Larapie\DataTransferObject;
6
7
use Larapie\DataTransferObject\Exceptions\ValidatorException;
8
use ReflectionProperty;
9
use Doctrine\Common\Cache\ArrayCache;
10
use Doctrine\Common\Annotations\Reader;
11
use Doctrine\Common\Annotations\CachedReader;
12
use Doctrine\Common\Annotations\AnnotationReader;
13
use Larapie\DataTransferObject\Annotations\Optional;
14
use Larapie\DataTransferObject\Annotations\Immutable;
15
use Larapie\DataTransferObject\Contracts\DtoContract;
16
use Larapie\DataTransferObject\Contracts\PropertyContract;
17
use Larapie\DataTransferObject\Exceptions\InvalidTypeDtoException;
18
use Symfony\Component\Validator\Constraint;
19
use Symfony\Component\Validator\ValidatorBuilder;
20
21
class Property implements PropertyContract
22
{
23
    /** @var array */
24
    protected const TYPE_MAPPING = [
25
        'int' => 'integer',
26
        'bool' => 'boolean',
27
        'float' => 'double',
28
    ];
29
30
    /** @var bool */
31
    protected $hasTypeDeclaration = false;
32
33
    /** @var bool */
34
    protected $nullable = false;
35
36
    /** @var bool */
37
    protected $optional;
38
39
    /** @var bool */
40
    protected $initialised = false;
41
42
    /** @var bool */
43
    protected $immutable;
44
45
    /** @var bool */
46
    protected $visible = true;
47
48
    /** @var array */
49
    protected $types = [];
50
51
    /** @var array */
52
    protected $arrayTypes = [];
53
54
    /** @var mixed */
55
    protected $default;
56
57
    /** @var mixed */
58
    public $value;
59
60
    /** @var ReflectionProperty */
61
    protected $reflection;
62
63
    /** @var array */
64
    protected $annotations = [];
65
66
    /** @var ?Reader */
67
    protected static $reader;
68
69
    public function __construct(ReflectionProperty $reflectionProperty)
70
    {
71
        $this->reflection = $reflectionProperty;
72
        $this->resolveTypeDefinition();
73
    }
74
75
    protected function resolveTypeDefinition()
76
    {
77
        $docComment = $this->reflection->getDocComment();
78
79
        if (!$docComment) {
80
            $this->setNullable(true);
81
82
            return;
83
        }
84
85
        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

85
        preg_match('/\@var ((?:(?:[\w|\\\\])+(?:\[\])?)+)/', /** @scrutinizer ignore-type */ $docComment, $matches);
Loading history...
86
87
        if (!count($matches)) {
88
            $this->setNullable(true);
89
90
            return;
91
        }
92
93
        $varDocComment = end($matches);
94
95
        $this->types = explode('|', $varDocComment);
96
        $this->arrayTypes = str_replace('[]', '', $this->types);
97
        $this->setAnnotations();
98
99
        $this->hasTypeDeclaration = true;
100
101
        $this->setNullable(strpos($varDocComment, 'null') !== false);
102
    }
103
104
    protected function isValidType($value): bool
105
    {
106
        if (!$this->hasTypeDeclaration) {
107
            return true;
108
        }
109
110
        if ($this->nullable() && $value === null) {
111
            return true;
112
        }
113
114
        foreach ($this->types as $currentType) {
115
            $isValidType = $this->assertTypeEquals($currentType, $value);
116
117
            if ($isValidType) {
118
                return true;
119
            }
120
        }
121
122
        return false;
123
    }
124
125
    protected function cast($value)
126
    {
127
        $castTo = null;
128
129
        foreach ($this->types as $type) {
130
            if (!is_subclass_of($type, DtoContract::class)) {
131
                continue;
132
            }
133
134
            $castTo = $type;
135
136
            break;
137
        }
138
139
        if (!$castTo) {
140
            return $value;
141
        }
142
143
        return new $castTo($value);
144
    }
145
146
    protected function castCollection(array $values)
147
    {
148
        $castTo = null;
149
150
        foreach ($this->arrayTypes as $type) {
151
            if (!is_subclass_of($type, DtoContract::class)) {
152
                continue;
153
            }
154
155
            $castTo = $type;
156
157
            break;
158
        }
159
160
        if (!$castTo) {
161
            return $values;
162
        }
163
164
        $casts = [];
165
166
        foreach ($values as $value) {
167
            $casts[] = new $castTo($value);
168
        }
169
170
        return $casts;
171
    }
172
173
    protected function shouldBeCastToCollection(array $values): bool
174
    {
175
        if (empty($values)) {
176
            return false;
177
        }
178
179
        foreach ($values as $key => $value) {
180
            if (is_string($key)) {
181
                return false;
182
            }
183
184
            if (!is_array($value)) {
185
                return false;
186
            }
187
        }
188
189
        return true;
190
    }
191
192
    protected function assertTypeEquals(string $type, $value): bool
193
    {
194
        if (strpos($type, '[]') !== false) {
195
            return $this->isValidGenericCollection($type, $value);
196
        }
197
198
        if ($type === 'mixed' && $value !== null) {
199
            return true;
200
        }
201
202
        return $value instanceof $type
203
            || gettype($value) === (self::TYPE_MAPPING[$type] ?? $type);
204
    }
205
206
    protected function isValidGenericCollection(string $type, $collection): bool
207
    {
208
        if (!is_array($collection)) {
209
            return false;
210
        }
211
212
        $valueType = str_replace('[]', '', $type);
213
214
        foreach ($collection as $value) {
215
            if (!$this->assertTypeEquals($valueType, $value)) {
216
                return false;
217
            }
218
        }
219
220
        return true;
221
    }
222
223
    public function set($value): void
224
    {
225
        if (is_array($value)) {
226
            $value = $this->shouldBeCastToCollection($value) ? $this->castCollection($value) : $this->cast($value);
227
        }
228
229
        if (!$this->isValidType($value)) {
230
            throw new InvalidTypeDtoException($this, $value);
231
        }
232
233
        $this->setInitialized(true);
234
235
        $this->value = $value;
236
    }
237
238
    public function setInitialized(bool $bool): void
239
    {
240
        $this->initialised = $bool;
241
    }
242
243
    public function isInitialized(): bool
244
    {
245
        return $this->initialised;
246
    }
247
248
    public function getTypes(): array
249
    {
250
        return $this->types;
251
    }
252
253
    public function getFqn(): string
254
    {
255
        return "{$this->reflection->getDeclaringClass()->getName()}::{$this->reflection->getName()}";
256
    }
257
258
    public function nullable(): bool
259
    {
260
        return $this->nullable;
261
    }
262
263
    public function setNullable(bool $bool): void
264
    {
265
        $this->nullable = $bool;
266
    }
267
268
    public function validate(): void
269
    {
270
        $constraints = [];
271
        foreach ($this->annotations as $annotation) {
272
            if ($annotation instanceof Constraint)
273
                $constraints[] = $annotation;
274
        }
275
        if (empty($constraints))
276
            return;
277
        $validator = new ValidatorBuilder();
278
        $violations = $validator->getValidator()->validate($this->getValue(), $constraints);
279
280
        if ($violations->count() > 0)
281
            throw new ValidatorException($this->getName(), $violations);
282
        return;
283
    }
284
285
    public function immutable(): bool
286
    {
287
        if (!isset($this->immutable)) {
288
            $this->immutable = $this->getAnnotation(Immutable::class) !== null;
289
        }
290
291
        return $this->immutable;
292
    }
293
294
    public function setImmutable(bool $immutable): void
295
    {
296
        $this->immutable = $immutable;
297
    }
298
299
    public function getDefault()
300
    {
301
        return $this->default;
302
    }
303
304
    public function setDefault($default): void
305
    {
306
        $this->default = $default;
307
    }
308
309
    public function isVisible(): bool
310
    {
311
        return $this->visible;
312
    }
313
314
    public function setVisible(bool $bool): bool
315
    {
316
        return $this->visible = $bool;
317
    }
318
319
    public function getValue()
320
    {
321
        if (!$this->nullable() && $this->value == null) {
322
            return $this->getDefault();
323
        }
324
325
        return $this->value;
326
    }
327
328
    public function getValueFromReflection($object)
329
    {
330
        return $this->reflection->getValue($object);
331
    }
332
333
    public function getName(): string
334
    {
335
        return $this->reflection->getName();
336
    }
337
338
    public function isOptional(): bool
339
    {
340
        if (!isset($this->optional)) {
341
            $this->optional = $this->getAnnotation(Optional::class) !== null;
342
        }
343
344
        return $this->optional;
345
    }
346
347
    public function setOptional(): bool
348
    {
349
        return $this->optional = true;
350
    }
351
352
    public function setRequired(): bool
353
    {
354
        return $this->optional = false;
355
    }
356
357
    protected function getReader(): Reader
358
    {
359
        if (self::$reader === null) {
360
            self::setReader(new CachedReader(new AnnotationReader(), new ArrayCache()));
361
        }
362
363
        return self::$reader;
364
    }
365
366
    public static function setReader(Reader $reader)
367
    {
368
        \Doctrine\Common\Annotations\AnnotationRegistry::registerUniqueLoader('class_exists');
0 ignored issues
show
Deprecated Code introduced by
The function Doctrine\Common\Annotati...:registerUniqueLoader() has been deprecated: this method is deprecated and will be removed in doctrine/annotations 2.0 ( Ignorable by Annotation )

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

368
        /** @scrutinizer ignore-deprecated */ \Doctrine\Common\Annotations\AnnotationRegistry::registerUniqueLoader('class_exists');

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
369
        self::$reader = $reader;
370
    }
371
372
    protected function setAnnotations()
373
    {
374
        $annotations = [];
375
        foreach (self::getReader()->getPropertyAnnotations($this->reflection) as $annotation) {
0 ignored issues
show
Bug Best Practice introduced by
The method Larapie\DataTransferObject\Property::getReader() is not static, but was called statically. ( Ignorable by Annotation )

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

375
        foreach (self::/** @scrutinizer ignore-call */ getReader()->getPropertyAnnotations($this->reflection) as $annotation) {
Loading history...
376
            $annotations[get_class($annotation)] = $annotation;
377
        }
378
        $this->annotations = $annotations;
379
    }
380
381
    protected function getAnnotation($annotation)
382
    {
383
        return $this->annotations[$annotation] ?? null;
384
    }
385
}
386