• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

10up / wp_mock / 28018311463

23 Jun 2026 10:02AM UTC coverage: 59.201% (-6.9%) from 66.142%
28018311463

Pull #269

github

web-flow
Merge 53328419b into 446ea7083
Pull Request #269: Compatibility with PHPUnit 9, 10, 11, 12, 13 and PHP 7.4-8.4

12 of 12 new or added lines in 3 files covered. (100.0%)

34 existing lines in 3 files now uncovered.

415 of 701 relevant lines covered (59.2%)

2.09 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

77.44
/php/WP_Mock/Functions.php
1
<?php
2

3
namespace WP_Mock;
4

5
use Closure;
6
use InvalidArgumentException;
7
use Mockery;
8
use Mockery\Matcher\AnyOf;
9
use Mockery\Matcher\Type;
10
use WP_Mock;
11
use WP_Mock\Functions\Handler;
12
use WP_Mock\Functions\ReturnSequence;
13

14
/**
15
 * Functions mocking manager.
16
 *
17
 * This internal class is responsible for mocking WordPress functions and methods.
18
 *
19
 * @see WP_Mock::userFunction()
20
 * @see WP_Mock::echoFunction()
21
 * @see WP_Mock::passthruFunction()
22
 */
23
class Functions
24
{
25
    /** @var array<string, Mockery\Mock> container of function names holding a Mock object each handled by WP_Mock */
26
    private array $mockedFunctions = [];
27

28
    /** @var string[] list of user-defined functions (e.g. WordPress functions) mocked by WP_Mock */
29
    private static array $userMockedFunctions = [];
30

31
    /** @var string[] list of functions redefined by WP_Mock through Patchwork */
32
    private array $patchworkFunctions = [];
33

34
    /** @var string[] list of PHP internal functions as per {@see get_defined_functions()} */
35
    private array $internalFunctions = [];
36

37
    /**
38
     * Initializes the handler.
39
     */
40
    public function __construct()
41
    {
42
        Handler::cleanup();
16✔
43

44
        $this->flush();
16✔
45
    }
46

47
    /**
48
     * Flushes (resets) the registered mocked functions.
49
     *
50
     * @return void
51
     */
52
    public function flush(): void
53
    {
54
        $this->mockedFunctions = [];
16✔
55

56
        Handler::cleanup();
16✔
57

58
        $this->patchworkFunctions = [];
16✔
59

60
        if (function_exists('Patchwork\undoAll')) {
16✔
61
            \Patchwork\restoreAll();
16✔
62
        }
63

64
        if (empty(self::$userMockedFunctions)) {
16✔
65
            self::$userMockedFunctions = [
4✔
66
                '__',
4✔
67
                '_e',
4✔
68
                '_n',
4✔
69
                '_x',
4✔
70
                'add_action',
4✔
71
                'add_filter',
4✔
72
                'apply_filters',
4✔
73
                'do_action',
4✔
74
                'esc_attr',
4✔
75
                'esc_attr__',
4✔
76
                'esc_attr_e',
4✔
77
                'esc_attr_x',
4✔
78
                'esc_html',
4✔
79
                'esc_html__',
4✔
80
                'esc_html_e',
4✔
81
                'esc_html_x',
4✔
82
                'esc_js',
4✔
83
                'esc_textarea',
4✔
84
                'esc_url',
4✔
85
                'esc_url_raw',
4✔
86
            ];
4✔
87
        }
88
    }
89

90
    /**
91
     * Registers a function to be mocked and sets up its expectations.
92
     *
93
     * @param string|callable-string $function function name
94
     * @param array<string, mixed> $args optional arguments
95
     * @return Mockery\Expectation
96
     * @throws InvalidArgumentException
97
     */
98
    public function register(string $function, array $args = [])
99
    {
100
        $this->generateFunction($function);
5✔
101

102
        if (empty($this->mockedFunctions[$function])) {
5✔
103
            /** @phpstan-ignore-next-line */
104
            $this->mockedFunctions[$function] = Mockery::mock('wp_api');
5✔
105
        }
106

107
        /** @var Mockery\Mock $mock */
108
        $mock = $this->mockedFunctions[$function];
5✔
109

110
        /** @var callable-string $method */
111
        $method = preg_replace('/\\\\+/', '_', $function);
5✔
112

113
        /** @var Mockery\Expectation $expectation */
114
        $expectation = $this->setUpMock($mock, $method, $args);
5✔
115

116
        Handler::registerHandler($function, [$mock, $method]);
5✔
117

118
        return $expectation;
5✔
119
    }
120

121
    /**
122
     * Sets up the mock object with expectations.
123
     *
124
     * @param Mockery\Mock|Mockery\MockInterface|Mockery\LegacyMockInterface $mock mock object
125
     * @param string $functionName function name
126
     * @param array<string, mixed> $args optional arguments for setting expectations on the mock
127
     * @return Mockery\Expectation|Mockery\CompositeExpectation
128
     */
129
    protected function setUpMock($mock, string $functionName, array $args = [])
130
    {
131
        /** @var Mockery\Expectation|Mockery\CompositeExpectation $expectation */
132
        $expectation = $mock->shouldReceive($functionName);
5✔
133

134
        // set the expected times the function should be called
135
        if (isset($args['times'])) {
5✔
136
            $this->setExpectedTimes($expectation, $args['times']);
1✔
137
        }
138

139
        // set the expected arguments the function should be called with
140
        if (isset($args['args'])) {
5✔
141
            $this->setExpectedArgs($expectation, $args['args']);
1✔
142
        }
143

144
        // set the expected return value based on a passed argument or return values for each call in order
145
        if (isset($args['return_arg']) || isset($args['return_in_order'])) {
5✔
UNCOV
146
            $args['return'] = $this->parseExpectedReturn($args);
×
147
        }
148

149
        // set the expected return value of the function
150
        if (isset($args['return'])) {
5✔
151
            $this->setExpectedReturn($expectation, $args['return']);
1✔
152
        }
153

154
        return $expectation;
5✔
155
    }
156

157
    /**
158
     * Sets the expected times a function should be called based on arguments.
159
     *
160
     * @param Mockery\Expectation|Mockery\CompositeExpectation $expectation
161
     * @param int|string|mixed $times
162
     * @return Mockery\Expectation|Mockery\CompositeExpectation
163
     */
164
    protected function setExpectedTimes(&$expectation, $times)
165
    {
166
        if (is_int($times) || (is_string($times) && preg_match('/^\d+$/', $times))) {
1✔
167
            /** @phpstan-ignore-next-line method exists */
168
            $expectation->times((int) $times);
1✔
169
        } elseif (is_string($times)) {
×
170
            if (preg_match('/^(\d+)([\-+])$/', $times, $matches)) {
×
171
                $method = '+' === $matches[2] ? 'atLeast' : 'atMost';
×
172

173
                $expectation->$method()->times((int) $matches[1]);
×
174
            } elseif (preg_match('/^(\d+)-(\d+)$/', $times, $matches)) {
×
175
                $num1 = (int) $matches[1];
×
176
                $num2 = (int) $matches[2];
×
177

178
                if ($num1 === $num2) {
×
179
                    /** @phpstan-ignore-next-line method exists */
180
                    $expectation->times($num1);
×
181
                } else {
182
                    /** @phpstan-ignore-next-line method exists */
183
                    $expectation->between(min($num1, $num2), max($num1, $num2));
×
184
                }
185
            }
186
        }
187

188
        return $expectation;
1✔
189
    }
190

191
    /**
192
     * Sets the expected arguments that a function should be called with.
193
     *
194
     * @param Mockery\Expectation|Mockery\CompositeExpectation $expectation
195
     * @param mixed $args expected arguments passed to the function
196
     * @return Mockery\Expectation|Mockery\CompositeExpectation
197
     */
198
    protected function setExpectedArgs(&$expectation, $args)
199
    {
200
        $args = array_map(function ($argument) {
1✔
201
            if ($argument instanceof Closure) {
1✔
202
                return Mockery::on($argument);
×
203
            }
204

205
            if ($argument === '*') {
1✔
206
                return Mockery::any();
×
207
            }
208

209
            return $argument;
1✔
210
        }, (array) $args);
1✔
211

212
        /** @phpstan-ignore-next-line method exists on expectation */
213
        call_user_func_array([$expectation, 'with'], $args);
1✔
214

215
        return $expectation;
1✔
216
    }
217

218
    /**
219
     * Parses arguments for setting the expectation `return` arg.
220
     *
221
     * @param array<string, mixed> $args
222
     * @return Closure|ReturnSequence|null
223
     */
224
    protected function parseExpectedReturn(array $args)
225
    {
UNCOV
226
        $returnValue = null;
×
227

UNCOV
228
        if (isset($args['return_arg'])) {
×
229
            /** @phpstan-ignore-next-line */
UNCOV
230
            $argPosition = max(true === $args['return_arg'] ? 0 : (int) $args['return_arg'], 0);
×
231

232
            // set the expected return value based on an argument passed to the function
UNCOV
233
            $returnValue = function () use ($argPosition) {
×
UNCOV
234
                if ($argPosition >= func_num_args()) {
×
235
                    return null;
×
236
                }
237

UNCOV
238
                return func_get_arg($argPosition);
×
UNCOV
239
            };
×
UNCOV
240
        } elseif (isset($args['return_in_order'])) {
×
241
            // sets the return values for each call in order
UNCOV
242
            $returnValue = new ReturnSequence();
×
UNCOV
243
            $returnValue->setReturnValues((array) $args['return_in_order']);
×
244
        }
245

UNCOV
246
        return $returnValue;
×
247
    }
248

249
    /**
250
     * Sets the expected return value for the expectation.
251
     *
252
     * @param Mockery\Expectation $expectation
253
     * @param Closure|ReturnSequence|mixed $return
254
     * @return Mockery\Expectation
255
     */
256
    protected function setExpectedReturn(&$expectation, $return)
257
    {
258
        if ($return instanceof ReturnSequence) {
1✔
UNCOV
259
            $expectation->andReturnValues($return->getReturnValues());
×
260
        } elseif ($return instanceof Closure) {
1✔
UNCOV
261
            $expectation->andReturnUsing($return);
×
262
        } else {
263
            $expectation->andReturn($return);
1✔
264
        }
265

266
        return $expectation;
1✔
267
    }
268

269
    /**
270
     * Dynamically declares a function if it doesn't already exist.
271
     *
272
     * The declared function is namespace-aware.
273
     *
274
     * @param string $functionName function name
275
     * @return void
276
     * @throws InvalidArgumentException
277
     */
278
    protected function generateFunction(string $functionName): void
279
    {
280
        $functionName = $this->sanitizeFunctionName($functionName);
5✔
281

282
        $this->validateFunctionName($functionName);
5✔
283

284
        $this->createFunction($functionName) or $this->replaceFunction($functionName);
5✔
285
    }
286

287
    /**
288
     * Creates a function using eval.
289
     *
290
     * @param string $functionName function name
291
     * @return bool true if this function created the mock, false otherwise
292
     */
293
    protected function createFunction(string $functionName): bool
294
    {
295
        if (in_array($functionName, self::$userMockedFunctions, true)) {
3✔
296
            return true;
1✔
297
        }
298

299
        if (function_exists($functionName)) {
2✔
300
            return false;
1✔
301
        }
302

303
        $parts = explode('\\', $functionName);
1✔
304
        $name = array_pop($parts);
1✔
305
        $namespace = empty($parts) ? '' : 'namespace '.implode('\\', $parts).';'.PHP_EOL;
1✔
306

307
        $declaration = <<<EOF
1✔
308
$namespace
1✔
309
function $name() {
1✔
310
        return \\WP_Mock\\Functions\\Handler::handleFunction('$functionName', func_get_args());
1✔
311
}
312
EOF;
1✔
313
        eval($declaration);
1✔
314

315
        self::$userMockedFunctions[] = $functionName;
1✔
316

317
        return true;
1✔
318
    }
319

320
    /**
321
     * Replaces a function using Patchwork.
322
     *
323
     * @param string $functionName function name
324
     * @return bool
325
     */
326
    protected function replaceFunction(string $functionName): bool
327
    {
328
        if (in_array($functionName, $this->patchworkFunctions, true)) {
1✔
329
            return true;
×
330
        }
331

332
        if (! function_exists('Patchwork\\replace')) {
1✔
333
            return true;
×
334
        }
335

336
        $this->patchworkFunctions[] = $functionName;
1✔
337

338
        \Patchwork\redefine($functionName, function () use ($functionName) {
1✔
339
            return Handler::handleFunction($functionName, func_get_args());
×
340
        });
1✔
341

342
        return true;
1✔
343
    }
344

345
    /**
346
     * Cleans a function name to be of a standard shape.
347
     *
348
     * Trims any namespace separators from the function name.
349
     *
350
     * @param string $functionName
351
     * @return string
352
     */
353
    protected function sanitizeFunctionName(string $functionName): string
354
    {
355
        return trim($functionName, '\\');
1✔
356
    }
357

358
    /**
359
     * Validates a function name for format and other considerations.
360
     *
361
     * Validation will fail if not a valid function name, if it's an internal function, or if it is a reserved word in PHP.
362
     *
363
     * @param string $functionName
364
     * @return void
365
     * @throws InvalidArgumentException
366
     */
367
    protected function validateFunctionName(string $functionName): void
368
    {
369
        if (function_exists($functionName)) {
4✔
370
            if (empty($this->internalFunctions)) {
1✔
371
                $definedFunctions = get_defined_functions();
1✔
372

373
                $this->internalFunctions = $definedFunctions['internal'];
1✔
374
            }
375

376
            if (in_array($functionName, $this->internalFunctions)) {
1✔
377
                throw new InvalidArgumentException('Cannot override internal PHP functions!');
1✔
378
            }
379
        }
380

381
        $parts = explode('\\', $functionName);
3✔
382
        $name = array_pop($parts);
3✔
383

384
        if (! preg_match('/[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*/', $functionName)) {
3✔
385
            throw new InvalidArgumentException('Function name not properly formatted!');
1✔
386
        }
387

388
        $reservedWords = ' __halt_compiler abstract and array as break callable case catch class clone const continue declare default die do echo else elseif empty enddeclare endfor endforeach endif endswitch endwhile eval exit extends final for foreach function global goto if implements include include_once instanceof insteadof interface isset list namespace new or print private protected public require require_once return static switch throw trait try unset use var while xor __CLASS__ __DIR__ __FILE__ __FUNCTION__ __LINE__ __METHOD__ __NAMESPACE__ __TRAIT__ ';
2✔
389

390
        if (false !== strpos($reservedWords, " $name ")) {
2✔
391
            throw new InvalidArgumentException('Function name cannot be a reserved word!');
1✔
392
        }
393
    }
394

395
    /**
396
     * Sets up an argument placeholder that allows it to be any of an enumerated list of possibilities.
397
     *
398
     * @return AnyOf
399
     */
400
    public static function anyOf(): AnyOf
401
    {
402
        /** @phpstan-ignore-next-line */
403
        return call_user_func_array(['\\Mockery', 'anyOf'], func_get_args());
6✔
404
    }
405

406
    /**
407
     * Sets up an argument placeholder that requires the argument to be of a certain type.
408
     *
409
     * This may be any type for which there is a "is_*" function, or any class or interface.
410
     *
411
     * @param string $expected
412
     * @return Type
413
     */
414
    public static function type(string $expected): Type
415
    {
416
        $type = Mockery::type($expected);
6✔
417
        Filter::$objects[ $expected ] = spl_object_hash($type);
6✔
418

419
        return $type;
6✔
420
    }
421
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc