Passed
Pull Request — 1.x (#321)
by Akihito
02:34
created

InputParamMeta::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 2
c 1
b 0
f 0
nc 1
nop 0
dl 0
loc 4
rs 10
1
<?php
2
3
declare(strict_types=1);
4
5
namespace BEAR\Resource\Options;
6
7
use BEAR\Resource\InputAttributeIterator;
8
use BEAR\Resource\OptionsMethodDocBolck;
9
use ReflectionClass;
10
use ReflectionMethod;
11
use ReflectionNamedType;
12
13
use function array_merge;
14
use function class_exists;
15
use function is_array;
16
use function is_bool;
17
use function is_numeric;
18
use function is_string;
19
20
/**
21
 * Extracts metadata from Input attribute parameters
22
 */
23
final class InputParamMeta implements InputParamMetaInterface
24
{
25
    private InputAttributeIterator $inputIterator;
26
    private OptionsMethodDocBolck $docBlock;
27
28
    public function __construct()
29
    {
30
        $this->inputIterator = new InputAttributeIterator();
31
        $this->docBlock = new OptionsMethodDocBolck();
32
    }
33
34
    /**
35
     * {@inheritDoc}
36
     */
37
    public function get(ReflectionMethod $method): array
38
    {
39
        if (! $this->inputIterator->hasInputParameters($method)) {
40
            return [];
41
        }
42
43
        [, $methodParamDocs] = ($this->docBlock)($method);
44
45
        $allParameters = [];
46
        $allRequired = [];
47
48
        foreach (($this->inputIterator)($method) as $paramName => $param) {
49
            $type = $param->getType();
50
            if (! $type instanceof ReflectionNamedType) {
51
                continue;
52
            }
53
54
            $className = $type->getName();
55
            if (! class_exists($className)) {
56
                continue;
57
            }
58
59
            $groupDescription = $methodParamDocs[$paramName]['description'] ?? null;
60
            [$flatParams, $required] = $this->flattenInputClass(
61
                $className,
62
                $paramName,
63
                $groupDescription,
64
            );
65
66
            $allParameters = array_merge($allParameters, $flatParams);
67
            $allRequired = array_merge($allRequired, $required);
68
        }
69
70
        $result = [];
71
        if (! empty($allParameters)) {
72
            $result['parameters'] = $allParameters;
73
        }
74
75
        if (! empty($allRequired)) {
76
            $result['required'] = $allRequired;
77
        }
78
79
        return $result;
80
    }
81
82
    /**
83
     * Flatten Input class into query parameters
84
     *
85
     * @param class-string $className
0 ignored issues
show
Documentation Bug introduced by
The doc comment class-string at position 0 could not be parsed: Unknown type name 'class-string' at position 0 in class-string.
Loading history...
86
     *
87
     * @return array{0: array<string, array<string, mixed>>, 1: array<int, string>}
88
     */
89
    private function flattenInputClass(
90
        string $className,
91
        string $group,
92
        string|null $groupDescription,
93
    ): array {
94
        $refClass = new ReflectionClass($className);
95
        $constructor = $refClass->getConstructor();
96
97
        if (! $constructor) {
98
            return [[], []];
99
        }
100
101
        // Get constructor documentation using OptionsMethodDocBolck
102
        // This already handles type conversion (int -> integer) and description extraction
103
        [, $constructorParamDocs] = ($this->docBlock)($constructor);
104
105
        $parameters = [];
106
        $required = [];
107
108
        foreach ($constructor->getParameters() as $param) {
109
            $paramName = $param->getName();
110
111
            // Check if this is a nested Input parameter
112
            if ($this->inputIterator->getInputAttribute($param) !== null) {
113
                $type = $param->getType();
114
                if ($type instanceof ReflectionNamedType && class_exists($type->getName())) {
115
                    $nestedDescription = null;
116
                    if (isset($constructorParamDocs[$paramName]['description'])) {
117
                        $nestedDescription = $constructorParamDocs[$paramName]['description'];
118
                    }
119
120
                    [$nestedParams, $nestedRequired] = $this->flattenInputClass(
121
                        $type->getName(),
122
                        $paramName,
123
                        $nestedDescription,
124
                    );
125
126
                    $parameters = array_merge($parameters, $nestedParams);
127
                    $required = array_merge($required, $nestedRequired);
128
                    continue;
129
                }
130
            }
131
132
            // Start with phpdoc information (already has type conversion)
133
            $paramMeta = [];
134
            if (isset($constructorParamDocs[$paramName])) {
135
                $paramMeta = $constructorParamDocs[$paramName];
136
            }
137
138
            // Override with reflection type if phpdoc doesn't have type
139
            if (! isset($paramMeta['type'])) {
140
                $type = $param->getType();
141
                if ($type instanceof ReflectionNamedType) {
142
                    $typeName = $type->getName();
143
                    // Apply same conversion as OptionsMethodDocBolck
144
                    $paramMeta['type'] = $typeName === 'int' ? 'integer' : $typeName;
145
                }
146
            }
147
148
            // Add default value
149
            if ($param->isDefaultValueAvailable()) {
150
                /** @var mixed $defaultValue */
151
                $defaultValue = $param->getDefaultValue();
152
                if (is_string($defaultValue)) {
153
                    $paramMeta['default'] = $defaultValue;
154
                } elseif (is_bool($defaultValue)) {
155
                    $paramMeta['default'] = $defaultValue ? 'true' : 'false';
156
                } elseif (is_numeric($defaultValue)) {
157
                    $paramMeta['default'] = (string) $defaultValue;
158
                } elseif (is_array($defaultValue)) {
159
                    $paramMeta['default'] = '[]';
160
                }
161
            }
162
163
            // Add group information
164
            $paramMeta['group'] = $group;
165
            if ($groupDescription) {
166
                $paramMeta['group_description'] = $groupDescription;
167
            }
168
169
            $parameters[$paramName] = $paramMeta;
170
171
            // Check if required
172
            if ($param->isDefaultValueAvailable() || $param->allowsNull()) {
173
                continue;
174
            }
175
176
            $required[] = $paramName;
177
        }
178
179
        return [$parameters, $required];
180
    }
181
}
182