- 书名: 解耦PHP——为超越框架的应用程序提供洁净与六边形架构
- 亦出自我手: 《Go语言思维》(二册书系)——《Go编程完全指南》 + 《Go中的六边形架构》
- 吾之项目: 《赫尔墨斯IDE》 |GitHub — 一款供开发者使用,与Claude Code及其他AI编程工具协同之IDE
- 吾言: xgabriel.com | GitHub
PHP 8.4于2024年11月推出非对称可见性,多数团队采用之,仅用于跳过getter。此乃显见之例。然有四优者。
此功能甚微。其语法为public public(set),public protected(set),public private(set)及对称之变体。读之修饰,处其素位。写之修饰,括之侧畔。set此乃全矣。
所取代者,其大也。乃取代十五年来PHP团队所撰之私有财产加公共获取模式。亦取代构造函数之冗余模板。与属性钩子相配,readonly,乃代内建构造模式。吾等详述之。
何哉public(set)实为更易
此乃Order 類於 PHP 8.3 中,其狀態變化自域內發生,項目重計後總變化數,然兩項均可自處讀取:模板、序列化器、控制器。
// PHP 8.3, the old way
final class Order
{
private OrderStatus $status;
private Money $total;
public function __construct(
private readonly OrderId $id,
OrderStatus $status,
Money $total,
) {
$this->status = $status;
$this->total = $total;
}
public function getId(): OrderId
{
return $this->id;
}
public function getStatus(): OrderStatus
{
return $this->status;
}
public function getTotal(): Money
{
return $this->total;
}
public function markPaid(): void
{
$this->status = OrderStatus::Paid;
}
}
三個無所事事之取值器。構造器唯為分派而存。形制同於 PHP 8.4:
// PHP 8.4, same semantics, one third the code
final class Order
{
public function __construct(
public readonly OrderId $id,
public private(set) OrderStatus $status,
public private(set) Money $total,
) {}
public function markPaid(): void
{
$this->status = OrderStatus::Paid;
}
}
读面无改。$order->status以 Twig 模板之方式而运作$order->getStatus()已矣。今书写之面,已专属于其类本身。控制器不能为也。$order->status = OrderStatus::Refunded. 将至Cannot modify private(set) property Order::$status from global scope.
此乃标题。今言模式。
范式一:阅无方,书有域
此乃显见之例,亦为多数博文所止之境。汝欲一属性,使任一调用者可读,然唯所属聚合方可变之。八四之前,汝撰一取值器及一私有字段。今,汝但书此字段一次。
final class EmailAddress
{
public public(set) string $value {
set (string $value) {
if (!filter_var($value, FILTER_VALIDATE_EMAIL)) {
throw new InvalidArgumentException(
"Invalid email: {$value}"
);
}
$this->value = strtolower($value);
}
}
public function __construct(string $value)
{
$this->value = $value;
}
}
public public(set)者,声宏而可读可写,遍及四方。配以属性钩子(PHP 8.4之另一大特征),则每赋值皆得验证,constructor内之赋值亦然。彼EmailAddress之class,今为值对象,五行业务逻辑,无getter-setter之繁文缛节。
堪记者:钩子于构造器赋值亦发。尔若欲绕过之,不可于构造器内行之,非更易属性声明不可。此乃特性,非谬误。尔之不变式自对象存在时即已成立.
模式二:无构建者之构建者对象
PHP中构建者模式已得清理。readonly于8.2中,复制语义于8.4中。非对称可见性乃最后之拼图。可无别法,流畅不变之对象,汝能书之。OrderBuilder班次。
final class Order
{
public function __construct(
public readonly OrderId $id,
public private(set) OrderStatus $status = OrderStatus::Draft,
public private(set) ?Address $shippingAddress = null,
public private(set) array $items = [],
) {}
public function withShippingAddress(Address $address): self
{
$clone = clone $this;
$clone->shippingAddress = $address;
return $clone;
}
public function withItem(OrderItem $item): self
{
$clone = clone $this;
$clone->items = [...$this->items, $item];
return $clone;
}
}
private(set)谓此属性可由类内(包括类之克隆内)所写。故$clone->shippingAddress = $address功成者,盖吾辈犹在也。Order吾等行之,外者可阅诸项。外者不可易之。
此乃使构建类于多数情形下过时之所在。构建类存于世,为别于最终对象,持半成之态。有克隆写入之法,则private(set)尔可同持二态于一类,而勿泄其变于世。
模式三:聚合根字段守卫
在领域驱动设计中,有聚合根,其拥有部分状态,并对其执行不变式约束。订单拥有其明细项。聚合外之人不得直接干预明细数组。在8.4之前,这意味着private array $items加上public function getItems(): array { return $this->items; },若需顾及调用者变更返回数组,还需加上防御性拷贝。
非对称可见性结合属性钩子,使防护显明且简练:
final class Order
{
public function __construct(
public readonly OrderId $id,
public private(set) Money $total = new Money(0, 'EUR'),
public private(set) array $items = [],
) {}
public function addItem(OrderItem $item): void
{
if ($this->status !== OrderStatus::Draft) {
throw new DomainException(
"Cannot add items to a {$this->status->value} order"
);
}
$this->items[] = $item;
$this->recalculateTotal();
}
private function recalculateTotal(): void
{
$this->total = array_reduce(
$this->items,
fn(Money $sum, OrderItem $i) => $sum->add($i->subtotal()),
new Money(0, 'EUR'),
);
}
public public(set) OrderStatus $status = OrderStatus::Draft {
set (OrderStatus $next) {
if (!$this->status->canTransitionTo($next)) {
throw new DomainException(
"Cannot move from {$this->status->value} to {$next->value}"
);
}
$this->status = $next;
}
}
}
有两事当察。其一,$this->items[]= $item之效发自addItem()内,盖因吾处于类中。其二,状之属用public public(set) 佐以钩,意谓人人可指派,然钩主控迁转之防。此乃外唤驱动迁转(如工作流引擎、授权后之控制器)而域仍拒非法之变者,其形正也。
public(set) 之合,加钩之属,乃不对称显见之至配也。
模式四:选择性不可变之 DTO 输入
DTO 之弊,在于滥设 readonly class,致使其难于反序列化。欲使 DTO 之多数字段,成之既立,则不可更易。然犹需一二字段,可由设施所设(如中介件所定之请求 ID,或事件总线所注之关联 ID)。此不对称可见性,恰合此理。
final class CreateOrderCommand
{
public function __construct(
public readonly CustomerId $customerId,
public readonly array $items,
public readonly Address $shippingAddress,
public public(set) ?string $correlationId = null,
public public(set) ?string $idempotencyKey = null,
) {}
}
三域实为不可易。二域可书。凡人皆可设之,然值自基构流来,非自用户。命仍为值物。构架可注所需,无需君书设法.
public(set)于开域,则无域限之规。欲锁之于特定层,当易protected(set) 與之相需,框架融貫以延 DTO。吾當避之。巧則巧矣,然巧者,DTO 之弊也。
模式五:破處在何:序列化、Eloquent 轉換、Doctrine 製作
模式甚善。然其周遭之基礎未備。
Symfony Serializer (6.4 與 7.x)善处非对称可见性以达反序列化,盖因其用反射,且于写入时重属主之界。故$serializer->deserialize($json, Order::class, 'json')运作无碍,盖因序列化者权柄足矣,可书之。private(set)田野。新近之点发布,处理之甚洁。
雅致之属性投射(Laravel 11.x及12.x)是故噬整合之码。Eloquent以赋属性。__set() 在模型上,非在值对象上。若汝有定制之转换,能生非对称可见之值对象,则转换本身有效。然若汝于转换之 set() 法中,变易既有之实例,必遇 Cannot modify private(set) property。其解乃于 CastsAttributes::set() 中,恒返新之实例,勿变异。此亦佳习;PHP 8.4 但使之行之耳。
Doctrine ORM (3.x) 依反射之法,滋养实体,故私有及 private(set) 之属,滋养无碍。所困者,乃 Doctrine 之变轨耳。汝以法变易所踪之实体,Doctrine 必察其变。及至 clone 之(前述第二式),Doctrine 视其克隆为游离。勿杂克隆与只读之式于 Doctrine 所辖之实体;此框架,期其所辖之物,可变易也。
JMS Serializer(今之Symfony商家犹常用之)于3.31版本未予支持。其以公域赋值,致private(set)破环。当升迁至Symfony Serializer,或自撰定制之处理器.
惊险之所在:protected(set)合乎继承,复加构造器提升
此乃未载之边际情形。构造器提升之属性与protected(set)之形貌,于定义处尚佳。然于延伸之际,则异矣。
class Money
{
public function __construct(
public protected(set) int $amount,
public readonly string $currency,
) {}
}
class TaxableMoney extends Money
{
public function withTax(float $rate): self
{
$clone = clone $this;
$clone->amount = (int) ($this->amount * (1 + $rate));
return $clone;
}
}
此法可行。子类处乎protected(set)之域,故赋值合律。然其患在:若第三方之庠序,延尔之类,而欲变amount于己之支类之法中,亦得逞焉。protected(set)之广,逾人所测。犹若以八三之法著一protected function setAmount(),是使诸子无论何地,皆属其书表之列.
若欲成事之后永不可变,当用public private(set)。若欲使子嗣可变,则用public protected(set),并载明其子系乃书表之约.
不可用之候
非事事物须有偏私之见。一请求数据传输对象(Request DTO),仅存于一次HTTP调用之间,经检验,付与用例,复归垃圾回收,此等物,何须乎?private(set)修饰之认知代价,实也。运行时之代价,零也,然有人读汝之码,疑汝何故费心,此代价非零也。
吾所荐之形也。
- 聚根与实体之常:可.
- 值物越界而行:可,配以
readonly或钩子. - 内藏之数据传输对象,仅存于单次请求:不可,
public readonly或素朴之public足矣. - 具公之API之库代码:可,凡契约为"调用者读,库写"之字段皆然.
- ORM所管理之模型:唯以上所陈之例外。
PHP 8.4 之非对称可见性非革命也。乃缺失之拼图,使汝可止书取值类,始书真对象。五载之代码库,充斥private $foo益之public function getFoo()此正应运行 Rector 于相关规则,以收千行虚文也。
此五者之中,汝于码库首取何式?又首改何性?
若此有益
非对称可见性者,乃语言之特征,导人于小而诚之物,此物可存于框架易换之际。是书所载之架构层也:何以建PHP之应用,使其存续于Laravel、Symfony,或今年所用之任何框架。若此文中之模式有所共鸣,解耦PHP乃长篇版本,有章论聚合、值对象,及领域代码与框架代码之界。
有售于Kindle、平装本及精装本。英文、德文、日文版已出,葡文及西文版将即至。













