Codeception / Codeception/Stub
makeEmpty/make silently loses property values for PHP 8.4+ hooked properties
- Ngôn ngữ chính
- PHP
- Star
- 301
- Fork
- 19
- Chỉ số merge pull request
- Không có pull request nào được merge trong 30 ngày
Mô tả
## 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.
Hướng dẫn đóng góp
Chưa lập chỉ mục được hướng dẫn đóng góp cho kho mã nguồn này
Hướng nghiên cứu
Start in Stub.php around lines 523–526, where Stub::bindParameters() switches to the parent reflection for PHPUnit mock objects, and reproduce the issue with a PHP 8.4+ hooked property. Check how PHPUnit 13 exposes hooked properties through '$prop::get' and determine how hooked properties should be detected. Done means values passed to makeEmpty() and make() remain readable, including values whose property types are final classes.
Do mô hình lập chỉ mục viết ra từ nội dung của issue.
Đánh giá
- Công nghệ
- php
- Lĩnh vực
- testing
- Loại issue
- Lỗi
- Độ khó
- 4/5
- Thời gian dự kiến
- 3-5 ngày
- Mức độ hoạt động
- Ít trao đổi
- Độ rõ ràng
- Khá rõ ràng
- Mức phù hợp với người mới
- 68/100