Passed
Push — master ( 6ab678...fd8e7d )
by Valentin
51s queued 10s
created

Methods::fetchReflectionMethods()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
nc 2
nop 1
dl 0
loc 14
rs 9.7998
c 0
b 0
f 0
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 ReflectionParameter;
24
use ReflectionType;
25
26
final class Methods
27
{
28
    private const MAGIC_METHODS = [
29
        '__construct',
30
        '__destruct',
31
        '__call',
32
        '__callStatic',
33
        '__get',
34
        '__set',
35
        '__isset',
36
        '__unset',
37
        '__sleep',
38
        '__wakeup',
39
        '__toString',
40
        '__invoke',
41
        '__set_state',
42
        '__debugInfo',
43
    ];
44
45
    private const RESERVED_UNQUALIFIED_RETURN_TYPES = ['self', 'static', 'object'];
46
47
    /** @varTraverser */
48
    private $traverser;
49
50
    /** @var Parser */
51
    private $parser;
52
53
    /** @var array */
54
    private $extendedMethodsCache = [];
55
56
    /**
57
     * @param Traverser   $traverser
58
     * @param Parser|null $parser
59
     */
60
    public function __construct(
61
        Traverser $traverser,
62
        Parser $parser = null
63
    ) {
64
        $this->traverser = $traverser;
65
        $this->parser = $parser ?? (new ParserFactory())->create(ParserFactory::ONLY_PHP7);
66
    }
67
68
    /**
69
     * @param ReflectionClass $reflection
70
     * @return array
71
     * @throws ReflectionException
72
     */
73
    public function getMethods(ReflectionClass $reflection): array
74
    {
75
        $parents = [$reflection->name => $reflection];
76
        foreach ($reflection->getMethods() as $method) {
77
            $class = $method->getDeclaringClass();
78
            $parents[$class->name] = $class;
79
        }
80
81
        $methodNodes = $this->getExtendedMethodNodes($parents);
82
        $methods = [];
83
84
        foreach ($reflection->getMethods() as $method) {
85
            if ($this->isIgnoredMethod($method)) {
86
                continue;
87
            }
88
89
            $methods[] = new Node\Stmt\ClassMethod($method->name, [
90
                'flags'      => $this->packFlags($method),
91
                'returnType' => $this->defineReturnType($method),
92
                'params'     => $this->packParams($method),
93
                'byRef'      => $method->returnsReference(),
94
                'stmts'      => !empty($methodNodes[$method->name]) ? $methodNodes[$method->name]->stmts : []
95
            ]);
96
        }
97
98
        return $methods;
99
    }
100
101
    /**
102
     * @param ReflectionMethod $method
103
     * @return bool
104
     */
105
    private function isIgnoredMethod(ReflectionMethod $method): bool
106
    {
107
        return $method->isPrivate()
108
            || $method->isStatic()
109
            || $method->isFinal()
110
            || $method->isAbstract()
111
            || $this->isMagicMethod($method->name);
112
    }
113
114
    /**
115
     * @param string $name
116
     * @return bool
117
     */
118
    private function isMagicMethod(string $name): bool
119
    {
120
        return in_array($name, self::MAGIC_METHODS, true);
121
    }
122
123
    /**
124
     * @param ReflectionMethod $method
125
     * @return int
126
     */
127
    private function packFlags(ReflectionMethod $method): int
128
    {
129
        $flags = [];
130
        if ($method->isPublic()) {
131
            $flags[] = Node\Stmt\Class_::MODIFIER_PUBLIC;
132
        } elseif ($method->isProtected()) {
133
            $flags[] = Node\Stmt\Class_::MODIFIER_PROTECTED;
134
        }
135
136
        return array_reduce($flags, static function ($a, $b) {
137
            return $a | $b;
138
        }, 0);
139
    }
140
141
    /**
142
     * @param ReflectionMethod $method
143
     * @return Node|null
144
     */
145
    private function defineReturnType(ReflectionMethod $method): ?Node
146
    {
147
        if (!$method->hasReturnType()) {
148
            return null;
149
        }
150
151
        $name = $this->defineType($method, $method->getReturnType());
152
153
        return $name !== null ? new Node\Identifier($name) : null;
154
    }
155
156
    /**
157
     * @param ReflectionMethod $method
158
     * @return array
159
     * @throws ReflectionException
160
     */
161
    private function packParams(ReflectionMethod $method): array
162
    {
163
        $params = [];
164
        foreach ($method->getParameters() as $parameter) {
165
            $param = new Param($parameter->name);
166
167
            if ($parameter->isDefaultValueAvailable()) {
168
                $param->setDefault($parameter->getDefaultValue());
169
            }
170
171
            if ($parameter->isVariadic()) {
172
                $param->makeVariadic();
173
            }
174
175
            if ($parameter->isPassedByReference()) {
176
                $param->makeByRef();
177
            }
178
179
            $type = $this->defineParamType($parameter, $method);
180
            if ($type !== null) {
181
                $param->setType($type);
182
            }
183
184
            $params[] = $param->getNode();
185
        }
186
187
        return $params;
188
    }
189
190
    /**
191
     * @param ReflectionParameter $parameter
192
     * @param ReflectionMethod    $method
193
     * @return string|null
194
     */
195
    private function defineParamType(ReflectionParameter $parameter, ReflectionMethod $method): ?string
196
    {
197
        if (!$parameter->hasType()) {
198
            return null;
199
        }
200
201
        return $this->defineType($method, $parameter->getType());
202
    }
203
204
    /**
205
     * @param ReflectionMethod    $method
206
     * @param ReflectionType|null $type
207
     * @return string|null
208
     */
209
    private function defineType(ReflectionMethod $method, ?ReflectionType $type): ?string
210
    {
211
        if ($type === null) {
212
            return null;
213
        }
214
215
        $name = $type->getName();
216
        $name = $this->replacedSelfTypeName($method, $name);
217
218
        if ($this->typeShouldBeQualified($type, $name)) {
219
            $name = '\\' . $name;
220
        }
221
222
        if ($type->allowsNull()) {
223
            $name = "?$name";
224
        }
225
226
        return $name;
227
    }
228
229
    /**
230
     * @param ReflectionMethod $method
231
     * @param string           $name
232
     * @return string
233
     */
234
    private function replacedSelfTypeName(ReflectionMethod $method, string $name): string
235
    {
236
        return $name === 'self' ? $method->class : $name;
237
    }
238
239
    /**
240
     * @param ReflectionType $returnType
241
     * @param string         $name
242
     * @return bool
243
     */
244
    private function typeShouldBeQualified(ReflectionType $returnType, string $name): bool
245
    {
246
        if (in_array($name, self::RESERVED_UNQUALIFIED_RETURN_TYPES, true)) {
247
            return false;
248
        }
249
250
        return !$returnType->isBuiltin();
251
    }
252
253
    /**
254
     * @param ReflectionClass[] $reflections
255
     * @return Node\Stmt\ClassMethod[]
256
     */
257
    private function getExtendedMethodNodes(array $reflections): array
258
    {
259
        $nodes = [];
260
        foreach ($reflections as $reflection) {
261
            $fileName = $reflection->getFileName();
262
            if (!$fileName) {
263
                continue;
264
            }
265
266
            if (!isset($this->extendedMethodsCache[$fileName])) {
267
                $this->extendedMethodsCache[$fileName] = $this->fetchReflectionMethods($fileName);
268
            }
269
270
            foreach ($this->extendedMethodsCache[$fileName] as $name => $method) {
271
                if (!isset($nodes[$name])) {
272
                    $nodes[$name] = $method;
273
                }
274
            }
275
        }
276
277
        return $nodes;
278
    }
279
280
    /**
281
     * @param string $fileName
282
     * @return array
283
     */
284
    private function fetchReflectionMethods(string $fileName): array
285
    {
286
        if (!is_file($fileName)) {
287
            return [];
288
        }
289
290
        $methods = new FetchMethods();
291
        $this->traverser->traverse(
292
            $this->parser->parse(file_get_contents($fileName)),
293
            $methods
294
        );
295
296
        return $methods->getMethods();
297
    }
298
}
299