From 52d3928dfd3319bddca2389ec07119f855aeaf01 Mon Sep 17 00:00:00 2001 From: Brian Henry Date: Thu, 15 Jan 2026 17:25:49 -0800 Subject: [PATCH 01/11] Add PHP 8.0 named parameters support --- php/WP_Mock/Functions.php | 28 ++++++++++++++++++++++------ tests/Integration/WP_MockTest.php | 29 +++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 6 deletions(-) diff --git a/php/WP_Mock/Functions.php b/php/WP_Mock/Functions.php index ddf5cf6..e7755a7 100644 --- a/php/WP_Mock/Functions.php +++ b/php/WP_Mock/Functions.php @@ -97,7 +97,8 @@ public function flush(): void */ public function register(string $function, array $args = []) { - $this->generateFunction($function); + $functionArgs = isset($args['args']) ? $args['args'] : []; + $this->generateFunction($function, $functionArgs); if (empty($this->mockedFunctions[$function])) { /** @phpstan-ignore-next-line */ @@ -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 @@ -272,25 +276,27 @@ protected function setExpectedReturn(&$expectation, $return) * The declared function is namespace-aware. * * @param string $functionName function name + * @param string $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 $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; @@ -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))); + } + $declaration = <<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', + array( + 'times' => 1, + 'args' => array( + 'transient' => 'my-transient-name', + ), + 'return' => 'the-mocked-transient-value', + ) + ); + + /** @phpstan-ignore-next-line function "exists" */ + $this->assertEquals('the-mocked-transient-value', get_transient(transient: 'my-transient-name')); + } } From 79eb61a2f6e1fabc4da90add859a5e2a24d5f484 Mon Sep 17 00:00:00 2001 From: Brian Henry Date: Thu, 15 Jan 2026 17:36:41 -0800 Subject: [PATCH 02/11] Use `[]` array syntax --- tests/Integration/WP_MockTest.php | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/tests/Integration/WP_MockTest.php b/tests/Integration/WP_MockTest.php index f5e8c4c..fad4bb4 100644 --- a/tests/Integration/WP_MockTest.php +++ b/tests/Integration/WP_MockTest.php @@ -350,16 +350,13 @@ public function testCanMockNamedParameters(): void { $this->markTestSkipped('PHP 8.0 required for named parameters.'); } - WP_Mock::userFunction( - 'get_transient', - array( - 'times' => 1, - 'args' => array( - 'transient' => 'my-transient-name', - ), - 'return' => 'the-mocked-transient-value', - ) - ); + WP_Mock::userFunction('get_transient', [ + 'times' => 1, + 'args' => [ + 'transient' => 'my-transient-name', + ], + 'return' => 'the-mocked-transient-value', + ]); /** @phpstan-ignore-next-line function "exists" */ $this->assertEquals('the-mocked-transient-value', get_transient(transient: 'my-transient-name')); From d39cac068357023cf9d136538fa43fbaa2c03530 Mon Sep 17 00:00:00 2001 From: Brian Henry Date: Thu, 15 Jan 2026 17:41:16 -0800 Subject: [PATCH 03/11] Add `is_array()` check --- php/WP_Mock/Functions.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/php/WP_Mock/Functions.php b/php/WP_Mock/Functions.php index e7755a7..3e73424 100644 --- a/php/WP_Mock/Functions.php +++ b/php/WP_Mock/Functions.php @@ -97,7 +97,7 @@ public function flush(): void */ public function register(string $function, array $args = []) { - $functionArgs = isset($args['args']) ? $args['args'] : []; + $functionArgs = isset($args['args']) && is_array($args['args']) ? $args['args'] : []; $this->generateFunction($function, $functionArgs); if (empty($this->mockedFunctions[$function])) { From b179741a512f3658fbe544f8c4e3614e222e8773 Mon Sep 17 00:00:00 2001 From: Brian Henry Date: Thu, 15 Jan 2026 17:47:35 -0800 Subject: [PATCH 04/11] Add named parameters example --- docs/usage/using-wp-mock.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/docs/usage/using-wp-mock.md b/docs/usage/using-wp-mock.md index 304a031..29fed4a 100644 --- a/docs/usage/using-wp-mock.md +++ b/docs/usage/using-wp-mock.md @@ -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. From 553b551c78953a0d54bb233bbac65654f29fce4e Mon Sep 17 00:00:00 2001 From: Brian Henry Date: Thu, 15 Jan 2026 17:54:59 -0800 Subject: [PATCH 05/11] Add array shapes --- php/WP_Mock/Functions.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/php/WP_Mock/Functions.php b/php/WP_Mock/Functions.php index 3e73424..c69c076 100644 --- a/php/WP_Mock/Functions.php +++ b/php/WP_Mock/Functions.php @@ -276,7 +276,7 @@ protected function setExpectedReturn(&$expectation, $return) * The declared function is namespace-aware. * * @param string $functionName function name - * @param string $functionArgs function arguments + * @param array $functionArgs function arguments * @return void * @throws InvalidArgumentException */ @@ -293,7 +293,7 @@ protected function generateFunction(string $functionName, array $functionArgs = * Creates a function using eval. * * @param string $functionName function name - * @param array $functionArgs function arguments, required only when calling mocked functions using named parameters + * @param array $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, array $functionArgs = []): bool From 8fe92ebc081058cc50431aea41050aeaf4d1b4e2 Mon Sep 17 00:00:00 2001 From: Brian Henry Date: Thu, 15 Jan 2026 17:55:23 -0800 Subject: [PATCH 06/11] Use `eval()` to hide code that fails on PHP 7.4 --- tests/Integration/WP_MockTest.php | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/tests/Integration/WP_MockTest.php b/tests/Integration/WP_MockTest.php index fad4bb4..54916c3 100644 --- a/tests/Integration/WP_MockTest.php +++ b/tests/Integration/WP_MockTest.php @@ -358,7 +358,13 @@ public function testCanMockNamedParameters(): void { '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', get_transient(transient: 'my-transient-name')); + $this->assertEquals('the-mocked-transient-value', eval($hidePhp8CodeFromOlderVersions)); } } From 8ac4274b58da3f8647ea9abca361900f81fc32ac Mon Sep 17 00:00:00 2001 From: Brian Henry Date: Tue, 22 Sep 2026 08:29:30 -0700 Subject: [PATCH 07/11] Update php/WP_Mock/Functions.php Co-authored-by: Andrew Dawes <53574062+AndrewJDawes@users.noreply.github.com> --- php/WP_Mock/Functions.php | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/php/WP_Mock/Functions.php b/php/WP_Mock/Functions.php index c69c076..73396fd 100644 --- a/php/WP_Mock/Functions.php +++ b/php/WP_Mock/Functions.php @@ -311,15 +311,11 @@ protected function createFunction(string $functionName, array $functionArgs = [] $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))); + foreach (array_keys($functionArgs) as $k) { + if (!is_int($k)) { + $functionNamedParameters .= ($functionNamedParameters ? ', ' : '') . '$' . $k; + } } - $declaration = << Date: Tue, 22 Sep 2026 08:34:45 -0700 Subject: [PATCH 08/11] Test out-of-order named params; trailing named param --- tests/Integration/WP_MockTest.php | 42 ++++++++++++++++++++----------- 1 file changed, 27 insertions(+), 15 deletions(-) diff --git a/tests/Integration/WP_MockTest.php b/tests/Integration/WP_MockTest.php index 54916c3..254ee62 100644 --- a/tests/Integration/WP_MockTest.php +++ b/tests/Integration/WP_MockTest.php @@ -341,30 +341,42 @@ public function testCanExpectHooksNotAdded() : void * * @covers \WP_Mock::userFunction() * @see WP_Mock\Functions::createFunction() + * @dataProvider providerNamedParameters * + * @param array $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(): void { - - if(!version_compare(PHP_VERSION, '8.0', '>=')) { + 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::userFunction('get_transient', [ + WP_Mock::userFunction('get_option', [ 'times' => 1, - 'args' => [ - 'transient' => 'my-transient-name', - ], - 'return' => 'the-mocked-transient-value', + 'args' => $expectedArgs, + 'return' => 'the-mocked-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)); + $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)", + ]; } } From 87307e4c884fa60f7fc1cc635a64bdb9c8b06840 Mon Sep 17 00:00:00 2001 From: Brian Henry Date: Tue, 22 Sep 2026 08:49:58 -0700 Subject: [PATCH 09/11] Use valid variable name for positional args --- php/WP_Mock/Functions.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/php/WP_Mock/Functions.php b/php/WP_Mock/Functions.php index 73396fd..b919c61 100644 --- a/php/WP_Mock/Functions.php +++ b/php/WP_Mock/Functions.php @@ -312,10 +312,10 @@ protected function createFunction(string $functionName, array $functionArgs = [] $functionNamedParameters = ''; foreach (array_keys($functionArgs) as $k) { - if (!is_int($k)) { - $functionNamedParameters .= ($functionNamedParameters ? ', ' : '') . '$' . $k; - } + $functionNamedParameters .= ($functionNamedParameters ? ', ' : ''); + $functionNamedParameters .= sprintf('$%s%s', is_int($k) ? '__positional_' : '', $k); } + $declaration = << Date: Tue, 22 Sep 2026 08:50:28 -0700 Subject: [PATCH 10/11] `@runInSeparateProcess` to avoid interference --- tests/Integration/WP_MockTest.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/Integration/WP_MockTest.php b/tests/Integration/WP_MockTest.php index 254ee62..c10004f 100644 --- a/tests/Integration/WP_MockTest.php +++ b/tests/Integration/WP_MockTest.php @@ -343,6 +343,9 @@ public function testCanExpectHooksNotAdded() : void * @see WP_Mock\Functions::createFunction() * @dataProvider providerNamedParameters * + * @runInSeparateProcess + * @preserveGlobalState disabled + * * @param array $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 @@ -353,6 +356,8 @@ public function testCanMockNamedParameters(array $expectedArgs, string $call): v $this->markTestSkipped('PHP 8.0 required for named parameters.'); } + WP_Mock::bootstrap(); + WP_Mock::userFunction('get_option', [ 'times' => 1, 'args' => $expectedArgs, From 556e7a369dc0c672da1051a6d45019fc5481439b Mon Sep 17 00:00:00 2001 From: Brian Henry Date: Tue, 22 Sep 2026 08:58:46 -0700 Subject: [PATCH 11/11] Update PhpDoc for `::testCanMockNamedParameters()` --- tests/Integration/WP_MockTest.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/Integration/WP_MockTest.php b/tests/Integration/WP_MockTest.php index c10004f..068a576 100644 --- a/tests/Integration/WP_MockTest.php +++ b/tests/Integration/WP_MockTest.php @@ -337,7 +337,9 @@ public function testCanExpectHooksNotAdded() : void } /** - * Fix "Error : Unknown named parameter $transient" when passing named parameters in the argument list. + * 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()