Skip to content

Commit eee79a1

Browse files
Ocramiussebastianbergmann
authored andcommitted
Documented callable parameter structure for Assert::callback()
`Assert::callback()` accepts a `$callable` that, **AT MOST**, accepts **1** parameter and returns `bool`. The number of times I've written `->with(self::callback(function ($a, $b, $c) : void {` is very high: * I generally forget that `->with()` is variadic (that's OK) * I forget that `self::callback()` applies to a single parameter * I forget that the `callable` must return a `bool` in order to operate In order to avoid these mistakes, the `Callback` constraint is now templated, and the given `callable` is enforced to require at most one parameter, and to always return `bool`. As a counter-example to verify that this type signature works, I've run this against `tests/static-analysis/TestUsingCallbacks.php` with following snippet: ```php public function testInvalid(): void { $mock = $this->createMock(SayHello::class); $mock ->expects(self::once()) ->method('hey') ->with(self::callback(static function (string $a, string $b): bool { return true; })) ->willReturn('Hey Joe!'); self::assertSame('Hey Joe!', $mock->hey('Joe')); } ``` The above now raises: ``` ERROR: InvalidArgument - tests/static-analysis/TestUsingCallbacks.php:62:35 - Argument 1 of PHPUnit\StaticAnalysis\TestUsingCallbacks::callback expects callable(mixed):bool, pure-Closure(string, string):true provided (see https://psalm.dev/004) ->with(self::callback(static function (string $a, string $b): bool { return true; })) ``` This produces
1 parent b912a34 commit eee79a1

File tree

3 files changed

+69
-3
lines changed

3 files changed

+69
-3
lines changed

src/Framework/Assert.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3114,6 +3114,13 @@ public static function isTrue(): IsTrue
31143114
return new IsTrue;
31153115
}
31163116

3117+
/**
3118+
* @psalm-template CallbackInput of mixed
3119+
*
3120+
* @psalm-param callable(mixed $callback): bool $callback
3121+
*
3122+
* @psalm-return Callback<CallbackInput>
3123+
*/
31173124
public static function callback(callable $callback): Callback
31183125
{
31193126
return new Callback($callback);

src/Framework/Constraint/Callback.php

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,18 +9,21 @@
99
*/
1010
namespace PHPUnit\Framework\Constraint;
1111

12-
use function call_user_func;
13-
1412
/**
1513
* Constraint that evaluates against a specified closure.
14+
*
15+
* @psalm-template CallbackInput of mixed
1616
*/
1717
final class Callback extends Constraint
1818
{
1919
/**
2020
* @var callable
21+
*
22+
* @psalm-var callable(CallbackInput $input): bool
2123
*/
2224
private $callback;
2325

26+
/** @psalm-param callable(CallbackInput $input): bool $callback */
2427
public function __construct(callable $callback)
2528
{
2629
$this->callback = $callback;
@@ -39,9 +42,11 @@ public function toString(): string
3942
* constraint is met, false otherwise.
4043
*
4144
* @param mixed $other value or object to evaluate
45+
*
46+
* @psalm-param CallbackInput $other
4247
*/
4348
protected function matches($other): bool
4449
{
45-
return call_user_func($this->callback, $other);
50+
return ($this->callback)($other);
4651
}
4752
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
<?php declare(strict_types=1);
2+
/*
3+
* This file is part of PHPUnit.
4+
*
5+
* (c) Sebastian Bergmann <sebastian@phpunit.de>
6+
*
7+
* For the full copyright and license information, please view the LICENSE
8+
* file that was distributed with this source code.
9+
*/
10+
namespace PHPUnit\StaticAnalysis;
11+
12+
use PHPUnit\Framework\TestCase;
13+
14+
/** @see https://www.youtube.com/watch?v=rXwMrBb2x1Q */
15+
interface SayHello
16+
{
17+
public function hey(string $toPerson): string;
18+
}
19+
20+
/** @small */
21+
final class TestUsingCallbacks extends TestCase
22+
{
23+
public function testWillSayHelloAndCheckCallbackInput(): void
24+
{
25+
$mock = $this->createMock(SayHello::class);
26+
27+
$mock
28+
->expects(self::once())
29+
->method('hey')
30+
->with(self::callback(static function (string $input): bool {
31+
self::assertStringContainsString('Joe', $input);
32+
33+
return true;
34+
}))
35+
->willReturn('Hey Joe!');
36+
37+
self::assertSame('Hey Joe!', $mock->hey('Joe'));
38+
}
39+
40+
public function testWillSayHelloAndCheckCallbackWithoutAnyInput(): void
41+
{
42+
$mock = $this->createMock(SayHello::class);
43+
44+
$mock
45+
->expects(self::once())
46+
->method('hey')
47+
->with(self::callback(static function (): bool {
48+
return true;
49+
}))
50+
->willReturn('Hey Joe!');
51+
52+
self::assertSame('Hey Joe!', $mock->hey('Joe'));
53+
}
54+
}

0 commit comments

Comments
 (0)