You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在主类中实现子类及数组类型属性的变更跟踪?

问题分析

核心问题在于:引用类型(子类对象、数组)的内部修改不会触发主类的__set方法——你只是修改了引用指向的对象/数组内容,并没有直接给主类的属性重新赋值(比如$contact->Name = new ...才会触发__set,但$contact->Name->Last = "Smith"不会);数组的push、unset等操作同理,属于修改数组内部状态,而非重新赋值整个数组,所以也不会触发主类的变更记录逻辑。

解决方案

分两类场景处理,核心思路是把引用类型的内部操作转化为能触发主类变更记录的事件:

1. 子类属性变更:让子类支持变更通知

给所有需要跟踪的子类添加变更跟踪能力,并让它们在属性修改时通知主类更新变更记录。可以通过一个基类复用逻辑:

步骤1:创建可跟踪基类

abstract class TrackableObject {
    protected ?ContactObject $parent = null;
    protected string $parentPropertyName = '';

    // 关联父对象和当前属性在父类中的名称
    public function setParent(ContactObject $parent, string $propertyName): void {
        $this->parent = $parent;
        $this->parentPropertyName = $propertyName;
    }

    public function __set($name, $value) {
        $this->$name = $value;
        // 如果父对象开启日志,通知父类记录当前属性的变更
        if ($this->parent && $this->parent->isJournallingActive()) {
            $this->parent->recordChange($this->parentPropertyName, $this);
        }
    }

    // 覆盖__get确保属性访问正常
    public function __get($name) {
        return isset($this->$name) ? $this->$name : null;
    }
}

步骤2:修改子类继承基类

class ContactNameObject extends TrackableObject {
    public ?string $Last;
    public ?string $First;
    public ?string $Other;
    public ?string $Prefix;
    public ?string $Suffix;
    public ?string $PhoneticLast;
    public ?string $PhoneticFirst;
    public ?string $PhoneticOther;
    public ?string $Aliases;
    
    public function __construct(
        ?string $last = null,
        ?string $first = null,
        ?string $other = null,
        ?string $prefix = null,
        ?string $suffix = null,
        ?string $phoneticlast = null,
        ?string $phoneticfirst = null,
        ?string $aliases = null
    ) {
        $this->Last = $last;
        $this->First = $first;
        $this->Other = $other;
        $this->Prefix = $prefix;
        $this->Suffix = $suffix;
        $this->PhoneticLast = $phoneticlast;
        $this->PhoneticFirst = $phoneticfirst;
        $this->Aliases = $aliases;
    }
}

class ContactPhoneObject extends TrackableObject {
    public ?string $Type;
    public ?string $SubType;
    public ?string $Number;
    
    public function __construct(?string $type = null, ?string $subtype = null, ?string $number = null) {
        $this->Type = $type;
        $this->SubType = $subtype;
        $this->Number = $number;
    }
}

步骤3:修改主类,关联子类与父类

在主类中添加必要方法,并在初始化子类时建立关联:

class ContactObject {
    private bool $Journalling = false;
    private $Data = null;
    private ?string $ID = null;
    private ?string $State = null;
    private ?string $Label = null;
    private ?ContactNameObject $Name = null;
    private array $Phone = [];

    public function __construct($data = null) {
        $this->Data = (object) [
            'Original' => (object) [],
            'Changed' => (object) []
        ];
        $this->Name = new ContactNameObject();
        // 关联Name属性的父对象
        $this->Name->setParent($this, 'Name');
    }

    public function __set($name, $value) {
        $this->$name = $value;
        if ($this->Journalling) {
            $this->Data->Changed->$name = $this->$name;
        } else {
            $this->Data->Original->$name = $this->$name;
        }
        // 如果是子类对象,自动关联父类
        if ($value instanceof TrackableObject) {
            $value->setParent($this, $name);
        }
    }

    public function __get($name) {
        return isset($this->$name) ? $this->$name : null;
    }

    public function __isset($name) {
        return isset($this->$name);
    }

    public function activateJournal() {
        $this->Journalling = true;
    }

    // 暴露给子类的日志状态检查方法
    public function isJournallingActive(): bool {
        return $this->Journalling;
    }

    // 手动记录变更的方法
    public function recordChange(string $propertyName, $value): void {
        if ($this->Journalling) {
            $this->Data->Changed->$propertyName = $value;
        }
    }

    public function addPhone(string $type, ?string $subtype, string $number) {
        $phone = new ContactPhoneObject($type, $subtype, $number);
        $phone->setParent($this, 'Phone');
        $this->Phone[] = $phone;
        // 添加后手动记录Phone数组的变更
        $this->recordChange('Phone', $this->Phone);
    }

    // 添加删除电话的方法,确保变更被记录
    public function removePhone(int $index): void {
        if (isset($this->Phone[$index])) {
            unset($this->Phone[$index]);
            // 重新索引数组
            $this->Phone = array_values($this->Phone);
            $this->recordChange('Phone', $this->Phone);
        }
    }
}

2. 数组操作变更:通过封装方法操作数组

直接操作数组(如$contact->Phone[] = ...)不会触发__set,必须通过主类提供的方法来增删改数组元素,在方法内部手动调用recordChange记录变更。上面的addPhone和新增的removePhone就是这个思路,确保每次数组状态变化都被追踪。

测试验证

执行以下代码即可验证变更跟踪效果:

$contact = new ContactObject();
$contact->activateJournal();

// 修改子类属性
$contact->Name->Last = 'Doe';
// 添加电话
$contact->addPhone('mobile', null, '123456789');
// 删除电话
$contact->removePhone(0);

// 查看变更记录
var_dump($contact->Data->Changed);

内容的提问来源于stack exchange,提问作者Sebastian

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.10 17:17:33