PhpFunctionSimpleMocker::reset()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 1
eloc 2
c 2
b 0
f 0
nc 1
nop 0
dl 0
loc 4
ccs 3
cts 3
cp 1
crap 1
rs 10
1
<?php
2
declare(strict_types=1);
3
4
namespace idimsh\PhpInternalsMocker;
5
6
use idimsh\PhpInternalsMocker\Exception\InvalidArgumentException;
7
use idimsh\PhpInternalsMocker\Exception\NotEnoughCalls;
8
9
/**
10
 * The method used will utilize namespaces, and require that the call to PHP internals is
11
 * 1- Not called with absolute name space: like: \date()
12
 * 2- Used in a class which has a namespace and not used in global namespace.
13
 *
14
 * Usage:
15
 *
16
 * 1- Example 1:
17
 *
18
 * PhpFunctionSimpleMocker::add(
19
 *   'ini_get',
20
 *   \Vendor\Package\Namespace\MyClass::class,
21
 *   function ($key) {
22
 *     static::assertSame('apc.enabled', $key);
23
 *     return true;
24
 *   }
25
 * );
26
 *
27
 * Will register a call to ini_get() from within ANY Method of ANY Class inside the namespace:
28
 * \Vendor\Package\Namespace\
29
 *
30
 *
31
 * 2- Example 2:
32
 *
33
 * PhpFunctionSimpleMocker::add(
34
 *   'register_shutdown_function',
35
 *   \Vendor\Package\Namespace\MyClass::class,
36
 *   null
37
 * );
38
 *
39
 * Will assure that register_shutdown_function() will never be called from any method inside that namespace
40
 *
41
 */
42
class PhpFunctionSimpleMocker
43
{
44
    /**
45
     * [
46
     *   {internal function name} => [ array of callable(s) ],
47
     *   ...
48
     * ]
49
     *
50
     * @var array | callable[][]
51
     */
52
    protected static $registeredCallBacks = [];
53
54
    /**
55
     * [
56
     *    {internal function name} => integer (how many times it is called before),
57
     *    ....
58
     * ]
59
     *
60
     * @var array | int[]
61
     */
62
    protected static $invocationsCounter = [];
63
64
65
    /**
66
     * Reset registered callbacks and start over
67
     */
68 15
    public static function reset()
69
    {
70 15
        static::$invocationsCounter  = [];
71 15
        static::$registeredCallBacks = [];
72 15
    }
73
74
    /**
75
     * Register a call back to be called for the PHP internal function which is to be used in the class passed.
76
     *
77
     * If the $callback is null, then this PHP function is not expected to be called.
78
     *
79
     * Assertions can be done inside the callback.
80
     *
81
     * @param string        $internalFunctionName The PHP function name to mock
82
     * @param string        $beingCalledFromClass The class FQN which calls $internalFunctionName
83
     * @param callable|null $callback
84
     * @param int           $numberOfCalls        To mock more than once for the same callback, pass the number here
85
     */
86 15
    public static function add(
87
        string $internalFunctionName,
88
        string $beingCalledFromClass,
89
        ?callable $callback,
90
        int $numberOfCalls = 1
91
    ): void
92
    {
93 15
        static::$registeredCallBacks[$internalFunctionName] = static::$registeredCallBacks[$internalFunctionName] ?? [];
94 15
        if ($callback === null) {
95 3
            static::addNullCallback($internalFunctionName);
96
        }
97
        else {
98 12
            while (--$numberOfCalls >= 0) {
99 12
                static::$registeredCallBacks[$internalFunctionName][] = $callback;
100
            }
101
        }
102 15
        static::register($internalFunctionName, $beingCalledFromClass);
103 15
    }
104
105
    /**
106
     * This can be used in PHPUnit method: 'protected function assertPostConditions()' in TestCase.
107
     * And be called from there statically to either:
108
     * 1- throw an exception after the test method assertions has been evaluated (which is not very elegant as the name of the test
109
     * method will not be available then).
110
     * OR
111
     * 2- cause the passed TestCase to ::fail() if the conditions are not met, which is a better approach and which to be
112
     * used only if ::phpUnitAssertNotEnoughCalls() is not called at the end of each test method.
113
     *
114
     * @param object | null $testCase   An instance of \PHPUnit\Framework\TestCase, type is not enforced since
115
     *                                  the class might not be available.
116
     * @throws Exception\NotEnoughCalls only if the passed object is not an instance of TestCase, otherwise
117
     *                                  the passed TestCase will fail.
118
     */
119 9
    public static function assertPostConditions($testCase = null): void
120
    {
121
        try {
122 9
            $isTestCase = $testCase === null
123 9
                ? false
124 9
                : self::isTestCaseOrThrow($testCase);
125
        }
126
        catch (InvalidArgumentException $e) {
127
            $isTestCase = false;
128
        }
129
130 9
        foreach (\array_keys(static::$registeredCallBacks) as $internalFunctionName) {
131 9
            $invocationCount         = static::$invocationsCounter[$internalFunctionName] ?? 0;
132 9
            $callbacks               = static::$registeredCallBacks[$internalFunctionName] ?? [];
133 9
            $expectedInvocationCount = \count($callbacks);
134 9
            if ($expectedInvocationCount === 0 || \in_array(null, $callbacks, true)) {
135
                // these cases are not handled here, but in ::call()
136
                continue;
137
            }
138 9
            if ($invocationCount < $expectedInvocationCount) {
139 3
                $exception = Exception\NotEnoughCalls::fromFunctionName(
140 3
                    $internalFunctionName,
141 2
                    $expectedInvocationCount,
142 2
                    $invocationCount
143
                );
144 3
                if ($isTestCase) {
145
                    /** @var \PHPUnit\Framework\TestCase $testCase */
146
                    $testCase::fail($exception->getMessage());
147
                }
148
                else {
149 7
                    throw $exception;
150
                }
151
            }
152
        }
153 6
    }
154
155
    /**
156
     * This is for PhpUnit only
157
     * This can be (and should be) called from PhpUnit test methods. Assuming that the self::reset() method
158
     * is being called from the \PHPUnit\Framework\TestCase::setUp() method AND the self::add() method
159
     * is being called from that particular test method, then a call to this method should be invoked as the
160
     * last assertion in that test method.
161
     *
162
     * @param object $testCase An instance of \PHPUnit\Framework\TestCase, type is not enforced since
163
     *                         the class might not be available.
164
     *
165
     * @throws Exception\InvalidArgumentException
166
     */
167 6
    public static function phpUnitAssertNotEnoughCalls($testCase): void
168
    {
169 6
        if (!self::isTestCaseOrThrow($testCase)) {
170
            return;
171
        }
172
        try {
173 6
            static::assertPostConditions();
174
        }
175
        catch (Exception\NotEnoughCalls $exception) {
176
            /** @var \PHPUnit\Framework\TestCase $testCase */
177
            $testCase::fail($exception->getMessage());
178
        }
179 6
    }
180
181
    /**
182
     * This must be Public, but must not be called externally.
183
     *
184
     * @param string     $internalFunctionName
185
     * @param null|array ...$args
186
     *
187
     * @return mixed
188
     * @throws Exception\CallsLimitExceeded
189
     * @throws Exception\NeverExpected
190
     * @internal
191
     */
192 12
    public static function call(string $internalFunctionName, &...$args)
193
    {
194 12
        $invocationCount         = static::$invocationsCounter[$internalFunctionName] ?? 0;
195 12
        $callbacks               = static::$registeredCallBacks[$internalFunctionName] ?? [];
196 12
        $expectedInvocationCount = \count($callbacks);
197
198 12
        if ($expectedInvocationCount === 0) {
199
            /**
200
             * We will reach here if
201
             * 1- the function was registered to be called then the class was reset by
202
             * calling self::reset(), this will not however remove the registered function in
203
             * the namespace requested, so here we call the original PHP function with arguments.
204
             *
205
             */
206
            if (is_callable($internalFunctionName)) {
207
                return $internalFunctionName(...$args);
208
            }
209
            return null;
210
        }
211
212 12
        if (\in_array(null, $callbacks, true)) {
213 3
            throw Exception\NeverExpected::fromFunctionName($internalFunctionName);
214
        }
215
216 9
        if ($invocationCount >= $expectedInvocationCount) {
217 3
            throw Exception\CallsLimitExceeded::fromFunctionNameLimit($internalFunctionName, $expectedInvocationCount);
218
        }
219
220
        /**
221
         * @var $callback callable
222
         */
223 9
        $callback = static::$registeredCallBacks[$internalFunctionName][$invocationCount];
224
225 9
        static::$invocationsCounter[$internalFunctionName] = $invocationCount + 1;
226
227 9
        $ret = $args === null
0 ignored issues
show
introduced by
The condition $args === null is always false.
Loading history...
228
            ? $callback()
229 9
            : $callback(...$args);
230
231 9
        return $ret;
232
    }
233
234
    /**
235
     * @param object $testCase An instance of \PHPUnit\Framework\TestCase, type is not enforced since
236
     *                         the class might not be available.
237
     *
238
     * @throws Exception\InvalidArgumentException
239
     * @return bool
240
     */
241 6
    protected static function isTestCaseOrThrow($testCase): bool
242
    {
243 6
        if (!\class_exists('PHPUnit\Framework\TestCase')) {
244
            return false;
245
        }
246 6
        if (!$testCase instanceof \PHPUnit\Framework\TestCase) {
247
            throw new Exception\InvalidArgumentException(
248
                \sprintf(
249
                    'Parameter to method: [%s] is expected to be an instance of: [%s], got type: [%s] which is invalid',
250
                    __METHOD__,
251
                    \PHPUnit\Framework\TestCase::class,
252
                    \gettype($testCase)
253
                )
254
            );
255
        }
256
257 6
        return true;
258
    }
259
260
    /**
261
     * Add null callback if it is not already there
262
     *
263
     * @param string $internalFunctionName
264
     */
265 3
    protected static function addNullCallback(string $internalFunctionName): void
266
    {
267 3
        static::$registeredCallBacks[$internalFunctionName] = static::$registeredCallBacks[$internalFunctionName] ?? [];
268 3
        if (!\in_array(null, static::$registeredCallBacks[$internalFunctionName], true)) {
269 3
            static::$registeredCallBacks[$internalFunctionName][] = null;
270
        }
271 3
    }
272
273 15
    protected static function register(string $internalFunctionName, string $class): void
274
    {
275 15
        $self     = \get_called_class();
276 15
        $mockedNs = [\substr($class, 0, \strrpos($class, '\\'))];
277 15
        if (0 < strpos($class, '\\Tests\\')) {
278
            $ns         = \str_replace('\\Tests\\', '\\', $class);
279
            $mockedNs[] = \substr($ns, 0, \strrpos($ns, '\\'));
280
        }
281 15
        elseif (0 === \strpos($class, 'Tests\\')) {
282
            $mockedNs[] = \substr($class, 6, \strrpos($class, '\\') - 6);
283
        }
284 15
        foreach ($mockedNs as $ns) {
285 15
            if (\function_exists($ns . '\\' . $internalFunctionName)) {
286 12
                continue;
287
            }
288
            eval(
0 ignored issues
show
introduced by
The use of eval() is discouraged.
Loading history...
289
            <<<EOPHP
290 3
namespace $ns;
291
292 3
function $internalFunctionName(...\$args)
293
{
294 3
   return \\$self::call('$internalFunctionName', ...\$args);
295
}
296
297
EOPHP
298
            );
299
        }
300 15
    }
301
}
302