Passed
Push — master ( a4eec4...d740e1 )
by Valentin
01:40
created

Methods::packParams()   B

Complexity

Conditions 6
Paths 17

Size

Total Lines 28

Duplication

Lines 0
Ratio 0 %

Importance

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