Completed
Pull Request — master (#439)
by Theodore
01:16
created

ClassMirror::reflectMethodToNode()   D

Complexity

Conditions 14
Paths 272

Size

Total Lines 47

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 47
rs 4.5333
c 0
b 0
f 0
cc 14
nc 272
nop 2

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
/*
4
 * This file is part of the Prophecy.
5
 * (c) Konstantin Kudryashov <[email protected]>
6
 *     Marcello Duarte <[email protected]>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11
12
namespace Prophecy\Doubler\Generator;
13
14
use Prophecy\Exception\InvalidArgumentException;
15
use Prophecy\Exception\Doubler\ClassMirrorException;
16
use ReflectionClass;
17
use ReflectionMethod;
18
use ReflectionParameter;
19
20
/**
21
 * Class mirror.
22
 * Core doubler class. Mirrors specific class and/or interfaces into class node tree.
23
 *
24
 * @author Konstantin Kudryashov <[email protected]>
25
 */
26
class ClassMirror
27
{
28
    private static $reflectableMethods = array(
29
        '__construct',
30
        '__destruct',
31
        '__sleep',
32
        '__wakeup',
33
        '__toString',
34
        '__call',
35
        '__invoke'
36
    );
37
38
    /**
39
     * Reflects provided arguments into class node.
40
     *
41
     * @param ReflectionClass   $class
42
     * @param ReflectionClass[] $interfaces
43
     *
44
     * @return Node\ClassNode
45
     *
46
     * @throws \Prophecy\Exception\InvalidArgumentException
47
     */
48
    public function reflect(ReflectionClass $class = null, array $interfaces)
49
    {
50
        $node = new Node\ClassNode;
51
52
        if (null !== $class) {
53
            if (true === $class->isInterface()) {
54
                throw new InvalidArgumentException(sprintf(
55
                    "Could not reflect %s as a class, because it\n".
56
                    "is interface - use the second argument instead.",
57
                    $class->getName()
58
                ));
59
            }
60
61
            $this->reflectClassToNode($class, $node);
62
        }
63
64
        foreach ($interfaces as $interface) {
65 View Code Duplication
            if (!$interface instanceof ReflectionClass) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
66
                throw new InvalidArgumentException(sprintf(
67
                    "[ReflectionClass \$interface1 [, ReflectionClass \$interface2]] array expected as\n".
68
                    "a second argument to `ClassMirror::reflect(...)`, but got %s.",
69
                    is_object($interface) ? get_class($interface).' class' : gettype($interface)
70
                ));
71
            }
72
            if (false === $interface->isInterface()) {
73
                throw new InvalidArgumentException(sprintf(
74
                    "Could not reflect %s as an interface, because it\n".
75
                    "is class - use the first argument instead.",
76
                    $interface->getName()
77
                ));
78
            }
79
80
            $this->reflectInterfaceToNode($interface, $node);
81
        }
82
83
        $node->addInterface('Prophecy\Doubler\Generator\ReflectionInterface');
84
85
        return $node;
86
    }
87
88
    private function reflectClassToNode(ReflectionClass $class, Node\ClassNode $node)
89
    {
90
        if (true === $class->isFinal()) {
91
            throw new ClassMirrorException(sprintf(
92
                'Could not reflect class %s as it is marked final.', $class->getName()
93
            ), $class);
94
        }
95
96
        $node->setParentClass($class->getName());
97
98
        foreach ($class->getMethods(ReflectionMethod::IS_ABSTRACT) as $method) {
99
            if (false === $method->isProtected()) {
100
                continue;
101
            }
102
103
            $this->reflectMethodToNode($method, $node);
104
        }
105
106
        foreach ($class->getMethods(ReflectionMethod::IS_PUBLIC) as $method) {
107
            if (0 === strpos($method->getName(), '_')
108
                && !in_array($method->getName(), self::$reflectableMethods)) {
109
                continue;
110
            }
111
112
            if (true === $method->isFinal()) {
113
                $node->addUnextendableMethod($method->getName());
114
                continue;
115
            }
116
117
            $this->reflectMethodToNode($method, $node);
118
        }
119
    }
120
121
    private function reflectInterfaceToNode(ReflectionClass $interface, Node\ClassNode $node)
122
    {
123
        $node->addInterface($interface->getName());
124
125
        foreach ($interface->getMethods() as $method) {
126
            $this->reflectMethodToNode($method, $node);
127
        }
128
    }
129
130
    private function reflectMethodToNode(ReflectionMethod $method, Node\ClassNode $classNode)
131
    {
132
        $node = new Node\MethodNode($method->getName());
133
134
        if (true === $method->isProtected()) {
135
            $node->setVisibility('protected');
136
        }
137
138
        if (true === $method->isStatic()) {
139
            $node->setStatic();
140
        }
141
142
        if (true === $method->returnsReference()) {
143
            $node->setReturnsReference();
144
        }
145
146
        if (version_compare(PHP_VERSION, '7.0', '>=') && $method->hasReturnType()) {
147
            if (version_compare(PHP_VERSION, '7.1', '>=')) {
148
                $returnType = $method->getReturnType()->getName();
149
            } else {
150
                $returnType = (string) $method->getReturnType();
151
            }
152
153
            $returnTypeLower = strtolower($returnType);
154
155
            if ('self' === $returnTypeLower) {
156
                $returnType = $method->getDeclaringClass()->getName();
157
            }
158
            if ('parent' === $returnTypeLower) {
159
                $returnType = $method->getDeclaringClass()->getParentClass()->getName();
160
            }
161
162
            $node->setReturnType($returnType);
163
164
            if (version_compare(PHP_VERSION, '7.1', '>=') && $method->getReturnType()->allowsNull()) {
165
                $node->setNullableReturnType(true);
166
            }
167
        }
168
169
        if (is_array($params = $method->getParameters()) && count($params)) {
170
            foreach ($params as $param) {
171
                $this->reflectArgumentToNode($param, $node);
172
            }
173
        }
174
175
        $classNode->addMethod($node);
176
    }
177
178
    private function reflectArgumentToNode(ReflectionParameter $parameter, Node\MethodNode $methodNode)
179
    {
180
        $name = $parameter->getName() == '...' ? '__dot_dot_dot__' : $parameter->getName();
181
        $node = new Node\ArgumentNode($name);
182
183
        $node->setTypeHint($this->getTypeHint($parameter));
184
185
        if ($this->isVariadic($parameter)) {
186
            $node->setAsVariadic();
187
        }
188
189
        if ($this->hasDefaultValue($parameter)) {
190
            $node->setDefault($this->getDefaultValue($parameter));
191
        }
192
193
        if ($parameter->isPassedByReference()) {
194
            $node->setAsPassedByReference();
195
        }
196
197
        $node->setAsNullable($this->isNullable($parameter));
198
199
        $methodNode->addArgument($node);
200
    }
201
202
    private function hasDefaultValue(ReflectionParameter $parameter)
203
    {
204
        if ($this->isVariadic($parameter)) {
205
            return false;
206
        }
207
208
        if ($parameter->isDefaultValueAvailable()) {
209
            return true;
210
        }
211
212
        return $parameter->isOptional() || $this->isNullable($parameter);
213
    }
214
215
    private function getDefaultValue(ReflectionParameter $parameter)
216
    {
217
        if (!$parameter->isDefaultValueAvailable()) {
218
            return null;
219
        }
220
221
        return $parameter->getDefaultValue();
222
    }
223
224
    private function getTypeHint(ReflectionParameter $parameter)
225
    {
226
        if (null !== $className = $this->getParameterClassName($parameter)) {
227
            return $className;
228
        }
229
230
        if (true === $parameter->isArray()) {
231
            return 'array';
232
        }
233
234
        if (version_compare(PHP_VERSION, '5.4', '>=') && true === $parameter->isCallable()) {
235
            return 'callable';
236
        }
237
238
        if (version_compare(PHP_VERSION, '7.0', '>=') && true === $parameter->hasType()) {
239
            return (string) $parameter->getType();
240
        }
241
242
        return null;
243
    }
244
245
    private function isVariadic(ReflectionParameter $parameter)
246
    {
247
        return PHP_VERSION_ID >= 50600 && $parameter->isVariadic();
248
    }
249
250
    private function isNullable(ReflectionParameter $parameter)
251
    {
252
        return $parameter->allowsNull() && null !== $this->getTypeHint($parameter);
253
    }
254
255
    private function getParameterClassName(ReflectionParameter $parameter)
256
    {
257
        try {
258
            return $parameter->getClass() ? $parameter->getClass()->getName() : null;
259
        } catch (\ReflectionException $e) {
260
            preg_match('/\[\s\<\w+?>\s([\w,\\\]+)/s', $parameter, $matches);
261
262
            return isset($matches[1]) ? $matches[1] : null;
263
        }
264
    }
265
}
266