Completed
Push — master ( ae2925...c1caca )
by Dominik
55:18 queued 06:21
created

MockByCallsTrait::getMockByCalls()   A

Complexity

Conditions 3
Paths 1

Size

Total Lines 52

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 33
CRAP Score 3

Importance

Changes 0
Metric Value
dl 0
loc 52
ccs 33
cts 33
cp 1
rs 9.0472
c 0
b 0
f 0
cc 3
nc 1
nop 2
crap 3

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 9
    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 9
        $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 9
            ->disableOriginalConstructor()
22 9
            ->disableOriginalClone();
23
24 9
        $mock = $mockBuilder->getMock();
25
26 9
        $mockName = (new \ReflectionObject($mock))->getShortName();
27
28 9
        $class = $this->getMockClassAsString($class);
29
30 9
        $options = JSON_PRETTY_PRINT | JSON_PRESERVE_ZERO_FRACTION | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES;
31
32 9
        $callIndex = -1;
33
34 9
        $mock->expects(self::any())->method(self::anything())->willReturnCallback(
35 9
            function () use ($class, $mock, $mockName, &$callIndex, &$calls, $options) {
36 9
                ++$callIndex;
37 9
                $call = array_shift($calls);
38
39 9
                if (!$call instanceof Call) {
40 1
                    self::fail(
41 1
                        sprintf('Additional call at index %d on class "%s"', $callIndex, $class)
42 1
                        .PHP_EOL
43 1
                        .json_encode($this->getStackTrace($mock), $options)
44
                    );
45
                }
46
47 9
                $method = $call->getMethod();
48 9
                $mocketMethod = $this->getMockedMethod($mockName);
49
50 9
                if ($mocketMethod !== $method) {
51 1
                    self::fail(
52 1
                        sprintf(
53 1
                            'Call at index %d on class "%s" expected method "%s", "%s" given',
54 1
                            $callIndex,
55 1
                            $class,
56 1
                            $method,
57 1
                            $mocketMethod
58
                        )
59 1
                        .PHP_EOL
60 1
                        .json_encode($this->getStackTrace($mock), $options)
61
                    );
62
                }
63
64 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 37 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...
65 9
            }
66
        );
67
68 9
        return $mock;
69
    }
70
71
    /**
72
     * @param string[]|string $class
73
     *
74
     * @return string
75
     */
76 9
    private function getMockClassAsString($class): string
77
    {
78 9
        if (is_array($class)) {
79 1
            return implode('|', $class);
80
        }
81
82 8
        return $class;
83
    }
84
85
    /**
86
     * @param string $mockName
87
     *
88
     * @return string
89
     */
90 9
    private function getMockedMethod(string $mockName): string
91
    {
92 9
        foreach (debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS) as $trace) {
93 9
            if ($mockName === $trace['class']) {
94 9
                return $trace['function'];
95
            }
96
        }
97
    }
98
99
    /**
100
     * @param string     $class
101
     * @param int        $callIndex
102
     * @param Call       $call
103
     * @param MockObject $mock
104
     *
105
     * @return \Closure
106
     */
107
    private function getMockCallback(
108
        string $class,
109
        int $callIndex,
110
        Call $call,
111
        MockObject $mock
112
    ): \Closure {
113 8
        return function () use ($class, $callIndex, $call, $mock) {
114 8
            if ($call->hasWith()) {
115 8
                $this->compareArguments($class, $call->getMethod(), $callIndex, $call->getWith(), func_get_args());
116
            }
117
118 7
            if (null !== $exception = $call->getException()) {
119 1
                throw $exception;
120
            }
121
122 6
            if ($call->hasReturnSelf()) {
123 2
                return $mock;
124
            }
125
126 4
            if ($call->hasReturn()) {
127 2
                return $call->getReturn();
128
            }
129 8
        };
130
    }
131
132
    /**
133
     * @param string $class
134
     * @param string $method
135
     * @param int    $at
136
     * @param array  $expectedArguments
137
     * @param array  $arguments
138
     */
139 8
    private function compareArguments(
140
        string $class,
141
        string $method,
142
        int $at,
143
        array $expectedArguments,
144
        array $arguments
145
    ) {
146 8
        $expectedArgumentsCount = count($expectedArguments);
147 8
        $argumentsCount = count($arguments);
148
149 8
        self::assertSame(
150 8
            $expectedArgumentsCount,
151 8
            $argumentsCount,
152 8
            sprintf(
153 8
                'Method "%s" on class "%s" at call %d, got %d arguments, but %d are expected',
154 8
                $method,
155 8
                $class,
156 8
                $at,
157 8
                $expectedArgumentsCount,
158 8
                $argumentsCount
159
            )
160
        );
161
162 8
        foreach ($expectedArguments as $index => $expectedArgument) {
163 8
            if ($expectedArgument instanceof ArgumentInterface) {
164 3
                $expectedArgument->assert(
165 3
                    $arguments[$index],
166 3
                    ['class' => $class, 'method' => $method, 'at' => $at, 'index' => $index]
167
                );
168
169 2
                continue;
170
            }
171
172 7
            self::assertSame(
173 7
                $expectedArgument,
174 7
                $arguments[$index],
175 7
                sprintf(
176 7
                    'Method "%s" on class "%s" at call %d, argument %d',
177 7
                    $method,
178 7
                    $class,
179 7
                    $at,
180 7
                    $index
181
                )
182
            );
183
        }
184 7
    }
185
186
    /**
187
     * @param MockObject $mock
188
     *
189
     * @return array
190
     */
191 2
    private function getStackTrace(MockObject $mock): array
192
    {
193 2
        $mockName = (new \ReflectionObject($mock))->getShortName();
194
195 2
        $trace = [];
196 2
        $enableTrace = false;
197 2
        foreach (debug_backtrace() as $i => $row) {
198 2
            if (isset($row['class']) && $mockName === $row['class']) {
199 2
                $enableTrace = true;
200
            }
201
202 2
            if ($enableTrace) {
203 2
                $traceRow = '';
204
205 2
                if (isset($row['class'])) {
206 2
                    $traceRow .= $row['class'];
207
                }
208
209 2
                if (isset($row['type'])) {
210 2
                    $traceRow .= $row['type'];
211
                }
212
213 2
                if (isset($row['function'])) {
214 2
                    $traceRow .= $row['function'];
215
                }
216
217 2
                if (isset($row['file'])) {
218 2
                    $traceRow .= sprintf(' (%s:%d)', $row['file'], $row['line']);
219
                }
220
221 2
                $trace[] = $traceRow;
222
            }
223
        }
224
225 2
        krsort($trace);
226
227 2
        return array_values($trace);
228
    }
229
}
230