Passed
Push — master ( 276843...15914c )
by Arthur
12:28
created

Property::setReader()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

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

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

347
        /** @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...
348
        self::$reader = $reader;
349
    }
350
351
    protected function setAnnotations()
352
    {
353
        $annotations = [];
354
        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

354
        foreach(self::/** @scrutinizer ignore-call */ getReader()->getPropertyAnnotations($this->reflection) as $annotation){
Loading history...
355
            $annotations[get_class($annotation)]=$annotation;
356
        }
357
        $this->annotations = $annotations;
358
    }
359
360
    protected function getAnnotation($annotation)
361
    {
362
        return $this->annotations[$annotation] ?? null;
363
    }
364
}
365