Test Failed
Pull Request — master (#872)
by Maxim
10:54 queued 03:52
created

Parser::parseOptions()   B

Complexity

Conditions 8
Paths 7

Size

Total Lines 32
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 19
dl 0
loc 32
rs 8.4444
c 0
b 0
f 0
cc 8
nc 7
nop 1
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
145
            if ($attribute->mode === InputOption::VALUE_NONE || $attribute->mode === InputOption::VALUE_NEGATABLE) {
146
                if ($type->getName() !== 'bool') {
147
                    throw new ConfiguratorException(
148
                        'Options properties with mode `VALUE_NONE` or `VALUE_NEGATABLE` must be bool!'
149
                    );
150
                }
151
            }
152
153
            $hasDefaultValue = $attribute->mode !== InputOption::VALUE_NONE && $property->hasDefaultValue();
154
155
            $result[] = new InputOption(
156
                name: $attribute->name ?? $property->getName(),
157
                shortcut: $attribute->shortcut,
158
                mode: $attribute->mode,
159
                description: (string) $attribute->description,
160
                default: $hasDefaultValue ? $property->getDefaultValue() : null,
161
                suggestedValues: $attribute->suggestedValues
162
            );
163
        }
164
165
        return $result;
166
    }
167
168
    private function typecast(mixed $value, \ReflectionProperty $property): mixed
169
    {
170
        $type = $property->hasType() ? $property->getType() : null;
171
172
        if (!$type instanceof \ReflectionNamedType) {
173
            return $value;
174
        }
175
176
        return match ($type->getName()) {
177
            'int' => (int) $value,
178
            'string' => (string) $value,
179
            'bool' => (bool) $value,
180
            'float' => (float) $value,
181
            'array' => (array) $value,
182
            default => $value
183
        };
184
    }
185
186
    private function getPropertyType(\ReflectionProperty $property): \ReflectionNamedType
187
    {
188
        if (!$property->hasType()) {
189
            throw new ConfiguratorException(
190
                \sprintf('Please, specify the type for the `%s` property!', $property->getName())
191
            );
192
        }
193
194
        $type = $property->getType();
195
196
        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...
197
            throw new ConfiguratorException(\sprintf('Invalid type for the `%s` property.', $property->getName()));
198
        }
199
200
        if ($type instanceof \ReflectionUnionType) {
201
            foreach ($type->getTypes() as $type) {
202
                if ($type->isBuiltin()) {
203
                    return $type;
204
                }
205
            }
206
        }
207
208
        if ($type instanceof \ReflectionNamedType && $type->isBuiltin()) {
209
            return $type;
210
        }
211
212
        throw new ConfiguratorException(\sprintf('Invalid type for the `%s` property.', $property->getName()));
213
    }
214
}
215