1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace Cerbero\Dto; |
4
|
|
|
|
5
|
|
|
use Cerbero\Dto\Exceptions\DtoNotFoundException; |
6
|
|
|
use Cerbero\Dto\Exceptions\MissingValueException; |
7
|
|
|
use Cerbero\Dto\Exceptions\UnknownDtoPropertyException; |
8
|
|
|
use Cerbero\Dto\Manipulators\ArrayConverter; |
9
|
|
|
use ReflectionClass; |
10
|
|
|
use ReflectionException; |
11
|
|
|
|
12
|
|
|
/** |
13
|
|
|
* The DTO properties mapper. |
14
|
|
|
* |
15
|
|
|
*/ |
16
|
|
|
class DtoPropertiesMapper |
17
|
|
|
{ |
18
|
|
|
/** |
19
|
|
|
* The property doc comment pattern |
20
|
|
|
* |
21
|
|
|
* - Start with "@property" or "@property-read" |
22
|
|
|
* - Capture property type with possible "[]" suffix |
23
|
|
|
* - Capture variable name "$foo" or "foo" |
24
|
|
|
*/ |
25
|
|
|
protected const RE_PROPERTY = '/@property(?:-read)?\s+((?:[\w\\\_]+(?:\[])?\|?)+)\s+\$?([\w_]+)/'; |
26
|
|
|
|
27
|
|
|
/** |
28
|
|
|
* The "use" statement pattern |
29
|
|
|
* |
30
|
|
|
* - Capture fully qualified class name |
31
|
|
|
* - Capture possible class alias after "as" |
32
|
|
|
*/ |
33
|
|
|
protected const RE_USE_STATEMENT = '/([\w\\\_]+)(?:\s+as\s+([\w_]+))?;/i'; |
34
|
|
|
|
35
|
|
|
/** |
36
|
|
|
* The DTO properties mapper instances. |
37
|
|
|
* |
38
|
|
|
* @var DtoPropertiesMapper[] |
39
|
|
|
*/ |
40
|
|
|
protected static $instances = []; |
41
|
|
|
|
42
|
|
|
/** |
43
|
|
|
* The DTO class to map properties for. |
44
|
|
|
* |
45
|
|
|
* @var string |
46
|
|
|
*/ |
47
|
|
|
protected $dtoClass; |
48
|
|
|
|
49
|
|
|
/** |
50
|
|
|
* The reflection of the DTO class. |
51
|
|
|
* |
52
|
|
|
* @var ReflectionClass |
53
|
|
|
*/ |
54
|
|
|
protected $reflection; |
55
|
|
|
|
56
|
|
|
/** |
57
|
|
|
* The cached raw properties. |
58
|
|
|
* |
59
|
|
|
* @var array |
60
|
|
|
*/ |
61
|
|
|
protected $rawProperties; |
62
|
|
|
|
63
|
|
|
/** |
64
|
|
|
* The cached use statements. |
65
|
|
|
* |
66
|
|
|
* @var array |
67
|
|
|
*/ |
68
|
|
|
protected $useStatements; |
69
|
|
|
|
70
|
|
|
/** |
71
|
|
|
* The cached mapped properties. |
72
|
|
|
* |
73
|
|
|
* @var array |
74
|
|
|
*/ |
75
|
|
|
protected $mappedProperties; |
76
|
|
|
|
77
|
|
|
/** |
78
|
|
|
* Instantiate the class. |
79
|
|
|
* |
80
|
|
|
* @param string $dtoClass |
81
|
|
|
* @throws DtoNotFoundException |
82
|
|
|
*/ |
83
|
15 |
|
protected function __construct(string $dtoClass) |
84
|
|
|
{ |
85
|
15 |
|
$this->dtoClass = $dtoClass; |
86
|
15 |
|
$this->reflection = $this->reflectDto(); |
87
|
14 |
|
} |
88
|
|
|
|
89
|
|
|
/** |
90
|
|
|
* Retrieve the reflection of the given DTO class |
91
|
|
|
* |
92
|
|
|
* @return ReflectionClass |
93
|
|
|
* @throws DtoNotFoundException |
94
|
|
|
*/ |
95
|
15 |
|
protected function reflectDto(): ReflectionClass |
96
|
|
|
{ |
97
|
|
|
try { |
98
|
15 |
|
return new ReflectionClass($this->dtoClass); |
99
|
1 |
|
} catch (ReflectionException $e) { |
100
|
1 |
|
throw new DtoNotFoundException($this->dtoClass); |
101
|
|
|
} |
102
|
|
|
} |
103
|
|
|
|
104
|
|
|
/** |
105
|
|
|
* Retrieve the mapper instance for the given DTO class |
106
|
|
|
* |
107
|
|
|
* @param string $dtoClass |
108
|
|
|
* @return DtoPropertiesMapper |
109
|
|
|
* @throws DtoNotFoundException |
110
|
|
|
*/ |
111
|
89 |
|
public static function for(string $dtoClass): DtoPropertiesMapper |
112
|
|
|
{ |
113
|
89 |
|
return static::$instances[$dtoClass] = static::$instances[$dtoClass] ?? new static($dtoClass); |
114
|
|
|
} |
115
|
|
|
|
116
|
|
|
/** |
117
|
|
|
* Retrieve the mapped property names |
118
|
|
|
* |
119
|
|
|
* @return array |
120
|
|
|
*/ |
121
|
1 |
|
public function getNames(): array |
122
|
|
|
{ |
123
|
1 |
|
$rawProperties = $this->cacheRawProperties(); |
124
|
|
|
|
125
|
1 |
|
return array_keys($rawProperties); |
126
|
|
|
} |
127
|
|
|
|
128
|
|
|
/** |
129
|
|
|
* Retrieve and cache the raw properties to map |
130
|
|
|
* |
131
|
|
|
* @return array |
132
|
|
|
*/ |
133
|
87 |
|
protected function cacheRawProperties(): array |
134
|
|
|
{ |
135
|
87 |
|
if (!isset($this->rawProperties)) { |
136
|
13 |
|
$this->cacheRawPropertiesOfReflection($this->reflection); |
137
|
|
|
} |
138
|
|
|
|
139
|
87 |
|
return $this->rawProperties; |
140
|
|
|
} |
141
|
|
|
|
142
|
|
|
/** |
143
|
|
|
* Cache the raw properties of the given reflection |
144
|
|
|
* |
145
|
|
|
* @param ReflectionClass $reflection |
146
|
|
|
* @return void |
147
|
|
|
*/ |
148
|
13 |
|
protected function cacheRawPropertiesOfReflection(ReflectionClass $reflection): void |
149
|
|
|
{ |
150
|
13 |
|
$this->rawProperties = $this->rawProperties ?? []; |
151
|
|
|
|
152
|
13 |
|
if (preg_match_all(static::RE_PROPERTY, $reflection->getDocComment(), $matches, PREG_SET_ORDER) !== 0) { |
153
|
11 |
|
foreach ($matches as $match) { |
154
|
11 |
|
[, $rawTypes, $name] = $match; |
155
|
11 |
|
$this->rawProperties[$name] = $rawTypes; |
156
|
|
|
} |
157
|
|
|
} |
158
|
|
|
|
159
|
13 |
|
$parentDto = $reflection->getParentClass(); |
160
|
|
|
|
161
|
13 |
|
if ($parentDto !== false && $parentDto != Dto::class) { |
162
|
13 |
|
$this->cacheRawPropertiesOfReflection($parentDto); |
163
|
|
|
} |
164
|
13 |
|
} |
165
|
|
|
|
166
|
|
|
/** |
167
|
|
|
* Retrieve the mapped DTO properties |
168
|
|
|
* |
169
|
|
|
* @param array $data |
170
|
|
|
* @param int $flags |
171
|
|
|
* @return array |
172
|
|
|
* @throws MissingValueException |
173
|
|
|
* @throws UnexpectedValueException |
174
|
|
|
* @throws UnknownDtoPropertyException |
175
|
|
|
*/ |
176
|
86 |
|
public function map(array $data, int $flags): array |
177
|
|
|
{ |
178
|
86 |
|
$mappedProperties = []; |
179
|
86 |
|
$rawProperties = $this->cacheRawProperties(); |
180
|
86 |
|
$useStatements = $this->cacheUseStatements(); |
181
|
|
|
|
182
|
86 |
|
foreach ($rawProperties as $name => $rawTypes) { |
183
|
77 |
|
$cachedProperty = $this->mappedProperties[$name] ?? null; |
184
|
77 |
|
$types = $cachedProperty ? $cachedProperty->getTypes() : $this->parseTypes($rawTypes, $useStatements); |
185
|
77 |
|
$key = $this->getPropertyKeyFromData($name, $data); |
186
|
|
|
|
187
|
77 |
|
if (!array_key_exists($key, $data)) { |
188
|
73 |
|
if ($flags & PARTIAL) { |
189
|
72 |
|
continue; |
190
|
|
|
} |
191
|
|
|
|
192
|
1 |
|
throw new MissingValueException($this->dtoClass, $name); |
193
|
|
|
} |
194
|
|
|
|
195
|
67 |
|
$mappedProperties[$name] = DtoProperty::create($name, $data[$key], $types, $flags); |
196
|
67 |
|
unset($data[$key]); |
197
|
|
|
} |
198
|
|
|
|
199
|
85 |
|
$this->checkUnknownProperties($data, $flags); |
200
|
|
|
|
201
|
85 |
|
return $this->mappedProperties = $mappedProperties; |
202
|
|
|
} |
203
|
|
|
|
204
|
|
|
/** |
205
|
|
|
* Retrieve and cache the DTO "use" statements |
206
|
|
|
* |
207
|
|
|
* @return array |
208
|
|
|
*/ |
209
|
86 |
|
protected function cacheUseStatements(): array |
210
|
|
|
{ |
211
|
86 |
|
if (isset($this->useStatements)) { |
212
|
76 |
|
return $this->useStatements; |
213
|
|
|
} |
214
|
|
|
|
215
|
12 |
|
$this->useStatements = []; |
216
|
12 |
|
$rawStatements = null; |
217
|
12 |
|
$handle = fopen($this->reflection->getFileName(), 'rb'); |
218
|
|
|
|
219
|
|
|
do { |
220
|
12 |
|
$line = trim(fgets($handle, 120)); |
221
|
12 |
|
$begin = substr($line, 0, 3); |
222
|
12 |
|
$rawStatements .= $begin == 'use' ? $line : null; |
223
|
12 |
|
} while ($begin != '/**'); |
224
|
|
|
|
225
|
12 |
|
fclose($handle); |
226
|
|
|
|
227
|
12 |
|
preg_match_all(static::RE_USE_STATEMENT, $rawStatements, $useMatches, PREG_SET_ORDER); |
228
|
|
|
|
229
|
12 |
|
foreach ($useMatches as $match) { |
230
|
12 |
|
$segments = explode('\\', $match[1]); |
231
|
12 |
|
$name = $match[2] ?? end($segments); |
232
|
12 |
|
$this->useStatements[$name] = $match[1]; |
233
|
|
|
} |
234
|
|
|
|
235
|
12 |
|
return $this->useStatements; |
236
|
|
|
} |
237
|
|
|
|
238
|
|
|
/** |
239
|
|
|
* Parse the given raw property types |
240
|
|
|
* |
241
|
|
|
* @param string $rawTypes |
242
|
|
|
* @param array $useStatements |
243
|
|
|
* @return DtoPropertyTypes |
244
|
|
|
*/ |
245
|
76 |
|
protected function parseTypes(string $rawTypes, array $useStatements): DtoPropertyTypes |
246
|
|
|
{ |
247
|
76 |
|
return array_reduce(explode('|', $rawTypes), function (DtoPropertyTypes $types, $rawType) use ($useStatements) { |
248
|
76 |
|
$name = str_replace('[]', '', $rawType, $count); |
249
|
76 |
|
$isCollection = $count > 0; |
250
|
|
|
|
251
|
|
|
// fully qualified class name exists |
252
|
76 |
|
if (strpos($rawType, '\\') === 0 && class_exists($name)) { |
253
|
20 |
|
return $types->addType(new DtoPropertyType(substr($name, 1), $isCollection)); |
254
|
|
|
} |
255
|
|
|
|
256
|
76 |
|
if (isset($useStatements[$name])) { |
257
|
18 |
|
return $types->addType(new DtoPropertyType($useStatements[$name], $isCollection)); |
258
|
|
|
} |
259
|
|
|
|
260
|
|
|
// class in DTO namespace exists |
261
|
76 |
|
if (class_exists($class = $this->reflection->getNamespaceName() . '\\' . $name)) { |
262
|
67 |
|
return $types->addType(new DtoPropertyType($class, $isCollection)); |
263
|
|
|
} |
264
|
|
|
|
265
|
76 |
|
return $types->addType(new DtoPropertyType($name, $isCollection)); |
266
|
76 |
|
}, new DtoPropertyTypes()); |
267
|
|
|
} |
268
|
|
|
|
269
|
|
|
/** |
270
|
|
|
* Retrieve the key for the given property in the provided data |
271
|
|
|
* |
272
|
|
|
* @param string $property |
273
|
|
|
* @param array $data |
274
|
|
|
* @return string |
275
|
|
|
*/ |
276
|
77 |
|
protected function getPropertyKeyFromData(string $property, array $data): string |
277
|
|
|
{ |
278
|
77 |
|
if (array_key_exists($property, $data)) { |
279
|
66 |
|
return $property; |
280
|
|
|
} |
281
|
|
|
|
282
|
74 |
|
return ArrayConverter::instance()->formatKey($property, true); |
283
|
|
|
} |
284
|
|
|
|
285
|
|
|
/** |
286
|
|
|
* Check whether the given data contains unknown properties |
287
|
|
|
* |
288
|
|
|
* @param array $data |
289
|
|
|
* @param int $flags |
290
|
|
|
* @return void |
291
|
|
|
* @throws UnknownDtoPropertyException |
292
|
|
|
*/ |
293
|
85 |
|
protected function checkUnknownProperties(array $data, int $flags): void |
294
|
|
|
{ |
295
|
85 |
|
if ($data && !($flags & IGNORE_UNKNOWN_PROPERTIES)) { |
|
|
|
|
296
|
2 |
|
throw new UnknownDtoPropertyException($this->dtoClass, key($data)); |
297
|
|
|
} |
298
|
85 |
|
} |
299
|
|
|
} |
300
|
|
|
|
This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.
Consider making the comparison explicit by using
empty(..)
or! empty(...)
instead.