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\Prophecy; 13 14use Prophecy\Argument; 15use Prophecy\Prophet; 16use Prophecy\Promise; 17use Prophecy\Prediction; 18use Prophecy\Exception\Doubler\MethodNotFoundException; 19use Prophecy\Exception\InvalidArgumentException; 20use Prophecy\Exception\Prophecy\MethodProphecyException; 21 22/** 23 * Method prophecy. 24 * 25 * @author Konstantin Kudryashov <ever.zet@gmail.com> 26 */ 27class MethodProphecy 28{ 29 private $objectProphecy; 30 private $methodName; 31 private $argumentsWildcard; 32 private $promise; 33 private $prediction; 34 private $checkedPredictions = array(); 35 private $bound = false; 36 private $voidReturnType = false; 37 38 /** 39 * Initializes method prophecy. 40 * 41 * @param ObjectProphecy $objectProphecy 42 * @param string $methodName 43 * @param null|Argument\ArgumentsWildcard|array $arguments 44 * 45 * @throws \Prophecy\Exception\Doubler\MethodNotFoundException If method not found 46 */ 47 public function __construct(ObjectProphecy $objectProphecy, $methodName, $arguments = null) 48 { 49 $double = $objectProphecy->reveal(); 50 if (!method_exists($double, $methodName)) { 51 throw new MethodNotFoundException(sprintf( 52 'Method `%s::%s()` is not defined.', get_class($double), $methodName 53 ), get_class($double), $methodName, $arguments); 54 } 55 56 $this->objectProphecy = $objectProphecy; 57 $this->methodName = $methodName; 58 59 $reflectedMethod = new \ReflectionMethod($double, $methodName); 60 if ($reflectedMethod->isFinal()) { 61 throw new MethodProphecyException(sprintf( 62 "Can not add prophecy for a method `%s::%s()`\n". 63 "as it is a final method.", 64 get_class($double), 65 $methodName 66 ), $this); 67 } 68 69 if (null !== $arguments) { 70 $this->withArguments($arguments); 71 } 72 73 if (version_compare(PHP_VERSION, '7.0', '>=') && true === $reflectedMethod->hasReturnType()) { 74 $type = (string) $reflectedMethod->getReturnType(); 75 76 if ('void' === $type) { 77 $this->voidReturnType = true; 78 } 79 80 $this->will(function () use ($type) { 81 switch ($type) { 82 case 'void': return; 83 case 'string': return ''; 84 case 'float': return 0.0; 85 case 'int': return 0; 86 case 'bool': return false; 87 case 'array': return array(); 88 89 case 'callable': 90 case 'Closure': 91 return function () {}; 92 93 case 'Traversable': 94 case 'Generator': 95 // Remove eval() when minimum version >=5.5 96 /** @var callable $generator */ 97 $generator = eval('return function () { yield; };'); 98 return $generator(); 99 100 default: 101 $prophet = new Prophet; 102 return $prophet->prophesize($type)->reveal(); 103 } 104 }); 105 } 106 } 107 108 /** 109 * Sets argument wildcard. 110 * 111 * @param array|Argument\ArgumentsWildcard $arguments 112 * 113 * @return $this 114 * 115 * @throws \Prophecy\Exception\InvalidArgumentException 116 */ 117 public function withArguments($arguments) 118 { 119 if (is_array($arguments)) { 120 $arguments = new Argument\ArgumentsWildcard($arguments); 121 } 122 123 if (!$arguments instanceof Argument\ArgumentsWildcard) { 124 throw new InvalidArgumentException(sprintf( 125 "Either an array or an instance of ArgumentsWildcard expected as\n". 126 'a `MethodProphecy::withArguments()` argument, but got %s.', 127 gettype($arguments) 128 )); 129 } 130 131 $this->argumentsWildcard = $arguments; 132 133 return $this; 134 } 135 136 /** 137 * Sets custom promise to the prophecy. 138 * 139 * @param callable|Promise\PromiseInterface $promise 140 * 141 * @return $this 142 * 143 * @throws \Prophecy\Exception\InvalidArgumentException 144 */ 145 public function will($promise) 146 { 147 if (is_callable($promise)) { 148 $promise = new Promise\CallbackPromise($promise); 149 } 150 151 if (!$promise instanceof Promise\PromiseInterface) { 152 throw new InvalidArgumentException(sprintf( 153 'Expected callable or instance of PromiseInterface, but got %s.', 154 gettype($promise) 155 )); 156 } 157 158 $this->bindToObjectProphecy(); 159 $this->promise = $promise; 160 161 return $this; 162 } 163 164 /** 165 * Sets return promise to the prophecy. 166 * 167 * @see \Prophecy\Promise\ReturnPromise 168 * 169 * @return $this 170 */ 171 public function willReturn() 172 { 173 if ($this->voidReturnType) { 174 throw new MethodProphecyException( 175 "The method \"$this->methodName\" has a void return type, and so cannot return anything", 176 $this 177 ); 178 } 179 180 return $this->will(new Promise\ReturnPromise(func_get_args())); 181 } 182 183 /** 184 * Sets return argument promise to the prophecy. 185 * 186 * @param int $index The zero-indexed number of the argument to return 187 * 188 * @see \Prophecy\Promise\ReturnArgumentPromise 189 * 190 * @return $this 191 */ 192 public function willReturnArgument($index = 0) 193 { 194 if ($this->voidReturnType) { 195 throw new MethodProphecyException("The method \"$this->methodName\" has a void return type", $this); 196 } 197 198 return $this->will(new Promise\ReturnArgumentPromise($index)); 199 } 200 201 /** 202 * Sets throw promise to the prophecy. 203 * 204 * @see \Prophecy\Promise\ThrowPromise 205 * 206 * @param string|\Exception $exception Exception class or instance 207 * 208 * @return $this 209 */ 210 public function willThrow($exception) 211 { 212 return $this->will(new Promise\ThrowPromise($exception)); 213 } 214 215 /** 216 * Sets custom prediction to the prophecy. 217 * 218 * @param callable|Prediction\PredictionInterface $prediction 219 * 220 * @return $this 221 * 222 * @throws \Prophecy\Exception\InvalidArgumentException 223 */ 224 public function should($prediction) 225 { 226 if (is_callable($prediction)) { 227 $prediction = new Prediction\CallbackPrediction($prediction); 228 } 229 230 if (!$prediction instanceof Prediction\PredictionInterface) { 231 throw new InvalidArgumentException(sprintf( 232 'Expected callable or instance of PredictionInterface, but got %s.', 233 gettype($prediction) 234 )); 235 } 236 237 $this->bindToObjectProphecy(); 238 $this->prediction = $prediction; 239 240 return $this; 241 } 242 243 /** 244 * Sets call prediction to the prophecy. 245 * 246 * @see \Prophecy\Prediction\CallPrediction 247 * 248 * @return $this 249 */ 250 public function shouldBeCalled() 251 { 252 return $this->should(new Prediction\CallPrediction); 253 } 254 255 /** 256 * Sets no calls prediction to the prophecy. 257 * 258 * @see \Prophecy\Prediction\NoCallsPrediction 259 * 260 * @return $this 261 */ 262 public function shouldNotBeCalled() 263 { 264 return $this->should(new Prediction\NoCallsPrediction); 265 } 266 267 /** 268 * Sets call times prediction to the prophecy. 269 * 270 * @see \Prophecy\Prediction\CallTimesPrediction 271 * 272 * @param $count 273 * 274 * @return $this 275 */ 276 public function shouldBeCalledTimes($count) 277 { 278 return $this->should(new Prediction\CallTimesPrediction($count)); 279 } 280 281 /** 282 * Sets call times prediction to the prophecy. 283 * 284 * @see \Prophecy\Prediction\CallTimesPrediction 285 * 286 * @return $this 287 */ 288 public function shouldBeCalledOnce() 289 { 290 return $this->shouldBeCalledTimes(1); 291 } 292 293 /** 294 * Checks provided prediction immediately. 295 * 296 * @param callable|Prediction\PredictionInterface $prediction 297 * 298 * @return $this 299 * 300 * @throws \Prophecy\Exception\InvalidArgumentException 301 */ 302 public function shouldHave($prediction) 303 { 304 if (is_callable($prediction)) { 305 $prediction = new Prediction\CallbackPrediction($prediction); 306 } 307 308 if (!$prediction instanceof Prediction\PredictionInterface) { 309 throw new InvalidArgumentException(sprintf( 310 'Expected callable or instance of PredictionInterface, but got %s.', 311 gettype($prediction) 312 )); 313 } 314 315 if (null === $this->promise && !$this->voidReturnType) { 316 $this->willReturn(); 317 } 318 319 $calls = $this->getObjectProphecy()->findProphecyMethodCalls( 320 $this->getMethodName(), 321 $this->getArgumentsWildcard() 322 ); 323 324 try { 325 $prediction->check($calls, $this->getObjectProphecy(), $this); 326 $this->checkedPredictions[] = $prediction; 327 } catch (\Exception $e) { 328 $this->checkedPredictions[] = $prediction; 329 330 throw $e; 331 } 332 333 return $this; 334 } 335 336 /** 337 * Checks call prediction. 338 * 339 * @see \Prophecy\Prediction\CallPrediction 340 * 341 * @return $this 342 */ 343 public function shouldHaveBeenCalled() 344 { 345 return $this->shouldHave(new Prediction\CallPrediction); 346 } 347 348 /** 349 * Checks no calls prediction. 350 * 351 * @see \Prophecy\Prediction\NoCallsPrediction 352 * 353 * @return $this 354 */ 355 public function shouldNotHaveBeenCalled() 356 { 357 return $this->shouldHave(new Prediction\NoCallsPrediction); 358 } 359 360 /** 361 * Checks no calls prediction. 362 * 363 * @see \Prophecy\Prediction\NoCallsPrediction 364 * @deprecated 365 * 366 * @return $this 367 */ 368 public function shouldNotBeenCalled() 369 { 370 return $this->shouldNotHaveBeenCalled(); 371 } 372 373 /** 374 * Checks call times prediction. 375 * 376 * @see \Prophecy\Prediction\CallTimesPrediction 377 * 378 * @param int $count 379 * 380 * @return $this 381 */ 382 public function shouldHaveBeenCalledTimes($count) 383 { 384 return $this->shouldHave(new Prediction\CallTimesPrediction($count)); 385 } 386 387 /** 388 * Checks call times prediction. 389 * 390 * @see \Prophecy\Prediction\CallTimesPrediction 391 * 392 * @return $this 393 */ 394 public function shouldHaveBeenCalledOnce() 395 { 396 return $this->shouldHaveBeenCalledTimes(1); 397 } 398 399 /** 400 * Checks currently registered [with should(...)] prediction. 401 */ 402 public function checkPrediction() 403 { 404 if (null === $this->prediction) { 405 return; 406 } 407 408 $this->shouldHave($this->prediction); 409 } 410 411 /** 412 * Returns currently registered promise. 413 * 414 * @return null|Promise\PromiseInterface 415 */ 416 public function getPromise() 417 { 418 return $this->promise; 419 } 420 421 /** 422 * Returns currently registered prediction. 423 * 424 * @return null|Prediction\PredictionInterface 425 */ 426 public function getPrediction() 427 { 428 return $this->prediction; 429 } 430 431 /** 432 * Returns predictions that were checked on this object. 433 * 434 * @return Prediction\PredictionInterface[] 435 */ 436 public function getCheckedPredictions() 437 { 438 return $this->checkedPredictions; 439 } 440 441 /** 442 * Returns object prophecy this method prophecy is tied to. 443 * 444 * @return ObjectProphecy 445 */ 446 public function getObjectProphecy() 447 { 448 return $this->objectProphecy; 449 } 450 451 /** 452 * Returns method name. 453 * 454 * @return string 455 */ 456 public function getMethodName() 457 { 458 return $this->methodName; 459 } 460 461 /** 462 * Returns arguments wildcard. 463 * 464 * @return Argument\ArgumentsWildcard 465 */ 466 public function getArgumentsWildcard() 467 { 468 return $this->argumentsWildcard; 469 } 470 471 /** 472 * @return bool 473 */ 474 public function hasReturnVoid() 475 { 476 return $this->voidReturnType; 477 } 478 479 private function bindToObjectProphecy() 480 { 481 if ($this->bound) { 482 return; 483 } 484 485 $this->getObjectProphecy()->addMethodProphecy($this); 486 $this->bound = true; 487 } 488} 489