Completed
Push — master ( ff0dbc...e0f682 )
by Konstantin
01:26
created

MethodProphecy   C

Complexity

Total Complexity 55

Size/Duplication

Total Lines 453
Duplicated Lines 7.95 %

Coupling/Cohesion

Components 1
Dependencies 15

Importance

Changes 7
Bugs 0 Features 0
Metric Value
wmc 55
lcom 1
cbo 15
dl 36
loc 453
rs 6
c 7
b 0
f 0

25 Methods

Rating   Name   Duplication   Size   Complexity  
C __construct() 0 60 16
A withArguments() 0 18 3
A will() 18 18 3
A willReturn() 0 11 2
A willReturnArgument() 0 8 2
A willThrow() 0 4 1
A should() 18 18 3
A shouldBeCalled() 0 4 1
A shouldNotBeCalled() 0 4 1
A shouldBeCalledTimes() 0 4 1
B shouldHave() 0 33 6
A shouldHaveBeenCalled() 0 4 1
A shouldNotHaveBeenCalled() 0 4 1
A shouldNotBeenCalled() 0 4 1
A shouldHaveBeenCalledTimes() 0 4 1
A checkPrediction() 0 8 2
A addCall() 0 6 1
A getPromise() 0 4 1
A getPrediction() 0 4 1
A getCheckedPredictions() 0 4 1
A getObjectProphecy() 0 4 1
A getMethodName() 0 4 1
A getArgumentsWildcard() 0 4 1
A hasReturnVoid() 0 4 1
A bindToObjectProphecy() 0 9 2

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like MethodProphecy often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use MethodProphecy, and based on these observations, apply Extract Interface, too.

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