Completed
Push — master ( c1caca...fd6e9d )
by Dominik
03:09
created

MockByCallsTrait::getMockByCalls()   B

Complexity

Conditions 4
Paths 2

Size

Total Lines 58

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 36
CRAP Score 4

Importance

Changes 0
Metric Value
dl 0
loc 58
ccs 36
cts 36
cp 1
rs 8.9163
c 0
b 0
f 0
cc 4
nc 2
nop 2
crap 4

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
declare(strict_types=1);
4
5
namespace Chubbyphp\Mock;
6
7
use Chubbyphp\Mock\Argument\ArgumentInterface;
8
use PHPUnit\Framework\MockObject\MockObject;
9
10
trait MockByCallsTrait
11
{
12
    /**
13
     * @param string[]|string $class
14
     * @param Call[]          $calls
15
     *
16
     * @return MockObject
17
     */
18 10
    private function getMockByCalls($class, array $calls = []): MockObject
0 ignored issues
show
Unused Code introduced by
This method is not used, and could be removed.
Loading history...
19
    {
20 10
        $mockBuilder = $this->getMockBuilder($class)
0 ignored issues
show
Bug introduced by
It seems like getMockBuilder() must be provided by classes using this trait. How about adding it as abstract method to this trait?

This check looks for methods that are used by a trait but not required by it.

To illustrate, let’s look at the following code example

trait Idable {
    public function equalIds(Idable $other) {
        return $this->getId() === $other->getId();
    }
}

The trait Idable provides a method equalsId that in turn relies on the method getId(). If this method does not exist on a class mixing in this trait, the method will fail.

Adding the getId() as an abstract method to the trait will make sure it is available.

Loading history...
21 10
            ->disableOriginalConstructor()
22 10
            ->disableOriginalClone();
23
24 10
        $mock = $mockBuilder->getMock();
25
26 10
        if ([] === $calls) {
27 1
            $mock->expects(self::never())->method(self::anything());
28
29 1
            return $mock;
30
        }
31
32 9
        $mockName = (new \ReflectionObject($mock))->getShortName();
33
34 9
        $class = $this->getMockClassAsString($class);
35
36 9
        $options = JSON_PRETTY_PRINT | JSON_PRESERVE_ZERO_FRACTION | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES;
37
38 9
        $callIndex = -1;
39
40 9
        $mock->expects(self::any())->method(self::anything())->willReturnCallback(
41 9
            function () use ($class, $mock, $mockName, &$callIndex, &$calls, $options) {
42 9
                ++$callIndex;
43 9
                $call = array_shift($calls);
44
45 9
                if (!$call instanceof Call) {
46 1
                    self::fail(
47 1
                        sprintf('Additional call at index %d on class "%s"', $callIndex, $class)
48 1
                        .PHP_EOL
49 1
                        .json_encode($this->getStackTrace($mock), $options)
50
                    );
51
                }
52
53 9
                $method = $call->getMethod();
54 9
                $mocketMethod = $this->getMockedMethod($mockName);
55
56 9
                if ($mocketMethod !== $method) {
57 1
                    self::fail(
58 1
                        sprintf(
59 1
                            'Call at index %d on class "%s" expected method "%s", "%s" given',
60 1
                            $callIndex,
61 1
                            $class,
62 1
                            $method,
63 1
                            $mocketMethod
64
                        )
65 1
                        .PHP_EOL
66 1
                        .json_encode($this->getStackTrace($mock), $options)
67
                    );
68
                }
69
70 8
                return $this->getMockCallback($class, $callIndex, $call, $mock)(...func_get_args());
0 ignored issues
show
Bug introduced by
It seems like $call defined by array_shift($calls) on line 43 can be null; however, Chubbyphp\Mock\MockByCallsTrait::getMockCallback() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
71 9
            }
72
        );
73
74 9
        return $mock;
75
    }
76
77
    /**
78
     * @param string[]|string $class
79
     *
80
     * @return string
81
     */
82 9
    private function getMockClassAsString($class): string
83
    {
84 9
        if (is_array($class)) {
85 1
            return implode('|', $class);
86
        }
87
88 8
        return $class;
89
    }
90
91
    /**
92
     * @param string $mockName
93
     *
94
     * @return string
95
     */
96 9
    private function getMockedMethod(string $mockName): string
97
    {
98 9
        foreach (debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS) as $trace) {
99 9
            if ($mockName === $trace['class']) {
100 9
                return $trace['function'];
101
            }
102
        }
103
    }
104
105
    /**
106
     * @param string     $class
107
     * @param int        $callIndex
108
     * @param Call       $call
109
     * @param MockObject $mock
110
     *
111
     * @return \Closure
112
     */
113
    private function getMockCallback(
114
        string $class,
115
        int $callIndex,
116
        Call $call,
117
        MockObject $mock
118
    ): \Closure {
119 8
        return function () use ($class, $callIndex, $call, $mock) {
120 8
            if ($call->hasWith()) {
121 8
                $this->compareArguments($class, $call->getMethod(), $callIndex, $call->getWith(), func_get_args());
122
            }
123
124 7
            if (null !== $exception = $call->getException()) {
125 1
                throw $exception;
126
            }
127
128 6
            if ($call->hasReturnSelf()) {
129 2
                return $mock;
130
            }
131
132 4
            if ($call->hasReturn()) {
133 2
                return $call->getReturn();
134
            }
135 8
        };
136
    }
137
138
    /**
139
     * @param string $class
140
     * @param string $method
141
     * @param int    $at
142
     * @param array  $expectedArguments
143
     * @param array  $arguments
144
     */
145 8
    private function compareArguments(
146
        string $class,
147
        string $method,
148
        int $at,
149
        array $expectedArguments,
150
        array $arguments
151
    ) {
152 8
        $expectedArgumentsCount = count($expectedArguments);
153 8
        $argumentsCount = count($arguments);
154
155 8
        self::assertSame(
156 8
            $expectedArgumentsCount,
157 8
            $argumentsCount,
158 8
            sprintf(
159 8
                'Method "%s" on class "%s" at call %d, got %d arguments, but %d are expected',
160 8
                $method,
161 8
                $class,
162 8
                $at,
163 8
                $expectedArgumentsCount,
164 8
                $argumentsCount
165
            )
166
        );
167
168 8
        foreach ($expectedArguments as $index => $expectedArgument) {
169 8
            if ($expectedArgument instanceof ArgumentInterface) {
170 3
                $expectedArgument->assert(
171 3
                    $arguments[$index],
172 3
                    ['class' => $class, 'method' => $method, 'at' => $at, 'index' => $index]
173
                );
174
175 2
                continue;
176
            }
177
178 7
            self::assertSame(
179 7
                $expectedArgument,
180 7
                $arguments[$index],
181 7
                sprintf(
182 7
                    'Method "%s" on class "%s" at call %d, argument %d',
183 7
                    $method,
184 7
                    $class,
185 7
                    $at,
186 7
                    $index
187
                )
188
            );
189
        }
190 7
    }
191
192
    /**
193
     * @param MockObject $mock
194
     *
195
     * @return array
196
     */
197 2
    private function getStackTrace(MockObject $mock): array
198
    {
199 2
        $mockName = (new \ReflectionObject($mock))->getShortName();
200
201 2
        $trace = [];
202 2
        $enableTrace = false;
203 2
        foreach (debug_backtrace() as $i => $row) {
204 2
            if (isset($row['class']) && $mockName === $row['class']) {
205 2
                $enableTrace = true;
206
            }
207
208 2
            if ($enableTrace) {
209 2
                $traceRow = '';
210
211 2
                if (isset($row['class'])) {
212 2
                    $traceRow .= $row['class'];
213
                }
214
215 2
                if (isset($row['type'])) {
216 2
                    $traceRow .= $row['type'];
217
                }
218
219 2
                if (isset($row['function'])) {
220 2
                    $traceRow .= $row['function'];
221
                }
222
223 2
                if (isset($row['file'])) {
224 2
                    $traceRow .= sprintf(' (%s:%d)', $row['file'], $row['line']);
225
                }
226
227 2
                $trace[] = $traceRow;
228
            }
229
        }
230
231 2
        krsort($trace);
232
233 2
        return array_values($trace);
234
    }
235
}
236