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