Passed
Pull Request — master (#1036)
by Maxim
12:01
created

Parser::parseOptions()   B

Complexity

Conditions 9
Paths 12

Size

Total Lines 37
Code Lines 22

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 22
CRAP Score 9.1399

Importance

Changes 0
Metric Value
eloc 22
dl 0
loc 37
ccs 22
cts 25
cp 0.88
rs 8.0555
c 0
b 0
f 0
cc 9
nc 12
nop 1
crap 9.1399
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 146
    public function __construct(
26
        private readonly ReaderInterface $reader = new AttributeReader()
27
    ) {
28 146
    }
29
30 107
    public function hasCommandAttribute(\ReflectionClass $reflection): bool
31
    {
32 107
        return $this->reader->firstClassMetadata($reflection, AsCommand::class) !== null ||
33 107
            $reflection->getAttributes(SymfonyAsCommand::class) !== [];
34
    }
35
36 114
    public function parse(\ReflectionClass $reflection): CommandDefinition
37
    {
38 114
        $attribute = $this->reader->firstClassMetadata($reflection, AsCommand::class);
39
40 114
        if ($attribute === null) {
41 1
            $attribute = $reflection->getAttributes(SymfonyAsCommand::class)[0]->newInstance();
42
        }
43
44 114
        return new CommandDefinition(
45 114
            name: $attribute->name,
46 114
            arguments: $this->parseArguments($reflection),
47 114
            options: $this->parseOptions($reflection),
48 114
            description: $attribute->description,
49 114
            help: $attribute instanceof AsCommand ? $attribute->help : null
50 114
        );
51
    }
52
53 100
    public function fillProperties(Command $command, InputInterface $input): void
54
    {
55 100
        $reflection = new \ReflectionClass($command);
56
57 100
        foreach ($reflection->getProperties() as $property) {
58 100
            $attribute = $this->reader->firstPropertyMetadata($property, Argument::class);
59 100
            if ($attribute === null) {
60 100
                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 100
        foreach ($reflection->getProperties() as $property) {
72 100
            $attribute = $this->reader->firstPropertyMetadata($property, Option::class);
73 100
            if ($attribute === null) {
74 100
                continue;
75
            }
76
77 39
            if ($input->hasOption($attribute->name ?? $property->getName())) {
78 38
                $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 114
    private function parseArguments(\ReflectionClass $reflection): array
88
    {
89 114
        $result = [];
90 114
        $arrayArgument = null;
91 114
        foreach ($reflection->getProperties() as $property) {
92 114
            $attribute = $this->reader->firstPropertyMetadata($property, Argument::class);
93 114
            if ($attribute === null) {
94 100
                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 110
        if ($arrayArgument !== null) {
129 4
            $result[] = $arrayArgument;
130
        }
131
132 110
        return $result;
133
    }
134
135 110
    private function parseOptions(\ReflectionClass $reflection): array
136
    {
137 110
        $result = [];
138 110
        foreach ($reflection->getProperties() as $property) {
139 110
            $attribute = $this->reader->firstPropertyMetadata($property, Option::class);
140 110
            if ($attribute === null) {
141 89
                continue;
142
            }
143
144 87
            $type = $this->getPropertyType($property);
145 84
            $mode = $attribute->mode;
146
147 84
            if ($mode === null) {
148 67
                $mode = $this->guessOptionMode($type, $property);
149
            }
150
151 84
            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 84
            $hasDefaultValue = $attribute->mode !== InputOption::VALUE_NONE && $property->hasDefaultValue();
160
161 84
            $result[] = new InputOption(
162 84
                name: $attribute->name ?? $property->getName(),
163 84
                shortcut: $attribute->shortcut,
164 84
                mode: $mode,
165 84
                description: (string) $attribute->description,
166 84
                default: $hasDefaultValue ? $property->getDefaultValue() : null,
167 84
                suggestedValues: $attribute->suggestedValues
168 84
            );
169
        }
170
171 107
        return $result;
172
    }
173
174 39
    private function typecast(mixed $value, \ReflectionProperty $property): mixed
175
    {
176 39
        $type = $property->hasType() ? $property->getType() : null;
177
178 39
        if (!$type instanceof \ReflectionNamedType || $value === null) {
179 37
            return $value;
180
        }
181
182 37
        $typeName = $type->getName();
183
184 37
        return match (true) {
185 37
            $typeName === 'int' => (int) $value,
186 37
            $typeName === 'string' => (string) $value,
187 37
            $typeName === 'bool' => (bool) $value,
188 37
            $typeName === 'float' => (float) $value,
189 37
            $typeName === 'array' => (array) $value,
190 37
            \enum_exists($typeName) => $typeName::from($value),
191 37
            default => $value
192 37
        };
193
    }
194
195 103
    private function getPropertyType(\ReflectionProperty $property): \ReflectionNamedType
196
    {
197 103
        if (!$property->hasType()) {
198
            throw new ConfiguratorException(
199
                \sprintf('Please, specify the type for the `%s` property!', $property->getName())
200
            );
201
        }
202
203 103
        $type = $property->getType();
204
205 103
        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...
206 2
            throw new ConfiguratorException(\sprintf('Invalid type for the `%s` property.', $property->getName()));
207
        }
208
209 101
        if ($type instanceof \ReflectionUnionType) {
210 2
            foreach ($type->getTypes() as $type) {
211 2
                if ($type instanceof \ReflectionNamedType && $type->isBuiltin()) {
212 2
                    return $type;
213
                }
214
            }
215
        }
216
217 99
        if (!$type->isBuiltin() && \enum_exists($type->getName())) {
218 1
            return $type;
219
        }
220
221 98
        if ($type instanceof \ReflectionNamedType && $type->isBuiltin() && $type->getName() !== 'object') {
222 94
            return $type;
223
        }
224
225 4
        throw new ConfiguratorException(\sprintf('Invalid type for the `%s` property.', $property->getName()));
226
    }
227
228
    /**
229
     * @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...
230
     */
231 67
    private function guessOptionMode(\ReflectionNamedType $type, \ReflectionProperty $property): int
232
    {
233 67
        $isOptional = $type->allowsNull() || $property->hasDefaultValue();
234
235 67
        return match (true) {
236 67
            $type->getName() === 'bool' => InputOption::VALUE_NEGATABLE,
237 67
            $type->getName() === 'array' && $isOptional => InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY,
238 67
            $type->getName() === 'array' && !$isOptional => InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY,
239 67
            $type->allowsNull() || $property->hasDefaultValue() => InputOption::VALUE_OPTIONAL,
240 67
            default => InputOption::VALUE_REQUIRED
241 67
        };
242
    }
243
}
244