Test Setup Failed
Push — master ( 4d8696...980fb9 )
by Valentin
05:05
created

Methods::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 7
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 2
1
<?php
2
3
/**
4
 * Spiral Framework.
5
 *
6
 * @license MIT
7
 * @author  Valentin V (Vvval)
8
 */
9
declare(strict_types=1);
10
11
namespace Cycle\ORM\Promise\Declaration\Extractor;
12
13
use Cycle\ORM\Promise\Traverser;
14
use Cycle\ORM\Promise\Visitor\Declaration\FetchMethods;
15
use PhpParser\Builder\Param;
16
use PhpParser\Node;
17
use PhpParser\Parser;
18
use PhpParser\ParserFactory;
19
use Spiral\Files\FilesInterface;
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
    /**
43
     * @param Traverser      $traverser
44
     * @param Parser|null    $parser
45
     */
46
    public function __construct(
47
        Traverser $traverser,
48
        Parser $parser = null
49
    ) {
50
        $this->traverser = $traverser;
0 ignored issues
show
Bug introduced by
The property traverser does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
51
        $this->parser = $parser ?? (new ParserFactory())->create(ParserFactory::ONLY_PHP7);
0 ignored issues
show
Bug introduced by
The property parser does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
52
    }
53
54
    /**
55
     * @param \ReflectionClass $reflection
56
     * @return array
57
     */
58
    public function getMethods(\ReflectionClass $reflection): array
59
    {
60
        $parents = [$reflection->name => $reflection];
61
        foreach ($reflection->getMethods() as $method) {
62
            $class = $method->getDeclaringClass();
63
            $parents[$class->name] = $class;
64
        }
65
66
        $methodNodes = $this->getExtendedMethodNodes($parents);
67
        $methods = [];
68
69
        foreach ($reflection->getMethods() as $method) {
70
            if ($this->isIgnoredMethod($method)) {
71
                continue;
72
            }
73
74
            $methods[] = new Node\Stmt\ClassMethod($method->name, [
75
                'flags'      => $this->packFlags($method),
76
                'returnType' => $this->defineReturnType($method),
77
                'params'     => $this->packParams($method),
78
                'byRef'      => $method->returnsReference(),
79
                'stmts'      => !empty($methodNodes[$method->name]) ? $methodNodes[$method->name]->stmts : []
80
            ]);
81
        }
82
83
        return $methods;
84
    }
85
86
    /**
87
     * @param \ReflectionMethod $method
88
     * @return bool
89
     */
90
    private function isIgnoredMethod(\ReflectionMethod $method): bool
91
    {
92
        return $method->isPrivate() || $method->isStatic() || $method->isFinal() || $method->isAbstract() || $this->isMagicMethod($method->name);
93
    }
94
95
    /**
96
     * @param string $name
97
     * @return bool
98
     */
99
    private function isMagicMethod(string $name): bool
100
    {
101
        return in_array($name, self::MAGIC_METHODS, true);
102
    }
103
104
    /**
105
     * @param \ReflectionMethod $method
106
     * @return int
107
     */
108
    private function packFlags(\ReflectionMethod $method): int
109
    {
110
        $flags = [];
111
        if ($method->isPublic()) {
112
            $flags[] = Node\Stmt\Class_::MODIFIER_PUBLIC;
113
        } elseif ($method->isProtected()) {
114
            $flags[] = Node\Stmt\Class_::MODIFIER_PROTECTED;
115
        }
116
117
        return array_reduce($flags, static function ($a, $b) {
118
            return $a | $b;
119
        }, 0);
120
    }
121
122
    /**
123
     * @param \ReflectionMethod $method
124
     * @return Node|null
125
     */
126
    private function defineReturnType(\ReflectionMethod $method): ?Node
127
    {
128
        if (!$method->hasReturnType()) {
129
            return null;
130
        }
131
132
        $returnType = $method->getReturnType();
133
134
        if ($returnType === null) {
135
            return null;
136
        }
137
138
        $name = $returnType->getName();
139
        $name = $this->replacedSelfReturnTypeName($method, $name);
140
141
        if ($this->returnTypeShouldBeQualified($returnType, $name)) {
142
            $name = '\\' . $name;
143
        }
144
145
        if ($returnType->allowsNull()) {
146
            $name = '?' . $name;
147
        }
148
149
        return new Node\Identifier($name);
150
    }
151
152
    /**
153
     * @param \ReflectionMethod $method
154
     * @param string            $name
155
     * @return string
156
     */
157
    private function replacedSelfReturnTypeName(\ReflectionMethod $method, string $name): string
158
    {
159
        return $name === 'self' ? $method->class : $name;
160
    }
161
162
    /**
163
     * @param \ReflectionType $returnType
164
     * @param string          $name
165
     * @return bool
166
     */
167
    private function returnTypeShouldBeQualified(\ReflectionType $returnType, string $name): bool
168
    {
169
        if (in_array($name, self::RESERVED_UNQUALIFIED_RETURN_TYPES, true)) {
170
            return false;
171
        }
172
173
        return !$returnType->isBuiltin();
174
    }
175
176
    /**
177
     * @param \ReflectionMethod $method
178
     * @return array
179
     */
180
    private function packParams(\ReflectionMethod $method): array
181
    {
182
        $params = [];
183
        foreach ($method->getParameters() as $parameter) {
184
            $param = new Param($parameter->name);
185
186
            $type = $this->defineParamReturnType($parameter);
187
            if ($type !== null) {
188
                $param->setType($type);
189
            }
190
191
            $params[] = $param->getNode();
192
        }
193
194
        return $params;
195
    }
196
197
    /**
198
     * @param \ReflectionParameter $parameter
199
     * @return string|null
200
     */
201
    private function defineParamReturnType(\ReflectionParameter $parameter): ?string
202
    {
203
        if (!$parameter->hasType()) {
204
            return null;
205
        }
206
207
        $typeReflection = $parameter->getType();
208
        if ($typeReflection === null) {
209
            return null;
210
        }
211
212
        $type = $typeReflection->getName();
213
        if ($typeReflection->allowsNull()) {
214
            $type = "?$type";
215
        }
216
217
        return $type;
218
    }
219
220
    /**
221
     * @param \ReflectionClass[] $reflections
222
     * @return Node\Stmt\ClassMethod[]
223
     */
224
    private function getExtendedMethodNodes(array $reflections): array
225
    {
226
        $nodes = [];
227
        foreach ($reflections as $reflection) {
228
            if (file_exists($reflection->getFileName())) {
229
                $methods = new FetchMethods();
230
                $this->traverser->traverse($this->parser->parse(
231
                    file_get_contents($reflection->getFileName())
232
                ), $methods);
233
234
                foreach ($methods->getMethods() as $name => $method) {
235
                    if (!isset($nodes[$name])) {
236
                        $nodes[$name] = $method;
237
                    }
238
                }
239
            }
240
        }
241
242
        return $nodes;
243
    }
244
}
245