Skip to content

Commit e4c774c

Browse files
Closes #5948
1 parent 8723410 commit e4c774c

28 files changed

+1145
-12
lines changed

.php-cs-fixer.dist.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,11 @@
1414
->in(__DIR__ . '/tests/_files')
1515
->in(__DIR__ . '/tests/end-to-end')
1616
->in(__DIR__ . '/tests/unit')
17+
// *WithPropertyWith*Hook.php use PHP 8.4 syntax that currently leads to PHP-CS-Fixer errors
18+
->notName('ExtendableClassWithPropertyWithGetHook.php')
19+
->notName('ExtendableClassWithPropertyWithSetHook.php')
20+
->notName('InterfaceWithPropertyWithGetHook.php')
21+
->notName('InterfaceWithPropertyWithSetHook.php')
1722
// DeprecatedPhpFeatureTest.php must not use declare(strict_types=1);
1823
->notName('DeprecatedPhpFeatureTest.php')
1924
// UseBaselineTest.php must not use declare(strict_types=1);

ChangeLog-11.5.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,8 @@ All notable changes of the PHPUnit 11.5 release series are documented in this fi
44

55
## [11.5.0] - 2024-12-06
66

7+
### Added
8+
9+
* [#5948](https://github.com/sebastianbergmann/phpunit/pull/5948): Support for Property Hooks in Test Doubles
10+
711
[11.5.0]: https://github.com/sebastianbergmann/phpunit/compare/11.4...main

src/Framework/MockObject/Generator/Generator.php

Lines changed: 193 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
use function is_array;
2929
use function is_object;
3030
use function md5;
31+
use function method_exists;
3132
use function mt_rand;
3233
use function preg_match;
3334
use function preg_match_all;
@@ -63,9 +64,13 @@
6364
use PHPUnit\Framework\MockObject\StubApi;
6465
use PHPUnit\Framework\MockObject\StubInternal;
6566
use PHPUnit\Framework\MockObject\TestDoubleState;
67+
use PropertyHookType;
6668
use ReflectionClass;
6769
use ReflectionMethod;
6870
use ReflectionObject;
71+
use ReflectionProperty;
72+
use SebastianBergmann\Type\ReflectionMapper;
73+
use SebastianBergmann\Type\Type;
6974
use SoapClient;
7075
use SoapFault;
7176
use Throwable;
@@ -796,18 +801,13 @@ private function generateCodeForTestDoubleClass(string $type, bool $mockObject,
796801
}
797802
}
798803

804+
$propertiesWithHooks = $this->properties($class);
805+
$configurableMethods = $this->configurableMethods($mockMethods, $propertiesWithHooks);
806+
799807
$mockedMethods = '';
800-
$configurable = [];
801808

802809
foreach ($mockMethods->asArray() as $mockMethod) {
803810
$mockedMethods .= $mockMethod->generateCode();
804-
805-
$configurable[] = new ConfigurableMethod(
806-
$mockMethod->methodName(),
807-
$mockMethod->defaultParameterValues(),
808-
$mockMethod->numberOfParameters(),
809-
$mockMethod->returnType(),
810-
);
811811
}
812812

813813
/** @var trait-string[] $traits */
@@ -890,14 +890,15 @@ private function generateCodeForTestDoubleClass(string $type, bool $mockObject,
890890
),
891891
'use_statements' => $useStatements,
892892
'mock_class_name' => $_mockClassName['className'],
893-
'mocked_methods' => $mockedMethods,
893+
'methods' => $mockedMethods,
894+
'property_hooks' => $this->codeForPropertyHooks($propertiesWithHooks, $_mockClassName['className']),
894895
],
895896
);
896897

897898
return new MockClass(
898899
$classTemplate->render(),
899900
$_mockClassName['className'],
900-
$configurable,
901+
$configurableMethods,
901902
);
902903
}
903904

@@ -1159,4 +1160,186 @@ private function interfaceMethods(string $interfaceName, bool $cloneArguments):
11591160

11601161
return $methods;
11611162
}
1163+
1164+
/**
1165+
* @param list<Property> $propertiesWithHooks
1166+
*
1167+
* @return list<ConfigurableMethod>
1168+
*/
1169+
private function configurableMethods(MockMethodSet $methods, array $propertiesWithHooks): array
1170+
{
1171+
$configurable = [];
1172+
1173+
foreach ($methods->asArray() as $method) {
1174+
$configurable[] = new ConfigurableMethod(
1175+
$method->methodName(),
1176+
$method->defaultParameterValues(),
1177+
$method->numberOfParameters(),
1178+
$method->returnType(),
1179+
);
1180+
}
1181+
1182+
foreach ($propertiesWithHooks as $property) {
1183+
if ($property->hasGetHook()) {
1184+
$configurable[] = new ConfigurableMethod(
1185+
sprintf(
1186+
'$%s::get',
1187+
$property->name(),
1188+
),
1189+
[],
1190+
0,
1191+
$property->type(),
1192+
);
1193+
}
1194+
1195+
if ($property->hasSetHook()) {
1196+
$configurable[] = new ConfigurableMethod(
1197+
sprintf(
1198+
'$%s::set',
1199+
$property->name(),
1200+
),
1201+
[],
1202+
1,
1203+
Type::fromName('void', false),
1204+
);
1205+
}
1206+
}
1207+
1208+
return $configurable;
1209+
}
1210+
1211+
/**
1212+
* @param ?ReflectionClass<object> $class
1213+
*
1214+
* @return list<Property>
1215+
*/
1216+
private function properties(?ReflectionClass $class): array
1217+
{
1218+
if (!method_exists(ReflectionProperty::class, 'isFinal')) {
1219+
// @codeCoverageIgnoreStart
1220+
return [];
1221+
// @codeCoverageIgnoreEnd
1222+
}
1223+
1224+
if ($class === null) {
1225+
return [];
1226+
}
1227+
1228+
$mapper = new ReflectionMapper;
1229+
$properties = [];
1230+
1231+
foreach ($class->getProperties() as $property) {
1232+
assert(method_exists($property, 'getHook'));
1233+
assert(method_exists($property, 'hasHooks'));
1234+
assert(method_exists($property, 'hasHook'));
1235+
assert(method_exists($property, 'isFinal'));
1236+
assert(class_exists(PropertyHookType::class));
1237+
1238+
if (!$property->isPublic()) {
1239+
continue;
1240+
}
1241+
1242+
if ($property->isFinal()) {
1243+
continue;
1244+
}
1245+
1246+
if (!$property->hasHooks()) {
1247+
continue;
1248+
}
1249+
1250+
$hasGetHook = false;
1251+
$hasSetHook = false;
1252+
1253+
if ($property->hasHook(PropertyHookType::Get) &&
1254+
!$property->getHook(PropertyHookType::Get)->isFinal()) {
1255+
$hasGetHook = true;
1256+
}
1257+
1258+
if ($property->hasHook(PropertyHookType::Set) &&
1259+
!$property->getHook(PropertyHookType::Set)->isFinal()) {
1260+
$hasSetHook = true;
1261+
}
1262+
1263+
if (!$hasGetHook && !$hasSetHook) {
1264+
continue;
1265+
}
1266+
1267+
$properties[] = new Property(
1268+
$property->getName(),
1269+
$mapper->fromPropertyType($property),
1270+
$hasGetHook,
1271+
$hasSetHook,
1272+
);
1273+
}
1274+
1275+
return $properties;
1276+
}
1277+
1278+
/**
1279+
* @param list<Property> $propertiesWithHooks
1280+
* @param class-string $className
1281+
*
1282+
* @return non-empty-string
1283+
*/
1284+
private function codeForPropertyHooks(array $propertiesWithHooks, string $className): string
1285+
{
1286+
$propertyHooks = '';
1287+
1288+
foreach ($propertiesWithHooks as $property) {
1289+
$propertyHooks .= sprintf(
1290+
<<<'EOT'
1291+
1292+
public %s $%s {
1293+
EOT,
1294+
$property->type()->asString(),
1295+
$property->name(),
1296+
);
1297+
1298+
if ($property->hasGetHook()) {
1299+
$propertyHooks .= sprintf(
1300+
<<<'EOT'
1301+
1302+
get {
1303+
return $this->__phpunit_getInvocationHandler()->invoke(
1304+
new \PHPUnit\Framework\MockObject\Invocation(
1305+
'%s', '$%s::get', [], '%s', $this, false
1306+
)
1307+
);
1308+
}
1309+
1310+
EOT,
1311+
$className,
1312+
$property->name(),
1313+
$property->type()->asString(),
1314+
);
1315+
}
1316+
1317+
if ($property->hasSetHook()) {
1318+
$propertyHooks .= sprintf(
1319+
<<<'EOT'
1320+
1321+
set (%s $value) {
1322+
$this->__phpunit_getInvocationHandler()->invoke(
1323+
new \PHPUnit\Framework\MockObject\Invocation(
1324+
'%s', '$%s::set', [$value], 'void', $this, false
1325+
)
1326+
);
1327+
}
1328+
1329+
EOT,
1330+
$property->type()->asString(),
1331+
$className,
1332+
$property->name(),
1333+
);
1334+
}
1335+
1336+
$propertyHooks .= <<<'EOT'
1337+
}
1338+
1339+
EOT;
1340+
1341+
}
1342+
1343+
return $propertyHooks;
1344+
}
11621345
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
<?php declare(strict_types=1);
2+
/*
3+
* This file is part of PHPUnit.
4+
*
5+
* (c) Sebastian Bergmann <[email protected]>
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\Framework\MockObject\Generator;
11+
12+
use SebastianBergmann\Type\Type;
13+
14+
/**
15+
* @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit
16+
*
17+
* @internal This class is not covered by the backward compatibility promise for PHPUnit
18+
*/
19+
final class Property
20+
{
21+
/**
22+
* @var non-empty-string
23+
*/
24+
private string $name;
25+
private Type $type;
26+
private bool $getHook;
27+
private bool $setHook;
28+
29+
/**
30+
* @param non-empty-string $name
31+
*/
32+
public function __construct(string $name, Type $type, bool $getHook, bool $setHook)
33+
{
34+
$this->name = $name;
35+
$this->type = $type;
36+
$this->getHook = $getHook;
37+
$this->setHook = $setHook;
38+
}
39+
40+
public function name(): string
41+
{
42+
return $this->name;
43+
}
44+
45+
public function type(): Type
46+
{
47+
return $this->type;
48+
}
49+
50+
public function hasGetHook(): bool
51+
{
52+
return $this->getHook;
53+
}
54+
55+
public function hasSetHook(): bool
56+
{
57+
return $this->setHook;
58+
}
59+
}

src/Framework/MockObject/Generator/templates/test_double_class.tpl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@ declare(strict_types=1);
22

33
{prologue}{class_declaration}
44
{
5-
{use_statements}{mocked_methods}}{epilogue}
5+
{use_statements}{property_hooks}{methods}}{epilogue}

src/Framework/MockObject/Runtime/Builder/InvocationMocker.php

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
use PHPUnit\Framework\MockObject\MethodNameNotConfiguredException;
3232
use PHPUnit\Framework\MockObject\MethodParametersAlreadyConfiguredException;
3333
use PHPUnit\Framework\MockObject\Rule;
34+
use PHPUnit\Framework\MockObject\Runtime\PropertyHook;
3435
use PHPUnit\Framework\MockObject\Stub\ConsecutiveCalls;
3536
use PHPUnit\Framework\MockObject\Stub\Exception;
3637
use PHPUnit\Framework\MockObject\Stub\ReturnArgument;
@@ -246,12 +247,16 @@ public function withAnyParameters(): self
246247
*
247248
* @return $this
248249
*/
249-
public function method(Constraint|string $constraint): self
250+
public function method(Constraint|PropertyHook|string $constraint): self
250251
{
251252
if ($this->matcher->hasMethodNameRule()) {
252253
throw new MethodNameAlreadyConfiguredException;
253254
}
254255

256+
if ($constraint instanceof PropertyHook) {
257+
$constraint = $constraint->asString();
258+
}
259+
255260
if (is_string($constraint)) {
256261
$this->configurableMethodNames ??= array_flip(
257262
array_map(
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<?php declare(strict_types=1);
2+
/*
3+
* This file is part of PHPUnit.
4+
*
5+
* (c) Sebastian Bergmann <[email protected]>
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\Framework\MockObject\Runtime;
11+
12+
use function sprintf;
13+
14+
/**
15+
* @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit
16+
*/
17+
final readonly class PropertyGetHook extends PropertyHook
18+
{
19+
/**
20+
* @return non-empty-string
21+
*
22+
* @internal This method is not covered by the backward compatibility promise for PHPUnit
23+
*/
24+
public function asString(): string
25+
{
26+
return sprintf(
27+
'$%s::get',
28+
$this->propertyName(),
29+
);
30+
}
31+
}

0 commit comments

Comments
 (0)