Skip to content

Commit a443152

Browse files
authored
Fix PHP 8.4 compatibility for getModifiers() on properties (#152)
* Add PHP 8.4 to build matrix and update documentation * Fix PHP 8.4 compatibility in ReflectionProperty getModifiers() method
1 parent 3814312 commit a443152

File tree

10 files changed

+177
-8
lines changed

10 files changed

+177
-8
lines changed

.github/copilot-instructions.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ php /tmp/test_reflection.php
8888
- `composer.json` - Dependencies: php >=8.2, nikic/php-parser ^5.0
8989
- `phpunit.xml.dist` - Test configuration (1536M memory limit)
9090
- `rector.php` - Code quality rules
91-
- `.github/workflows/phpunit.yml` - CI pipeline (PHP 8.2, 8.3)
91+
- `.github/workflows/phpunit.yml` - CI pipeline (PHP 8.2, 8.3, 8.4)
9292

9393
## Common Issues and Troubleshooting
9494

@@ -143,12 +143,12 @@ php /tmp/test_reflection.php
143143
- Core functionality requires nikic/php-parser for AST generation
144144
- Tests use Composer's autoloader for class location
145145
- Memory usage can be high for large codebases (configure php.ini accordingly)
146-
- Compatible with PHP 8.2+ (tested on 8.2, 8.3)
146+
- Compatible with PHP 8.2+ (tested on 8.2, 8.3, 8.4)
147147

148148
## CI/Build Pipeline Reference
149149

150150
The GitHub Actions pipeline (`.github/workflows/phpunit.yml`) runs:
151-
- Matrix testing: PHP 8.2, 8.3 on Ubuntu
151+
- Matrix testing: PHP 8.2, 8.3, 8.4 on Ubuntu
152152
- Dependency variations: lowest, highest
153153
- Standard `composer install` (works in CI with GitHub tokens)
154154
- PHPUnit test suite execution

.github/workflows/phpunit.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ name: "PHPUnit tests"
22

33
on:
44
pull_request:
5-
push:
65

76
jobs:
87
phpunit:
@@ -16,8 +15,9 @@ jobs:
1615
- "lowest"
1716
- "highest"
1817
php-version:
19-
- "8.2"
18+
- "8.4"
2019
- "8.3"
20+
- "8.2"
2121
operating-system:
2222
- "ubuntu-latest"
2323

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
/vendor/
22
composer.lock
3-
/.phpunit.result.cache
3+
/.phpunit.result.cache
4+
*.zip

composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
},
2424
"require": {
2525
"php": ">=8.2",
26-
"nikic/php-parser": "^5.0"
26+
"nikic/php-parser": "^5.4"
2727
},
2828
"require-dev": {
2929
"phpunit/phpunit": "^11.0.7",

phpunit.xml.dist

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,5 +12,8 @@
1212
</source>
1313
<php>
1414
<ini name="memory_limit" value="1536M" />
15+
<ini name="error_reporting" value="E_ALL" />
16+
<ini name="display_errors" value="1" />
17+
<ini name="display_startup_errors" value="1" />
1518
</php>
1619
</phpunit>

src/ReflectionProperty.php

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,23 @@ public function getModifiers(): int
199199
if ($this->isReadOnly()) {
200200
$modifiers += self::IS_READONLY;
201201
}
202+
if (PHP_VERSION_ID >= 80400 && $this->isAbstract()) {
203+
$modifiers += self::IS_ABSTRACT;
204+
}
205+
if (PHP_VERSION_ID >= 80400 && $this->isFinal()) {
206+
$modifiers += self::IS_FINAL;
207+
}
208+
if (PHP_VERSION_ID >= 80400 && $this->isProtectedSet()) {
209+
$modifiers += self::IS_PROTECTED_SET;
210+
}
211+
if (PHP_VERSION_ID >= 80400 && $this->isPrivateSet()) {
212+
$modifiers += self::IS_PRIVATE_SET;
213+
}
214+
215+
// Handle PHP 8.4+ asymmetric visibility modifiers
216+
// Note: IS_PRIVATE_SET and IS_PROTECTED_SET are only added for properties with explicit
217+
// asymmetric visibility syntax like "public private(set) $prop", not for regular readonly properties
218+
// TODO: Implement when nikic/php-parser supports asymmetric visibility syntax
202219

203220
return $modifiers;
204221
}
@@ -272,6 +289,18 @@ public function getDefaultValue(): mixed
272289
return $this->defaultValue;
273290
}
274291

292+
/**
293+
* @inheritDoc
294+
*/
295+
public function isAbstract(): bool
296+
{
297+
if ($this->propertyOrPromotedParam instanceof Property) {
298+
return $this->propertyOrPromotedParam->isAbstract();
299+
}
300+
301+
return false;
302+
}
303+
275304
/**
276305
* @inheritDoc
277306
*/
@@ -282,6 +311,22 @@ public function isDefault(): bool
282311
return true;
283312
}
284313

314+
/**
315+
* {@inheritDoc}
316+
*
317+
* @see Property::isFinal()
318+
*/
319+
public function isFinal(): bool
320+
{
321+
$explicitFinal = false;
322+
if ($this->propertyOrPromotedParam instanceof Property) {
323+
$explicitFinal = $this->propertyOrPromotedParam->isFinal();
324+
}
325+
326+
// Property with private(set) modifier is implicitly final
327+
return $explicitFinal || $this->isPrivateSet();
328+
}
329+
285330
/**
286331
* {@inheritDoc}
287332
*
@@ -293,6 +338,17 @@ public function isPrivate(): bool
293338
return $this->propertyOrPromotedParam->isPrivate();
294339
}
295340

341+
/**
342+
* @inheritDoc
343+
*
344+
* @see Property::isPrivateSet()
345+
* @see Param::isPrivateSet()
346+
*/
347+
public function isPrivateSet(): bool
348+
{
349+
return ($this->propertyOrPromotedParam->isPrivateSet() && !$this->propertyOrPromotedParam->isPrivate());
350+
}
351+
296352
/**
297353
* {@inheritDoc}
298354
*
@@ -304,6 +360,22 @@ public function isProtected(): bool
304360
return $this->propertyOrPromotedParam->isProtected();
305361
}
306362

363+
/**
364+
* @inheritDoc
365+
*
366+
* @see Property::isProtectedSet()
367+
* @see Param::isProtectedSet()
368+
*/
369+
public function isProtectedSet(): bool
370+
{
371+
/*
372+
* Behavior of readonly is to imply protected(set), not private(set).
373+
* A readonly property may still be explicitly declared private(set), in which case it will also be implicitly final
374+
*/
375+
return ($this->propertyOrPromotedParam->isProtectedSet() && !$this->propertyOrPromotedParam->isProtected())
376+
|| ($this->isPublic() && $this->isReadonly() && !$this->isPrivateSet() && !$this->propertyOrPromotedParam->isPublicSet());
377+
}
378+
307379
/**
308380
* {@inheritDoc}
309381
*
@@ -363,6 +435,14 @@ public function isInitialized(?object $object = null): bool
363435
return $this->hasDefaultValue();
364436
}
365437

438+
/**
439+
* @inheritDoc
440+
*/
441+
public function isVirtual(): bool
442+
{
443+
return $this->propertyOrPromotedParam->isVirtual();
444+
}
445+
366446
/**
367447
* {@inheritDoc}
368448
*/

tests/AbstractTestCase.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,9 @@ public static function getFilesToAnalyze(): \Generator
8585
if (PHP_VERSION_ID >= 80300) {
8686
yield 'PHP8.3' => [__DIR__ . '/Stub/FileWithClasses83.php'];
8787
}
88+
if (PHP_VERSION_ID >= 80400) {
89+
yield 'PHP8.4' => [__DIR__ . '/Stub/FileWithClasses84.php'];
90+
}
8891
}
8992

9093
/**

tests/ReflectionClassTest.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
use Go\ParserReflection\Stub\ClassWithPhp50ConstantsAndInheritance;
77
use Go\ParserReflection\Stub\ClassWithPhp50MagicConstants;
8+
use Go\ParserReflection\Stub\ClassWithPhp84PropertyHooks;
89
use Go\ParserReflection\Stub\SimplePhp50ClassWithMethodsAndProperties;
910
use Go\ParserReflection\Stub\ClassWithPhp50ScalarConstants;
1011
use Go\ParserReflection\Stub\ClassWithPhp50FinalKeyword;
@@ -69,6 +70,12 @@ public function testReflectionGetterParity(
6970
"See https://github.com/goaop/parser-reflection/issues/132"
7071
);
7172
}
73+
if ($parsedClass->getName() === ClassWithPhp84PropertyHooks::class && in_array($getterName, ['isIterable', 'isIterateable'], true)) {
74+
$this->markTestSkipped(
75+
"isIterable for class with hooks returns true.\n" .
76+
"See https://github.com/php/php-src/issues/20217"
77+
);
78+
}
7279
$this->assertSame(
7380
$expectedValue,
7481
$actualValue,

tests/ReflectionPropertyTest.php

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -175,10 +175,16 @@ public static function propertiesDataProvider(): \Generator
175175
*/
176176
protected static function getGettersToCheck(): array
177177
{
178-
return [
178+
$getters = [
179179
'isDefault', 'getName', 'getModifiers', 'getDocComment',
180180
'isPrivate', 'isProtected', 'isPublic', 'isStatic', 'isReadOnly', 'isInitialized',
181181
'hasType', 'hasDefaultValue', 'getDefaultValue', '__toString'
182182
];
183+
184+
if (PHP_VERSION_ID >= 80400) {
185+
array_push($getters, 'isAbstract', 'isProtectedSet', 'isPrivateSet', 'isFinal');
186+
}
187+
188+
return $getters;
183189
}
184190
}

tests/Stub/FileWithClasses84.php

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
<?php
2+
/**
3+
* Parser Reflection API
4+
*
5+
* @copyright Copyright 2025, Lisachenko Alexander <lisachenko.it@gmail.com>
6+
*
7+
* This source file is subject to the license that is bundled
8+
* with this source code in the file LICENSE.
9+
*/
10+
declare(strict_types=1);
11+
12+
namespace Go\ParserReflection\Stub;
13+
14+
/**
15+
* @see https://wiki.php.net/rfc/property-hooks
16+
*/
17+
18+
class ClassWithPhp84PropertyHooks
19+
{
20+
private string $backing = 'default';
21+
22+
public string $name {
23+
get => $this->backing;
24+
set => $this->backing = strtoupper($value);
25+
}
26+
}
27+
28+
/* Not supported yet
29+
interface InterfaceWithPhp84AbstractProperty
30+
{
31+
public string $name { get; }
32+
}
33+
*/
34+
35+
/**
36+
* https://wiki.php.net/rfc/asymmetric-visibility-v2
37+
*/
38+
class ClassWithPhp84AsymmetricVisibility
39+
{
40+
// These create a public-read, protected-write, write-once property.
41+
public protected(set) readonly string $explicitPublicWriteOnceProtectedProperty;
42+
public readonly string $implicitPublicReadonlyWriteOnceProperty;
43+
readonly string $implicitReadonlyWriteOnceProperty;
44+
45+
// These creates a public-read, private-set, write-once, final property.
46+
public private(set) readonly string $explicitPublicWriteOncePrivateProperty;
47+
private(set) readonly string $implicitPublicReadonlyWriteOncePrivateProperty;
48+
49+
// These create a public-read, public-write, write-once property.
50+
// While use cases for this configuration are likely few,
51+
// there's no intrinsic reason it should be forbidden.
52+
public public(set) readonly string $explicitPublicWriteOncePublicProperty;
53+
public(set) readonly string $implicitPublicReadonlyWriteOncePublicProperty;
54+
55+
// These create a private-read, private-write, write-once, final property.
56+
private private(set) readonly string $explicitPrivateWriteOncePrivateProperty;
57+
private readonly string $implicitPrivateReadonlyWriteOncePrivateProperty;
58+
59+
// These create a protected-read, protected-write, write-once property.
60+
protected protected(set) readonly string $explicitProtectedWriteOnceProtectedProperty;
61+
protected readonly string $implicitProtectedReadonlyWriteOnceProtectedProperty;
62+
63+
public function __construct(
64+
private(set) string $promotedPrivateSetStringProperty,
65+
protected(set) string $promotedProtectedSetStringProperty,
66+
protected private(set) int $promotedProtectedPrivateSetIntProperty,
67+
) {}
68+
69+
}

0 commit comments

Comments
 (0)