Completed
Pull Request — master (#473)
by Dimitris
01:20
created

MethodProphecy::shouldHave()   B

Complexity

Conditions 6
Paths 14

Size

Total Lines 33

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 33
rs 8.7697
c 0
b 0
f 0
cc 6
nc 14
nop 1
1
<?php
2
3
/*
4
 * This file is part of the Prophecy.
5
 * (c) Konstantin Kudryashov <[email protected]>
6
 *     Marcello Duarte <[email protected]>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11
12
namespace Prophecy\Prophecy;
13
14
use Prophecy\Argument;
15
use Prophecy\Prophet;
16
use Prophecy\Promise;
17
use Prophecy\Prediction;
18
use Prophecy\Exception\Doubler\MethodNotFoundException;
19
use Prophecy\Exception\InvalidArgumentException;
20
use Prophecy\Exception\Prophecy\MethodProphecyException;
21
22
/**
23
 * Method prophecy.
24
 *
25
 * @author Konstantin Kudryashov <[email protected]>
26
 */
27
class 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);
0 ignored issues
show
Bug introduced by
It seems like $arguments defined by parameter $arguments on line 47 can also be of type object<Prophecy\Argument\ArgumentsWildcard>; however, Prophecy\Exception\Doubl...xception::__construct() does only seem to accept null|object<Prophecy\Exc...rgumentsWildcard>|array, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
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 = PHP_VERSION_ID >= 70100 ? $reflectedMethod->getReturnType()->getName() : (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 View Code Duplication
    public function will($promise)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
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
     * @param array $items
185
     *
186
     * @return $this
187
     *
188
     * @throws \Prophecy\Exception\InvalidArgumentException
189
     */
190
    public function willYield($items)
191
    {
192
        if ($this->voidReturnType) {
193
            throw new MethodProphecyException(
194
                "The method \"$this->methodName\" has a void return type, and so cannot yield anything",
195
                $this
196
            );
197
        }
198
199
        if (!is_array($items)) {
200
            throw new InvalidArgumentException(sprintf(
201
                'Expected array, but got %s.',
202
                gettype($items)
203
            ));
204
        }
205
206
        // Remove eval() when minimum version >=5.5
207
        /** @var callable $generator */
208
        $generator = eval('return function() use ($items) {
209
            foreach ($items as $key => $value) {
210
                yield $key => $value;
211
            }
212
        };');
213
214
        return $this->will($generator);
215
    }
216
217
    /**
218
     * Sets return argument promise to the prophecy.
219
     *
220
     * @param int $index The zero-indexed number of the argument to return
221
     *
222
     * @see \Prophecy\Promise\ReturnArgumentPromise
223
     *
224
     * @return $this
225
     */
226
    public function willReturnArgument($index = 0)
227
    {
228
        if ($this->voidReturnType) {
229
            throw new MethodProphecyException("The method \"$this->methodName\" has a void return type", $this);
230
        }
231
232
        return $this->will(new Promise\ReturnArgumentPromise($index));
233
    }
234
235
    /**
236
     * Sets throw promise to the prophecy.
237
     *
238
     * @see \Prophecy\Promise\ThrowPromise
239
     *
240
     * @param string|\Exception $exception Exception class or instance
241
     *
242
     * @return $this
243
     */
244
    public function willThrow($exception)
245
    {
246
        return $this->will(new Promise\ThrowPromise($exception));
247
    }
248
249
    /**
250
     * Sets custom prediction to the prophecy.
251
     *
252
     * @param callable|Prediction\PredictionInterface $prediction
253
     *
254
     * @return $this
255
     *
256
     * @throws \Prophecy\Exception\InvalidArgumentException
257
     */
258 View Code Duplication
    public function should($prediction)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
259
    {
260
        if (is_callable($prediction)) {
261
            $prediction = new Prediction\CallbackPrediction($prediction);
262
        }
263
264
        if (!$prediction instanceof Prediction\PredictionInterface) {
265
            throw new InvalidArgumentException(sprintf(
266
                'Expected callable or instance of PredictionInterface, but got %s.',
267
                gettype($prediction)
268
            ));
269
        }
270
271
        $this->bindToObjectProphecy();
272
        $this->prediction = $prediction;
273
274
        return $this;
275
    }
276
277
    /**
278
     * Sets call prediction to the prophecy.
279
     *
280
     * @see \Prophecy\Prediction\CallPrediction
281
     *
282
     * @return $this
283
     */
284
    public function shouldBeCalled()
285
    {
286
        return $this->should(new Prediction\CallPrediction);
287
    }
288
289
    /**
290
     * Sets no calls prediction to the prophecy.
291
     *
292
     * @see \Prophecy\Prediction\NoCallsPrediction
293
     *
294
     * @return $this
295
     */
296
    public function shouldNotBeCalled()
297
    {
298
        return $this->should(new Prediction\NoCallsPrediction);
299
    }
300
301
    /**
302
     * Sets call times prediction to the prophecy.
303
     *
304
     * @see \Prophecy\Prediction\CallTimesPrediction
305
     *
306
     * @param $count
307
     *
308
     * @return $this
309
     */
310
    public function shouldBeCalledTimes($count)
311
    {
312
        return $this->should(new Prediction\CallTimesPrediction($count));
313
    }
314
315
    /**
316
     * Increments the call times prediction for the prophecy.
317
     *
318
     * If the prediction already exist, it adds the given count to the
319
     * prediction's times count.
320
     *
321
     * If the prediction does not exist, or it is not a call times prediction,
322
     * it adds a new call times prediction with the given count.
323
     *
324
     * @see \Prophecy\Prediction\CallTimesPrediction
325
     *
326
     * @param $count
327
     *
328
     * @return $this
329
     */
330
    public function shouldBeCalledAddTimes($count)
331
    {
332
        if (!$this->prediction || !$this->prediction instanceof Prediction\CallTimesPrediction) {
333
            return $this->shouldBeCalledTimes($count);
334
        }
335
336
        $times = $this->prediction->getTimes();
337
        return $this->should(new Prediction\CallTimesPrediction($times + $count));
338
    }
339
340
    /**
341
     * Sets call times prediction to the prophecy.
342
     *
343
     * @see \Prophecy\Prediction\CallTimesPrediction
344
     *
345
     * @return $this
346
     */
347
    public function shouldBeCalledOnce()
348
    {
349
        return $this->shouldBeCalledTimes(1);
350
    }
351
352
    /**
353
     * Checks provided prediction immediately.
354
     *
355
     * @param callable|Prediction\PredictionInterface $prediction
356
     *
357
     * @return $this
358
     *
359
     * @throws \Prophecy\Exception\InvalidArgumentException
360
     */
361
    public function shouldHave($prediction)
362
    {
363
        if (is_callable($prediction)) {
364
            $prediction = new Prediction\CallbackPrediction($prediction);
365
        }
366
367
        if (!$prediction instanceof Prediction\PredictionInterface) {
368
            throw new InvalidArgumentException(sprintf(
369
                'Expected callable or instance of PredictionInterface, but got %s.',
370
                gettype($prediction)
371
            ));
372
        }
373
374
        if (null === $this->promise && !$this->voidReturnType) {
375
            $this->willReturn();
376
        }
377
378
        $calls = $this->getObjectProphecy()->findProphecyMethodCalls(
379
            $this->getMethodName(),
380
            $this->getArgumentsWildcard()
381
        );
382
383
        try {
384
            $prediction->check($calls, $this->getObjectProphecy(), $this);
385
            $this->checkedPredictions[] = $prediction;
386
        } catch (\Exception $e) {
387
            $this->checkedPredictions[] = $prediction;
388
389
            throw $e;
390
        }
391
392
        return $this;
393
    }
394
395
    /**
396
     * Checks call prediction.
397
     *
398
     * @see \Prophecy\Prediction\CallPrediction
399
     *
400
     * @return $this
401
     */
402
    public function shouldHaveBeenCalled()
403
    {
404
        return $this->shouldHave(new Prediction\CallPrediction);
405
    }
406
407
    /**
408
     * Checks no calls prediction.
409
     *
410
     * @see \Prophecy\Prediction\NoCallsPrediction
411
     *
412
     * @return $this
413
     */
414
    public function shouldNotHaveBeenCalled()
415
    {
416
        return $this->shouldHave(new Prediction\NoCallsPrediction);
417
    }
418
419
    /**
420
     * Checks no calls prediction.
421
     *
422
     * @see \Prophecy\Prediction\NoCallsPrediction
423
     * @deprecated
424
     *
425
     * @return $this
426
     */
427
    public function shouldNotBeenCalled()
428
    {
429
        return $this->shouldNotHaveBeenCalled();
430
    }
431
432
    /**
433
     * Checks call times prediction.
434
     *
435
     * @see \Prophecy\Prediction\CallTimesPrediction
436
     *
437
     * @param int $count
438
     *
439
     * @return $this
440
     */
441
    public function shouldHaveBeenCalledTimes($count)
442
    {
443
        return $this->shouldHave(new Prediction\CallTimesPrediction($count));
444
    }
445
446
    /**
447
     * Checks call times prediction.
448
     *
449
     * @see \Prophecy\Prediction\CallTimesPrediction
450
     *
451
     * @return $this
452
     */
453
    public function shouldHaveBeenCalledOnce()
454
    {
455
        return $this->shouldHaveBeenCalledTimes(1);
456
    }
457
458
    /**
459
     * Checks currently registered [with should(...)] prediction.
460
     */
461
    public function checkPrediction()
462
    {
463
        if (null === $this->prediction) {
464
            return;
465
        }
466
467
        $this->shouldHave($this->prediction);
468
    }
469
470
    /**
471
     * Returns currently registered promise.
472
     *
473
     * @return null|Promise\PromiseInterface
474
     */
475
    public function getPromise()
476
    {
477
        return $this->promise;
478
    }
479
480
    /**
481
     * Returns currently registered prediction.
482
     *
483
     * @return null|Prediction\PredictionInterface
484
     */
485
    public function getPrediction()
486
    {
487
        return $this->prediction;
488
    }
489
490
    /**
491
     * Returns predictions that were checked on this object.
492
     *
493
     * @return Prediction\PredictionInterface[]
494
     */
495
    public function getCheckedPredictions()
496
    {
497
        return $this->checkedPredictions;
498
    }
499
500
    /**
501
     * Returns object prophecy this method prophecy is tied to.
502
     *
503
     * @return ObjectProphecy
504
     */
505
    public function getObjectProphecy()
506
    {
507
        return $this->objectProphecy;
508
    }
509
510
    /**
511
     * Returns method name.
512
     *
513
     * @return string
514
     */
515
    public function getMethodName()
516
    {
517
        return $this->methodName;
518
    }
519
520
    /**
521
     * Returns arguments wildcard.
522
     *
523
     * @return Argument\ArgumentsWildcard
524
     */
525
    public function getArgumentsWildcard()
526
    {
527
        return $this->argumentsWildcard;
528
    }
529
530
    /**
531
     * @return bool
532
     */
533
    public function hasReturnVoid()
534
    {
535
        return $this->voidReturnType;
536
    }
537
538
    private function bindToObjectProphecy()
539
    {
540
        if ($this->bound) {
541
            return;
542
        }
543
544
        $this->getObjectProphecy()->addMethodProphecy($this);
545
        $this->bound = true;
546
    }
547
}
548