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