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 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 |
||
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); |
||
|
|||
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) |
||
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) |
|
163 | |||
164 | /** |
||
165 | * Sets return promise to the prophecy. |
||
166 | * |
||
167 | * @see \Prophecy\Promise\ReturnPromise |
||
168 | * |
||
169 | * @return $this |
||
170 | */ |
||
171 | public function willReturn() |
||
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) |
||
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) |
||
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 | View Code Duplication | public function should($prediction) |
|
242 | |||
243 | /** |
||
244 | * Sets call prediction to the prophecy. |
||
245 | * |
||
246 | * @see \Prophecy\Prediction\CallPrediction |
||
247 | * |
||
248 | * @return $this |
||
249 | */ |
||
250 | public function shouldBeCalled() |
||
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() |
||
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) |
||
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() |
||
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) |
||
335 | |||
336 | /** |
||
337 | * Checks call prediction. |
||
338 | * |
||
339 | * @see \Prophecy\Prediction\CallPrediction |
||
340 | * |
||
341 | * @return $this |
||
342 | */ |
||
343 | public function shouldHaveBeenCalled() |
||
347 | |||
348 | /** |
||
349 | * Checks no calls prediction. |
||
350 | * |
||
351 | * @see \Prophecy\Prediction\NoCallsPrediction |
||
352 | * |
||
353 | * @return $this |
||
354 | */ |
||
355 | public function shouldNotHaveBeenCalled() |
||
359 | |||
360 | /** |
||
361 | * Checks no calls prediction. |
||
362 | * |
||
363 | * @see \Prophecy\Prediction\NoCallsPrediction |
||
364 | * @deprecated |
||
365 | * |
||
366 | * @return $this |
||
367 | */ |
||
368 | public function shouldNotBeenCalled() |
||
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) |
||
386 | |||
387 | /** |
||
388 | * Checks call times prediction. |
||
389 | * |
||
390 | * @see \Prophecy\Prediction\CallTimesPrediction |
||
391 | * |
||
392 | * @return $this |
||
393 | */ |
||
394 | public function shouldHaveBeenCalledOnce() |
||
398 | |||
399 | /** |
||
400 | * Checks currently registered [with should(...)] prediction. |
||
401 | */ |
||
402 | public function checkPrediction() |
||
410 | |||
411 | /** |
||
412 | * Returns currently registered promise. |
||
413 | * |
||
414 | * @return null|Promise\PromiseInterface |
||
415 | */ |
||
416 | public function getPromise() |
||
420 | |||
421 | /** |
||
422 | * Returns currently registered prediction. |
||
423 | * |
||
424 | * @return null|Prediction\PredictionInterface |
||
425 | */ |
||
426 | public function getPrediction() |
||
430 | |||
431 | /** |
||
432 | * Returns predictions that were checked on this object. |
||
433 | * |
||
434 | * @return Prediction\PredictionInterface[] |
||
435 | */ |
||
436 | public function getCheckedPredictions() |
||
440 | |||
441 | /** |
||
442 | * Returns object prophecy this method prophecy is tied to. |
||
443 | * |
||
444 | * @return ObjectProphecy |
||
445 | */ |
||
446 | public function getObjectProphecy() |
||
450 | |||
451 | /** |
||
452 | * Returns method name. |
||
453 | * |
||
454 | * @return string |
||
455 | */ |
||
456 | public function getMethodName() |
||
460 | |||
461 | /** |
||
462 | * Returns arguments wildcard. |
||
463 | * |
||
464 | * @return Argument\ArgumentsWildcard |
||
465 | */ |
||
466 | public function getArgumentsWildcard() |
||
470 | |||
471 | /** |
||
472 | * @return bool |
||
473 | */ |
||
474 | public function hasReturnVoid() |
||
478 | |||
479 | private function bindToObjectProphecy() |
||
488 | } |
||
489 |
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.