Parser   F
last analyzed

Complexity

Total Complexity 60

Size/Duplication

Total Lines 228
Duplicated Lines 0 %

Test Coverage

Coverage 95.59%

Importance

Changes 0
Metric Value
wmc 60
eloc 120
dl 0
loc 228
ccs 130
cts 136
cp 0.9559
rs 3.6
c 0
b 0
f 0

9 Methods

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

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 152
    public function __construct(
26
        private readonly ReaderInterface $reader = new AttributeReader(),
27 152
    ) {}
28
29 113
    public function hasCommandAttribute(\ReflectionClass $reflection): bool
30
    {
31 113
        return $this->reader->firstClassMetadata($reflection, AsCommand::class) !== null ||
32 113
            $reflection->getAttributes(SymfonyAsCommand::class) !== [];
33
    }
34
35 120
    public function parse(\ReflectionClass $reflection): CommandDefinition
36
    {
37 120
        $attribute = $this->reader->firstClassMetadata($reflection, AsCommand::class);
38
39 120
        if ($attribute === null) {
40 1
            $attribute = $reflection->getAttributes(SymfonyAsCommand::class)[0]->newInstance();
41
        }
42
43 120
        return new CommandDefinition(
44 120
            name: $attribute->name,
45 120
            arguments: $this->parseArguments($reflection),
46 120
            options: $this->parseOptions($reflection),
47 120
            description: $attribute->description,
48 120
            help: $attribute instanceof AsCommand ? $attribute->help : null,
49 120
        );
50
    }
51
52 106
    public function fillProperties(Command $command, InputInterface $input): void
53
    {
54 106
        $reflection = new \ReflectionClass($command);
55
56 106
        foreach ($reflection->getProperties() as $property) {
57 106
            $attribute = $this->reader->firstPropertyMetadata($property, Argument::class);
58 106
            if ($attribute === null) {
59 106
                continue;
60
            }
61
62 37
            if ($input->hasArgument($attribute->name ?? $property->getName())) {
63 36
                $property->setValue(
64 36
                    $command,
65 36
                    $this->typecast($input->getArgument($attribute->name ?? $property->getName()), $property),
66 36
                );
67
            }
68
        }
69
70 106
        foreach ($reflection->getProperties() as $property) {
71 106
            $attribute = $this->reader->firstPropertyMetadata($property, Option::class);
72 106
            if ($attribute === null) {
73 105
                continue;
74
            }
75
76 40
            if ($input->hasOption($attribute->name ?? $property->getName())) {
77 39
                $value = $this->typecast($input->getOption($attribute->name ?? $property->getName()), $property);
78
79 38
                if ($value !== null || $this->getPropertyType($property)->allowsNull()) {
80 37
                    $property->setValue($command, $value);
81
                }
82
            }
83
        }
84
    }
85
86 120
    private function parseArguments(\ReflectionClass $reflection): array
87
    {
88 120
        $result = [];
89 120
        $arrayArgument = null;
90 120
        foreach ($reflection->getProperties() as $property) {
91 120
            $attribute = $this->reader->firstPropertyMetadata($property, Argument::class);
92 120
            if ($attribute === null) {
93 106
                continue;
94
            }
95
96 82
            $type = $this->getPropertyType($property);
97
98 79
            $isOptional = $property->hasDefaultValue() || $type->allowsNull();
99 79
            $isArray = $type->getName() === 'array';
100 79
            $mode = match (true) {
101 79
                $isArray && !$isOptional => InputArgument::IS_ARRAY | InputArgument::REQUIRED,
102 77
                $isArray && $isOptional => InputArgument::IS_ARRAY | InputArgument::OPTIONAL,
103 76
                $isOptional => InputArgument::OPTIONAL,
104 74
                default => InputArgument::REQUIRED,
105 79
            };
106
107 79
            $argument = new InputArgument(
108 79
                name: $attribute->name ?? $property->getName(),
109 79
                mode: $mode,
110 79
                description: (string) $attribute->description,
111 79
                default: $property->hasDefaultValue() ? $property->getDefaultValue() : null,
112 79
                suggestedValues: $attribute->suggestedValues,
113 79
            );
114
115 79
            if ($arrayArgument !== null && $isArray) {
116 1
                throw new ConfiguratorException('There must be only one array argument!');
117
            }
118
119
            // It must be used at the end of the argument list.
120 79
            if ($isArray) {
121 5
                $arrayArgument = $argument;
122 5
                continue;
123
            }
124 76
            $result[] = $argument;
125
        }
126
127 116
        if ($arrayArgument !== null) {
128 4
            $result[] = $arrayArgument;
129
        }
130
131 116
        return $result;
132
    }
133
134 116
    private function parseOptions(\ReflectionClass $reflection): array
135
    {
136 116
        $result = [];
137 116
        foreach ($reflection->getProperties() as $property) {
138 116
            $attribute = $this->reader->firstPropertyMetadata($property, Option::class);
139 116
            if ($attribute === null) {
140 95
                continue;
141
            }
142
143 93
            $type = $this->getPropertyType($property);
144 90
            $mode = $attribute->mode;
145
146 90
            if ($mode === null) {
147 73
                $mode = $this->guessOptionMode($type, $property);
148
            }
149
150 90
            if ($mode === InputOption::VALUE_NONE || $mode === InputOption::VALUE_NEGATABLE) {
151 69
                if ($type->getName() !== 'bool') {
152
                    throw new ConfiguratorException(
153
                        'Options properties with mode `VALUE_NONE` or `VALUE_NEGATABLE` must be bool!',
154
                    );
155
                }
156
            }
157
158 90
            $hasDefaultValue = $attribute->mode !== InputOption::VALUE_NONE && $property->hasDefaultValue();
159
160 90
            $result[] = new InputOption(
161 90
                name: $attribute->name ?? $property->getName(),
162 90
                shortcut: $attribute->shortcut,
163 90
                mode: $mode,
164 90
                description: (string) $attribute->description,
165 90
                default: $hasDefaultValue ? $property->getDefaultValue() : null,
166 90
                suggestedValues: $attribute->suggestedValues,
167 90
            );
168
        }
169
170 113
        return $result;
171
    }
172
173 41
    private function typecast(mixed $value, \ReflectionProperty $property): mixed
174
    {
175 41
        $type = $property->hasType() ? $property->getType() : null;
176
177 41
        if (!$type instanceof \ReflectionNamedType || $value === null) {
178 37
            return $value;
179
        }
180
181 39
        if (!$type->isBuiltin() && \enum_exists($type->getName())) {
182
            try {
183
                /** @var class-string<\BackedEnum> $enum */
184 2
                $enum = $type->getName();
185
186 2
                return $enum::from($value);
187 1
            } catch (\Throwable) {
188 1
                throw new ConfiguratorException(\sprintf('Wrong option value. Allowed options: `%s`.', \implode(
189 1
                    '`, `',
190 1
                    \array_map(static fn(\BackedEnum $item): string => (string) $item->value, $enum::cases()),
0 ignored issues
show
Bug introduced by
Accessing value on the interface BackedEnum suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
191 1
                )));
192
            }
193
        }
194
195 37
        return match ($type->getName()) {
196 2
            'int' => (int) $value,
197 37
            'string' => (string) $value,
198 23
            'bool' => (bool) $value,
199 2
            'float' => (float) $value,
200 18
            'array' => (array) $value,
201 37
            default => $value,
202 37
        };
203
    }
204
205 109
    private function getPropertyType(\ReflectionProperty $property): \ReflectionNamedType
206
    {
207 109
        if (!$property->hasType()) {
208
            throw new ConfiguratorException(
209
                \sprintf('Please, specify the type for the `%s` property!', $property->getName()),
210
            );
211
        }
212
213 109
        $type = $property->getType();
214
215 109
        if ($type instanceof \ReflectionIntersectionType) {
216 2
            throw new ConfiguratorException(\sprintf('Invalid type for the `%s` property.', $property->getName()));
217
        }
218
219 107
        if ($type instanceof \ReflectionUnionType) {
220 2
            foreach ($type->getTypes() as $type) {
221 2
                if ($type instanceof \ReflectionNamedType && $type->isBuiltin()) {
222 2
                    return $type;
223
                }
224
            }
225
        }
226
227 105
        if ($type instanceof \ReflectionNamedType && !$type->isBuiltin() && \enum_exists($type->getName())) {
228 2
            return $type;
229
        }
230
231 103
        if ($type instanceof \ReflectionNamedType && $type->isBuiltin() && $type->getName() !== 'object') {
232 99
            return $type;
233
        }
234
235 4
        throw new ConfiguratorException(\sprintf('Invalid type for the `%s` property.', $property->getName()));
236
    }
237
238
    /**
239
     * @return int<0, 31>
240
     */
241 73
    private function guessOptionMode(\ReflectionNamedType $type, \ReflectionProperty $property): int
242
    {
243 73
        $isOptional = $type->allowsNull() || $property->hasDefaultValue();
244
245
        return match (true) {
246 73
            $type->getName() === 'bool' => InputOption::VALUE_NEGATABLE,
247 73
            $type->getName() === 'array' && $isOptional => InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY,
248 71
            $type->getName() === 'array' && !$isOptional => InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY,
249 70
            $type->allowsNull() || $property->hasDefaultValue() => InputOption::VALUE_OPTIONAL,
250 73
            default => InputOption::VALUE_REQUIRED,
251
        };
252
    }
253
}
254