Passed
Push — master ( 059b9c...93b7da )
by butschster
06:42 queued 18s
created

Parser   D

Complexity

Total Complexity 59

Size/Duplication

Total Lines 229
Duplicated Lines 0 %

Test Coverage

Coverage 95.62%

Importance

Changes 0
Metric Value
wmc 59
eloc 120
dl 0
loc 229
ccs 131
cts 137
cp 0.9562
rs 4.08
c 0
b 0
f 0

9 Methods

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