Skip to content
10 changes: 10 additions & 0 deletions docs/usage/using-wp-mock.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,16 @@ For example, the invocation below will set the expectation that the `get_permali
WP_Mock::userFunction('get_permalink')->once()->with(42)->andReturn('https://example.com/foo');
```

To mock functions that are called using named parameters, e.g. `get_permalink(post: 42)`, you must pass the arguments as an associative array:

```php
WP_Mock::userFunction('get_permalink', [
'args' => [
'post' => '42',
],
])->once()->andReturn('https://example.com/foo');
```

## Using expectations in arguments

You can also pass an associative array of arguments to the second parameter of `WP_Mock::userFunction()` to set expectations about the function's arguments, the number of times it should be called, and what it should return.
Expand Down
24 changes: 18 additions & 6 deletions php/WP_Mock/Functions.php
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,8 @@ public function flush(): void
*/
public function register(string $function, array $args = [])
{
$this->generateFunction($function);
$functionArgs = isset($args['args']) && is_array($args['args']) ? $args['args'] : [];
$this->generateFunction($function, $functionArgs);

if (empty($this->mockedFunctions[$function])) {
/** @phpstan-ignore-next-line */
Expand Down Expand Up @@ -138,7 +139,10 @@ protected function setUpMock($mock, string $functionName, array $args = [])

// set the expected arguments the function should be called with
if (isset($args['args'])) {
$this->setExpectedArgs($expectation, $args['args']);
$this->setExpectedArgs(
$expectation,
is_array($args['args']) ? array_values($args['args']): $args['args']
);
}

// set the expected return value based on a passed argument or return values for each call in order
Expand Down Expand Up @@ -272,25 +276,27 @@ protected function setExpectedReturn(&$expectation, $return)
* The declared function is namespace-aware.
*
* @param string $functionName function name
* @param array<int|string, mixed> $functionArgs function arguments
* @return void
* @throws InvalidArgumentException
*/
protected function generateFunction(string $functionName): void
protected function generateFunction(string $functionName, array $functionArgs = []): void
{
$functionName = $this->sanitizeFunctionName($functionName);

$this->validateFunctionName($functionName);

$this->createFunction($functionName) or $this->replaceFunction($functionName);
$this->createFunction($functionName, $functionArgs) or $this->replaceFunction($functionName);
}

/**
* Creates a function using eval.
*
* @param string $functionName function name
* @param array<int|string, mixed> $functionArgs function arguments, required only when calling mocked functions using named parameters
* @return bool true if this function created the mock, false otherwise
*/
protected function createFunction(string $functionName): bool
protected function createFunction(string $functionName, array $functionArgs = []): bool
{
if (in_array($functionName, self::$userMockedFunctions, true)) {
return true;
Expand All @@ -304,9 +310,15 @@ protected function createFunction(string $functionName): bool
$name = array_pop($parts);
$namespace = empty($parts) ? '' : 'namespace '.implode('\\', $parts).';'.PHP_EOL;

$functionNamedParameters = '';
foreach (array_keys($functionArgs) as $k) {
$functionNamedParameters .= ($functionNamedParameters ? ', ' : '');
$functionNamedParameters .= sprintf('$%s%s', is_int($k) ? '__positional_' : '', $k);
}

$declaration = <<<EOF
$namespace
function $name() {
function $name($functionNamedParameters) {
return \\WP_Mock\\Functions\\Handler::handleFunction('$functionName', func_get_args());
}
EOF;
Expand Down
51 changes: 51 additions & 0 deletions tests/Integration/WP_MockTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -335,4 +335,55 @@ public function testCanExpectHooksNotAdded() : void

$this->assertConditionsMet();
}

/**
* Fix "Error : Unknown named parameter $x" when passing named parameters in the argument list.
*
* @see https://wiki.php.net/rfc/named_params
*
* @covers \WP_Mock::userFunction()
* @see WP_Mock\Functions::createFunction()
* @dataProvider providerNamedParameters
*
* @runInSeparateProcess
* @preserveGlobalState disabled
*
* @param array<int|string, mixed> $expectedArgs the `args` passed to `WP_Mock::userFunction()`
* @param string $call PHP source of the mocked call, evaluated so the file still parses on PHP 7.4
* @throws Exception
*/
public function testCanMockNamedParameters(array $expectedArgs, string $call): void
{
if (! version_compare(PHP_VERSION, '8.0', '>=')) {
$this->markTestSkipped('PHP 8.0 required for named parameters.');
}

WP_Mock::bootstrap();

WP_Mock::userFunction('get_option', [
'times' => 1,
'args' => $expectedArgs,
'return' => 'the-mocked-value',
]);

/** @phpstan-ignore-next-line function "exists" */
$this->assertEquals('the-mocked-value', eval("return $call;"));
}

/** @see testCanMockNamedParameters */
public static function providerNamedParameters(): Generator
{
yield 'named, declared order' => [
['option' => 'my_option_name', 'default_value' => false],
"get_option(option: 'my_option_name', default_value: false)",
];
yield 'named, reversed order' => [
['option' => 'my_option_name', 'default_value' => false],
"get_option(default_value: false, option: 'my_option_name')",
];
yield 'positional then named' => [
['my_option_name', 'default_value' => false],
"get_option('my_option_name', default_value: false)",
];
}
}
Loading