1<?php
2
3/*
4 * This file is part of the Prophecy.
5 * (c) Konstantin Kudryashov <ever.zet@gmail.com>
6 *     Marcello Duarte <marcello.duarte@gmail.com>
7 *
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code.
10 */
11
12namespace Prophecy\Doubler\Generator;
13
14use Prophecy\Exception\InvalidArgumentException;
15use Prophecy\Exception\Doubler\ClassMirrorException;
16use ReflectionClass;
17use ReflectionMethod;
18use 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 <ever.zet@gmail.com>
25 */
26class 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            if (!$interface instanceof ReflectionClass) {
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            $returnType = (string) $method->getReturnType();
148            $returnTypeLower = strtolower($returnType);
149
150            if ('self' === $returnTypeLower) {
151                $returnType = $method->getDeclaringClass()->getName();
152            }
153            if ('parent' === $returnTypeLower) {
154                $returnType = $method->getDeclaringClass()->getParentClass()->getName();
155            }
156
157            $node->setReturnType($returnType);
158
159            if (version_compare(PHP_VERSION, '7.1', '>=') && $method->getReturnType()->allowsNull()) {
160                $node->setNullableReturnType(true);
161            }
162        }
163
164        if (is_array($params = $method->getParameters()) && count($params)) {
165            foreach ($params as $param) {
166                $this->reflectArgumentToNode($param, $node);
167            }
168        }
169
170        $classNode->addMethod($node);
171    }
172
173    private function reflectArgumentToNode(ReflectionParameter $parameter, Node\MethodNode $methodNode)
174    {
175        $name = $parameter->getName() == '...' ? '__dot_dot_dot__' : $parameter->getName();
176        $node = new Node\ArgumentNode($name);
177
178        $node->setTypeHint($this->getTypeHint($parameter));
179
180        if ($this->isVariadic($parameter)) {
181            $node->setAsVariadic();
182        }
183
184        if ($this->hasDefaultValue($parameter)) {
185            $node->setDefault($this->getDefaultValue($parameter));
186        }
187
188        if ($parameter->isPassedByReference()) {
189            $node->setAsPassedByReference();
190        }
191
192        $node->setAsNullable($this->isNullable($parameter));
193
194        $methodNode->addArgument($node);
195    }
196
197    private function hasDefaultValue(ReflectionParameter $parameter)
198    {
199        if ($this->isVariadic($parameter)) {
200            return false;
201        }
202
203        if ($parameter->isDefaultValueAvailable()) {
204            return true;
205        }
206
207        return $parameter->isOptional() || $this->isNullable($parameter);
208    }
209
210    private function getDefaultValue(ReflectionParameter $parameter)
211    {
212        if (!$parameter->isDefaultValueAvailable()) {
213            return null;
214        }
215
216        return $parameter->getDefaultValue();
217    }
218
219    private function getTypeHint(ReflectionParameter $parameter)
220    {
221        if (null !== $className = $this->getParameterClassName($parameter)) {
222            return $className;
223        }
224
225        if (true === $parameter->isArray()) {
226            return 'array';
227        }
228
229        if (version_compare(PHP_VERSION, '5.4', '>=') && true === $parameter->isCallable()) {
230            return 'callable';
231        }
232
233        if (version_compare(PHP_VERSION, '7.0', '>=') && true === $parameter->hasType()) {
234            return (string) $parameter->getType();
235        }
236
237        return null;
238    }
239
240    private function isVariadic(ReflectionParameter $parameter)
241    {
242        return PHP_VERSION_ID >= 50600 && $parameter->isVariadic();
243    }
244
245    private function isNullable(ReflectionParameter $parameter)
246    {
247        return $parameter->allowsNull() && null !== $this->getTypeHint($parameter);
248    }
249
250    private function getParameterClassName(ReflectionParameter $parameter)
251    {
252        try {
253            return $parameter->getClass() ? $parameter->getClass()->getName() : null;
254        } catch (\ReflectionException $e) {
255            preg_match('/\[\s\<\w+?>\s([\w,\\\]+)/s', $parameter, $matches);
256
257            return isset($matches[1]) ? $matches[1] : null;
258        }
259    }
260}
261