1 <?php declare(strict_types=1);
3 * This file is part of PHPUnit.
5 * (c) Sebastian Bergmann <sebastian@phpunit.de>
7 * For the full copyright and license information, please view the LICENSE
8 * file that was distributed with this source code.
10 namespace PHPUnit\Framework\MockObject;
12 use SebastianBergmann\Type\ObjectType;
13 use SebastianBergmann\Type\Type;
14 use SebastianBergmann\Type\UnknownType;
15 use SebastianBergmann\Type\VoidType;
18 * @internal This class is not covered by the backward compatibility promise for PHPUnit
20 final class MockMethod
23 * @var \Text_Template[]
25 private static $templates = [];
40 private $cloneArguments;
50 private $argumentsForDeclaration;
55 private $argumentsForCall;
70 private $callOriginalMethod;
85 private $allowsReturnNull;
88 * @throws RuntimeException
90 public static function fromReflection(\ReflectionMethod $method, bool $callOriginalMethod, bool $cloneArguments): self
92 if ($method->isPrivate()) {
93 $modifier = 'private';
94 } elseif ($method->isProtected()) {
95 $modifier = 'protected';
100 if ($method->isStatic()) {
101 $modifier .= ' static';
104 if ($method->returnsReference()) {
110 $docComment = $method->getDocComment();
112 if (\is_string($docComment) &&
113 \preg_match('#\*[ \t]*+@deprecated[ \t]*+(.*?)\r?+\n[ \t]*+\*(?:[ \t]*+@|/$)#s', $docComment, $deprecation)) {
114 $deprecation = \trim(\preg_replace('#[ \t]*\r?\n[ \t]*+\*[ \t]*+#', ' ', $deprecation[1]));
120 $method->getDeclaringClass()->getName(),
124 self::getMethodParameters($method),
125 self::getMethodParameters($method, true),
126 self::deriveReturnType($method),
131 $method->hasReturnType() && $method->getReturnType()->allowsNull()
135 public static function fromName(string $fullClassName, string $methodName, bool $cloneArguments): self
153 public function __construct(string $className, string $methodName, bool $cloneArguments, string $modifier, string $argumentsForDeclaration, string $argumentsForCall, Type $returnType, string $reference, bool $callOriginalMethod, bool $static, ?string $deprecation, bool $allowsReturnNull)
155 $this->className = $className;
156 $this->methodName = $methodName;
157 $this->cloneArguments = $cloneArguments;
158 $this->modifier = $modifier;
159 $this->argumentsForDeclaration = $argumentsForDeclaration;
160 $this->argumentsForCall = $argumentsForCall;
161 $this->returnType = $returnType;
162 $this->reference = $reference;
163 $this->callOriginalMethod = $callOriginalMethod;
164 $this->static = $static;
165 $this->deprecation = $deprecation;
166 $this->allowsReturnNull = $allowsReturnNull;
169 public function getName(): string
171 return $this->methodName;
175 * @throws RuntimeException
177 public function generateCode(): string
180 $templateFile = 'mocked_static_method.tpl';
181 } elseif ($this->returnType instanceof VoidType) {
182 $templateFile = \sprintf(
183 '%s_method_void.tpl',
184 $this->callOriginalMethod ? 'proxied' : 'mocked'
187 $templateFile = \sprintf(
189 $this->callOriginalMethod ? 'proxied' : 'mocked'
193 $deprecation = $this->deprecation;
195 if (null !== $this->deprecation) {
196 $deprecation = "The $this->className::$this->methodName method is deprecated ($this->deprecation).";
197 $deprecationTemplate = $this->getTemplate('deprecation.tpl');
199 $deprecationTemplate->setVar([
200 'deprecation' => \var_export($deprecation, true),
203 $deprecation = $deprecationTemplate->render();
206 $template = $this->getTemplate($templateFile);
210 'arguments_decl' => $this->argumentsForDeclaration,
211 'arguments_call' => $this->argumentsForCall,
212 'return_declaration' => $this->returnType->getReturnTypeDeclaration(),
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,
223 return $template->render();
226 public function getReturnType(): Type
228 return $this->returnType;
231 private function getTemplate(string $template): \Text_Template
233 $filename = __DIR__ . \DIRECTORY_SEPARATOR . 'Generator' . \DIRECTORY_SEPARATOR . $template;
235 if (!isset(self::$templates[$filename])) {
236 self::$templates[$filename] = new \Text_Template($filename);
239 return self::$templates[$filename];
243 * Returns the parameters of a function or method.
245 * @throws RuntimeException
247 private static function getMethodParameters(\ReflectionMethod $method, bool $forCall = false): string
251 foreach ($method->getParameters() as $i => $parameter) {
252 $name = '$' . $parameter->getName();
254 /* Note: PHP extensions may use empty names for reference arguments
255 * or "..." for methods taking a variable number of arguments.
257 if ($name === '$' || $name === '$...') {
261 if ($parameter->isVariadic()) {
266 $name = '...' . $name;
272 $typeDeclaration = '';
275 if ($parameter->hasType() && $parameter->allowsNull()) {
279 if ($parameter->hasType()) {
280 $type = $parameter->getType();
282 if ($type instanceof \ReflectionNamedType && $type->getName() !== 'self') {
283 $typeDeclaration = $type->getName() . ' ';
286 $class = $parameter->getClass();
287 // @codeCoverageIgnoreStart
288 } catch (\ReflectionException $e) {
289 throw new RuntimeException(
291 'Cannot mock %s::%s() because a class or ' .
292 'interface used in the signature is not loaded',
293 $method->getDeclaringClass()->getName(),
300 // @codeCoverageIgnoreEnd
302 if ($class !== null) {
303 $typeDeclaration = $class->getName() . ' ';
308 if (!$parameter->isVariadic()) {
309 if ($parameter->isDefaultValueAvailable()) {
311 $value = \var_export($parameter->getDefaultValue(), true);
312 // @codeCoverageIgnoreStart
313 } catch (\ReflectionException $e) {
314 throw new RuntimeException(
320 // @codeCoverageIgnoreEnd
322 $default = ' = ' . $value;
323 } elseif ($parameter->isOptional()) {
324 $default = ' = null';
329 if ($parameter->isPassedByReference()) {
333 $parameters[] = $nullable . $typeDeclaration . $reference . $name . $default;
336 return \implode(', ', $parameters);
339 private static function deriveReturnType(\ReflectionMethod $method): Type
341 $returnType = $method->getReturnType();
343 if ($returnType === null) {
344 return new UnknownType();
347 // @see https://bugs.php.net/bug.php?id=70722
348 if ($returnType->getName() === 'self') {
349 return ObjectType::fromName($method->getDeclaringClass()->getName(), $returnType->allowsNull());
352 // @see https://github.com/sebastianbergmann/phpunit-mock-objects/issues/406
353 if ($returnType->getName() === 'parent') {
354 $parentClass = $method->getDeclaringClass()->getParentClass();
356 if ($parentClass === false) {
357 throw new RuntimeException(
359 'Cannot mock %s::%s because "parent" return type declaration is used but %s does not have a parent class',
360 $method->getDeclaringClass()->getName(),
362 $method->getDeclaringClass()->getName()
367 return ObjectType::fromName($parentClass->getName(), $returnType->allowsNull());
370 return Type::fromName($returnType->getName(), $returnType->allowsNull());