Passed
Push — master ( 952fd2...f07abe )
by Valentin
03:44 queued 02:05
created

Methods   B

Complexity

Total Complexity 46

Size/Duplication

Total Lines 296
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 7

Importance

Changes 0
Metric Value
dl 0
loc 296
rs 8.72
c 0
b 0
f 0
wmc 46
lcom 1
cbo 7

14 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 7 1
A getMethods() 0 27 5
A isIgnoredMethod() 0 8 5
A isMagicMethod() 0 4 1
A packFlags() 0 13 3
A defineReturnType() 0 10 3
B packParams() 0 28 6
A defineParamType() 0 8 2
A defineType() 0 21 4
A defineSingularType() 0 15 4
A replacedSelfTypeName() 0 4 2
A typeShouldBeQualified() 0 8 2
B getExtendedMethodNodes() 0 22 6
A fetchReflectionMethods() 0 14 2

How to fix   Complexity   

Complex Class

Complex classes like Methods often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Methods, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
/**
4
 * Spiral Framework. Cycle ProxyFactory
5
 *
6
 * @license MIT
7
 * @author  Valentin V (Vvval)
8
 */
9
10
declare(strict_types=1);
11
12
namespace Cycle\ORM\Promise\Declaration\Extractor;
13
14
use Cycle\ORM\Promise\Traverser;
15
use Cycle\ORM\Promise\Visitor\Declaration\FetchMethods;
16
use PhpParser\Builder\Param;
17
use PhpParser\Node;
18
use PhpParser\Parser;
19
use PhpParser\ParserFactory;
20
use ReflectionClass;
21
use ReflectionException;
22
use ReflectionMethod;
23
use ReflectionNamedType;
24
use ReflectionParameter;
25
use ReflectionType;
26
27
final class Methods
28
{
29
    private const MAGIC_METHODS = [
30
        '__construct',
31
        '__destruct',
32
        '__call',
33
        '__callStatic',
34
        '__get',
35
        '__set',
36
        '__isset',
37
        '__unset',
38
        '__sleep',
39
        '__wakeup',
40
        '__toString',
41
        '__invoke',
42
        '__set_state',
43
        '__debugInfo',
44
    ];
45
46
    private const RESERVED_UNQUALIFIED_RETURN_TYPES = ['self', 'static', 'object'];
47
48
    /** @varTraverser */
49
    private $traverser;
50
51
    /** @var Parser */
52
    private $parser;
53
54
    /** @var array */
55
    private $extendedMethodsCache = [];
56
57
    /**
58
     * @param Traverser   $traverser
59
     * @param Parser|null $parser
60
     */
61
    public function __construct(
62
        Traverser $traverser,
63
        Parser $parser = null
64
    ) {
65
        $this->traverser = $traverser;
66
        $this->parser = $parser ?? (new ParserFactory())->create(ParserFactory::ONLY_PHP7);
67
    }
68
69
    /**
70
     * @param ReflectionClass $reflection
71
     * @return array
72
     * @throws ReflectionException
73
     */
74
    public function getMethods(ReflectionClass $reflection): array
75
    {
76
        $parents = [$reflection->name => $reflection];
77
        foreach ($reflection->getMethods() as $method) {
78
            $class = $method->getDeclaringClass();
79
            $parents[$class->name] = $class;
80
        }
81
82
        $methodNodes = $this->getExtendedMethodNodes($parents);
83
        $methods = [];
84
85
        foreach ($reflection->getMethods() as $method) {
86
            if ($this->isIgnoredMethod($method)) {
87
                continue;
88
            }
89
90
            $methods[] = new Node\Stmt\ClassMethod($method->name, [
91
                'flags'      => $this->packFlags($method),
92
                'returnType' => $this->defineReturnType($method),
93
                'params'     => $this->packParams($method),
94
                'byRef'      => $method->returnsReference(),
95
                'stmts'      => !empty($methodNodes[$method->name]) ? $methodNodes[$method->name]->stmts : []
96
            ]);
97
        }
98
99
        return $methods;
100
    }
101
102
    /**
103
     * @param ReflectionMethod $method
104
     * @return bool
105
     */
106
    private function isIgnoredMethod(ReflectionMethod $method): bool
107
    {
108
        return $method->isPrivate()
109
            || $method->isStatic()
110
            || $method->isFinal()
111
            || $method->isAbstract()
112
            || $this->isMagicMethod($method->name);
113
    }
114
115
    /**
116
     * @param string $name
117
     * @return bool
118
     */
119
    private function isMagicMethod(string $name): bool
120
    {
121
        return in_array($name, self::MAGIC_METHODS, true);
122
    }
123
124
    /**
125
     * @param ReflectionMethod $method
126
     * @return int
127
     */
128
    private function packFlags(ReflectionMethod $method): int
129
    {
130
        $flags = [];
131
        if ($method->isPublic()) {
132
            $flags[] = Node\Stmt\Class_::MODIFIER_PUBLIC;
133
        } elseif ($method->isProtected()) {
134
            $flags[] = Node\Stmt\Class_::MODIFIER_PROTECTED;
135
        }
136
137
        return array_reduce($flags, static function ($a, $b) {
138
            return $a | $b;
139
        }, 0);
140
    }
141
142
    /**
143
     * @param ReflectionMethod $method
144
     * @return Node|null
145
     */
146
    private function defineReturnType(ReflectionMethod $method): ?Node
147
    {
148
        if (!$method->hasReturnType()) {
149
            return null;
150
        }
151
152
        $name = $this->defineType($method, $method->getReturnType());
153
154
        return $name !== null ? new Node\Identifier($name) : null;
155
    }
156
157
    /**
158
     * @param ReflectionMethod $method
159
     * @return array
160
     * @throws ReflectionException
161
     */
162
    private function packParams(ReflectionMethod $method): array
163
    {
164
        $params = [];
165
        foreach ($method->getParameters() as $parameter) {
166
            $param = new Param($parameter->name);
167
168
            if ($parameter->isDefaultValueAvailable()) {
169
                $param->setDefault($parameter->getDefaultValue());
170
            }
171
172
            if ($parameter->isVariadic()) {
173
                $param->makeVariadic();
174
            }
175
176
            if ($parameter->isPassedByReference()) {
177
                $param->makeByRef();
178
            }
179
180
            $type = $this->defineParamType($parameter, $method);
181
            if ($type !== null) {
182
                $param->setType($type);
183
            }
184
185
            $params[] = $param->getNode();
186
        }
187
188
        return $params;
189
    }
190
191
    /**
192
     * @param ReflectionParameter $parameter
193
     * @param ReflectionMethod    $method
194
     * @return string|null
195
     */
196
    private function defineParamType(ReflectionParameter $parameter, ReflectionMethod $method): ?string
197
    {
198
        if (!$parameter->hasType()) {
199
            return null;
200
        }
201
202
        return $this->defineType($method, $parameter->getType());
203
    }
204
205
    /**
206
     * @param ReflectionMethod    $method
207
     * @param ReflectionType|null $type
208
     * @return string|null
209
     */
210
    private function defineType(ReflectionMethod $method, ?ReflectionType $type): ?string
211
    {
212
        if ($type instanceof ReflectionNamedType) {
0 ignored issues
show
Bug introduced by
The class ReflectionNamedType does not exist. Is this class maybe located in a folder that is not analyzed, or in a newer version of your dependencies than listed in your composer.lock/composer.json?
Loading history...
213
            return $this->defineSingularType($method, $type);
214
        }
215
216
        if ($type instanceof \ReflectionUnionType) {
0 ignored issues
show
Bug introduced by
The class ReflectionUnionType does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
217
            $types = array_map(
218
                function (ReflectionNamedType $type) use ($method): ?string {
219
                    return $this->defineSingularType($method, $type);
220
                },
221
                $type->getTypes()
222
            );
223
224
            if ($types) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $types of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
225
                return implode('|', array_filter($types));
226
            }
227
        }
228
229
        return null;
230
    }
231
232
    /**
233
     * @param ReflectionMethod    $method
234
     * @param ReflectionNamedType $type
235
     * @return string|null
236
     */
237
    private function defineSingularType(ReflectionMethod $method, ReflectionNamedType $type): ?string
238
    {
239
        $name = $type->getName();
240
        $name = $this->replacedSelfTypeName($method, $name);
241
242
        if ($this->typeShouldBeQualified($type, $name)) {
243
            $name = '\\' . $name;
244
        }
245
246
        if ($name !== 'mixed' && $type->allowsNull()) {
247
            $name = "?$name";
248
        }
249
250
        return $name;
251
    }
252
253
    /**
254
     * @param ReflectionMethod $method
255
     * @param string           $name
256
     * @return string
257
     */
258
    private function replacedSelfTypeName(ReflectionMethod $method, string $name): string
259
    {
260
        return $name === 'self' ? $method->class : $name;
261
    }
262
263
    /**
264
     * @param ReflectionType $returnType
265
     * @param string         $name
266
     * @return bool
267
     */
268
    private function typeShouldBeQualified(ReflectionType $returnType, string $name): bool
269
    {
270
        if (in_array($name, self::RESERVED_UNQUALIFIED_RETURN_TYPES, true)) {
271
            return false;
272
        }
273
274
        return !$returnType->isBuiltin();
275
    }
276
277
    /**
278
     * @param ReflectionClass[] $reflections
279
     * @return Node\Stmt\ClassMethod[]
280
     */
281
    private function getExtendedMethodNodes(array $reflections): array
282
    {
283
        $nodes = [];
284
        foreach ($reflections as $reflection) {
285
            $fileName = $reflection->getFileName();
286
            if (!$fileName) {
287
                continue;
288
            }
289
290
            if (!isset($this->extendedMethodsCache[$fileName])) {
291
                $this->extendedMethodsCache[$fileName] = $this->fetchReflectionMethods($fileName);
292
            }
293
294
            foreach ($this->extendedMethodsCache[$fileName] as $name => $method) {
295
                if (!isset($nodes[$name])) {
296
                    $nodes[$name] = $method;
297
                }
298
            }
299
        }
300
301
        return $nodes;
302
    }
303
304
    /**
305
     * @param string $fileName
306
     * @return array
307
     */
308
    private function fetchReflectionMethods(string $fileName): array
309
    {
310
        if (!is_file($fileName)) {
311
            return [];
312
        }
313
314
        $methods = new FetchMethods();
315
        $this->traverser->traverse(
316
            $this->parser->parse(file_get_contents($fileName)),
317
            $methods
318
        );
319
320
        return $methods->getMethods();
321
    }
322
}
323