9: 实例管理
Phlo 使用其自己的 instance manager 来高效且可预测地初始化和重用对象。该系统确定 何时运行控制器代码,实例如何存储,以及如何防止循环引用。
9.1: 基础知识
当你定义一个 .phlo 文件时,构建阶段将其转换为一个类。
每次通过 %name 调用一个对象时,都会经过 instance manager(在 /phlo/phlo.php 中的 phlo())。
示例:
prop title = 'Welcome'
route GET home => $this->main
method main => view($this->home)
view home:
<h1>$this->title</h1>
当请求路由 /home 时:
- 实例管理器通过
__handle()(或默认规则)从参数中派生的身份检查是否已经存在匹配的实例。 - 如果没有,则创建并存储。
- 创建后,控制器代码运行(见 §8.2)。
- 然后调用请求的方法。
9.2: 控制器代码
在 .phlo 文件中,所有不属于 route、prop、static、method、function、view、<style> 或 <script> 的代码被称为控制器代码。
这段代码在实例化后运行,一旦实例完全存在。
示例:
prop ready = false
%session->start()
$this->ready = true
最后两行是控制器代码,因为它们位于顶层。
- 这段代码在构造后运行,每次管理器创建新实例时运行一次:对于共享实例,或者在
__handle()返回null的类的每次调用中。 - 与
__construct的区别在于,当控制器代码运行时,实例完全存在,这防止了循环引用和不完整对象的出现。
9.3: `__handle()` 的角色:实例身份
__handle() 是一个 可选的静态 方法,您可以自己定义以控制实例的身份。它与 __construct 交织在一起:转译器将您的构造函数的参数列表附加到它上面,因此句柄表达式使用构造函数参数,而实例管理器则使用即将构造的相同参数调用它:
static __handle => "img/$file"
method __construct(public string $file)
返回值决定了管理器如何处理实例:
__handle() 返回 |
意义 |
|---|---|
| 一个 字符串 | 注册表键:每次产生相同键的调用返回相同的共享实例(按参数的多例模式)。%img 对于同一文件是一个对象;标志也可以是身份的一部分,如 "INI/$path$filename".(!$parse ? '/0' : void)。 |
null |
从不缓存:每次调用构造一个新的实例(cookiewall,field)。 |
true |
重用在类名下注册的实例,并将新参数 objImport(...) 导入其中。 |
一个 没有 __handle() 的类遵循默认规则:在没有参数的情况下调用时,它是一个以其名称为键的单例;在有参数的情况下调用时,它是一个新的、未注册的实例。
你定义 __handle();实例管理器调用它。切勿自己调用。
9.4: 惰性初始化
因为控制器代码仅在构造后运行,实例可以相互引用而不会触发不必要的递归创建。
示例:
a.phlo:
prop message = 'A ready'
b.phlo:
prop message = 'B ready'
main.phlo:
route GET test => $this->show
method show {
dx(%a->message, %b->message)
}
%a和%b是惰性创建的。- 两个文件中的控制器代码在它们的实例完全存在后运行一次。
- 你可以自由地相互引用实例,因为在控制器代码运行之前,实例已经存在。
9.5: obj 基类:powertools
每个转译的类都扩展了 obj,而 obj 的功能不仅仅是 __get/__set。当一个类需要动态行为时,这些就是你要使用的工具。
临时值对象。 obj 也是你直接创建的日常容器:obj(x: 1, y: 2) 让你在一次调用中获得一个实时对象,无需类定义。读取一个不存在的键会返回 null,而不是警告,因此可选数据不需要 isset 的繁琐操作。分配一个闭包,它就变成了一个绑定成员:在 $point->sum = fn() => $this->x + $this->y 之后,$point->sum 和 $point->sum() 都会以 $this 绑定到对象来评估它。foreach 迭代存储的数据,而 json_encode($point) 则精确地序列化这些数据(闭包和计算值会被排除),因此一个 obj 可以干净地传递到 views、payloads 和 JSON 响应中。
$point = obj(x: 1, y: 2)
$point->sum = fn() => $this->x + $this->y
$total = $point->sum
拦截钩子。 实现 objCall、objGet 或 objSet 来捕获访问链。返回 null 将继续执行正常行为;任何非 null 的返回值将短路:
method objGet($key) => $this->cache[$key] ?? null
method objCall($method, ...$args) => str_starts_with($method, 'find') ? $this->finder($method, $args) : null
method objSet($key, $value) => $key === 'id' ? true : null
objGet 在每次读取时会在数据/闭包/方法/属性查找之前运行,objCall 在每次未知方法调用时运行,而 objSet 在每次写入之前运行(非空返回会吞掉写入)。这就是装饰器、延迟加载和只读保护的机制。
绑定闭包。 分配一个闭包,它会绑定到实例上:$obj->greet = fn() => "Hi $this->name",之后 $obj->greet() 运行时 $this 被绑定。对于每个实例的行为而不需要子类化非常方便。
数据 API。 objImport(name: 'x', age: 3) 批量分配并返回 $this(可链式调用)。objKeys()、objValues() 和 objLength() 检查数据;objClear() 则清空数据。迭代一个 obj(foreach $record AS $key => $value)和 json_encode($record) 精确暴露存储的数据。每次写入都会翻转 objChanged,这是 ORM 用来决定 objSave 是否写入任何内容的脏标志。
计算属性缓存。 prop x => ... 在首次访问时缓存;参数形式则按参数集缓存。计算静态属性也是如此,按类缓存。
工作者持久性。 prop objPers = true 使实例在工作模式请求之间存活:phlo() 注册表仅在每次请求重置时保留 objPers 实例。适用于数据库连接和解析的配置;不适用于任何请求或用户范围的内容。
教训。 父类中的普通属性会遮蔽子类中的计算属性。抽象父类中的
prop dir = void会转译为一个真实的 PHP 属性,因此子类的prop dir => guide获取器永远不会被查询:$this->dir默默读取void。当子类必须用计算属性重写时,父属性也应声明为计算属性:prop dir => void。
9.6: Best practices
- 使用控制器代码进行 初始设置,而不是用于依赖请求的逻辑。
- 将控制器代码 放在顶部 或直接放在 props 下面以提高可读性。
__construct捕获实例参数;最好仅限于此,理想情况下使用提升参数(method __construct(public string $file)),因为__handle()在构造之前从这些相同的参数中推导实例的身份。重型工作应放在懒惰的 props 中;应用级启动在控制器代码中进行。- 让实例通过
%name懒惰地初始化,而不是手动创建它们。 - 有意识地使用控制器代码来解决循环引用。