Completed
Push — master ( 69c0dd...234540 )
by Valentin
02:35
created

Methods::defineType()   A

Complexity

Conditions 4
Paths 5

Size

Total Lines 19

Duplication

Lines 0
Ratio 0 %

Importance

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