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

10up / wp_mock / 21052793947

16 Jan 2026 01:48AM UTC coverage: 66.321% (+0.2%) from 66.142%
21052793947

Pull #264

github

web-flow
Merge b179741a5 into 446ea7083
Pull Request #264: Add PHP 8.0 named parameters support

13 of 14 new or added lines in 1 file covered. (92.86%)

1 existing line in 1 file now uncovered.

512 of 772 relevant lines covered (66.32%)

2.72 hits per line

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

88.11
/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();
33✔
43

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

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

56
        Handler::cleanup();
33✔
57

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

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

64
        if (empty(self::$userMockedFunctions)) {
33✔
65
            self::$userMockedFunctions = [
6✔
66
                '__',
6✔
67
                '_e',
6✔
68
                '_n',
6✔
69
                '_x',
6✔
70
                'add_action',
6✔
71
                'add_filter',
6✔
72
                'apply_filters',
6✔
73
                'do_action',
6✔
74
                'esc_attr',
6✔
75
                'esc_attr__',
6✔
76
                'esc_attr_e',
6✔
77
                'esc_attr_x',
6✔
78
                'esc_html',
6✔
79
                'esc_html__',
6✔
80
                'esc_html_e',
6✔
81
                'esc_html_x',
6✔
82
                'esc_js',
6✔
83
                'esc_textarea',
6✔
84
                'esc_url',
6✔
85
                'esc_url_raw',
6✔
86
            ];
6✔
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
        $functionArgs = isset($args['args']) && is_array($args['args']) ? $args['args'] : [];
10✔
101
        $this->generateFunction($function, $functionArgs);
10✔
102

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

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

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

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

117
        Handler::registerHandler($function, [$mock, $method]);
10✔
118

119
        return $expectation;
10✔
120
    }
121

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

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

140
        // set the expected arguments the function should be called with
141
        if (isset($args['args'])) {
10✔
142
            $this->setExpectedArgs(
5✔
143
                $expectation,
5✔
144
                is_array($args['args']) ? array_values($args['args']): $args['args']
5✔
145
            );
5✔
146
        }
147

148
        // set the expected return value based on a passed argument or return values for each call in order
149
        if (isset($args['return_arg']) || isset($args['return_in_order'])) {
10✔
150
            $args['return'] = $this->parseExpectedReturn($args);
2✔
151
        }
152

153
        // set the expected return value of the function
154
        if (isset($args['return'])) {
10✔
155
            $this->setExpectedReturn($expectation, $args['return']);
6✔
156
        }
157

158
        return $expectation;
10✔
159
    }
160

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

177
                $expectation->$method()->times((int) $matches[1]);
×
178
            } elseif (preg_match('/^(\d+)-(\d+)$/', $times, $matches)) {
×
179
                $num1 = (int) $matches[1];
×
180
                $num2 = (int) $matches[2];
×
181

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

192
        return $expectation;
5✔
193
    }
194

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

209
            if ($argument === '*') {
5✔
210
                return Mockery::any();
×
211
            }
212

213
            return $argument;
5✔
214
        }, (array) $args);
5✔
215

216
        /** @phpstan-ignore-next-line method exists on expectation */
217
        call_user_func_array([$expectation, 'with'], $args);
5✔
218

219
        return $expectation;
5✔
220
    }
221

222
    /**
223
     * Parses arguments for setting the expectation `return` arg.
224
     *
225
     * @param array<string, mixed> $args
226
     * @return Closure|ReturnSequence|null
227
     */
228
    protected function parseExpectedReturn(array $args)
229
    {
230
        $returnValue = null;
2✔
231

232
        if (isset($args['return_arg'])) {
2✔
233
            /** @phpstan-ignore-next-line */
234
            $argPosition = max(true === $args['return_arg'] ? 0 : (int) $args['return_arg'], 0);
1✔
235

236
            // set the expected return value based on an argument passed to the function
237
            $returnValue = function () use ($argPosition) {
1✔
238
                if ($argPosition >= func_num_args()) {
1✔
239
                    return null;
×
240
                }
241

242
                return func_get_arg($argPosition);
1✔
243
            };
1✔
244
        } elseif (isset($args['return_in_order'])) {
1✔
245
            // sets the return values for each call in order
246
            $returnValue = new ReturnSequence();
1✔
247
            $returnValue->setReturnValues((array) $args['return_in_order']);
1✔
248
        }
249

250
        return $returnValue;
2✔
251
    }
252

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

270
        return $expectation;
6✔
271
    }
272

273
    /**
274
     * Dynamically declares a function if it doesn't already exist.
275
     *
276
     * The declared function is namespace-aware.
277
     *
278
     * @param string $functionName function name
279
     * @param string $functionArgs function arguments
280
     * @return void
281
     * @throws InvalidArgumentException
282
     */
283
    protected function generateFunction(string $functionName, array $functionArgs = []): void
284
    {
285
        $functionName = $this->sanitizeFunctionName($functionName);
10✔
286

287
        $this->validateFunctionName($functionName);
10✔
288

289
        $this->createFunction($functionName, $functionArgs) or $this->replaceFunction($functionName);
10✔
290
    }
291

292
    /**
293
     * Creates a function using eval.
294
     *
295
     * @param string $functionName function name
296
     * @param array $functionArgs function arguments, required only when calling mocked functions using named parameters
297
     * @return bool true if this function created the mock, false otherwise
298
     */
299
    protected function createFunction(string $functionName, array $functionArgs = []): bool
300
    {
301
        if (in_array($functionName, self::$userMockedFunctions, true)) {
3✔
302
            return true;
1✔
303
        }
304

305
        if (function_exists($functionName)) {
2✔
306
            return false;
1✔
307
        }
308

309
        $parts = explode('\\', $functionName);
1✔
310
        $name = array_pop($parts);
1✔
311
        $namespace = empty($parts) ? '' : 'namespace '.implode('\\', $parts).';'.PHP_EOL;
1✔
312

313
        $functionNamedParameters = '';
1✔
314

315
        $has_named_parameters = array_reduce( array_keys($functionArgs), function ($carry, $arg) {
1✔
NEW
316
            return $carry && !is_int($arg);
×
317
        }, true);
1✔
318

319
        if($has_named_parameters){
1✔
320
            $functionNamedParameters = implode(', ', array_map(fn($name) => '$' . $name, array_keys($functionArgs)));
1✔
321
        }
322

323
        $declaration = <<<EOF
1✔
324
$namespace
1✔
325
function $name($functionNamedParameters) {
1✔
326
        return \\WP_Mock\\Functions\\Handler::handleFunction('$functionName', func_get_args());
1✔
327
}
328
EOF;
1✔
329
        eval($declaration);
1✔
330

331
        self::$userMockedFunctions[] = $functionName;
1✔
332

333
        return true;
1✔
334
    }
335

336
    /**
337
     * Replaces a function using Patchwork.
338
     *
339
     * @param string $functionName function name
340
     * @return bool
341
     */
342
    protected function replaceFunction(string $functionName): bool
343
    {
344
        if (in_array($functionName, $this->patchworkFunctions, true)) {
1✔
345
            return true;
×
346
        }
347

348
        if (! function_exists('Patchwork\\replace')) {
1✔
349
            return true;
×
350
        }
351

352
        $this->patchworkFunctions[] = $functionName;
1✔
353

354
        \Patchwork\redefine($functionName, function () use ($functionName) {
1✔
355
            return Handler::handleFunction($functionName, func_get_args());
×
356
        });
1✔
357

358
        return true;
1✔
359
    }
360

361
    /**
362
     * Cleans a function name to be of a standard shape.
363
     *
364
     * Trims any namespace separators from the function name.
365
     *
366
     * @param string $functionName
367
     * @return string
368
     */
369
    protected function sanitizeFunctionName(string $functionName): string
370
    {
371
        return trim($functionName, '\\');
1✔
372
    }
373

374
    /**
375
     * Validates a function name for format and other considerations.
376
     *
377
     * Validation will fail if not a valid function name, if it's an internal function, or if it is a reserved word in PHP.
378
     *
379
     * @param string $functionName
380
     * @return void
381
     * @throws InvalidArgumentException
382
     */
383
    protected function validateFunctionName(string $functionName): void
384
    {
385
        if (function_exists($functionName)) {
4✔
386
            if (empty($this->internalFunctions)) {
1✔
387
                $definedFunctions = get_defined_functions();
1✔
388

389
                $this->internalFunctions = $definedFunctions['internal'];
1✔
390
            }
391

392
            if (in_array($functionName, $this->internalFunctions)) {
1✔
393
                throw new InvalidArgumentException('Cannot override internal PHP functions!');
1✔
394
            }
395
        }
396

397
        $parts = explode('\\', $functionName);
3✔
398
        $name = array_pop($parts);
3✔
399

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

404
        $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✔
405

406
        if (false !== strpos($reservedWords, " $name ")) {
2✔
407
            throw new InvalidArgumentException('Function name cannot be a reserved word!');
1✔
408
        }
409
    }
410

411
    /**
412
     * Sets up an argument placeholder that allows it to be any of an enumerated list of possibilities.
413
     *
414
     * @return AnyOf
415
     */
416
    public static function anyOf(): AnyOf
417
    {
418
        /** @phpstan-ignore-next-line */
419
        return call_user_func_array(['\\Mockery', 'anyOf'], func_get_args());
6✔
420
    }
421

422
    /**
423
     * Sets up an argument placeholder that requires the argument to be of a certain type.
424
     *
425
     * This may be any type for which there is a "is_*" function, or any class or interface.
426
     *
427
     * @param string $expected
428
     * @return Type
429
     */
430
    public static function type(string $expected): Type
431
    {
432
        $type = Mockery::type($expected);
6✔
433
        Filter::$objects[ $expected ] = spl_object_hash($type);
6✔
434

435
        return $type;
6✔
436
    }
437
}
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