Before we begin, let's create a minimal, reproducible example:
class User {}
class Example {
public User $user;
public function __construct(User $user) {
$this->user = $user;
}
}
class ExampleTest extends PHPUnit\Framework\TestCase {
public function testExample() {
$args = [];
$args['User'] = $this->getMockBuilder('User')->disableOriginalConstructor()->getMock();
$mock = $this->getMockBuilder(Example::class)
->setConstructorArgs($args)
->getMock();
$this->assertTrue(true);
}
}
Running with PHP 7.4 and PHPUnit 9.5.14, this passes; with PHP 8.0 and the same library, it gives the error you report:
Error: Unknown named parameter $User
Actually, we can simplify a bit further: rather than $args['User'] = $this->getMockBuilder('User')->disableOriginalConstructor()->getMock(); we can just say $args['User'] = new User; and get the same error.
Now, let's look at what we're doing:
- We create an associative array, mapping class names to (mock) objects
- We pass that associative array to a Mock Builder's
setConstructorArgs method
- Some magic happens...
So, what does happen? Maybe the source of PHPUnit will give some clues.
Well, setConstructorArgs just sets a property, which is used in getMock, then passed through a bunch of different methods; eventually, it ends up passed to MockObject\Generator::getObject, which if we strip out some error handling, does this:
$class = new ReflectionClass($className);
$object = $class->newInstanceArgs($arguments);
So, let's see if we can use that to make an even more minimal example of our problem:
class User {}
class Example {
public User $user;
public function __construct(User $user) {
$this->user = $user;
}
}
$class = new ReflectionClass(Example::class);
$object = $class->newInstanceArgs(['User' => new User]);
Since this is self-contained code, we can use the handy online tool at https://3v4l.org to compare output in different PHP versions: https://3v4l.org/QU4jS
As expected, PHP 7.4 is happy with it, PHP 8.0 and above give an error:
Fatal error: Uncaught Error: Unknown named parameter $User in /in/QU4jS:14
Stack trace:
#0 /in/QU4jS(14): ReflectionClass->newInstanceArgs(Array)
#1 {main}
thrown in /in/QU4jS on line 14
So, what is going on here? Well, the manual page for ReflectionClass::newInstanceArgs doesn't (currently) say much about what the provided array should look like, or named argument support, but we can take an educated guess: it's trying to match our associative array as named parameters to the constructor. Previous versions, since they had no named parameters, simply ignored the keys and applied the parameters in order.
We can test this theory easily enough by making a class with two parameters to its constructor:
class Example2 {
public function __construct($first, $second) {
echo "$first then $second\n";
}
}
$class = new ReflectionClass(Example2::class);
$object = $class->newInstanceArgs(['second' => 'two', 'first' => 'one']);
When run on multiple versions we can see that older versions of PHP output "two then one", based on the order of the array; and newer versions output "one then two", based on the keys of the array.
So, to cut a long story short, what's the fix? Quite simply, don't use keys in the array of constructor parameters:
class ExampleTest extends PHPUnit\Framework\TestCase {
public function testExample() {
$args = [];
$args[] = new User;
$mock = $this->getMockBuilder(Example::class)
->setConstructorArgs($args)
->getMock();
$this->assertTrue(true);
}
}
If you need to use them during the setup logic to keep track of things, just use array_values to discard them when you pass them in:
class ExampleTest extends PHPUnit\Framework\TestCase {
public function testExample() {
$args = [];
$args['User'] = new User;
$mock = $this->getMockBuilder(Example::class)
->setConstructorArgs(array_values($args))
->getMock();
$this->assertTrue(true);
}
}