Mapper::mapFromString()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 2
dl 0
loc 5
ccs 0
cts 3
cp 0
crap 2
rs 10
c 0
b 0
f 0
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
The doc comment class-string<T> at position 0 could not be parsed: Unknown type name 'class-string' at position 0 in class-string<T>.
Loading history...
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
Documentation Bug introduced by
The doc comment class-string<T> at position 0 could not be parsed: Unknown type name 'class-string' at position 0 in class-string<T>.
Loading history...
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
Documentation Bug introduced by
The doc comment class-string<T> at position 0 could not be parsed: Unknown type name 'class-string' at position 0 in class-string<T>.
Loading history...
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
introduced by
The condition $config === null is always false.
Loading history...
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
The variable $rawValue does not seem to be defined for all execution paths leading up to this point.
Loading history...
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
Bug introduced by
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 ignore-type  annotation

177
            $argResolver = new InstanceArgumentResolver($field->getClassInfo(), $this->getMappingConfig(/** @scrutinizer ignore-type */ $field->getClassInfo(), $rawValue, $field->newInstance()));
Loading history...
Bug introduced by
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 ignore-type  annotation

177
            $argResolver = new InstanceArgumentResolver(/** @scrutinizer ignore-type */ $field->getClassInfo(), $this->getMappingConfig($field->getClassInfo(), $rawValue, $field->newInstance()));
Loading history...
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