1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
declare(strict_types=1); |
4
|
|
|
|
5
|
|
|
namespace Spatie\DataTransferObject; |
6
|
|
|
|
7
|
|
|
use ReflectionProperty; |
8
|
|
|
|
9
|
|
|
class Property extends ReflectionProperty |
10
|
|
|
{ |
11
|
|
|
/** @var array */ |
12
|
|
|
protected static $typeMapping = [ |
13
|
|
|
'int' => 'integer', |
14
|
|
|
'bool' => 'boolean', |
15
|
|
|
'float' => 'double', |
16
|
|
|
]; |
17
|
|
|
|
18
|
|
|
/** @var \Spatie\DataTransferObject\DataTransferObject */ |
19
|
|
|
protected $valueObject; |
20
|
|
|
|
21
|
|
|
/** @var bool */ |
22
|
|
|
protected $hasTypeDeclaration = false; |
23
|
|
|
|
24
|
|
|
/** @var bool */ |
25
|
|
|
protected $isNullable = false; |
26
|
|
|
|
27
|
|
|
/** @var bool */ |
28
|
|
|
protected $isInitialised = false; |
29
|
|
|
|
30
|
|
|
/** @var array */ |
31
|
|
|
protected $types = []; |
32
|
|
|
|
33
|
|
|
/** @var array */ |
34
|
|
|
protected $arrayTypes = []; |
35
|
|
|
|
36
|
|
|
/** @var array[] */ |
37
|
|
|
protected static $cache = []; |
38
|
|
|
|
39
|
|
|
public static function fromReflection(DataTransferObject $valueObject, ReflectionProperty $reflectionProperty) |
40
|
|
|
{ |
41
|
|
|
return new self($valueObject, $reflectionProperty); |
42
|
|
|
} |
43
|
|
|
|
44
|
|
|
public function __construct(DataTransferObject $valueObject, ReflectionProperty $reflectionProperty) |
45
|
|
|
{ |
46
|
|
|
parent::__construct($reflectionProperty->class, $reflectionProperty->getName()); |
47
|
|
|
|
48
|
|
|
$this->valueObject = $valueObject; |
49
|
|
|
|
50
|
|
|
$this->resolveTypeDefinition(); |
51
|
|
|
} |
52
|
|
|
|
53
|
|
|
public function set($value) |
54
|
|
|
{ |
55
|
|
|
if (is_array($value)) { |
56
|
|
|
$value = $this->shouldBeCastToCollection($value) ? $this->castCollection($value) : $this->cast($value); |
57
|
|
|
} |
58
|
|
|
|
59
|
|
|
if (! $this->isValidType($value)) { |
60
|
|
|
throw DataTransferObjectError::invalidType($this, $value); |
61
|
|
|
} |
62
|
|
|
|
63
|
|
|
$this->isInitialised = true; |
64
|
|
|
|
65
|
|
|
$this->valueObject->{$this->getName()} = $value; |
66
|
|
|
} |
67
|
|
|
|
68
|
|
|
public function getTypes(): array |
69
|
|
|
{ |
70
|
|
|
return $this->types; |
71
|
|
|
} |
72
|
|
|
|
73
|
|
|
public function getFqn(): string |
74
|
|
|
{ |
75
|
|
|
return "{$this->getDeclaringClass()->getName()}::{$this->getName()}"; |
76
|
|
|
} |
77
|
|
|
|
78
|
|
|
public function isNullable(): bool |
79
|
|
|
{ |
80
|
|
|
return $this->isNullable; |
81
|
|
|
} |
82
|
|
|
|
83
|
|
|
protected function resolveTypeDefinition() |
84
|
|
|
{ |
85
|
|
|
if (isset(self::$cache[$this->class][$this->getName()])) { |
86
|
|
|
$this->isNullable = self::$cache[$this->class][$this->getName()]['is_nullable']; |
87
|
|
|
$this->hasTypeDeclaration = ( |
88
|
|
|
! empty(self::$cache[$this->class][$this->getName()]['types']) |
89
|
|
|
|| ! empty(self::$cache[$this->class][$this->getName()]['array_types']) |
90
|
|
|
); |
91
|
|
|
$this->types = self::$cache[$this->class][$this->getName()]['types'] ?? []; |
92
|
|
|
$this->arrayTypes = self::$cache[$this->class][$this->getName()]['array_types'] ?? []; |
93
|
|
|
|
94
|
|
|
return; |
95
|
|
|
} |
96
|
|
|
|
97
|
|
|
$docComment = $this->getDocComment(); |
98
|
|
|
|
99
|
|
View Code Duplication |
if (! $docComment) { |
|
|
|
|
100
|
|
|
$this->isNullable = true; |
101
|
|
|
self::$cache[$this->class][$this->getName()]['is_nullable'] = $this->isNullable; |
102
|
|
|
|
103
|
|
|
return; |
104
|
|
|
} |
105
|
|
|
|
106
|
|
|
preg_match('/\@var ((?:(?:[\w|\\\\])+(?:\[\])?)+)/', $docComment, $matches); |
107
|
|
|
|
108
|
|
View Code Duplication |
if (! count($matches)) { |
|
|
|
|
109
|
|
|
$this->isNullable = true; |
110
|
|
|
self::$cache[$this->class][$this->getName()]['is_nullable'] = $this->isNullable; |
111
|
|
|
|
112
|
|
|
return; |
113
|
|
|
} |
114
|
|
|
|
115
|
|
|
$this->hasTypeDeclaration = true; |
116
|
|
|
|
117
|
|
|
$varDocComment = end($matches); |
118
|
|
|
|
119
|
|
|
$this->types = explode('|', $varDocComment); |
120
|
|
|
self::$cache[$this->class][$this->getName()]['types'] = $this->types; |
121
|
|
|
|
122
|
|
|
$this->arrayTypes = str_replace('[]', '', $this->types); |
123
|
|
|
self::$cache[$this->class][$this->getName()]['array_types'] = $this->arrayTypes; |
124
|
|
|
|
125
|
|
|
$this->isNullable = strpos($varDocComment, 'null') !== false; |
126
|
|
|
self::$cache[$this->class][$this->getName()]['is_nullable'] = $this->isNullable; |
127
|
|
|
} |
128
|
|
|
|
129
|
|
|
protected function isValidType($value): bool |
130
|
|
|
{ |
131
|
|
|
if (! $this->hasTypeDeclaration) { |
132
|
|
|
return true; |
133
|
|
|
} |
134
|
|
|
|
135
|
|
|
if ($this->isNullable && $value === null) { |
136
|
|
|
return true; |
137
|
|
|
} |
138
|
|
|
|
139
|
|
|
foreach ($this->types as $currentType) { |
140
|
|
|
$isValidType = $this->assertTypeEquals($currentType, $value); |
141
|
|
|
|
142
|
|
|
if ($isValidType) { |
143
|
|
|
return true; |
144
|
|
|
} |
145
|
|
|
} |
146
|
|
|
|
147
|
|
|
return false; |
148
|
|
|
} |
149
|
|
|
|
150
|
|
|
protected function cast($value) |
151
|
|
|
{ |
152
|
|
|
$castTo = null; |
153
|
|
|
|
154
|
|
|
foreach ($this->types as $type) { |
155
|
|
|
if (! is_subclass_of($type, DataTransferObject::class)) { |
156
|
|
|
continue; |
157
|
|
|
} |
158
|
|
|
|
159
|
|
|
$castTo = $type; |
160
|
|
|
|
161
|
|
|
break; |
162
|
|
|
} |
163
|
|
|
|
164
|
|
|
if (! $castTo) { |
165
|
|
|
return $value; |
166
|
|
|
} |
167
|
|
|
|
168
|
|
|
return new $castTo($value); |
169
|
|
|
} |
170
|
|
|
|
171
|
|
|
protected function castCollection(array $values) |
172
|
|
|
{ |
173
|
|
|
$castTo = null; |
174
|
|
|
|
175
|
|
|
foreach ($this->arrayTypes as $type) { |
176
|
|
|
if (! is_subclass_of($type, DataTransferObject::class)) { |
177
|
|
|
continue; |
178
|
|
|
} |
179
|
|
|
|
180
|
|
|
$castTo = $type; |
181
|
|
|
|
182
|
|
|
break; |
183
|
|
|
} |
184
|
|
|
|
185
|
|
|
if (! $castTo) { |
186
|
|
|
return $values; |
187
|
|
|
} |
188
|
|
|
|
189
|
|
|
$casts = []; |
190
|
|
|
|
191
|
|
|
foreach ($values as $value) { |
192
|
|
|
$casts[] = new $castTo($value); |
193
|
|
|
} |
194
|
|
|
|
195
|
|
|
return $casts; |
196
|
|
|
} |
197
|
|
|
|
198
|
|
|
protected function shouldBeCastToCollection(array $values): bool |
199
|
|
|
{ |
200
|
|
|
if (empty($values)) { |
201
|
|
|
return false; |
202
|
|
|
} |
203
|
|
|
|
204
|
|
|
foreach ($values as $key => $value) { |
205
|
|
|
if (is_string($key)) { |
206
|
|
|
return false; |
207
|
|
|
} |
208
|
|
|
|
209
|
|
|
if (! is_array($value)) { |
210
|
|
|
return false; |
211
|
|
|
} |
212
|
|
|
} |
213
|
|
|
|
214
|
|
|
return true; |
215
|
|
|
} |
216
|
|
|
|
217
|
|
|
protected function assertTypeEquals(string $type, $value): bool |
218
|
|
|
{ |
219
|
|
|
if (strpos($type, '[]') !== false) { |
220
|
|
|
return $this->isValidGenericCollection($type, $value); |
221
|
|
|
} |
222
|
|
|
|
223
|
|
|
if ($type === 'mixed' && $value !== null) { |
224
|
|
|
return true; |
225
|
|
|
} |
226
|
|
|
|
227
|
|
|
return $value instanceof $type |
228
|
|
|
|| gettype($value) === (self::$typeMapping[$type] ?? $type); |
229
|
|
|
} |
230
|
|
|
|
231
|
|
|
protected function isValidGenericCollection(string $type, $collection): bool |
232
|
|
|
{ |
233
|
|
|
if (! is_array($collection)) { |
234
|
|
|
return false; |
235
|
|
|
} |
236
|
|
|
|
237
|
|
|
$valueType = str_replace('[]', '', $type); |
238
|
|
|
|
239
|
|
|
foreach ($collection as $value) { |
240
|
|
|
if (! $this->assertTypeEquals($valueType, $value)) { |
241
|
|
|
return false; |
242
|
|
|
} |
243
|
|
|
} |
244
|
|
|
|
245
|
|
|
return true; |
246
|
|
|
} |
247
|
|
|
} |
248
|
|
|
|
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.
You can also find more detailed suggestions in the “Code” section of your repository.