1 | <?php declare(strict_types=1); |
||||||
2 | |||||||
3 | namespace Diezz\YamlToObjectMapper; |
||||||
4 | |||||||
5 | use Diezz\YamlToObjectMapper\Attributes\DefaultValueResolver; |
||||||
6 | use Diezz\YamlToObjectMapper\Resolver\ArgumentResolver; |
||||||
7 | use Diezz\YamlToObjectMapper\Resolver\ArgumentResolverFactory; |
||||||
8 | use Diezz\YamlToObjectMapper\Resolver\Context; |
||||||
9 | use Diezz\YamlToObjectMapper\Resolver\InstanceArgumentResolver; |
||||||
10 | use Diezz\YamlToObjectMapper\Resolver\ListArgumentResolver; |
||||||
11 | use Diezz\YamlToObjectMapper\Resolver\Parser\Parser; |
||||||
12 | use Diezz\YamlToObjectMapper\Resolver\ScalarArgumentResolver; |
||||||
13 | use ReflectionException; |
||||||
14 | use Symfony\Component\Yaml\Yaml; |
||||||
15 | |||||||
16 | class Mapper |
||||||
17 | { |
||||||
18 | private ArgumentResolverFactory $argumentResolverFactory; |
||||||
19 | |||||||
20 | 13 | public function __construct(ArgumentResolverFactory $argumentResolverFactory = null) |
|||||
21 | { |
||||||
22 | 13 | if (null === $argumentResolverFactory) { |
|||||
23 | 13 | $argumentResolverFactory = new ArgumentResolverFactory(); |
|||||
24 | } |
||||||
25 | 13 | $this->argumentResolverFactory = $argumentResolverFactory; |
|||||
26 | 13 | } |
|||||
27 | |||||||
28 | 13 | public static function make(): static |
|||||
29 | { |
||||||
30 | 13 | return new static(); |
|||||
31 | } |
||||||
32 | |||||||
33 | /** |
||||||
34 | * Register new custom argument resolver. |
||||||
35 | * |
||||||
36 | * @param string $resolverAlias - resolver name |
||||||
37 | * @param string $resolverClassName - class name of argument resolver |
||||||
38 | * |
||||||
39 | * @return void |
||||||
40 | */ |
||||||
41 | 1 | public function registerCustomArgumentResolver(string $resolverAlias, string $resolverClassName): void |
|||||
42 | { |
||||||
43 | 1 | $this->argumentResolverFactory->addResolver($resolverAlias, $resolverClassName); |
|||||
44 | 1 | } |
|||||
45 | |||||||
46 | /** |
||||||
47 | * @template T |
||||||
48 | * |
||||||
49 | * @param class-string<T> $targetClass |
||||||
0 ignored issues
–
show
Documentation
Bug
introduced
by
![]() |
|||||||
50 | * |
||||||
51 | * @throws ReflectionException |
||||||
52 | * @throws ValidationException |
||||||
53 | * @throws Resolver\ArgumentResolverException |
||||||
54 | * @throws Resolver\Parser\SyntaxException |
||||||
55 | * @return T |
||||||
56 | * |
||||||
57 | */ |
||||||
58 | 13 | public function mapFromFile(string $targetClass, string $configFile): object |
|||||
59 | { |
||||||
60 | 13 | $config = Yaml::parseFile($configFile); |
|||||
61 | |||||||
62 | 13 | return $this->map($targetClass, $config); |
|||||
63 | } |
||||||
64 | |||||||
65 | /** |
||||||
66 | * @template T |
||||||
67 | * |
||||||
68 | * @param class-string<T> $targetClass |
||||||
0 ignored issues
–
show
|
|||||||
69 | * |
||||||
70 | * @throws ValidationException |
||||||
71 | * @throws ReflectionException|Resolver\ArgumentResolverException|Resolver\Parser\SyntaxException |
||||||
72 | * |
||||||
73 | * @return T |
||||||
74 | */ |
||||||
75 | 13 | public function map(string $targetClass, array $config): object |
|||||
76 | { |
||||||
77 | 13 | $instance = new $targetClass; |
|||||
78 | 13 | $classInfoReflector = new ClassInfoReflector(); |
|||||
79 | 13 | $classInfo = $classInfoReflector->introspect($targetClass); |
|||||
80 | 13 | $validationResult = $this->validate($classInfo, $config); |
|||||
81 | |||||||
82 | 13 | if (!$validationResult->isValid()) { |
|||||
83 | 1 | throw new ValidationException($validationResult); |
|||||
84 | } |
||||||
85 | |||||||
86 | 12 | $mappingConfig = $this->getMappingConfig($classInfo, $config, $instance); |
|||||
87 | 12 | $rootResolver = new InstanceArgumentResolver($classInfo, $mappingConfig); |
|||||
88 | |||||||
89 | 12 | return $rootResolver->resolve(new Context($config, $rootResolver)); |
|||||
90 | } |
||||||
91 | |||||||
92 | /** |
||||||
93 | * @template T |
||||||
94 | * |
||||||
95 | * @param class-string<T> $targetClass |
||||||
0 ignored issues
–
show
|
|||||||
96 | * |
||||||
97 | * @throws ValidationException |
||||||
98 | * @throws ReflectionException|Resolver\ArgumentResolverException|Resolver\Parser\SyntaxException |
||||||
99 | * |
||||||
100 | * @return T |
||||||
101 | */ |
||||||
102 | public function mapFromString(string $targetClass, string $yaml): object |
||||||
103 | { |
||||||
104 | $config = Yaml::parse($yaml); |
||||||
105 | |||||||
106 | return $this->map($targetClass, $config); |
||||||
107 | } |
||||||
108 | |||||||
109 | /** |
||||||
110 | * @throws Resolver\ArgumentResolverException |
||||||
111 | * @throws Resolver\Parser\SyntaxException |
||||||
112 | */ |
||||||
113 | 12 | private function getMappingConfig(ClassInfo $classInfo, ?array $config, $parentKey = null): array |
|||||
114 | { |
||||||
115 | 12 | $mappingConfig = []; |
|||||
116 | |||||||
117 | 12 | if ($config === null) { |
|||||
0 ignored issues
–
show
|
|||||||
118 | $config = []; |
||||||
119 | } |
||||||
120 | |||||||
121 | 12 | foreach ($classInfo->getFields() as $field) { |
|||||
122 | 12 | $fieldName = $field->getName(); |
|||||
123 | 12 | if (!array_key_exists($fieldName, $config)) { |
|||||
124 | //Skip values that doesn't exist in config file but has a default values |
||||||
125 | 2 | if (!$field->hasDefaultValueResolver()) { |
|||||
126 | 1 | continue; |
|||||
127 | } |
||||||
128 | |||||||
129 | //fallback to defaultValueResolver |
||||||
130 | 1 | switch ($field->getDefaultValueResolver()) { |
|||||
131 | 1 | case DefaultValueResolver::PARENT_KEY: |
|||||
132 | { |
||||||
133 | 1 | $rawValue = $parentKey; |
|||||
134 | 1 | break; |
|||||
135 | } |
||||||
136 | 1 | case DefaultValueResolver::NESTED_LIST: |
|||||
137 | { |
||||||
138 | 1 | $rawValue = $config; |
|||||
139 | 1 | $config = []; |
|||||
140 | 1 | break; |
|||||
141 | } |
||||||
142 | } |
||||||
143 | } else { |
||||||
144 | 12 | $rawValue = $config[$fieldName]; |
|||||
145 | 12 | unset($config[$fieldName]); |
|||||
146 | } |
||||||
147 | |||||||
148 | 12 | $mappingConfig[$fieldName] = $this->toArgumentResolver($field, $rawValue); |
|||||
0 ignored issues
–
show
Comprehensibility
Best Practice
introduced
by
|
|||||||
149 | } |
||||||
150 | |||||||
151 | 12 | if (!empty($config) && !$classInfo->isIgnoreUnknown()) { |
|||||
152 | throw new InvalidConfigPathException($config, $classInfo); |
||||||
153 | } |
||||||
154 | 12 | foreach ($config as $fieldName => $rawValue) { |
|||||
155 | 3 | $mappingConfig[$fieldName] = $this->createArgumentResolverForPrimitive($rawValue); |
|||||
156 | } |
||||||
157 | |||||||
158 | 12 | return $mappingConfig; |
|||||
159 | } |
||||||
160 | |||||||
161 | |||||||
162 | /** |
||||||
163 | * @param ClassField $field |
||||||
164 | * @param mixed $rawValue |
||||||
165 | * |
||||||
166 | * @throws Resolver\ArgumentResolverException |
||||||
167 | * @throws Resolver\Parser\SyntaxException |
||||||
168 | * @return ArgumentResolver |
||||||
169 | */ |
||||||
170 | 12 | private function toArgumentResolver(ClassField $field, mixed $rawValue): ArgumentResolver |
|||||
171 | { |
||||||
172 | 12 | if ($field->isPrimitive()) { |
|||||
173 | 12 | $argResolver = $this->createArgumentResolverForPrimitive($rawValue); |
|||||
174 | 9 | } else if ($field->isList()) { |
|||||
175 | 9 | $argResolver = new ListArgumentResolver($this->doMapList($field, $rawValue)); |
|||||
176 | } else { |
||||||
177 | 1 | $argResolver = new InstanceArgumentResolver($field->getClassInfo(), $this->getMappingConfig($field->getClassInfo(), $rawValue, $field->newInstance())); |
|||||
0 ignored issues
–
show
It seems like
$field->getClassInfo() can also be of type null ; however, parameter $classInfo of Diezz\YamlToObjectMapper...per::getMappingConfig() does only seem to accept Diezz\YamlToObjectMapper\ClassInfo , 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
![]() It seems like
$field->getClassInfo() can also be of type null ; however, parameter $classInfo of Diezz\YamlToObjectMapper...Resolver::__construct() does only seem to accept Diezz\YamlToObjectMapper\ClassInfo , 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
![]() |
|||||||
178 | } |
||||||
179 | |||||||
180 | 12 | return $argResolver; |
|||||
181 | } |
||||||
182 | |||||||
183 | /** |
||||||
184 | * @param ClassField $field |
||||||
185 | * @param mixed $rawValue |
||||||
186 | * |
||||||
187 | * @throws Resolver\ArgumentResolverException |
||||||
188 | * @throws Resolver\Parser\SyntaxException |
||||||
189 | * @return array |
||||||
190 | */ |
||||||
191 | 9 | private function doMapList(ClassField $field, mixed $rawValue): array |
|||||
192 | { |
||||||
193 | 9 | $value = []; |
|||||
194 | 9 | if ($field->isTypedCollection() && $field->getClassInfo() !== null) { |
|||||
195 | 3 | foreach ($rawValue as $key => $item) { |
|||||
196 | 3 | $value[] = new InstanceArgumentResolver($field->getClassInfo(), $this->getMappingConfig($field->getClassInfo(), $item, $key)); |
|||||
197 | } |
||||||
198 | } else { |
||||||
199 | 8 | $value = $this->doMapArray($rawValue); |
|||||
200 | } |
||||||
201 | |||||||
202 | 9 | return $value; |
|||||
203 | } |
||||||
204 | |||||||
205 | /** |
||||||
206 | * @param mixed $rawValue |
||||||
207 | * |
||||||
208 | * @throws Resolver\Parser\SyntaxException |
||||||
209 | * @return ArgumentResolver |
||||||
210 | */ |
||||||
211 | 12 | private function createArgumentResolverForPrimitive(mixed $rawValue): ArgumentResolver |
|||||
212 | { |
||||||
213 | 12 | if (is_bool($rawValue) || is_int($rawValue) || is_array($rawValue)) { |
|||||
214 | 3 | return new ScalarArgumentResolver($rawValue); |
|||||
215 | } |
||||||
216 | |||||||
217 | 12 | return $this->processExpression($rawValue); |
|||||
218 | } |
||||||
219 | |||||||
220 | /** |
||||||
221 | * @throws Resolver\Parser\SyntaxException |
||||||
222 | */ |
||||||
223 | 12 | private function processExpression(string $expression): ArgumentResolver |
|||||
224 | { |
||||||
225 | 12 | $parser = new Parser($expression); |
|||||
226 | |||||||
227 | 12 | return $parser->parse()->toResolver($this->argumentResolverFactory); |
|||||
228 | } |
||||||
229 | |||||||
230 | /** |
||||||
231 | * @throws Resolver\Parser\SyntaxException |
||||||
232 | */ |
||||||
233 | 8 | private function doMapArray(array $array): array |
|||||
234 | { |
||||||
235 | 8 | $result = []; |
|||||
236 | 8 | foreach ($array as $key => $value) { |
|||||
237 | 8 | if (is_array($value)) { |
|||||
238 | $result[$key] = $this->doMapArray($value); |
||||||
239 | 8 | } else if (is_string($value)) { |
|||||
240 | 7 | $result[$key] = $this->processExpression($value); |
|||||
241 | } else { |
||||||
242 | 1 | $result[$key] = new ScalarArgumentResolver($value); |
|||||
243 | } |
||||||
244 | } |
||||||
245 | |||||||
246 | 8 | return $result; |
|||||
247 | } |
||||||
248 | |||||||
249 | 13 | private function validate(ClassInfo $classInfo, ?array $config, ?array $parent = [], ?ValidationResult $validationResult = null): ValidationResult |
|||||
250 | { |
||||||
251 | 13 | if (null === $validationResult) { |
|||||
252 | 13 | $validationResult = new ValidationResult(); |
|||||
253 | } |
||||||
254 | |||||||
255 | 13 | $pathFunction = static fn(array $path) => implode(".", $path); |
|||||
256 | |||||||
257 | //Check for required fields |
||||||
258 | 13 | foreach ($classInfo->getFields() as $field) { |
|||||
259 | 13 | $fieldName = $field->getName(); |
|||||
260 | 13 | $isFieldExistsInConfig = $config !== null && array_key_exists($fieldName, $config); |
|||||
261 | 13 | $isRequired = $field->isRequired(); |
|||||
262 | 13 | $path = $parent; |
|||||
263 | 13 | $path[] = $fieldName; |
|||||
264 | |||||||
265 | 13 | if ($isRequired && !$isFieldExistsInConfig) { |
|||||
266 | 2 | if ($field->hasDefaultValueResolver()) { |
|||||
267 | 1 | $defaultValueResolver = $field->getDefaultValueResolver(); |
|||||
268 | switch ($defaultValueResolver) { |
||||||
269 | 1 | case DefaultValueResolver::PARENT_KEY: |
|||||
270 | 1 | $parentKeyExists = !empty($parent); |
|||||
271 | 1 | if (!$parentKeyExists) { |
|||||
272 | $validationResult->addError($path, sprintf("Field '%s' is required but not found in the config file", $pathFunction($path))); |
||||||
273 | } |
||||||
274 | 1 | break; |
|||||
275 | 1 | case DefaultValueResolver::NESTED_LIST: |
|||||
276 | 1 | if (empty($config) || !is_array($config)) { |
|||||
277 | $validationResult->addError($path, sprintf("Field '%s' is required but not found in the config file", $pathFunction($path))); |
||||||
278 | } |
||||||
279 | 1 | break; |
|||||
280 | } |
||||||
281 | } else { |
||||||
282 | 1 | $validationResult->addError($path, sprintf("Field '%s' is required but not found in the config file", $pathFunction($path))); |
|||||
283 | 1 | continue; |
|||||
284 | } |
||||||
285 | } |
||||||
286 | |||||||
287 | 13 | if (null !== $field->getClassInfo()) { |
|||||
288 | 4 | if ($field->isTypedCollection()) { |
|||||
289 | 3 | if ($isRequired === false && !$isFieldExistsInConfig) { |
|||||
290 | continue; |
||||||
291 | } |
||||||
292 | 3 | foreach ($config[$fieldName] as $key => $value) { |
|||||
293 | 3 | $path[] = $key; |
|||||
294 | 3 | $validationResult = $this->validate($field->getClassInfo(), $value, $path, $validationResult); |
|||||
295 | } |
||||||
296 | } else { |
||||||
297 | 1 | $validationResult = $this->validate($field->getClassInfo(), $config[$fieldName], $path, $validationResult); |
|||||
298 | } |
||||||
299 | } |
||||||
300 | } |
||||||
301 | |||||||
302 | 13 | return $validationResult; |
|||||
303 | } |
||||||
304 | } |
||||||
305 |