2.1 面向对象:把班组编成岗位与工种 本节摘要:面向对象解决"代码规模化后的秩序"问题:数据与操作装进同一个类,相似岗位抽象出接口,重复技能下沉为 traits。本节沿用 1.3 的校验班组案例升级为正式编制,讲类的定义与构造器提升、继承与多态、接口与依赖注入、traits 复用,读完你应当能把一段过程式代码 refactor 成职责清晰的类结构,并看懂现代 PHP 库的常见组织方式。 从校验班组到正式编制 1.3 节的校验班组是零散函数:能干活,但没有编制——函数、配置、错误文案散落各处,规模化后必然乱。面向对象的第一步就是"建编制":把相关的数据与动作装进同一个类。PHP 8 的构造器属性提升让这件事的仪式感降到了最低: 构造器参数直接声明可见性即成为属性,这是 PHP 8 的语法糖。
本节摘要:面向对象解决"代码规模化后的秩序"问题:数据与操作装进同一个类,相似岗位抽象出接口,重复技能下沉为 traits。本节沿用 1.3 的校验班组案例升级为正式编制,讲类的定义与构造器提升、继承与多态、接口与依赖注入、traits 复用,读完你应当能把一段过程式代码 refactor 成职责清晰的类结构,并看懂现代 PHP 库的常见组织方式。
1.3 节的校验班组是零散函数:能干活,但没有编制——函数、配置、错误文案散落各处,规模化后必然乱。面向对象的第一步就是"建编制":把相关的数据与动作装进同一个类。PHP 8 的构造器属性提升让这件事的仪式感降到了最低:
<?php class Ticket { public function __construct( private string $from, private string $to, private float $price, ) {} public function summary(): string { return "{$this->from} → {$this->to},票价 {$this->price} 元"; } public function discounted(float $rate): static { return new static($this->from, $this->to, $this->price * $rate); } } $t = new Ticket('南苑', '虹桥', 240.0); echo $t->summary(), "\n"; // 南苑 → 虹桥,票价 240 元 echo $t->discounted(0.8)->summary(); // 南苑 → 虹桥,票价 192 元
构造器参数直接声明可见性即成为属性,这是 PHP 8 的语法糖。$this 指当前对象实例;private 表示属性只在类内可见,外部一律通过方法访问——边界画清楚,内部怎么改都不影响调用方。discounted 的返回类型 static 表示"返回当前类的新实例",子类调用时返回子类对象,细节但好用。

继承表达"是一种":子类拿走父类的全部方法,按需改写。接口表达"会做什么":只列方法签名,谁实现谁承诺。两者各司其职,混用是新手最常见的结构病。看校验岗位的编制过程:
<?php interface ValidatorInterface { /** 校验失败返回错误文案,通过返回 null */ public function check(mixed $value): ?string; } abstract class BaseValidator implements ValidatorInterface { public function __construct(protected string $field) {} // 提升为受保护属性 abstract public function check(mixed $value): ?string; // 细节留给子岗 } class AgeValidator extends BaseValidator { public function check(mixed $value): ?string { $age = (int) $value; return ($age < 1 || $age > 120) ? "{$this->field} 不在合理范围" : null; } } class EmailValidator extends BaseValidator { public function check(mixed $value): ?string { return filter_var((string) $value, FILTER_VALIDATE_EMAIL) ? null : "{$this->field} 格式不正确"; } }
调度流程(下面实战里的编排器)只认 ValidatorInterface 这张工种证,具体岗位上的是年龄岗还是邮箱岗,它既不知道也不关心——将来加"手机号岗",调度流程一行不改。这就是多态的实际收益:换人不换流程。
traits 解决横向复用:几个不相干的类都需要"登记日志"能力,走继承不合适(它们不是一种东西),复制粘贴又难维护,就把这段方法写进 trait 让各类"贴"进去:
<?php trait Loggable { public function log(string $msg): void { echo date('H:i:s'), " [{$this->logTag()}] {$msg}\n"; } abstract public function logTag(): string; } class Booker { use Loggable; public function logTag(): string { return '售票'; } } (new Booker())->log('锁定座位 12A'); // 09:41:22 [售票] 锁定座位 12A
静态成员属于类本身而非实例,典型用途是"全站统一的计数器或工厂入口"。克制使用:静态状态在测试里难隔离,能注入就注入。依赖注入的名字听着唬人,实质就一句:对象需要的东西从构造参数递进来,而不是自己在内部 new:
<?php class SignupService { public function __construct( private ValidatorInterface $validator, // 依赖从门口递进来 ) {} public function handle(array $input): bool { return $this->validator->check($input['email'] ?? '') === null; } } $svc = new SignupService(new EmailValidator('邮箱')); var_dump($svc->handle(['email' => 'a@b.com'])); // bool(true)
好处马上能兑现:测试时传一个"永远通过"的假校验器,业务逻辑就能单独检修,不用真连邮箱服务——这正是 2.8 节测试的前提。
把编制落成一件能跑的事:多个校验岗排成流水线,一次跑完全部检查并汇总错误。
<?php class ValidationPipeline { /** @param ValidatorInterface[] $validators */ public function __construct(private array $validators) {} /** @return string[] 全部错误文案 */ public function run(array $input): array { $errors = []; foreach ($this->validators as $v) { $e = $v->check($input[$v->field()] ?? null); if ($e !== null) { $errors[] = $e; } } return $errors; } }
($v->field() 需要 BaseValidator 提供一个返回 $this->field 的公共方法,一行代码,留作动手练习。)对照 1.3 节的 validate 函数:逻辑没变,但岗位可插拔、错误文案归各岗自己管、新岗位零侵入接入。当你能顺手完成这种"函数到类"的改写,第 3 章框架里的控制器、服务、仓储三类对象就都不陌生了。
⚠️ 继承层次过深:四五层的继承链改一处牵全身,超过两层就该考虑组合与接口。
⚠️ 万能上帝类:一个类又管校验又管存储又管输出,职责拆分的标准是"一个类只有一个变化的理由"。
private 画边界,构造器提升让定义变短。下一节进入寄存总库:PDO。旅客数据要真正存下来、查出来,全靠它发的标准凭证。