Test Failed
Pull Request — master (#902)
by butschster
08:55
created

Parser   C

Complexity

Total Complexity 53

Size/Duplication

Total Lines 211
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 53
eloc 110
dl 0
loc 211
rs 6.96
c 0
b 0
f 0

9 Methods

Rating   Name   Duplication   Size   Complexity  
A hasCommandAttribute() 0 4 2
A __construct() 0 3 1
A parse() 0 14 3
B parseOptions() 0 37 9
B getPropertyType() 0 27 9
B fillProperties() 0 29 9
A typecast() 0 15 4
B parseArguments() 0 46 11
A guessOptionMode() 0 10 5

How to fix   Complexity   

Complex Class

Complex classes like Parser 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 Parser, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
declare(strict_types=1);
4
5
namespace Spiral\Console\Configurator\Attribute;
6
7
use Spiral\Attributes\AttributeReader;
8
use Spiral\Attributes\ReaderInterface;
9
use Spiral\Console\Attribute\Argument;
10
use Spiral\Console\Attribute\AsCommand;
11
use Spiral\Console\Attribute\Option;
12
use Spiral\Console\Command;
13
use Spiral\Console\Configurator\CommandDefinition;
14
use Spiral\Console\Exception\ConfiguratorException;
15
use Symfony\Component\Console\Attribute\AsCommand as SymfonyAsCommand;
16
use Symfony\Component\Console\Input\InputArgument;
17
use Symfony\Component\Console\Input\InputInterface;
18
use Symfony\Component\Console\Input\InputOption;
19
20
/**
21
 * @internal
22
 */
23
final class Parser
24
{
25
    public function __construct(
26
        private readonly ReaderInterface $reader = new AttributeReader()
27
    ) {
28
    }
29
30
    public function hasCommandAttribute(\ReflectionClass $reflection): bool
31
    {
32
        return $this->reader->firstClassMetadata($reflection, AsCommand::class) !== null ||
33
            $reflection->getAttributes(SymfonyAsCommand::class) !== [];
34
    }
35
36
    public function parse(\ReflectionClass $reflection): CommandDefinition
37
    {
38
        $attribute = $this->reader->firstClassMetadata($reflection, AsCommand::class);
39
40
        if ($attribute === null) {
41
            $attribute = $reflection->getAttributes(SymfonyAsCommand::class)[0]->newInstance();
42
        }
43
44
        return new CommandDefinition(
45
            name: $attribute->name,
46
            arguments: $this->parseArguments($reflection),
47
            options: $this->parseOptions($reflection),
48
            description: $attribute->description,
49
            help: $attribute instanceof AsCommand ? $attribute->help : null
50
        );
51
    }
52
53
    public function fillProperties(Command $command, InputInterface $input): void
54
    {
55
        $reflection = new \ReflectionClass($command);
56
57
        foreach ($reflection->getProperties() as $property) {
58
            $attribute = $this->reader->firstPropertyMetadata($property, Argument::class);
59
            if ($attribute === null) {
60
                continue;
61
            }
62
63
            if ($input->hasArgument($attribute->name ?? $property->getName())) {
64
                $property->setValue(
65
                    $command,
66
                    $this->typecast($input->getArgument($attribute->name ?? $property->getName()), $property)
67
                );
68
            }
69
        }
70
71
        foreach ($reflection->getProperties() as $property) {
72
            $attribute = $this->reader->firstPropertyMetadata($property, Option::class);
73
            if ($attribute === null) {
74
                continue;
75
            }
76
77
            if ($input->hasOption($attribute->name ?? $property->getName())) {
78
                $value = $this->typecast($input->getOption($attribute->name ?? $property->getName()), $property);
79
80
                if ($value !== null || $this->getPropertyType($property)->allowsNull()) {
81
                    $property->setValue($command, $value);
82
                }
83
            }
84
        }
85
    }
86
87
    private function parseArguments(\ReflectionClass $reflection): array
88
    {
89
        $result = [];
90
        $arrayArgument = null;
91
        foreach ($reflection->getProperties() as $property) {
92
            $attribute = $this->reader->firstPropertyMetadata($property, Argument::class);
93
            if ($attribute === null) {
94
                continue;
95
            }
96
97
            $type = $this->getPropertyType($property);
98
99
            $isOptional = $property->hasDefaultValue() || $type->allowsNull();
100
            $isArray = $type->getName() === 'array';
101
            $mode = match (true) {
102
                $isArray && !$isOptional => InputArgument::IS_ARRAY | InputArgument::REQUIRED,
103
                $isArray && $isOptional => InputArgument::IS_ARRAY | InputArgument::OPTIONAL,
104
                $isOptional => InputArgument::OPTIONAL,
105
                default => InputArgument::REQUIRED
106
            };
107
108
            $argument = new InputArgument(
109
                name: $attribute->name ?? $property->getName(),
110
                mode: $mode,
111
                description: (string) $attribute->description,
112
                default: $property->hasDefaultValue() ? $property->getDefaultValue() : null,
113
                suggestedValues: $attribute->suggestedValues
114
            );
115
116
            if ($arrayArgument !== null && $isArray) {
117
                throw new ConfiguratorException('There must be only one array argument!');
118
            }
119
120
            // It must be used at the end of the argument list.
121
            if ($isArray) {
122
                $arrayArgument = $argument;
123
                continue;
124
            }
125
            $result[] = $argument;
126
        }
127
128
        if ($arrayArgument !== null) {
129
            $result[] = $arrayArgument;
130
        }
131
132
        return $result;
133
    }
134
135
    private function parseOptions(\ReflectionClass $reflection): array
136
    {
137
        $result = [];
138
        foreach ($reflection->getProperties() as $property) {
139
            $attribute = $this->reader->firstPropertyMetadata($property, Option::class);
140
            if ($attribute === null) {
141
                continue;
142
            }
143
144
            $type = $this->getPropertyType($property);
145
            $mode = $attribute->mode;
146
147
            if ($mode === null) {
148
                $mode = $this->guessOptionMode($type, $property);
149
            }
150
151
            if ($mode === InputOption::VALUE_NONE || $mode === InputOption::VALUE_NEGATABLE) {
152
                if ($type->getName() !== 'bool') {
153
                    throw new ConfiguratorException(
154
                        'Options properties with mode `VALUE_NONE` or `VALUE_NEGATABLE` must be bool!'
155
                    );
156
                }
157
            }
158
159
            $hasDefaultValue = $attribute->mode !== InputOption::VALUE_NONE && $property->hasDefaultValue();
160
161
            $result[] = new InputOption(
162
                name: $attribute->name ?? $property->getName(),
163
                shortcut: $attribute->shortcut,
164
                mode: $mode,
165
                description: (string) $attribute->description,
166
                default: $hasDefaultValue ? $property->getDefaultValue() : null,
167
                suggestedValues: $attribute->suggestedValues
168
            );
169
        }
170
171
        return $result;
172
    }
173
174
    private function typecast(mixed $value, \ReflectionProperty $property): mixed
175
    {
176
        $type = $property->hasType() ? $property->getType() : null;
177
178
        if (!$type instanceof \ReflectionNamedType || $value === null) {
179
            return $value;
180
        }
181
182
        return match ($type->getName()) {
183
            'int' => (int) $value,
184
            'string' => (string) $value,
185
            'bool' => (bool) $value,
186
            'float' => (float) $value,
187
            'array' => (array) $value,
188
            default => $value
189
        };
190
    }
191
192
    private function getPropertyType(\ReflectionProperty $property): \ReflectionNamedType
193
    {
194
        if (!$property->hasType()) {
195
            throw new ConfiguratorException(
196
                \sprintf('Please, specify the type for the `%s` property!', $property->getName())
197
            );
198
        }
199
200
        $type = $property->getType();
201
202
        if ($type instanceof \ReflectionIntersectionType) {
0 ignored issues
show
Bug introduced by
The type ReflectionIntersectionType was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
203
            throw new ConfiguratorException(\sprintf('Invalid type for the `%s` property.', $property->getName()));
204
        }
205
206
        if ($type instanceof \ReflectionUnionType) {
207
            foreach ($type->getTypes() as $type) {
208
                if ($type->isBuiltin()) {
209
                    return $type;
210
                }
211
            }
212
        }
213
214
        if ($type instanceof \ReflectionNamedType && $type->isBuiltin() && $type->getName() !== 'object') {
215
            return $type;
216
        }
217
218
        throw new ConfiguratorException(\sprintf('Invalid type for the `%s` property.', $property->getName()));
219
    }
220
221
    /**
222
     * @return positive-int
0 ignored issues
show
Documentation Bug introduced by
The doc comment positive-int at position 0 could not be parsed: Unknown type name 'positive-int' at position 0 in positive-int.
Loading history...
223
     */
224
    private function guessOptionMode(\ReflectionNamedType $type, \ReflectionProperty $property): int
225
    {
226
        $isOptional = $type->allowsNull() || $property->hasDefaultValue();
227
228
        return match (true) {
229
            $type->getName() === 'bool' => InputOption::VALUE_NEGATABLE,
230
            $type->getName() === 'array' && $isOptional => InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY,
231
            $type->getName() === 'array' && !$isOptional => InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY,
232
            $type->allowsNull() || $property->hasDefaultValue() => InputOption::VALUE_OPTIONAL,
233
            default => InputOption::VALUE_REQUIRED
234
        };
235
    }
236
}
237