feat(property): add property override attribute support and validation

- Extend Override attribute to support properties in addition to methods
- Add comprehensive property override validation including parent property matching
- Implement property shadowing prevention for private parent properties
- Add trait property override validation at use site
- Support promoted and hooked properties in override validation
- Update error messages to include property targets for override rejection
- Add internal marker preservation for property override validation
- Implement final property and property hook inheritance restrictions
- Add parent property hook call syntax support with validation
- Support PHP 8.4 final property metadata preservation in reflection
- Add property hook inheritance and reflection test cases
- Update native class property validation to prevent private property hiding
- Implement interface property hook final restriction
- Add property get hook reference return validation error
- Update stub generation with property
master
韩天峰 1 day ago
parent 4c59d9a21b
commit 36fd0228f0
  1. 4
      docs/INTERFACE_PROPERTY_HOOKS.md
  2. 4
      docs/PROPERTY_HOOKS.md
  3. 15
      phpunit/code/inheritance_error_final_hooked_property.php
  4. 11
      phpunit/code/inheritance_error_final_property.php
  5. 15
      phpunit/code/inheritance_error_final_property_hook.php
  6. 11
      phpunit/code/inheritance_error_private_set_property.php
  7. 8
      phpunit/code/interface_property_hook_final.php
  8. 13
      phpunit/code/native-property-private-shadow.php
  9. 9
      phpunit/code/override-property-interface.php
  10. 7
      phpunit/code/override-property-missing.php
  11. 12
      phpunit/code/override-property-private-parent.php
  12. 12
      phpunit/code/override-property-trait-missing.php
  13. 41
      phpunit/code/override-property-valid.php
  14. 44
      phpunit/src/ClassTest.php
  15. 32
      phpunit/src/InheritanceErrorTest.php
  16. 8
      phpunit/src/InterfacePropertyHookTest.php
  17. 9
      phpunit/src/NativePropertyTest.php
  18. 14
      phpunit/src/NegativeCompatibilityTest.php
  19. 4
      src/Entity/PropertyDef.php
  20. 81
      src/Parser/MethodCallTrait.php
  21. 22
      src/Preprocessor.php
  22. 8
      src/Transform/CompileTimeAttributeRegistry.php
  23. 13
      src/Transform/FunctionAttributeLowering.php
  24. 10
      src/Transform/PropertyHookLowering.php
  25. 58
      src/Translator.php
  26. 16
      src/gen_stub.php
  27. 14
      tests/compiler/native-class/private-property-slots.phpt
  28. 52
      tests/compiler/object_property/final-property.phpt
  29. 29
      tests/compiler/object_property/override-property.phpt
  30. 92
      tests/compiler/object_property/property-hooks-inheritance.phpt
  31. 36
      tests/compiler/object_property/property-hooks-parent-call.phpt
  32. 4
      tests/compiler/object_property/property-hooks-reflection.phpt

@ -58,12 +58,12 @@ Interface Property Hook 不应伪装成普通属性或 lowering 后的普通方
## 4. PHPX 与 Zend 元数据
现有 `php::registerPropertyHooks()` 用于具有真实 AOT getter/setter 的具体类,不能复用于抽象 Interface Hook。
现有 `typephp_register_property_hooks()` 用于具有真实 AOT getter/setter 的具体类,不能复用于抽象 Interface Hook。
PHPX 增加独立 helper:
```cpp
php::registerAbstractPropertyHooks(
typephp_register_abstract_property_hooks(
zend_class_entry *interface_ce,
zend_property_info *property_info,
bool readable,

@ -44,7 +44,7 @@ public string $name {
`gen_stub.php` 声明属性并取得 `zend_property_info *` 后生成:
```cpp
php::registerPropertyHooks(
typephp_register_property_hooks(
class_entry,
property_info,
getter_method_name,
@ -56,7 +56,7 @@ php::registerPropertyHooks(
## 3. PHPX 注册流程
PHPX 的 `registerPropertyHooks()` 只在 PHP 8.4 及以上版本实现。
PHPX 的 `typephp_register_property_hooks()` 只在 PHP 8.4 及以上版本实现,并位于 TypePHP 专用 helper 中
### 3.1 查找 AOT 实现方法

@ -0,0 +1,15 @@
<?php
class FinalHookedPropertyParent
{
final public string $value {
get => 'parent';
}
}
class FinalHookedPropertyChild extends FinalHookedPropertyParent
{
public string $value {
get => 'child';
}
}

@ -0,0 +1,11 @@
<?php
class FinalPropertyParent
{
final public string $value = 'parent';
}
class FinalPropertyChild extends FinalPropertyParent
{
public string $value = 'child';
}

@ -0,0 +1,15 @@
<?php
class FinalPropertyHookParent
{
public string $value {
final get => 'parent';
}
}
class FinalPropertyHookChild extends FinalPropertyHookParent
{
public string $value {
get => 'child';
}
}

@ -0,0 +1,11 @@
<?php
class PrivateSetPropertyParent
{
public private(set) string $value = 'parent';
}
class PrivateSetPropertyChild extends PrivateSetPropertyParent
{
public string $value = 'child';
}

@ -0,0 +1,8 @@
<?php
interface FinalPropertyContract
{
final public string $value {
get;
}
}

@ -0,0 +1,13 @@
<?php
#[Native]
class NativePrivateShadowParent
{
private int $value = 1;
}
#[Native]
class NativePrivateShadowChild extends NativePrivateShadowParent
{
private int $value = 2;
}

@ -0,0 +1,9 @@
<?php
interface OverridePropertyInterface
{
#[\Override]
public string $value {
get;
}
}

@ -0,0 +1,7 @@
<?php
class OverridePropertyMissing
{
#[\Override]
public string $value = 'missing';
}

@ -0,0 +1,12 @@
<?php
class OverridePropertyPrivateParent
{
private string $value = 'private';
}
class OverridePropertyPrivateChild extends OverridePropertyPrivateParent
{
#[\Override]
public string $value = 'child';
}

@ -0,0 +1,12 @@
<?php
trait OverridePropertyMissingTrait
{
#[\Override]
public string $value = 'trait';
}
class OverridePropertyTraitConsumer
{
use OverridePropertyMissingTrait;
}

@ -0,0 +1,41 @@
<?php
class OverridePropertyValidParent
{
public string $plain = 'parent';
public string $promoted = 'parent';
public string $hooked {
get => 'parent';
set {
}
}
}
class OverridePropertyValidChild extends OverridePropertyValidParent
{
#[Override]
public string $plain = 'child';
#[\Override]
public string $hooked {
get => 'child';
}
public function __construct(
#[\Override]
public string $promoted = 'child',
) {
}
}
trait OverridePropertyValidTrait
{
#[\Override]
public string $plain = 'parent';
}
class OverridePropertyValidTraitChild extends OverridePropertyValidParent
{
use OverridePropertyValidTrait;
}

@ -460,6 +460,46 @@ class ClassTest extends \BaseTest
$this->compile('override-valid.php');
}
public function testOverrideAcceptsParentPropertiesIncludingPromotedAndHookedProperties(): void
{
$this->compile('override-property-valid.php');
}
public function testPropertyOverrideRequiresMatchingParentProperty(): void
{
$this->exec(
'OverridePropertyMissing::$value has #[\\Override] attribute, but no matching parent class property exists',
'override-property-missing.php',
);
}
public function testPropertyOverrideCannotHidePrivateParentProperty(): void
{
$this->exec(
'Declaration of `OverridePropertyPrivateChild::$value` conflicts with private property '
. '`OverridePropertyPrivateParent::$value`; property shadowing across inheritance is not allowed',
'override-property-private-parent.php',
);
}
public function testPropertyOverrideIsRejectedOnInterfaceProperty(): void
{
$this->exec(
'OverridePropertyInterface::$value has #[\\Override] attribute, '
. 'but no matching parent class property exists',
'override-property-interface.php',
);
}
public function testPropertyOverrideOnTraitIsValidatedAtUseSite(): void
{
$this->exec(
'OverridePropertyTraitConsumer::$value has #[\\Override] attribute, '
. 'but no matching parent class property exists',
'override-property-trait-missing.php',
);
}
public function testOverrideRequiresMatchingParentMethod(): void
{
$this->exec(
@ -500,10 +540,10 @@ class ClassTest extends \BaseTest
);
}
public function testOverrideRejectsNonMethodTargets(): void
public function testOverrideRejectsNonMethodOrPropertyTargets(): void
{
$this->expectException(\TypePhp\Exception\SyntaxError::class);
$this->expectExceptionMessage('Override can only be applied to methods');
$this->expectExceptionMessage('Override can only be applied to methods or properties');
$this->compile('override-invalid-target.php');
}

@ -148,6 +148,38 @@ class InheritanceErrorTest extends TestCase
$this->exec('Cannot override final method', 'inheritance_error_final_method.php');
}
public function testCannotOverrideFinalPropertyHook(): void
{
$this->exec(
'Cannot override final property hook FinalPropertyHookParent::$value::get()',
'inheritance_error_final_property_hook.php',
);
}
public function testCannotOverrideFinalProperty(): void
{
$this->exec(
'Cannot override final property FinalPropertyParent::$value',
'inheritance_error_final_property.php',
);
}
public function testCannotOverrideFinalHookedProperty(): void
{
$this->exec(
'Cannot override final property FinalHookedPropertyParent::$value',
'inheritance_error_final_hooked_property.php',
);
}
public function testPrivateSetPropertyIsImplicitlyFinal(): void
{
$this->exec(
'Cannot override final property PrivateSetPropertyParent::$value',
'inheritance_error_private_set_property.php',
);
}
public function testInterfaceMethodStaticMismatch()
{
$this->exec('must be compatible', 'interface_method_static_mismatch.php');

@ -76,6 +76,14 @@ final class InterfacePropertyHookTest extends TestCase
);
}
public function testFinalInterfacePropertyIsRejected(): void
{
$this->assertCompileError(
'interface_property_hook_final.php',
'Property in interface cannot be final',
);
}
public function testExplicitSetterParameterIsRejectedUntilItsIndependentTypeIsModeled(): void
{
$this->assertCompileError(

@ -162,6 +162,15 @@ class NativePropertyTest extends \BaseTest
$this->exec('Cannot access private property `value` of class `NativePrivateOwner`', 'native-property-private-other-class.php');
}
public function testNativeClassCannotHideParentPrivateProperty(): void
{
$this->exec(
'Declaration of `NativePrivateShadowChild::$value` conflicts with private property '
. '`NativePrivateShadowParent::$value`; property shadowing across inheritance is not allowed',
'native-property-private-shadow.php',
);
}
public function testCannotAccessProtectedNativePropertyFromUnrelatedClass(): void
{
$this->exec('Cannot access protected property `value` of class `NativeProtectedOwner`', 'native-property-protected-unrelated-class.php');

@ -185,6 +185,20 @@ function main(): void
PHP,
];
yield 'property get hook reference return' => [
'prepare',
'Property get hooks returning by reference are not supported',
<<<'PHP'
<?php
final class ReferencePropertyHook
{
public string $value {
&get => $this->value; // @diagnostic
}
}
PHP,
];
yield 'arrow function reference return' => [
'convert',
'Closure and arrow functions cannot return by reference',

@ -8,6 +8,7 @@
namespace TypePhp\Entity;
use PhpParser\NodeAbstract;
use PhpParser\Modifiers;
use TypePhp\ArrayDef\ArrayDefinition;
@ -34,6 +35,9 @@ class PropertyDef
public ?string $getter = null;
public ?string $setter = null;
public bool $virtual = false;
/** The source property carries TypePHP's compile-time #[Override] contract. */
public bool $overrideRequired = false;
public ?NodeAbstract $node = null;
public function __construct(string $name, int $flags, string $type, ?string $default = null, bool $nullable = false)
{

@ -17,9 +17,86 @@ use TypePhp\Exception\DynamicCall;
use TypePhp\Exception\PlaceHolder;
use TypePhp\Generator\Symbol;
use TypePhp\Resolver\Reflection;
use TypePhp\Transform\PropertyHookLowering;
trait MethodCallTrait
{
private function parseParentPropertyHookCall(Expr\StaticCall $expr): ?string
{
if (!$expr->class instanceof Expr\StaticPropertyFetch
|| !$expr->class->class instanceof Node\Name
|| strtolower($expr->class->class->toString()) !== 'parent'
|| !$expr->class->name instanceof Node\VarLikeIdentifier
|| !$expr->name instanceof Node\Identifier
) {
return null;
}
$kind = strtolower($expr->name->toString());
if ($kind !== 'get' && $kind !== 'set') {
return null;
}
if ($expr->isFirstClassCallable()) {
$this->fatalError($expr, 'Cannot create Closure for parent property hook call');
}
$property = $expr->class->name->toString();
$activeHook = $this->methodDef?->node?->getAttribute(PropertyHookLowering::METHOD_ATTRIBUTE);
if (!is_array($activeHook)) {
$this->fatalError(
$expr,
"Must not use parent::\${$property}::{$kind}() outside a property hook",
);
}
if (($activeHook['property'] ?? null) !== $property) {
$this->fatalError(
$expr,
"Must not use parent::\${$property}::{$kind}() in a different property (\$"
. ($activeHook['property'] ?? '') . ')',
);
}
if (($activeHook['kind'] ?? null) !== $kind) {
$this->fatalError(
$expr,
"Must not use parent::\${$property}::{$kind}() in a different property hook ("
. ($activeHook['kind'] ?? '') . ')',
);
}
if (!$this->classDef?->extends) {
$this->fatalError($expr, 'Cannot use "parent" when current class scope has no parent');
}
$parentClass = $this->classDef->extends;
$declaringClass = $parentClass;
$parentProperty = null;
while ($declaringClass !== '') {
$parentDef = $this->getClassDef($declaringClass);
if ($parentDef === null) {
break;
}
if ($parentDef->hasProperty($property)) {
$parentProperty = $parentDef->getProperty($property);
break;
}
$declaringClass = $parentDef->extends;
}
if ($parentProperty === null) {
$this->fatalError($expr, "Undefined property {$parentClass}::\${$property}");
}
if ($parentProperty->isPrivate()) {
$this->fatalError($expr, "Cannot access private property {$declaringClass}::\${$property}");
}
$hookKind = $kind === 'get' ? 'ZEND_PROPERTY_HOOK_GET' : 'ZEND_PROPERTY_HOOK_SET';
$function = 'typephp_get_parent_property_hook('
. $this->getClassEntryPtr($parentClass) . ', '
. $this->getLiteralString($property) . ', ' . $hookKind . ')';
if ($expr->args === []) {
return 'this_.call(' . $function . ')';
}
return 'this_.call(' . $function . ', ' . $this->parseCallArgs($expr->args) . ')';
}
protected function runtimeMethodRequiresDynamicScope(
string $class,
string $method,
@ -756,6 +833,10 @@ trait MethodCallTrait
protected function parseStaticCall(Expr\StaticCall $expr): string
{
$this->validateImmutableCall($expr);
$parentPropertyHookCall = $this->parseParentPropertyHookCall($expr);
if ($parentPropertyHookCall !== null) {
return $parentPropertyHookCall;
}
if (!$this->isNameExpr($expr->class)) {
$this->assertNotNativeObjectDynamicClassTarget($expr->class, $expr);
}

@ -1454,6 +1454,11 @@ class Preprocessor extends CompilerBase
}
$propDef = new PropertyDef($name, $flags, $type, $default, $nullable);
$propDef->overrideRequired = (bool) $errorNode->getAttribute(
FunctionAttributeLowering::OVERRIDE_ATTRIBUTE,
false,
);
$propDef->node = $errorNode;
if ($typeNode !== null
&& !$typeNode instanceof NullableType
&& !$typeNode instanceof UnionType
@ -1737,6 +1742,9 @@ class Preprocessor extends CompilerBase
foreach ($v->hooks as $hook) {
$kind = strtolower($hook->name->toString());
if ($kind === 'get') {
if ($hook->byRef) {
$this->fatalError($hook, 'Property get hooks returning by reference are not supported');
}
$propDef->getter = PropertyHookLowering::getterName($propName);
} elseif ($kind === 'set') {
$propDef->setter = PropertyHookLowering::setterName($propName);
@ -1958,6 +1966,9 @@ class Preprocessor extends CompilerBase
if ($property->flags & (Modifiers::PRIVATE | Modifiers::PROTECTED)) {
$this->fatalError($property, 'Property in interface cannot be protected or private');
}
if ($property->flags & Modifiers::FINAL) {
$this->fatalError($property, 'Property in interface cannot be final');
}
if ($property->flags & Modifiers::STATIC) {
$this->fatalError($property, 'Cannot declare hooks for static property');
}
@ -1973,6 +1984,9 @@ class Preprocessor extends CompilerBase
}
$kind = strtolower($hook->name->toString());
if ($kind === 'get') {
if ($hook->byRef) {
$this->fatalError($hook, 'Property get hooks returning by reference are not supported');
}
if ($readable) {
$this->fatalError($hook, 'Cannot redeclare property hook "get"');
}
@ -1997,6 +2011,14 @@ class Preprocessor extends CompilerBase
$nullable = $property->type instanceof NullableType;
foreach ($property->props as $prop) {
$name = $this->parseIdentifier($prop->name);
if ($property->getAttribute(FunctionAttributeLowering::OVERRIDE_ATTRIBUTE, false)) {
$this->fatalCompileTimeAttribute(
$property,
'Override',
"{$this->interfaceDef->getNamespacedName(false)}::\${$name} has #[\\Override] attribute, "
. 'but no matching parent class property exists',
);
}
if ($this->interfaceDef->hasProperty($name)) {
$this->fatalError($property, "Duplicate property `{$name}`");
}

@ -88,7 +88,13 @@ final class CompileTimeAttributeRegistry
$add($name, [self::TARGET_PARAMETER], $name . ' can only be applied to function or method parameters', self::ARGUMENTS_NONE, self::PHASE_FUNCTION_LEAVE);
}
$add('Validate', [self::TARGET_PARAMETER], 'Validate can only be applied to function or method parameters', self::ARGUMENTS_VALIDATE, self::PHASE_FUNCTION_LEAVE);
$add('Override', [self::TARGET_METHOD], 'Override can only be applied to methods', self::ARGUMENTS_NONE, self::PHASE_ENTER);
$add(
'Override',
[self::TARGET_METHOD, self::TARGET_PROPERTY],
'Override can only be applied to methods or properties',
self::ARGUMENTS_NONE,
self::PHASE_ENTER,
);
$add('MustUse', [self::TARGET_FUNCTION, self::TARGET_METHOD], 'MustUse can only be applied to functions or methods', self::ARGUMENTS_NONE, self::PHASE_ENTER);
$add('Immutable', [self::TARGET_METHOD, self::TARGET_PROPERTY_HOOK, self::TARGET_PARAMETER], 'Immutable can only be applied to methods, property hooks, or function parameters', self::ARGUMENTS_NONE, self::PHASE_ENTER);
$add('Hot', [self::TARGET_FUNCTION, self::TARGET_METHOD], 'Hot can only be applied to functions or methods', self::ARGUMENTS_NONE, self::PHASE_ENTER, true, ['Cold']);

@ -37,8 +37,19 @@ final class FunctionAttributeLowering
$node->setAttribute(self::IMMUTABLE_ATTRIBUTE, true);
continue;
}
if ($name === 'Override'
&& ($node instanceof Stmt\Property || ($node instanceof Node\Param && $node->isPromoted()))
) {
// Property override validation needs the fully linked parent
// class, so preserve only an internal marker and consume the
// compile-time attribute before stub generation.
CompileTimeAttribute::consume($node, $name);
$node->setAttribute(self::OVERRIDE_ATTRIBUTE, true);
continue;
}
if (!$node instanceof Stmt\Function_ && !$node instanceof Stmt\ClassMethod) {
throw new SyntaxError($name . ' can only be applied to functions or methods');
$target = $name === 'Override' ? 'methods or properties' : 'functions or methods';
throw new SyntaxError($name . ' can only be applied to ' . $target);
}
CompileTimeAttribute::consume($node, $name);
$node->setAttribute('typephp' . $name, true);

@ -101,7 +101,11 @@ final class PropertyHookLowering
}
$method = new Stmt\ClassMethod($methodName, [
'flags' => Modifiers::PUBLIC | Modifiers::FINAL,
// Hidden methods participate in inheritance exactly like the
// corresponding hooks. Marking every generated method final
// rejects legal child hook overrides and also forces PHPX to
// erase final unconditionally from the Zend hook metadata.
'flags' => Modifiers::PUBLIC | ($hook->flags & Modifiers::FINAL),
'byRef' => $kind === 'get' && $hook->byRef,
'params' => $params,
'returnType' => $returnType,
@ -165,7 +169,9 @@ final class PropertyHookLowering
private static function visibilityMarker(string $name, array $attributes): Stmt\ClassMethod
{
return new Stmt\ClassMethod($name, [
'flags' => Modifiers::PUBLIC | Modifiers::FINAL,
// A child declaration may replace the generated visibility marker.
// This method is metadata for the object handler, not a final PHP API.
'flags' => Modifiers::PUBLIC,
'returnType' => new Node\Identifier('void'),
'stmts' => [],
], $attributes);

@ -46,6 +46,7 @@ use TypePhp\Resolver\ClassConstantValueTrait;
use TypePhp\Transform\Visitor;
use TypePhp\Transform\ConstructorLowering;
use TypePhp\Transform\ConstantExpressionValidationVisitor;
use TypePhp\Transform\PropertyHookLowering;
use TypePhp\Transform\RuntimeAttributeFactoryLowering;
use TypePhp\Transform\VoidCastValidationVisitor;
use PhpParser\Modifiers;
@ -4051,7 +4052,17 @@ CODE;
}
if ($methodDef->flags & Modifiers::FINAL) {
_final_error:
$hook = $v->getAttribute(PropertyHookLowering::METHOD_ATTRIBUTE);
if (is_array($hook)
&& isset($hook['property'], $hook['kind'])
&& is_string($hook['property'])
&& is_string($hook['kind'])
) {
$message = 'Cannot override final property hook '
. $extends . '::$' . $hook['property'] . '::' . $hook['kind'] . '()';
} else {
$message = 'Cannot override final method `' . $extends . '::' . $name . '()`';
}
$this->fatalGeneratedMethodAttributeIfAny($v, $message, $extends, $name);
$this->fatalError($v,
$message);
@ -4804,6 +4815,7 @@ CODE;
{
$classDef = $this->classDef;
$className = $this->getFullClassName();
$matchedOverrides = [];
$chainNode = $classDef;
while ($chainNode->extends && !$chainNode->inheritedFromInternalClass) {
$parentClass = $chainNode->extends;
@ -4814,22 +4826,29 @@ CODE;
foreach ($this->classDef->properties as $name => $childProp) {
if ($chainNode->hasProperty($name)) {
$parentProp = $chainNode->getProperty($name);
// A parent private property would be a separate PHP slot
// hidden by the child declaration. Zend-backed TypePHP
// classes still forbid that dual-slot model, while Native
// classes have declaring-class-qualified C++ fields and
// can represent it without a runtime property table.
// Public/protected declarations instead
// describe the same inherited property slot and must obey
// PHP-compatible type, visibility and readonly rules.
// TypePHP deliberately forbids the two independent slots
// PHP would create when a child hides a parent private
// property. This applies equally to Zend-backed and Native
// classes, even though Native storage could represent it.
if ($parentProp->flags & Modifiers::PRIVATE) {
if ($classDef->nativeObject) {
continue;
}
$this->fatalError($classStmt,
"Declaration of `{$className}::\${$name}` conflicts with private property " .
"`{$parentClass}::\${$name}`; property shadowing across inheritance is not allowed");
}
$matchedOverrides[$name] = true;
// PHP inherits get and set independently. A child may
// override only one hook, or redeclare the property
// without hooks while retaining both parent hooks.
$childProp->getter ??= $parentProp->getter;
$childProp->setter ??= $parentProp->setter;
// PHP 8.4 treats private(set) properties as implicitly
// final because a child cannot widen their write scope.
if ($parentProp->flags & (Modifiers::FINAL | Modifiers::PRIVATE_SET)) {
$this->fatalError(
$classStmt,
"Cannot override final property {$parentClass}::\${$name}"
);
}
if ($childProp->type !== $parentProp->type || $childProp->class !== $parentProp->class) {
$this->fatalError($classStmt,
"Declaration of `{$className}::\${$name}` must be compatible " .
@ -4853,6 +4872,23 @@ CODE;
}
}
}
if ($classStmt instanceof Node\Stmt\Trait_) {
// A trait property is validated after it is composed into the
// consuming class, where the actual parent chain is known.
return;
}
foreach ($classDef->properties as $name => $property) {
if (!$property->overrideRequired || isset($matchedOverrides[$name])) {
continue;
}
$this->fatalCompileTimeAttribute(
$property->node ?? $classStmt,
'Override',
"{$className}::\${$name} has #[\\Override] attribute, "
. 'but no matching parent class property exists',
);
}
}
private function getPropertySetVisibilityRank(PropertyDef $property): int

@ -3602,6 +3602,7 @@ class PropertyInfo extends VariableLike
$this->phpVersionIdMinimumCompatibility
);
$code .= $stringInit;
$code .= "\ttypephp_prepare_property_redeclaration(class_entry, {$nameCode});\n";
if ($this->exposedDocComment) {
$commentCode = "property_{$propertyName}_comment";
@ -3635,7 +3636,7 @@ class PropertyInfo extends VariableLike
if ($this->abstractHooks) {
$getter = isset($this->hooks['get']) ? 'true' : 'false';
$setter = isset($this->hooks['set']) ? 'true' : 'false';
$code .= "\tphp::registerAbstractPropertyHooks(class_entry, property_{$propertyName}, {$getter}, {$setter});\n";
$code .= "\ttypephp_register_abstract_property_hooks(class_entry, property_{$propertyName}, {$getter}, {$setter});\n";
} else {
$getter = isset($this->hooks['get'])
? 'std::string_view{"' . addslashes($this->hooks['get']) . '"}'
@ -3643,7 +3644,7 @@ class PropertyInfo extends VariableLike
$setter = isset($this->hooks['set'])
? 'std::string_view{"' . addslashes($this->hooks['set']) . '"}'
: 'std::string_view{}';
$code .= "\tphp::registerPropertyHooks(class_entry, property_{$propertyName}, {$getter}, {$setter});\n";
$code .= "\ttypephp_register_property_hooks(class_entry, property_{$propertyName}, {$getter}, {$setter});\n";
}
}
@ -3658,7 +3659,9 @@ class PropertyInfo extends VariableLike
$flags->addForVersionsAbove("ZEND_ACC_STATIC", PHP_70_VERSION_ID);
}
if ($this->flags & Modifiers::FINAL) {
// PHP 8.4 makes private(set) properties implicitly final. Preserve
// that fact in Zend metadata as well as in TypePHP's override checks.
if ($this->flags & (Modifiers::FINAL | Modifiers::PRIVATE_SET)) {
$flags->addForVersionsAbove("ZEND_ACC_FINAL", PHP_84_VERSION_ID);
}
@ -4068,6 +4071,13 @@ class ClassInfo {
$code .= $property->getDeclaration($allConstInfos);
}
if ($this->type === 'class' && isset($this->extends[0])) {
// Internal classes are linked to their parent before their own
// properties are declared. Restore PHP's per-hook inheritance
// after those declarations have replaced inherited metadata.
$code .= "\ttypephp_finalize_property_hook_inheritance(class_entry);\n";
}
// Zend merges interface property contracts immediately. Declare the
// class/interface's own properties first so an implementation can
// replace a virtual abstract contract with its real property slot.

@ -1,37 +1,37 @@
--TEST--
Native class: parent and child private properties use independent native slots
Native class: inherited methods retain access to distinct private properties
--FILE--
<?php
#[Native]
class NativePrivateBase
{
private int $value = 10;
private int $baseValue = 10;
public function baseValue(): int
{
return $this->value;
return $this->baseValue;
}
public function setBaseValue(int $value): void
{
$this->value = $value;
$this->baseValue = $value;
}
}
#[Native]
class NativePrivateChild extends NativePrivateBase
{
private int $value = 20;
private int $childValue = 20;
public function childValue(): int
{
return $this->value;
return $this->childValue;
}
public function setChildValue(int $value): void
{
$this->value = $value;
$this->childValue = $value;
}
}

@ -0,0 +1,52 @@
--TEST--
PHP 8.4 final properties preserve runtime and reflection metadata
--FILE--
<?php
class FinalPropertyMetadata
{
final public string $plain = 'plain';
final public string $hooked {
get => 'hooked';
set {
}
}
final public string $finalHook {
final get => 'both';
}
public private(set) string $privateSet = 'private-set';
}
function main(): void
{
$object = new FinalPropertyMetadata();
var_dump($object->plain, $object->hooked, $object->finalHook, $object->privateSet);
foreach (['plain', 'hooked', 'finalHook', 'privateSet'] as $name) {
$property = new ReflectionProperty(FinalPropertyMetadata::class, $name);
echo $name,
':final=', $property->isFinal() ? 'yes' : 'no',
':hooks=', $property->hasHooks() ? 'yes' : 'no',
':virtual=', $property->isVirtual() ? 'yes' : 'no',
"\n";
foreach ($property->getHooks() as $kind => $hook) {
echo $name, '-', $kind, ':', $hook->isFinal() ? 'final' : 'open', "\n";
}
}
}
?>
--EXPECT--
string(5) "plain"
string(6) "hooked"
string(4) "both"
string(11) "private-set"
plain:final=yes:hooks=no:virtual=no
hooked:final=yes:hooks=yes:virtual=yes
hooked-get:open
hooked-set:open
finalHook:final=yes:hooks=yes:virtual=yes
finalHook-get:final
privateSet:final=yes:hooks=no:virtual=no

@ -0,0 +1,29 @@
--TEST--
TypePHP Override attribute validates and is consumed from properties
--FILE--
<?php
class OverridePropertyRuntimeParent
{
public string $value = 'parent';
}
class OverridePropertyRuntimeChild extends OverridePropertyRuntimeParent
{
#[Override]
public string $value = 'child';
}
function main(): void
{
$object = new OverridePropertyRuntimeChild();
var_dump($object->value);
$property = new ReflectionProperty(OverridePropertyRuntimeChild::class, 'value');
var_dump($property->getAttributes(\Override::class));
}
?>
--EXPECT--
string(5) "child"
array(0) {
}

@ -0,0 +1,92 @@
--TEST--
PHP 8.4 property hooks inherit and override get/set independently
--FILE--
<?php
class ParentHook
{
protected string $stored = '';
public string $value {
get => 'parent:' . $this->stored;
set {
$this->stored = 'set:' . $value;
}
}
public function writeFromParent(string $value): void
{
$this->value = $value;
}
public function readFromParent(): string
{
return $this->value;
}
}
class ChildHook extends ParentHook
{
public string $value {
get => 'child:' . $this->stored;
}
}
class PlainHookChild extends ParentHook
{
public string $value;
}
function writeHookDynamically(mixed $object, string $value): void
{
$object->value = $value;
}
function readHookDynamically(mixed $object): string
{
return $object->value;
}
function main(): void
{
$child = new ChildHook();
$child->value = 'direct';
var_dump($child->value, $child->readFromParent());
$child->writeFromParent('parent');
var_dump($child->value, $child->readFromParent());
writeHookDynamically($child, 'dynamic');
var_dump(readHookDynamically($child));
$property = new ReflectionProperty(ChildHook::class, 'value');
foreach ($property->getHooks() as $kind => $hook) {
echo $kind, ':', $hook->getDeclaringClass()->getName(), ':', $hook->isFinal() ? 'final' : 'open', "\n";
}
$plain = new PlainHookChild();
$plain->value = 'plain';
var_dump($plain->value, $plain->readFromParent());
writeHookDynamically($plain, 'plain-dynamic');
var_dump(readHookDynamically($plain));
$plainProperty = new ReflectionProperty(PlainHookChild::class, 'value');
foreach ($plainProperty->getHooks() as $kind => $hook) {
echo 'plain-', $kind, ':', $hook->getDeclaringClass()->getName(), "\n";
}
}
?>
--EXPECT--
string(16) "child:set:direct"
string(16) "child:set:direct"
string(16) "child:set:parent"
string(16) "child:set:parent"
string(17) "child:set:dynamic"
get:ChildHook:open
set:ParentHook:open
string(16) "parent:set:plain"
string(16) "parent:set:plain"
string(24) "parent:set:plain-dynamic"
plain-get:ParentHook
plain-set:ParentHook

@ -0,0 +1,36 @@
--TEST--
Property hooks may call the corresponding parent get and set hook
--FILE--
<?php
class ParentPropertyHookCall
{
protected string $stored = '';
public string $value {
get => 'parent-get:' . $this->stored;
set {
$this->stored = 'parent-set:' . $value;
}
}
}
class ChildPropertyHookCall extends ParentPropertyHookCall
{
public string $value {
get => parent::$value::get() . ':child-get';
set {
parent::$value::set($value . ':child-set');
}
}
}
function main(): void
{
$point = new ChildPropertyHookCall();
$point->value = 'data';
var_dump($point->value);
}
?>
--EXPECT--
string(46) "parent-get:parent-set:data:child-set:child-get"

@ -6,7 +6,7 @@ PHP 8.4 property hooks expose Zend reflection metadata
final class ReflectedPropertyHooks
{
public string $virtual {
get => 'value';
final get => 'value';
set {
}
}
@ -25,5 +25,5 @@ function main(): void
--EXPECT--
bool(true)
bool(true)
get:$virtual::get:not-final
get:$virtual::get:final
set:$virtual::set:not-final

Loading…
Cancel
Save