面向对象
这一课你将能做到
- 定义带可见性、类型声明属性的类,并用构造器属性提升写出紧凑的 DTO
- 用接口约束行为、用 trait 复用实现,说清两者与抽象类的分工
- 掌握 readonly 属性(8.1)与 readonly 类(8.2),写出不可变对象
为什么要学
PHP 8 之后的面向对象已经和 Java 非常接近:接口、抽象类、trait、枚举、final、类型声明一应俱全,还多了构造器属性提升这种 Java 没有的「减法语法」。现代框架的每一层——Controller、Service、Repository——都是类和接口的组合。不懂 OOP,读 Laravel 或 Symfony 源码就是读天书;懂了 OOP,你会发现两个框架的分层思想和 Spring 高度同构。
人话:类是「数据 + 行为」的模具,接口是「承诺书」(你能干什么),trait 是「外挂技能包」(横向复制实现),继承是「父子传家」。这四个工具组合起来,就是工程化的全部骨架。
先建立图像
一个典型的 PHP 服务类分层:
UserRepository ──► 查库(依赖 PDO)
UserServiceImpl ──► 业务逻辑(依赖 UserRepository 接口)
UserController ──► 接 HTTP 请求(依赖 Service 接口)
接口 UserRepositoryInterface ← 行为契约
类 MySqlUserRepository ← 一种实现
trait Loggable ← 横切能力,多个类复用
和 Spring 的 Controller/Service/Repository 分层完全对位,区别只是装配方式:PHP 在构造函数里手动 new 或由容器注入,Spring 交给 IoC 容器。
跟着做
步骤 1:类与构造器属性提升
新建 Order.php:
<?php
declare(strict_types=1);
class Order
{
// PHP 8 构造器属性提升:声明 + 赋值一行搞定
public function __construct(
private readonly int $id,
private readonly string $product,
private float $amount,
private string $status = 'pending',
) {
if ($amount < 0) {
throw new \InvalidArgumentException('金额不能为负');
}
}
public function pay(): void
{
if ($this->status !== 'pending') {
throw new \RuntimeException('订单状态不允许支付');
}
$this->status = 'paid';
}
public function status(): string
{
return $this->status;
}
}
$order = new Order(1, '机械键盘', 399.00);
$order->pay();
echo $order->status(), PHP_EOL; // paid
// $order->amount = -1; // private,外部不可直接改
对比 Java:相当于把字段声明、构造参数、赋值语句三件事压缩成一组带修饰符的参数。readonly 修饰的属性初始化后不可再改。
步骤 2:继承与可见性
<?php
declare(strict_types=1);
class Animal
{
public function __construct(protected string $name) {}
public function introduce(): string
{
return "我是 {$this->name}";
}
}
class Dog extends Animal
{
public function introduce(): string // 方法重写
{
return parent::introduce() . ',汪!';
}
}
echo (new Dog('旺财'))->introduce(), PHP_EOL; // 我是 旺财,汪!
可见性三层:public 谁都能访问,protected 本类和子类,private 仅本类。属性不写修饰符是非法的(PHP 8 起强制),这堵死了老代码的「隐式 public」。
步骤 3:接口 + 类型提示实现多态
<?php
declare(strict_types=1);
interface PaymentGateway
{
public function charge(float $amount): string;
}
class AliPay implements PaymentGateway
{
public function charge(float $amount): string
{
return "支付宝收款 {$amount} 元";
}
}
class WeChatPay implements PaymentGateway
{
public function charge(float $amount): string
{
return "微信收款 {$amount} 元";
}
}
function checkout(PaymentGateway $gateway, float $amount): void
{
echo $gateway->charge($amount), PHP_EOL;
}
checkout(new AliPay(), 99.0); // 支付宝收款 99 元
checkout(new WeChatPay(), 99.0); // 微信收款 99 元
checkout 只依赖接口,新增支付渠道零改动——这就是「依赖接口而非实现」,与 Java 面向接口编程完全一致。
步骤 4:trait 横向复用
<?php
declare(strict_types=1);
trait Loggable
{
public function log(string $message): void
{
echo '[' . date('H:i:s') . '] ' . static::class . ": {$message}\n";
}
}
class OrderService
{
use Loggable;
public function create(): void
{
$this->log('订单已创建');
}
}
class StockService
{
use Loggable;
public function deduct(): void
{
$this->log('库存已扣减');
}
}
(new OrderService())->create(); // [10:30:01] OrderService: 订单已创建
(new StockService())->deduct(); // [10:30:01] StockService: 库存已扣减
trait 是「把实现复制进类」:两个不相干的类都能 use Loggable 获得同一份方法。static::class 是后期静态绑定,返回实际调用者的类名。
讲清楚要点
- 接口管「能做什么」,trait 管「怎么做」,抽象类管「是什么」:接口只含方法签名和常量(PHP 8 后允许常量),抽象类可以有状态和构造器但只能单继承,trait 无状态纯实现且可多用。需要多继承能力时:继承一个抽象类 + use 多个 trait。
- 构造器属性提升是 PHP 8 的标配写法:参数带可见性修饰符即自动成为属性。注意规则:提升参数不能是可变参数、不能重复声明同名的提升属性;默认值写在参数上。
self、static、$this三兄弟:$this指当前实例;self::编译期绑定到「定义该方法的类」;static::运行期绑定到「实际调用的类」(后期静态绑定),子类重写常量或静态方法时static::才能拿到子类版本;new static()也据此实例化子类。- readonly 属性与 readonly 类:属性加
readonly只能在声明处或构造器内赋值一次;PHP 8.2 可整类readonly class(所有声明属性自动只读)。配合 promoted properties 就有了 PHP 版 record——但 PHP 不会自动生成 equals/hashCode。
你可能会懵的地方
- 抽象类和接口怎么选? 要给子类提供公共状态、构造逻辑、部分实现 → 抽象类;只定义行为契约、希望类保留继承名额 → 接口。PHP 支持一个类实现多个接口,所以框架里契约几乎全是接口,抽象基类做模板方法。
- trait 方法冲突了怎么办?
两个 trait 有同名方法时必须显式解决:
use A, B { A::hello insteadof B; B::hello as aliased; }。冲突不解决直接编译报错,PHP 不会偷偷选一个。 - PHP 有重载(overload)吗?
没有传统意义上的方法重载——同名方法只能定义一次。变通方案:默认参数、可变参数、命名参数模拟重载;
__call魔术方法做动态分发。构造器同样不能重载,静态工厂方法是主流替代。 new self()和new static()区别?new self()永远实例化「写这行代码的类」;new static()实例化「运行时实际调用的类」。在父类工厂方法里想返回子类实例,必须用new static()。- 属性不写可见性会怎样? PHP 8 起直接解析错误(老版本会被当作 public 并触发弃用警告)。所有属性必须显式 public/protected/private。
错法对照
| 错法 | 问题 | 正解 |
|---|---|---|
| 属性全 public,外部随意改 | 状态失控,无法维护不变式 | private + getter/setter(按需暴露) |
| 用继承复用工具方法 | 继承树膨胀,父类被迫懂子类 | 抽成 trait 或独立工具类 |
| 接口里写实现 | 接口应纯契约(除非默认方法有明确理由) | 实现放抽象类或 trait |
self::CONST 在子类拿到父类值 | self 是编译期绑定 | 需要多态时用 static::CONST |
| 构造器里做 IO/查库 | 对象创建副作用不可控 | 构造器只做赋值与校验,IO 交给方法 |
| 手写 $this->id = $id 三件套 | 冗长重复 | 构造器属性提升一行搞定 |
卡点急救
Cannot declare class xxx, because the name is already in use:同类被加载两次。确认走 Composer 自动加载而不是手动 require,或检查是否 require 了两个内容不同但类名相同的文件。Typed property must not be accessed before initialization:声明了类型的属性没初始化就被读。检查构造器是否漏赋值,或属性标注了非可空类型却没默认值。- trait 方法被类自身方法覆盖,以为 trait 生效:优先级是「类自身方法 > trait 方法 > 父类方法」。想让 trait 的实现生效,就别在类里写同名方法。
面试常问
- 抽象类和接口的区别? 抽象类:单继承、可有属性/构造器/具体方法、表达「is-a」;接口:多实现、只含方法签名与常量(PHP 8.0 起支持常量)、表达「can-do」。工程上服务契约用接口,公共骨架用抽象类。
- trait 和接口的区别? 接口只有契约没有实现,类 implements 后必须自己实现;trait 携带完整实现,use 后直接拥有方法体,解决 PHP 单继承下的横向代码复用。常见组合是「接口定义契约 + trait 提供默认实现」。
- self 和 static 的区别(后期静态绑定)?
self::绑定到方法定义所在的类,static::绑定到运行时实际调用的类。子类继承父类的自引用方法时,static::能取到子类的常量、静态属性与方法,new static()也能实例化子类——这就是后期静态绑定(LSB)。
自测三问
- 构造器属性提升把 Java 里的几步操作压缩成了一步?写出对应的 Java 版本。
- 一个类能同时继承抽象类和使用多个 trait 吗?优先级顺序是什么?
- readonly 属性什么时候可以赋值?第二次赋值会发生什么?
小结一句话
PHP 8 的 OOP 用「接口立契约、trait 做横切、抽象类供骨架、构造器属性提升 + readonly 写紧凑不可变模型」,分层思想与 Spring 完全同构。
延伸
- PHP 官方手册:类与对象
- 下一课:Composer 依赖——用 composer.json 和 Packagist 管理整个项目。