Code Coverage
 
Lines
Branches
Paths
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
29 / 29
96.97% covered (success)
96.97%
32 / 33
69.57% covered (warning)
69.57%
16 / 23
83.33% covered (warning)
83.33%
5 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
Type
100.00% covered (success)
100.00%
29 / 29
96.97% covered (success)
96.97%
32 / 33
69.57% covered (warning)
69.57%
16 / 23
100.00% covered (success)
100.00%
6 / 6
31.28
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 of
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
9 / 9
50.00% covered (danger)
50.00%
4 / 8
100.00% covered (success)
100.00%
1 / 1
8.12
 convert
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
5
 expected
100.00% covered (success)
100.00%
5 / 5
87.50% covered (warning)
87.50%
7 / 8
75.00% covered (warning)
75.00%
3 / 4
100.00% covered (success)
100.00%
1 / 1
4.25
 choices
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 case
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
6 / 6
50.00% covered (danger)
50.00%
2 / 4
100.00% covered (success)
100.00%
1 / 1
4.12
1<?php
2
3declare(strict_types=1);
4
5namespace Celema\Console;
6
7use BackedEnum;
8use ReflectionNamedType;
9use ReflectionParameter;
10
11/**
12 * The declared type of an `#[Arg]` or `#[Opt]` parameter.
13 *
14 * @internal
15 */
16final class Type
17{
18    private function __construct(
19        public readonly string $name,
20    ) {}
21
22    /**
23     * The parameter's type, or null if it is not a supported one.
24     */
25    public static function of(ReflectionParameter $parameter): ?self
26    {
27        $type = $parameter->getType();
28
29        if (!$type instanceof ReflectionNamedType) {
30            return null;
31        }
32
33        $name = $type->getName();
34
35        return match ($name) {
36            'string', 'int', 'float', 'bool', 'array' => new self($name),
37            default => is_subclass_of($name, BackedEnum::class) ? new self($name) : null,
38        };
39    }
40
41    /**
42     * Converts a command-line value, or returns null if it does not fit.
43     *
44     * Not for `bool`, which takes no value, nor `array`, which collects
45     * the strings as they are.
46     */
47    public function convert(string $value): string|int|float|BackedEnum|null
48    {
49        return match ($this->name) {
50            'int' => filter_var($value, FILTER_VALIDATE_INT, FILTER_NULL_ON_FAILURE),
51            'float' => filter_var($value, FILTER_VALIDATE_FLOAT, FILTER_NULL_ON_FAILURE),
52            'string' => $value,
53            default => $this->case($value),
54        };
55    }
56
57    /**
58     * Describes the values that convert, for error messages.
59     */
60    public function expected(): string
61    {
62        return match ($this->name) {
63            'int' => 'an integer',
64            'float' => 'a number',
65            default => 'one of ' . implode(', ', $this->choices()),
66        };
67    }
68
69    /**
70     * The backing values of an enum type, otherwise none.
71     *
72     * @return list<string>
73     */
74    public function choices(): array
75    {
76        $enum = $this->name;
77
78        if (!is_subclass_of($enum, BackedEnum::class)) {
79            return [];
80        }
81
82        return array_map(static fn(BackedEnum $case): string => (string) $case->value, $enum::cases());
83    }
84
85    private function case(string $value): ?BackedEnum
86    {
87        /** @var class-string<BackedEnum> $enum */
88        $enum = $this->name;
89
90        // Compared as strings: under strict types, tryFrom() rejects the
91        // string input of an int-backed enum.
92        foreach ($enum::cases() as $case) {
93            if ((string) $case->value === $value) {
94                return $case;
95            }
96        }
97
98        return null;
99    }
100}