Passed
Pull Request — master (#63)
by Sergei
02:23
created

ClassConfigFactory   A

Complexity

Total Complexity 24

Size/Duplication

Total Lines 245
Duplicated Lines 0 %

Test Coverage

Coverage 91.46%

Importance

Changes 10
Bugs 3 Features 0
Metric Value
eloc 74
c 10
b 3
f 0
dl 0
loc 245
ccs 75
cts 82
cp 0.9146
rs 10
wmc 24

11 Methods

Rating   Name   Duplication   Size   Complexity  
A getMethodParameterConfigs() 0 8 2
A getMethodParameterConfig() 0 15 4
A getUnionType() 0 8 1
A getIntersectionType() 0 12 1
A getMethodParameterTypeConfig() 0 18 2
A getClassConfig() 0 21 2
A getMethodConfig() 0 7 1
A getMethodReturnTypeConfig() 0 24 4
A convertTypeToString() 0 12 3
A getMethodModifiers() 0 12 2
A getMethodConfigs() 0 8 2
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Proxy;
6
7
use InvalidArgumentException;
8
use Reflection;
9
use ReflectionClass;
10
use ReflectionException;
11
use 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...
12
use ReflectionMethod;
13
use ReflectionNamedType;
14
use ReflectionParameter;
15
use ReflectionType;
16
use ReflectionUnionType;
17
use Yiisoft\Proxy\Config\ClassConfig;
18
use Yiisoft\Proxy\Config\MethodConfig;
19
use Yiisoft\Proxy\Config\ParameterConfig;
20
use Yiisoft\Proxy\Config\TypeConfig;
21
22
/**
23
 * @internal
24
 *
25
 * A factory for creating class configs ({@see ClassConfig}). Uses PHP `Reflection` to get the necessary metadata.
26
 *
27
 * @link https://www.php.net/manual/en/book.reflection.php
28
 */
29
final class ClassConfigFactory
30
{
31
    /**
32
     * Gets single class config based for individual class.
33
     *
34
     * @param string $className Full class or interface name (including namespace).
35
     *
36
     * @psalm-param class-string $className
37
     *
38
     * @throws InvalidArgumentException In case class or interface does not exist.
39
     *
40
     * @return ClassConfig Class config with all related configs (methods, parameters, types) linked.
41
     */
42 14
    public function getClassConfig(string $className): ClassConfig
43
    {
44
        try {
45 14
            $reflection = new ReflectionClass($className);
46 1
        } catch (ReflectionException) {
47 1
            throw new InvalidArgumentException("$className must exist.");
48
        }
49
50
        /**
51
         * @psalm-suppress MixedArgumentTypeCoercion Can be removed after release
52
         * https://github.com/vimeo/psalm/pull/8405
53
         */
54 13
        return new ClassConfig(
55 13
            isInterface: $reflection->isInterface(),
56 13
            namespace: $reflection->getNamespaceName(),
57 13
            modifiers: Reflection::getModifierNames($reflection->getModifiers()),
58 13
            name: $reflection->getName(),
59 13
            shortName: $reflection->getShortName(),
60 13
            parent: (string) $reflection->getParentClass(),
61 13
            interfaces: $reflection->getInterfaceNames(),
62 13
            methods: $this->getMethodConfigs($reflection),
63
        );
64
    }
65
66
    /**
67
     * Gets the complete set of method configs for a given class reflection.
68
     *
69
     * @param ReflectionClass $class Reflection of a class.
70
     *
71
     * @return MethodConfig[] List of method configs. The order is maintained.
72
     * @psalm-return array<string,MethodConfig>
73
     */
74 13
    private function getMethodConfigs(ReflectionClass $class): array
75
    {
76 13
        $methods = [];
77 13
        foreach ($class->getMethods() as $method) {
78 12
            $methods[$method->getName()] = $this->getMethodConfig($class, $method);
79
        }
80
81 13
        return $methods;
82
    }
83
84
    /**
85
     * Gets single method config for individual class / method reflection pair.
86
     *
87
     * @param ReflectionClass $class Reflection of a class.
88
     * @param ReflectionMethod $method Reflection of a method.
89
     *
90
     * @return MethodConfig Single method config.
91
     */
92 12
    private function getMethodConfig(ReflectionClass $class, ReflectionMethod $method): MethodConfig
93
    {
94 12
        return new MethodConfig(
95 12
            modifiers: $this->getMethodModifiers($class, $method),
96 12
            name: $method->getName(),
97 12
            parameters: $this->getMethodParameterConfigs($method),
98 12
            returnType: $this->getMethodReturnTypeConfig($method),
99
        );
100
    }
101
102
    /**
103
     * Gets the set of method modifiers for a given class / method reflection pair
104
     *
105
     * @param ReflectionClass $class Reflection of a class.
106
     * @param ReflectionMethod $method Reflection of a method.
107
     *
108
     * @return string[] List of method modifiers.
109
     */
110 12
    private function getMethodModifiers(ReflectionClass $class, ReflectionMethod $method): array
111
    {
112
        /** @psalm-var list<string> $modifiers Can be removed after release https://github.com/vimeo/psalm/pull/8405 */
113 12
        $modifiers = Reflection::getModifierNames($method->getModifiers());
114 12
        if (!$class->isInterface()) {
115 7
            return $modifiers;
116
        }
117
118 5
        return array_values(
119 5
            array_filter(
120
                $modifiers,
121 5
                static fn (string $modifier) => $modifier !== 'abstract'
122
            )
123
        );
124
    }
125
126
    /**
127
     * Gets the complete set of parameter configs for a given method reflection.
128
     *
129
     * @param ReflectionMethod $method Reflection of a method.
130
     *
131
     * @return ParameterConfig[] List of parameter configs. The order is maintained.
132
     * @psalm-return array<string,ParameterConfig>
133
     */
134 12
    private function getMethodParameterConfigs(ReflectionMethod $method): array
135
    {
136 12
        $parameters = [];
137 12
        foreach ($method->getParameters() as $param) {
138 7
            $parameters[$param->getName()] = $this->getMethodParameterConfig($param);
139
        }
140
141 12
        return $parameters;
142
    }
143
144
    /**
145
     * Gets single parameter config for individual method's parameter reflection.
146
     *
147
     * @param ReflectionParameter $param Reflection of a method's parameter.
148
     *
149
     * @return ParameterConfig Single parameter config.
150
     */
151 7
    private function getMethodParameterConfig(ReflectionParameter $param): ParameterConfig
152
    {
153 7
        return new ParameterConfig(
154 7
            type: $this->getMethodParameterTypeConfig($param),
155 7
            name: $param->getName(),
156 7
            isDefaultValueAvailable: $param->isDefaultValueAvailable(),
157 7
            isDefaultValueConstant: $param->isDefaultValueAvailable()
158 2
                ? $param->isDefaultValueConstant()
159 7
                : null,
160 7
            defaultValueConstantName: $param->isOptional()
161 2
                ? $param->getDefaultValueConstantName()
162 7
                : null,
163 7
            defaultValue: $param->isOptional()
164 2
                ? $param->getDefaultValue()
165 7
                : null,
166
        );
167
    }
168
169
    /**
170
     * Gets single type config for individual method's parameter reflection.
171
     *
172
     * @param ReflectionParameter $param Reflection pf a method's parameter.
173
     *
174
     * @return TypeConfig|null Single type config. `null` is returned when type is not specified.
175
     */
176 7
    private function getMethodParameterTypeConfig(ReflectionParameter $param): ?TypeConfig
177
    {
178
        /**
179
         * @var ReflectionIntersectionType|ReflectionNamedType|ReflectionUnionType|null $type
180
         * @psalm-suppress UndefinedDocblockClass Needed for PHP 8.0 only, because ReflectionIntersectionType is
181
         * not supported.
182
         */
183 7
        $type = $param->getType();
184 7
        if (!$type) {
185 2
            return null;
186
        }
187
188
        /**
189
         * @psalm-suppress UndefinedClass Needed for PHP 8.0 only, because ReflectionIntersectionType is not supported.
190
         */
191 7
        return new TypeConfig(
192 7
            name: $this->convertTypeToString($type),
193 7
            allowsNull: $type->allowsNull(),
194
        );
195
    }
196
197
    /**
198
     * Gets single return type config for individual method reflection.
199
     *
200
     * @param ReflectionMethod $method Reflection of a method.
201
     *
202
     * @return TypeConfig|null Single type config. `null` is returned when return type is not specified.
203
     */
204 12
    private function getMethodReturnTypeConfig(ReflectionMethod $method): ?TypeConfig
205
    {
206 12
        $returnType = $method->getReturnType();
207 12
        if (!$returnType && method_exists($method, 'getTentativeReturnType')) {
208
            /**
209
             * Needed for PHP 8.0 only, because getTentativeReturnType() is not supported.
210
             *
211
             * @var ReflectionType|null
212
             * @psalm-suppress UnnecessaryVarAnnotation
213
             */
214
            $returnType = $method->getTentativeReturnType();
215
        }
216
217 12
        if (!$returnType) {
218 4
            return null;
219
        }
220
221
        /**
222
         * @psalm-suppress ArgumentTypeCoercion Needed for PHP 8.0 only, because ReflectionIntersectionType is
223
         * not supported.
224
         */
225 11
        return new TypeConfig(
226 11
            name: $this->convertTypeToString($returnType),
227 11
            allowsNull: $returnType->allowsNull(),
228
        );
229
    }
230
231
    /**
232
     * @psalm-suppress UndefinedClass Needed for PHP 8.0 only, because ReflectionIntersectionType is not supported.
233
     */
234 11
    private function convertTypeToString(
235
        ReflectionNamedType|ReflectionUnionType|ReflectionIntersectionType $type
236
    ): string {
237 11
        if ($type instanceof ReflectionNamedType) {
238 11
            return $type->getName();
239
        }
240
241 6
        if ($type instanceof ReflectionUnionType) {
0 ignored issues
show
introduced by
$type is always a sub-type of ReflectionUnionType.
Loading history...
242 6
            return $this->getUnionType($type);
243
        }
244
245
        return $this->getIntersectionType($type);
246
    }
247
248 6
    private function getUnionType(ReflectionUnionType $type): string
249
    {
250 6
        $types = array_map(
251 6
            static fn (ReflectionNamedType $namedType) => $namedType->getName(),
252 6
            $type->getTypes()
253
        );
254
255 6
        return implode('|', $types);
256
    }
257
258
    /**
259
     * @psalm-suppress UndefinedClass, MixedArgument Needed for PHP 8.0 only, because ReflectionIntersectionType is
260
     * not supported.
261
     */
262
    private function getIntersectionType(ReflectionIntersectionType $type): string
263
    {
264
        /**
265
         * @psalm-suppress ArgumentTypeCoercion ReflectionIntersectionType::getTypes() always returns
266
         * array of `ReflectionNamedType`, at least until PHP 8.2 released.
267
         */
268
        $types = array_map(
269
            static fn (ReflectionNamedType $namedType) => $namedType->getName(),
270
            $type->getTypes()
271
        );
272
273
        return implode('&', $types);
274
    }
275
}
276