Passed
Push — master ( f02738...1d9e1d )
by Dominik
02:33
created

MockByCallsTrait::prepareMock()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 1

Importance

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