Codeception / Codeception/Stub
makeEmpty/make silently loses property values for PHP 8.4+ hooked properties
- Dominant language
- PHP
- Stars
- 301
- Forks
- 19
- PR merge metrics
- No merged PRs in 30d
Description
## Environment
- PHP: 8.5.6 (reproduces on PHP 8.4+, the version that introduced property hooks)
- codeception/stub: 4.3.0
- phpunit/phpunit: 13.0.6
## Problem
When mocking a class that has PHP 8.4 property hooks, values passed to `makeEmpty()` / `make()` for hooked properties are silently lost. Depending on the property type, this either returns `null` / an auto-generated stub, or throws a `RuntimeException` if the property type is a `final` class.
A real-world trigger: **symfony/http-foundation 8.1** added `set` hooks with deprecation warnings to all major public bags on `Request` (`$request`, `$query`, `$attributes`, etc.), causing all tests that mock `Request` with those properties to break.
## Minimal reproduction
```php
class Foo
{
public string $bar {
set { $this->bar = $value; }
}
}
// Value is silently lost
$mock = $this->makeEmpty(Foo::class, ['bar' => 'baz']);
var_dump($mock->bar); // expected: string(3) baz, actual: auto-stub or error
```
With a `final`-typed property (e.g. `InputBag`):
```php
// Throws RuntimeException:
// Return value for MockObject_Request::::get() cannot be generated:
// Class "Symfony\Component\HttpFoundation\InputBag" is declared "final"
$mock = $this->makeEmpty(Request::class, ['request' => new InputBag(['foo' => 'bar'])]);
$mock->request->get('foo'); // boom
```
## Root cause
`Stub::bindParameters()` deliberately switches to `getParentClass()` for PHPUnit mock objects (line 523–526 in `Stub.php`), then calls `ReflectionProperty::setValue()` on the parent's reflection:
```php
if ($mock instanceof PHPUnitMockObject) {
$reflectionClass = $reflectionClass->getParentClass(); // Request::class
}
// ...
$reflectionProperty->setValue($mock, $value); // writes to parent's backing store
```
In PHP 8.4, a class that **redeclares** a property with hooks (as PHPUnit's mock generator does when the source class has hooks — see [phpunit#6549](https://github.com/sebastianbergmann/phpunit/pull/6549)) gets its **own separate backing store**. The write goes into the parent's backing store; the read goes through the mock's generated `get` hook, which reads from the mock's backing store — they never meet.
## Possible fix direction
For properties that have hooks in the source class, `bindParameters()` should configure the mock via PHPUnit's mock API instead of `ReflectionProperty::setValue()`:
```php
// PHPUnit 13 exposes hooked properties as mockable methods named '$prop::get'
$mock->method('$bar::get')->willReturn('baz');
```
This requires detecting hooked properties (via `ReflectionProperty::hasHooks()` / `getHook()`, available since PHP 8.4) and branching accordingly.
Contributor guide
No contributing guide indexed for this repository
Assessment
This issue has not been assessed yet.