refactor(php): 重构属性写入逻辑以统一动态属性访问

- 将动态属性读写操作收敛到 emitDynamicPropertyRead/Write 辅助方法
- 引入 PropertyWriteTarget DTO 来统一属性写入目标的准备和检查
- 通过 preparePropertyWriteTarget 统一处理对象和静态属性写入目标
- 使用 assertCanAssignPropertyWrite 进行属性写入的静态检查
- 通过 wrapPropertyWriteTypeCheck 实现运行时类型检查包装
- 更新复合赋值和自增自减操作符的属性访问逻辑
- 在文档中记录属性写入路径的重构计划和当前进展
pull/4/head
韩天峰 2 months ago
parent 7d601a3f8c
commit c05c63b8aa
  1. 19
      docs/REFACTORING_PLAN.md
  2. 46
      src/Php/CompilerBase.php
  3. 37
      src/Php/Parser/AssignOpTrait.php
  4. 20
      src/Php/Resolver/PropertyWriteTarget.php

@ -239,12 +239,22 @@
- `findNativeStaticProperty()` 已通过 resolver 的 `resolveNativeStaticProperty()` 显式接口完成静态属性检查。
- nullsafe 属性链检查已通过 resolver 的 `resolveNullsafePropertyChain()` 显式接口完成类名推进和可见性检查。
- 旧的 `CompilerBase::findNativeProperty()` 泛型入口已移除,避免后续继续扩散带 `$static` 布尔参数的访问模式。
- 旧的 `findNativeStaticProperty(..., &$class)` by-ref 协议已移除,静态属性读取改为 `StaticPropertyFetchTarget``StaticPropertyFetchResolution` 显式 DTO。
- 实例属性读取的目标类解析已抽离为 `InstancePropertyFetchTarget`,`getPropertyIdentifier()` 不再混合目标解析、resolver 调用和动态 fallback 分支。
- 原先散落在 AST attribute 上的 `nativeProperty`、`nativePropertyDef`、`nativeClassDef` 已合并为 `NativePropertyAccess` metadata,避免三者状态不一致。
- `nativePropertyVar`、`nativePropertyValueSource`、`objectProps`、`staticPropRefs` 的直接读写已收敛到 helper 方法;业务路径不再通过字符串内容判断属性访问语义。
- typed instance property hoist 和 typed static property ref 注册已提取为独立 helper,当前仍保持原有生成代码结构。
- `CompilerBase::isSameClassName()`、`isSameOrSubclassOf()`、`canAccessProtectedProperty()` 已委托 resolver,避免规则继续扩散。
- 已添加 `prepare/convert/idle` 编译阶段状态;`PropertyAccessResolver` 只能在 convert 阶段创建和使用,避免预处理阶段误用不完整的类表状态。
- `PropertyAccessResolver` 已改为依赖 `PropertyAccessContext` 只读接口,而不是完整依赖 `CompilerBase` 大类。
- 已建立 `PropertyAssignTypeInfo`,抽离 typed property 写入的纯 metadata 计算,包括固定类型属性判断、默认值、runtime typecheck 列表和类型字符串。
- 当前迁移保持生成代码不变,后续阶段再统一 read/write emitter。
状态:
- 阶段 1 已基本收尾。后续除非发现属性读取 resolver 绕过或行为回归,否则不再继续扩大阶段 1 范围。
- 阶段 2 已开始;属性写入相关的 assignment、compound assignment、inc/dec、unset、refval 路径仍需继续统一。
验证:
- `phpunit/src/NativePropertyTest.php`
@ -277,6 +287,15 @@
- nullsafe 写上下文错误测试。
- private/protected/static 属性错误测试。
当前进展:
- 阶段 2 已开始。
- 已建立 `PropertyWriteTarget` 作为属性写入路径的最小目标 DTO。
- 普通赋值和 `??=` 已接入 `preparePropertyWriteTarget()`,在写入前统一完成属性 target 准备,并通过 `assertCanAssignPropertyWrite()``wrapPropertyWriteTypeCheck()` 执行静态检查和 runtime typecheck 包装。
- dynamic object property 的 `getProperty()` / `setProperty()` 生成已收敛到 `emitDynamicPropertyRead()` / `emitDynamicPropertyWrite()` helper;普通动态属性赋值、复合赋值、自增自减已复用该入口。
- 复合赋值的动态属性路径已接入 `preparePropertyWriteTarget()`,先统一完成属性写入 target 准备和静态检查。
- 当前步骤保持生成代码不变;后续继续收敛 dynamic/native property write emitter、compound assignment、inc/dec、unset 和 refval 路径。
### 阶段 3:类型系统模块化
目标:

@ -49,6 +49,7 @@ use PhpAot\Php\Resolver\NativePropertyAccess;
use PhpAot\Php\Resolver\PropertyAccessResult;
use PhpAot\Php\Resolver\PropertyAccessResolver;
use PhpAot\Php\Resolver\PropertyAssignTypeInfo;
use PhpAot\Php\Resolver\PropertyWriteTarget;
use PhpAot\Php\Resolver\StaticPropertyFetchResolution;
use PhpAot\Php\Resolver\StaticPropertyFetchTarget;
use PhpParser\Modifiers;
@ -2828,10 +2829,10 @@ class CompilerBase extends \PhpAot\Core\Translator implements PropertyAccessCont
$this->addLocalVar($tmpVar, self::TYPE_VAR);
if ($isPre) {
$this->context->beforeStmtLines[] = "{$tmpVar} = {$obj}.getProperty({$propName}) {$op} 1; {$obj}.setProperty({$propName}, {$tmpVar});";
$this->context->beforeStmtLines[] = "{$tmpVar} = " . $this->emitDynamicPropertyRead($obj, $propName) . " {$op} 1; " . $this->emitDynamicPropertyWrite($obj, $propName, $tmpVar) . ';';
} else {
$this->context->beforeStmtLines[] = "{$tmpVar} = {$obj}.getProperty({$propName});";
$this->context->afterStmtLines[] = "{$obj}.setProperty({$propName}, {$tmpVar} {$op} 1);";
$this->context->beforeStmtLines[] = "{$tmpVar} = " . $this->emitDynamicPropertyRead($obj, $propName) . ';';
$this->context->afterStmtLines[] = $this->emitDynamicPropertyWrite($obj, $propName, "{$tmpVar} {$op} 1") . ';';
}
return $tmpVar;
@ -4701,6 +4702,35 @@ class CompilerBase extends \PhpAot\Core\Translator implements PropertyAccessCont
$this->assertCanAssignObjectProperty($left, $right, 'static property');
}
protected function preparePropertyWriteTarget(NodeAbstract $left): ?PropertyWriteTarget
{
if ($left instanceof Expr\PropertyFetch) {
if ($this->isIdExpr($left->name)) {
$this->getPropertyIdentifier($left, $left->var, $left->name);
}
return new PropertyWriteTarget($left, 'object property');
}
if ($left instanceof Expr\StaticPropertyFetch) {
if ($this->isIdExpr($left->name)) {
$this->resolveNativeStaticPropertyFetch($left);
}
return new PropertyWriteTarget($left, 'static property');
}
return null;
}
protected function assertCanAssignPropertyWrite(PropertyWriteTarget $target, Expr $right): void
{
$this->assertCanAssignObjectProperty($target->node, $right, $target->label);
}
protected function wrapPropertyWriteTypeCheck(PropertyWriteTarget $target, Expr $right, string $rightExpr): string
{
return $this->wrapObjectPropertyAssignTypeCheck($target->node, $right, $rightExpr);
}
private function assertCanAssignObjectProperty(NodeAbstract $left, Expr $right, string $label): void
{
$def = $this->getNativePropertyDef($left);
@ -5442,6 +5472,16 @@ class CompilerBase extends \PhpAot\Core\Translator implements PropertyAccessCont
}
}
protected function emitDynamicPropertyRead(string $object, string $property): string
{
return "{$object}.getProperty({$property})";
}
protected function emitDynamicPropertyWrite(string $object, string $property, string $value): string
{
return "{$object}.setProperty({$property}, {$value})";
}
protected function getChainedFunc(string $op): string
{
return match ($op) {

@ -54,7 +54,7 @@ trait AssignOpTrait
$tmp = $this->genTmpVarName();
$this->addLocalVar($tmp, self::TYPE_VAR);
// Comma expression: store RHS → execute side effect → evaluate to stored value
return '((' . $tmp . ' = ' . $rightExpr . ', ' . "{$array}.setProperty({$propName}, {$tmp})" . '), ' . $tmp . ')';
return '((' . $tmp . ' = ' . $rightExpr . ', ' . $this->emitDynamicPropertyWrite($array, $propName, $tmp) . '), ' . $tmp . ')';
}
protected function parseRightAssociativeAssign(NodeAbstract $left, Expr\Assign $right): string
@ -152,6 +152,7 @@ trait AssignOpTrait
$this->context->inAssignExpr = true;
$var = $this->parseIdentifier($left);
$this->context->inAssignExpr = $oriInAssignExpr;
$propertyWriteTarget = $this->preparePropertyWriteTarget($left);
if ($var === 'this_') {
$this->fatalError($left, 'Cannot re-assign $this');
}
@ -273,15 +274,13 @@ trait AssignOpTrait
return $this->parseAssignArrayDim($left, $right);
}
if ($this->isPropertyFetch($left)) {
$this->assertCanAssignObjectProp($left, $right);
} elseif ($this->isStaticPropertyFetch($left)) {
$this->assertCanAssignStaticProp($left, $right);
if ($propertyWriteTarget !== null) {
$this->assertCanAssignPropertyWrite($propertyWriteTarget, $right);
}
$rightExpr = $this->parseAssignRightExpr($right);
if ($this->isPropertyFetch($left) || $this->isStaticPropertyFetch($left)) {
$rightExpr = $this->wrapObjectPropertyAssignTypeCheck($left, $right, $rightExpr);
if ($propertyWriteTarget !== null) {
$rightExpr = $this->wrapPropertyWriteTypeCheck($propertyWriteTarget, $right, $rightExpr);
}
$leftExprType = $this->detectTypeOfExpr($left);
$rightExprType = $this->detectTypeOfExpr($right);
@ -332,6 +331,7 @@ trait AssignOpTrait
$var = $this->parseIdentifier($node->var);
$this->context->inAssignExpr = $oriInAssignExpr;
$expr = $this->parseIdentifier($node->expr);
$propertyWriteTarget = $this->preparePropertyWriteTarget($node->var);
$this->guardLiteralDivisionByZero($node->expr, $op);
if ($this->isVarExpr($node->var)) {
@ -398,19 +398,23 @@ trait AssignOpTrait
}
if ($this->isPropertyFetch($node->var) and !$this->isNativePropertyAccess($node->var)) {
if ($propertyWriteTarget !== null) {
$this->assertCanAssignPropertyWrite($propertyWriteTarget, $node->expr);
}
$obj = $this->parseIdentifier($node->var->var);
$propName = $this->identifierToStr($node->var->name, literal: true);
$binaryOp = $this->removeAssignOp($op);
$tmpVar = $this->genTmpVarName();
$this->addLocalVar($tmpVar, self::TYPE_VAR);
$readProperty = $this->emitDynamicPropertyRead($obj, $propName);
if ($this->isAssignOpConcat($op)) {
$this->context->beforeStmtLines[] = "{$tmpVar} = php::concat({$obj}.getProperty({$propName}), {$expr});";
$this->context->beforeStmtLines[] = "{$tmpVar} = php::concat({$readProperty}, {$expr});";
} elseif ($this->isAssignOpPow($op)) {
$this->context->beforeStmtLines[] = "{$tmpVar} = php::fn::pow({$obj}.getProperty({$propName}), {$expr});";
$this->context->beforeStmtLines[] = "{$tmpVar} = php::fn::pow({$readProperty}, {$expr});";
} else {
$this->context->beforeStmtLines[] = "{$tmpVar} = {$obj}.getProperty({$propName}) {$binaryOp} ({$expr});";
$this->context->beforeStmtLines[] = "{$tmpVar} = {$readProperty} {$binaryOp} ({$expr});";
}
$this->context->afterStmtLines[] = "{$obj}.setProperty({$propName}, {$tmpVar});";
$this->context->afterStmtLines[] = $this->emitDynamicPropertyWrite($obj, $propName, $tmpVar) . ';';
return $tmpVar;
}
@ -595,16 +599,15 @@ trait AssignOpTrait
$this->context->inAssignExpr = true;
$var = $this->parseIdentifier($expr->var);
$this->context->inAssignExpr = $inAssignExpr;
$propertyWriteTarget = $this->preparePropertyWriteTarget($expr->var);
if ($this->isPropertyFetch($expr->var)) {
$this->assertCanAssignObjectProp($expr->var, $expr->expr);
} elseif ($this->isStaticPropertyFetch($expr->var)) {
$this->assertCanAssignStaticProp($expr->var, $expr->expr);
if ($propertyWriteTarget !== null) {
$this->assertCanAssignPropertyWrite($propertyWriteTarget, $expr->expr);
}
$right = $this->parseExpr($expr->expr);
if ($this->isPropertyFetch($expr->var) || $this->isStaticPropertyFetch($expr->var)) {
$right = $this->wrapObjectPropertyAssignTypeCheck($expr->var, $expr->expr, $right);
if ($propertyWriteTarget !== null) {
$right = $this->wrapPropertyWriteTypeCheck($propertyWriteTarget, $expr->expr, $right);
}
if ($this->isVarExpr($expr->expr) and !$this->hasVar($right)) {
$this->errorUndefinedVariable($expr->expr);

@ -0,0 +1,20 @@
<?php
/**
* This file is part of Swoole-Compiler(AOT).
*
* @link https://www.swoole.com/
* @contact service@swoole.com
*/
namespace PhpAot\Php\Resolver;
use PhpParser\NodeAbstract;
final readonly class PropertyWriteTarget
{
public function __construct(
public NodeAbstract $node,
public string $label,
) {
}
}
Loading…
Cancel
Save