Test Failed
Pull Request — master (#872)
by Maxim
08:22
created

Parser   A

Complexity

Total Complexity 40

Size/Duplication

Total Lines 186
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 40
eloc 97
dl 0
loc 186
rs 9.2
c 0
b 0
f 0

8 Methods

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