Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
28 changes: 22 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,19 @@ protected function createFunction(string $functionName): bool
$name = array_pop($parts);
$namespace = empty($parts) ? '' : 'namespace '.implode('\\', $parts).';'.PHP_EOL;

$functionNamedParameters = '';

$has_named_parameters = array_reduce( array_keys($functionArgs), function ($carry, $arg) {
return $carry && !is_int($arg);
}, true);

if($has_named_parameters){
$functionNamedParameters = implode(', ', array_map(fn($name) => '$' . $name, array_keys($functionArgs)));
}

Comment on lines +313 to +322
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
$functionNamedParameters = '';
$has_named_parameters = array_reduce( array_keys($functionArgs), function ($carry, $arg) {
return $carry && !is_int($arg);
}, true);
if($has_named_parameters){
$functionNamedParameters = implode(', ', array_map(fn($name) => '$' . $name, array_keys($functionArgs)));
}
$functionNamedParameters = '';
foreach (array_keys($functionArgs) as $k) {
if (!is_int($k)) {
$functionNamedParameters .= ($functionNamedParameters ? ', ' : '') . '$' . $k;
}
}

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

$this->assertConditionsMet();
}

/**
* Fix "Error : Unknown named parameter $transient" when passing named parameters in the argument list.
*
* @covers \WP_Mock::userFunction()
* @see WP_Mock\Functions::createFunction()
*
* @throws Exception
*/
public function testCanMockNamedParameters(): void {

if(!version_compare(PHP_VERSION, '8.0', '>=')) {
$this->markTestSkipped('PHP 8.0 required for named parameters.');
}

WP_Mock::userFunction('get_transient', [
'times' => 1,
'args' => [
'transient' => 'my-transient-name',
],
'return' => 'the-mocked-transient-value',
]);

// Without this, tests fail on PHP 7.4.
// PHP Fatal error: Uncaught ParseError: syntax error, unexpected ':', expecting ')' in :362
$hidePhp8CodeFromOlderVersions = <<<'PHP'
return get_transient(transient: 'my-transient-name');
PHP;

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