MockMethod.php 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363
  1. <?php declare(strict_types=1);
  2. /*
  3. * This file is part of PHPUnit.
  4. *
  5. * This file is modified to replace ReflectionParameter::getClass() usage,
  6. * which is deprecated in PHP 8.
  7. *
  8. * When the test suite is updated for compatibility with PHPUnit 9.x,
  9. * this override can be removed.
  10. *
  11. * (c) Sebastian Bergmann <sebastian@phpunit.de>
  12. *
  13. * For the full copyright and license information, please view the LICENSE
  14. * file that was distributed with this source code.
  15. */
  16. namespace PHPUnit\Framework\MockObject;
  17. use ReflectionClass;
  18. use ReflectionException;
  19. use ReflectionMethod;
  20. use Text_Template;
  21. final class MockMethod
  22. {
  23. /**
  24. * @var Text_Template[]
  25. */
  26. private static $templates = [];
  27. /**
  28. * @var string
  29. */
  30. private $className;
  31. /**
  32. * @var string
  33. */
  34. private $methodName;
  35. /**
  36. * @var bool
  37. */
  38. private $cloneArguments;
  39. /**
  40. * @var string string
  41. */
  42. private $modifier;
  43. /**
  44. * @var string
  45. */
  46. private $argumentsForDeclaration;
  47. /**
  48. * @var string
  49. */
  50. private $argumentsForCall;
  51. /**
  52. * @var string
  53. */
  54. private $returnType;
  55. /**
  56. * @var string
  57. */
  58. private $reference;
  59. /**
  60. * @var bool
  61. */
  62. private $callOriginalMethod;
  63. /**
  64. * @var bool
  65. */
  66. private $static;
  67. /**
  68. * @var ?string
  69. */
  70. private $deprecation;
  71. /**
  72. * @var bool
  73. */
  74. private $allowsReturnNull;
  75. public static function fromReflection(ReflectionMethod $method, bool $callOriginalMethod, bool $cloneArguments): self
  76. {
  77. if ($method->isPrivate()) {
  78. $modifier = 'private';
  79. } elseif ($method->isProtected()) {
  80. $modifier = 'protected';
  81. } else {
  82. $modifier = 'public';
  83. }
  84. if ($method->isStatic()) {
  85. $modifier .= ' static';
  86. }
  87. if ($method->returnsReference()) {
  88. $reference = '&';
  89. } else {
  90. $reference = '';
  91. }
  92. if ($method->hasReturnType()) {
  93. $returnType = $method->getReturnType()->getName();
  94. } else {
  95. $returnType = '';
  96. }
  97. $docComment = $method->getDocComment();
  98. if (\is_string($docComment)
  99. && \preg_match('#\*[ \t]*+@deprecated[ \t]*+(.*?)\r?+\n[ \t]*+\*(?:[ \t]*+@|/$)#s', $docComment, $deprecation)
  100. ) {
  101. $deprecation = \trim(\preg_replace('#[ \t]*\r?\n[ \t]*+\*[ \t]*+#', ' ', $deprecation[1]));
  102. } else {
  103. $deprecation = null;
  104. }
  105. return new self(
  106. $method->getDeclaringClass()->getName(),
  107. $method->getName(),
  108. $cloneArguments,
  109. $modifier,
  110. self::getMethodParameters($method),
  111. self::getMethodParameters($method, true),
  112. $returnType,
  113. $reference,
  114. $callOriginalMethod,
  115. $method->isStatic(),
  116. $deprecation,
  117. $method->hasReturnType() && $method->getReturnType()->allowsNull()
  118. );
  119. }
  120. public static function fromName(string $fullClassName, string $methodName, bool $cloneArguments): self
  121. {
  122. return new self(
  123. $fullClassName,
  124. $methodName,
  125. $cloneArguments,
  126. 'public',
  127. '',
  128. '',
  129. '',
  130. '',
  131. false,
  132. false,
  133. null,
  134. false
  135. );
  136. }
  137. public function __construct(string $className, string $methodName, bool $cloneArguments, string $modifier, string $argumentsForDeclaration, string $argumentsForCall, string $returnType, string $reference, bool $callOriginalMethod, bool $static, ?string $deprecation, bool $allowsReturnNull)
  138. {
  139. $this->className = $className;
  140. $this->methodName = $methodName;
  141. $this->cloneArguments = $cloneArguments;
  142. $this->modifier = $modifier;
  143. $this->argumentsForDeclaration = $argumentsForDeclaration;
  144. $this->argumentsForCall = $argumentsForCall;
  145. $this->returnType = $returnType;
  146. $this->reference = $reference;
  147. $this->callOriginalMethod = $callOriginalMethod;
  148. $this->static = $static;
  149. $this->deprecation = $deprecation;
  150. $this->allowsReturnNull = $allowsReturnNull;
  151. }
  152. public function getName(): string
  153. {
  154. return $this->methodName;
  155. }
  156. /**
  157. * @throws \ReflectionException
  158. * @throws \PHPUnit\Framework\MockObject\RuntimeException
  159. * @throws \InvalidArgumentException
  160. */
  161. public function generateCode(): string
  162. {
  163. if ($this->static) {
  164. $templateFile = 'mocked_static_method.tpl';
  165. } elseif ($this->returnType === 'void') {
  166. $templateFile = \sprintf(
  167. '%s_method_void.tpl',
  168. $this->callOriginalMethod ? 'proxied' : 'mocked'
  169. );
  170. } else {
  171. $templateFile = \sprintf(
  172. '%s_method.tpl',
  173. $this->callOriginalMethod ? 'proxied' : 'mocked'
  174. );
  175. }
  176. $returnType = $this->returnType;
  177. // @see https://bugs.php.net/bug.php?id=70722
  178. if ($returnType === 'self') {
  179. $returnType = $this->className;
  180. }
  181. // @see https://github.com/sebastianbergmann/phpunit-mock-objects/issues/406
  182. if ($returnType === 'parent') {
  183. $reflector = new ReflectionClass($this->className);
  184. $parentClass = $reflector->getParentClass();
  185. if ($parentClass === false) {
  186. throw new RuntimeException(
  187. \sprintf(
  188. 'Cannot mock %s::%s because "parent" return type declaration is used but %s does not have a parent class',
  189. $this->className,
  190. $this->methodName,
  191. $this->className
  192. )
  193. );
  194. }
  195. $returnType = $parentClass->getName();
  196. }
  197. $deprecation = $this->deprecation;
  198. if (null !== $this->deprecation) {
  199. $deprecation = "The $this->className::$this->methodName method is deprecated ($this->deprecation).";
  200. $deprecationTemplate = $this->getTemplate('deprecation.tpl');
  201. $deprecationTemplate->setVar([
  202. 'deprecation' => \var_export($deprecation, true),
  203. ]);
  204. $deprecation = $deprecationTemplate->render();
  205. }
  206. $template = $this->getTemplate($templateFile);
  207. $template->setVar(
  208. [
  209. 'arguments_decl' => $this->argumentsForDeclaration,
  210. 'arguments_call' => $this->argumentsForCall,
  211. 'return_delim' => $returnType ? ': ' : '',
  212. 'return_type' => $this->allowsReturnNull ? '?' . $returnType : $returnType,
  213. 'arguments_count' => !empty($this->argumentsForCall) ? \substr_count($this->argumentsForCall, ',') + 1 : 0,
  214. 'class_name' => $this->className,
  215. 'method_name' => $this->methodName,
  216. 'modifier' => $this->modifier,
  217. 'reference' => $this->reference,
  218. 'clone_arguments' => $this->cloneArguments ? 'true' : 'false',
  219. 'deprecation' => $deprecation,
  220. ]
  221. );
  222. return $template->render();
  223. }
  224. private function getTemplate(string $template): Text_Template
  225. {
  226. $filename = __DIR__ . \DIRECTORY_SEPARATOR . 'Generator' . \DIRECTORY_SEPARATOR . $template;
  227. if (!isset(self::$templates[$filename])) {
  228. self::$templates[$filename] = new Text_Template($filename);
  229. }
  230. return self::$templates[$filename];
  231. }
  232. /**
  233. * Returns the parameters of a function or method.
  234. *
  235. * @throws RuntimeException
  236. */
  237. private static function getMethodParameters(ReflectionMethod $method, bool $forCall = false): string
  238. {
  239. $parameters = [];
  240. foreach ($method->getParameters() as $i => $parameter) {
  241. $name = '$' . $parameter->getName();
  242. /* Note: PHP extensions may use empty names for reference arguments
  243. * or "..." for methods taking a variable number of arguments.
  244. */
  245. if ($name === '$' || $name === '$...') {
  246. $name = '$arg' . $i;
  247. }
  248. if ($parameter->isVariadic()) {
  249. if ($forCall) {
  250. continue;
  251. }
  252. $name = '...' . $name;
  253. }
  254. $nullable = '';
  255. $default = '';
  256. $reference = '';
  257. $typeDeclaration = '';
  258. if (!$forCall) {
  259. if ($parameter->hasType() && $parameter->allowsNull()) {
  260. $nullable = '?';
  261. }
  262. if ($parameter->hasType() && $parameter->getType()->getName() !== 'self') {
  263. $typeDeclaration = $parameter->getType()->getName() . ' ';
  264. } else {
  265. try {
  266. $class = $parameter->getType() && !$parameter->getType()->isBuiltin()
  267. ? new ReflectionClass($parameter->getType()->getName())
  268. : null;
  269. } catch (ReflectionException $e) {
  270. throw new RuntimeException(
  271. \sprintf(
  272. 'Cannot mock %s::%s() because a class or ' .
  273. 'interface used in the signature is not loaded',
  274. $method->getDeclaringClass()->getName(),
  275. $method->getName()
  276. ),
  277. 0,
  278. $e
  279. );
  280. }
  281. if ($class !== null) {
  282. $typeDeclaration = $class->getName() . ' ';
  283. }
  284. }
  285. if (!$parameter->isVariadic()) {
  286. if ($parameter->isDefaultValueAvailable()) {
  287. try {
  288. $value = \var_export($parameter->getDefaultValue(), true);
  289. } catch (\ReflectionException $e) {
  290. throw new RuntimeException(
  291. $e->getMessage(),
  292. (int) $e->getCode(),
  293. $e
  294. );
  295. }
  296. $default = ' = ' . $value;
  297. } elseif ($parameter->isOptional()) {
  298. $default = ' = null';
  299. }
  300. }
  301. }
  302. if ($parameter->isPassedByReference()) {
  303. $reference = '&';
  304. }
  305. $parameters[] = $nullable . $typeDeclaration . $reference . $name . $default;
  306. }
  307. return \implode(', ', $parameters);
  308. }
  309. }