Test Failed
Push — main ( 096ecb...6010cc )
by Stanislav
02:20
created

Mapper   B

Complexity

Total Complexity 49

Size/Duplication

Total Lines 277
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 109
dl 0
loc 277
rs 8.48
c 0
b 0
f 0
wmc 49

12 Methods

Rating   Name   Duplication   Size   Complexity  
B getMappingConfig() 0 46 10
A map() 0 15 2
C validate() 0 54 17
A mapFromString() 0 5 1
A createArgumentResolverForPrimitive() 0 9 4
A __construct() 0 6 2
A mapFromFile() 0 5 1
A processExpression() 0 5 1
A doMapArray() 0 14 4
A registerCustomArgumentResolver() 0 3 1
A make() 0 3 1
A toArgumentResolver() 0 20 5

How to fix   Complexity   

Complex Class

Complex classes like Mapper often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Mapper, and based on these observations, apply Extract Interface, too.

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
    public function __construct(ArgumentResolverFactory $argumentResolverFactory = null)
21
    {
22
        if (null === $argumentResolverFactory) {
23
            $argumentResolverFactory = new ArgumentResolverFactory();
24
        }
25
        $this->argumentResolverFactory = $argumentResolverFactory;
26
    }
27
28
    public static function make(): static
29
    {
30
        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
    public function registerCustomArgumentResolver(string $resolverAlias, string $resolverClassName): void
42
    {
43
        $this->argumentResolverFactory->addResolver($resolverAlias, $resolverClassName);
44
    }
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
    public function mapFromFile(string $targetClass, string $configFile): object
59
    {
60
        $config = Yaml::parseFile($configFile);
61
62
        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
    public function map(string $targetClass, array $config): object
76
    {
77
        $instance = new $targetClass;
78
        $classInfoReflector = new ClassInfoReflector();
79
        $classInfo = $classInfoReflector->introspect($targetClass);
80
        $validationResult = $this->validate($classInfo, $config);
81
82
        if (!$validationResult->isValid()) {
83
            throw new ValidationException($validationResult);
84
        }
85
86
        $mappingConfig = $this->getMappingConfig($classInfo, $config, $instance);
87
        $rootResolver = new InstanceArgumentResolver($classInfo, $mappingConfig);
88
89
        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
    private function getMappingConfig(ClassInfo $classInfo, ?array $config, $parentKey = null): array
114
    {
115
        $mappingConfig = [];
116
117
        if ($config === null) {
0 ignored issues
show
introduced by
The condition $config === null is always false.
Loading history...
118
            $config = [];
119
        }
120
121
        foreach ($classInfo->getFields() as $field) {
122
            $fieldName = $field->getName();
123
            if (!array_key_exists($fieldName, $config)) {
124
                //Skip values that doesn't exist in config file but has a default values
125
                if (!$field->hasDefaultValueResolver()) {
126
                    continue;
127
                }
128
129
                //fallback to defaultValueResolver
130
                switch ($field->getDefaultValueResolver()) {
131
                    case DefaultValueResolver::PARENT_KEY:
132
                    {
133
                        $rawValue = $parentKey;
134
                        break;
135
                    }
136
                    case DefaultValueResolver::NESTED_LIST:
137
                    {
138
                        $rawValue = $config;
139
                        $config = [];
140
                        break;
141
                    }
142
                }
143
            } else {
144
                $rawValue = $config[$fieldName];
145
                unset($config[$fieldName]);
146
            }
147
148
            $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
        if (!empty($config) && !$classInfo->isIgnoreUnknown()) {
152
            throw new InvalidConfigPathException($config, $classInfo);
153
        }
154
        foreach ($config as $fieldName => $rawValue) {
155
            $mappingConfig[$fieldName] = $this->createArgumentResolverForPrimitive($rawValue);
156
        }
157
158
        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
    private function toArgumentResolver(ClassField $field, mixed $rawValue): ArgumentResolver
171
    {
172
        if ($field->isPrimitive()) {
173
            $argResolver = $this->createArgumentResolverForPrimitive($rawValue);
174
        } else if ($field->isList()) {
175
            $value = [];
176
            if ($field->isTypedCollection()) {
177
                foreach ($rawValue as $key => $item) {
178
                    $value[] = new InstanceArgumentResolver($field->getClassInfo(), $this->getMappingConfig($field->getClassInfo(), $item, $key));
0 ignored issues
show
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

178
                    $value[] = new InstanceArgumentResolver(/** @scrutinizer ignore-type */ $field->getClassInfo(), $this->getMappingConfig($field->getClassInfo(), $item, $key));
Loading history...
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

178
                    $value[] = new InstanceArgumentResolver($field->getClassInfo(), $this->getMappingConfig(/** @scrutinizer ignore-type */ $field->getClassInfo(), $item, $key));
Loading history...
179
                }
180
            } else {
181
                $value = $this->doMapArray($rawValue);
182
            }
183
184
            $argResolver = new ListArgumentResolver($value);
185
        } else {
186
            $argResolver = new InstanceArgumentResolver($field->getClassInfo(), $this->getMappingConfig($field->getClassInfo(), $rawValue, $field->newInstance()));
187
        }
188
189
        return $argResolver;
190
    }
191
192
193
    /**
194
     * @param mixed $rawValue
195
     *
196
     * @throws Resolver\Parser\SyntaxException
197
     * @return ArgumentResolver
198
     */
199
    private function createArgumentResolverForPrimitive(mixed $rawValue): ArgumentResolver
200
    {
201
        if (is_bool($rawValue) || is_int($rawValue) || is_array($rawValue)) {
202
            $argResolver = new ScalarArgumentResolver($rawValue);
203
        } else {
204
            $argResolver = $this->processExpression($rawValue);
205
        }
206
207
        return $argResolver;
208
    }
209
210
    /**
211
     * @throws Resolver\Parser\SyntaxException
212
     */
213
    private function processExpression(string $expression): ArgumentResolver
214
    {
215
        $parser = new Parser($expression);
216
217
        return $parser->parse()->toResolver($this->argumentResolverFactory);
218
    }
219
220
    /**
221
     * @throws Resolver\Parser\SyntaxException
222
     */
223
    private function doMapArray(array $array): array
224
    {
225
        $result = [];
226
        foreach ($array as $key => $value) {
227
            if (is_array($value)) {
228
                $result[$key] = $this->doMapArray($value);
229
            } else if (is_string($value)) {
230
                $result[$key] = $this->processExpression($value);
231
            } else {
232
                $result[$key] = new ScalarArgumentResolver($value);
233
            }
234
        }
235
236
        return $result;
237
    }
238
239
    private function validate(ClassInfo $classInfo, ?array $config, ?array $parent = [], ?ValidationResult $validationResult = null): ValidationResult
240
    {
241
        if (null === $validationResult) {
242
            $validationResult = new ValidationResult();
243
        }
244
245
        $pathFunction = static fn(array $path) => implode(".", $path);
246
247
        //Check for required fields
248
        foreach ($classInfo->getFields() as $field) {
249
            $fieldName = $field->getName();
250
            $isFieldExistsInConfig = $config !== null && array_key_exists($fieldName, $config);
251
            $isRequired = $field->isRequired();
252
            $path = $parent;
253
            $path[] = $fieldName;
254
255
            if ($isRequired && !$isFieldExistsInConfig) {
256
                if ($field->hasDefaultValueResolver()) {
257
                    $defaultValueResolver = $field->getDefaultValueResolver();
258
                    switch ($defaultValueResolver) {
259
                        case DefaultValueResolver::PARENT_KEY:
260
                            $parentKeyExists = !empty($parent);
261
                            if (!$parentKeyExists) {
262
                                $validationResult->addError($path, sprintf("Field '%s' is required but not found in the config file", $pathFunction($path)));
263
                            }
264
                            break;
265
                        case DefaultValueResolver::NESTED_LIST:
266
                            if (empty($config) || !is_array($config)) {
267
                                $validationResult->addError($path, sprintf("Field '%s' is required but not found in the config file", $pathFunction($path)));
268
                            }
269
                            break;
270
                    }
271
                } else {
272
                    $validationResult->addError($path, sprintf("Field '%s' is required but not found in the config file", $pathFunction($path)));
273
                    continue;
274
                }
275
            }
276
277
            if (null !== $field->getClassInfo()) {
278
                if ($field->isTypedCollection()) {
279
                    if ($isRequired === false && !$isFieldExistsInConfig) {
280
                        continue;
281
                    }
282
                    foreach ($config[$fieldName] as $key => $value) {
283
                        $path[] = $key;
284
                        $validationResult = $this->validate($field->getClassInfo(), $value, $path, $validationResult);
285
                    }
286
                } else {
287
                    $validationResult = $this->validate($field->getClassInfo(), $config[$fieldName], $path, $validationResult);
288
                }
289
            }
290
        }
291
292
        return $validationResult;
293
    }
294
}
295