Test Failed
Pull Request — master (#893)
by Maxim
18:07
created

Parser::guessOptionMode()   A

Complexity

Conditions 5
Paths 2

Size

Total Lines 10
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 7
dl 0
loc 10
rs 9.6111
c 0
b 0
f 0
cc 5
nc 2
nop 2
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
                $property->setValue(
79
                    $command,
80
                    $this->typecast($input->getOption($attribute->name ?? $property->getName()), $property)
81
                );
82
            }
83
        }
84
    }
85
86
    private function parseArguments(\ReflectionClass $reflection): array
87
    {
88
        $result = [];
89
        $arrayArgument = null;
90
        foreach ($reflection->getProperties() as $property) {
91
            $attribute = $this->reader->firstPropertyMetadata($property, Argument::class);
92
            if ($attribute === null) {
93
                continue;
94
            }
95
96
            $type = $this->getPropertyType($property);
97
98
            $isOptional = $property->hasDefaultValue() || $type->allowsNull();
99
            $isArray = $type->getName() === 'array';
100
            $mode = match (true) {
101
                $isArray && !$isOptional => InputArgument::IS_ARRAY | InputArgument::REQUIRED,
102
                $isArray && $isOptional => InputArgument::IS_ARRAY | InputArgument::OPTIONAL,
103
                $isOptional => InputArgument::OPTIONAL,
104
                default => InputArgument::REQUIRED
105
            };
106
107
            $argument = new InputArgument(
108
                name: $attribute->name ?? $property->getName(),
109
                mode: $mode,
110
                description: (string) $attribute->description,
111
                default: $property->hasDefaultValue() ? $property->getDefaultValue() : null,
112
                suggestedValues: $attribute->suggestedValues
113
            );
114
115
            if ($arrayArgument !== null && $isArray) {
116
                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
            if ($isArray) {
121
                $arrayArgument = $argument;
122
                continue;
123
            }
124
            $result[] = $argument;
125
        }
126
127
        if ($arrayArgument !== null) {
128
            $result[] = $arrayArgument;
129
        }
130
131
        return $result;
132
    }
133
134
    private function parseOptions(\ReflectionClass $reflection): array
135
    {
136
        $result = [];
137
        foreach ($reflection->getProperties() as $property) {
138
            $attribute = $this->reader->firstPropertyMetadata($property, Option::class);
139
            if ($attribute === null) {
140
                continue;
141
            }
142
143
            $type = $this->getPropertyType($property);
144
            $mode = $attribute->mode;
145
146
            if ($mode === null) {
147
                $mode = $this->guessOptionMode($type, $property);
148
            }
149
150
            if ($mode === InputOption::VALUE_NONE || $attribute->mode === InputOption::VALUE_NEGATABLE) {
151
                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
            $hasDefaultValue = $attribute->mode !== InputOption::VALUE_NONE && $property->hasDefaultValue();
159
160
            $result[] = new InputOption(
161
                name: $attribute->name ?? $property->getName(),
162
                shortcut: $attribute->shortcut,
163
                mode: $mode,
164
                description: (string) $attribute->description,
165
                default: $hasDefaultValue ? $property->getDefaultValue() : null,
166
                suggestedValues: $attribute->suggestedValues
167
            );
168
        }
169
170
        return $result;
171
    }
172
173
    private function typecast(mixed $value, \ReflectionProperty $property): mixed
174
    {
175
        $type = $property->hasType() ? $property->getType() : null;
176
177
        if (!$type instanceof \ReflectionNamedType) {
178
            return $value;
179
        }
180
181
        return match ($type->getName()) {
182
            'int' => (int) $value,
183
            'string' => (string) $value,
184
            'bool' => (bool) $value,
185
            'float' => (float) $value,
186
            'array' => (array) $value,
187
            default => $value
188
        };
189
    }
190
191
    private function getPropertyType(\ReflectionProperty $property): \ReflectionNamedType
192
    {
193
        if (!$property->hasType()) {
194
            throw new ConfiguratorException(
195
                \sprintf('Please, specify the type for the `%s` property!', $property->getName())
196
            );
197
        }
198
199
        $type = $property->getType();
200
201
        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...
202
            throw new ConfiguratorException(\sprintf('Invalid type for the `%s` property.', $property->getName()));
203
        }
204
205
        if ($type instanceof \ReflectionUnionType) {
206
            foreach ($type->getTypes() as $type) {
207
                if ($type->isBuiltin()) {
208
                    return $type;
209
                }
210
            }
211
        }
212
213
        if ($type instanceof \ReflectionNamedType && $type->isBuiltin() && $type->getName() !== 'object') {
214
            return $type;
215
        }
216
217
        throw new ConfiguratorException(\sprintf('Invalid type for the `%s` property.', $property->getName()));
218
    }
219
220
    /**
221
     * @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...
222
     */
223
    private function guessOptionMode(\ReflectionNamedType $type, \ReflectionProperty $property): int
224
    {
225
        $isOptional = $type->allowsNull() || $property->hasDefaultValue();
226
227
        return match (true) {
228
            $type->getName() === 'bool' => InputOption::VALUE_NEGATABLE,
229
            $type->getName() === 'array' && $isOptional => InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY,
230
            $type->getName() === 'array' && !$isOptional => InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY,
231
            $type->allowsNull() || $property->hasDefaultValue() => InputOption::VALUE_OPTIONAL,
232
            default => InputOption::VALUE_REQUIRED
233
        };
234
    }
235
}
236