PHP 基本语法 08|接口、Trait、抽象类、魔术方法与枚举
上一篇讲的是"一个类怎么写",这一篇讲"类之间怎么组织"。
接口:定义契约
<?php
interface Repository
{
public function find(int $id): ?array;
public function save(array $data): bool;
}
class UserRepository implements Repository
{
public function find(int $id): ?array
{
return ['id' => $id];
}
public function save(array $data): bool
{
return true;
}
}
接口里只能声明方法签名和常量,不能有实现。实现类必须提供签名兼容的方法。
接口的三个实用价值:
- 类型提示:
function handle(Repository $repo)让实现可以随时替换。 - 多接口:一个类可以
implements A, B, C,这是 PHP 实现"多继承能力"的方式。 - 接口可继承接口:
interface B extends A。
<?php
interface HasId { public function id(): int; }
interface Jsonable { public function toJson(): string; }
class Post implements HasId, Jsonable
{
public function id(): int { return 1; }
public function toJson(): string { return '{"id":1}'; }
}
接口常量在 PHP 8.1 之后还能被实现类用 static:: 访问,用法和类常量一样。
抽象类:只差实现的一半
<?php
abstract class Controller
{
abstract public function handle(array $req): array;
public function run(array $req): string
{
$res = $this->handle($req); // 模板方法:流程固定,实现可变
return json_encode($res, JSON_UNESCAPED_UNICODE);
}
}
abstract class Base {} // 抽象类可以没有抽象方法
class UserController extends Controller
{
public function handle(array $req): array
{
return ['ok' => true];
}
}
抽象类与接口的分工:
| 接口 | 抽象类 | |
|---|---|---|
| 能否有实现 | 不能(纯契约) | 能(部分实现) |
| 能否有属性 | 不能 | 能 |
| 继承数量 | 可以多实现 | 只能单继承 |
| 用途 | 定义"是什么能力" | 定义"如何共同完成一件事" |
判断方法:如果你只需要"这是一个什么东西",用接口;如果你要"一堆子类共享同一套骨架,只需要各自填一处",用抽象类。
Trait:横向复用的补丁
trait 是 PHP 里最有争议、也最实用的东西。它解决的问题是:单继承限制下,多个类需要共享同一段实现。
<?php
trait Singleton
{
private static ?self $instance = null;
public static function getInstance(): static
{
return static::$instance ??= new static();
}
}
class Config
{
use Singleton;
}
常见的 trait 用途:日志、缓存、时间戳、软删除、序列化。框架里大量的"能力"都是 trait(Laravel 的 HasMany、SoftDeletes 就是)。
冲突处理:insteadof 和 as
<?php
trait A { public function hello() { return "A"; } }
trait B { public function hello() { return "B"; } }
class C
{
use A, B {
A::hello insteadof B; // 冲突时用 A 的实现
B::hello as helloB; // 给 B 的实现起个别名
}
}
$c = new C();
echo $c->hello(); // A
echo $c->helloB(); // B
as 还能改可见性:
<?php
class D
{
use A { hello as protected; }
}
Trait 的真实限制
- trait 不是类,不能
new,也不能被继承。 - trait 里的
$this指向"使用它的那个类的实例",所以 trait 方法可以访问类里的属性——但这层依赖是隐式的,别人读类定义时看不出来。 - trait 之间可以
use彼此,容易套娃。 - trait 里的抽象方法可以用来约束宿主类:
<?php
trait Loggable
{
abstract protected function logger(): Logger;
// 使用这个 trait 的类必须提供 logger()
}
第 2 条是 trait 被批评的核心:它把"类必须有哪些属性"变成了隐式约定。适度使用没问题,超过三四个 trait 混在一个类里,就该考虑重构了。
魔术方法:PHP 的钩子
魔术方法以 __ 开头,在特定时机被引擎自动调用。
__get / __set / __isset / __unset
<?php
class Config
{
private array $data = [];
public function __get(string $name): mixed
{
return $this->data[$name] ?? null;
}
public function __set(string $name, mixed $value): void
{
$this->data[$name] = $value;
}
public function __isset(string $name): bool
{
return isset($this->data[$name]);
}
}
只有在访问"不可见或不存在的属性"时才会触发。属性本身是 public 就不触发。这一条是理解魔术方法的关键。
一个重要的坑:__get 返回数组时,$obj->items[] = 'x' 不会生效(因为 __get 返回的是值不是引用)。要支持这种写法得让 __get 返回引用:
<?php
public function &__get(string $name): mixed
{
return $this->data[$name];
}
这就是为什么很多框架宁可显式声明属性,也不用 __get:魔术属性在 IDE、静态分析、$obj->items[] 这些地方都会打折扣。
__call / __callStatic
<?php
class Api
{
public function __call(string $method, array $args): mixed
{
// 例:$api->getUser(1) 转发到 HTTP 请求
return $this->request(strtoupper($method), "/{$method}", $args);
}
public static function __callStatic(string $m, array $a): mixed
{
return (new static())->$m(...$a);
}
}
__call 是"方法不存在时的兜底"。它让 API 客户端能写成 $client->getUsers() 这种漂亮形式,代价是 IDE 完全不认识这些方法(要靠 @method 注释补)。
__toString
<?php
class Money
{
public function __construct(private int $cents) {}
public function __toString(): string
{
return number_format($this->cents / 100, 2);
}
}
echo new Money(1990); // 19.90
__toString 抛异常会变成致命错误,所以里面不要写可能失败的逻辑。
__invoke:让对象可以当函数用
<?php
class Multiplier
{
public function __construct(private int $factor) {}
public function __invoke(int $x): int { return $x * $this->factor; }
}
$m = new Multiplier(3);
echo $m(5); // 15
echo implode(',', array_map($m, [1, 2, 3])); // 3,6,9 能直接当回调
生命周期相关的两个
<?php
class Conn
{
public function __construct() { echo "构造"; }
public function __destruct() { echo "析构"; }
}
__destruct 不保证在什么时候调用,只在"对象不再被引用且 GC 回收时"触发。所以不要依赖它在请求结束时做"一定要做的事"(比如关文件、提交事务)——用显式的 close() 更可靠。
枚举(PHP 8.1)
这是 PHP 近年最受欢迎的语法加法之一。
<?php
enum Status
{
case Pending;
case Paid;
case Shipped;
}
$s = Status::Paid;
var_dump($s === Status::Paid); // true —— 枚举实例是单例,可以直接 ===
带值的枚举(backed enum):
<?php
enum OrderStatus: string
{
case Pending = 'pending';
case Paid = 'paid';
case Shipped = 'shipped';
public function label(): string
{
return match ($this) {
self::Pending => '待支付',
self::Paid => '已支付',
self::Shipped => '已发货',
};
}
public static function fromCode(string $code): self
{
return self::from($code); // 找不到抛 ValueError
// return self::tryFrom($code); // 找不到返回 null
}
}
echo OrderStatus::Paid->value; // paid
echo OrderStatus::Paid->label(); // 已支付
echo OrderStatus::fromCode('shipped')->name; // Shipped
几个关键事实:
- 枚举可以带方法,这是它比常量组强的地方。
- 枚举可以实现接口,但不能继承类。
from()/tryFrom()只在有 backed 类型时存在。- 枚举实例可以当数组键、可以做
match的分支。 Status::cases()返回全部实例,常用于生成下拉选项:
<?php
foreach (OrderStatus::cases() as $c) {
echo "{$c->value} => {$c->label()}\n";
}
用枚举替换一堆 const 是值得的:类型安全、能带方法、IDE 能提示、match 能穷尽检查。
接口 + 枚举 + match 的一个完整例子
<?php
interface Payable
{
public function pay(int $cents): bool;
}
final class Alipay implements Payable
{
public function pay(int $cents): bool
{
// 调支付宝
return true;
}
}
final class WechatPay implements Payable
{
public function pay(int $cents): bool
{
return true;
}
}
final class PayFactory
{
public static function make(string $channel): Payable
{
return match ($channel) {
'alipay' => new Alipay(),
'wechat' => new WechatPay(),
default => throw new InvalidArgumentException("未知通道:{$channel}"),
};
}
}
这个结构就是"策略模式"的最小实现:接口定义契约、具体类实现、match 做分发。比一堆 if ($channel === 'alipay') 好维护得多,新增通道只加一个类加一个 case。
坑清单
__get不返回引用时,$obj->arr[] = x静默失效。- 魔术方法只在访问不可见属性时触发,
public属性不触发。 __call让 IDE 和静态分析失效,需要@method注释补救。- trait 里的隐式属性依赖,读类定义时看不出来。
- 多个 trait 同名方法冲突,必须
insteadof或as解决,否则是致命错误。 __destruct不保证时机,别在里面写关键业务。- 枚举不能
new,也不能继承。 from()在值不存在时抛ValueError,处理外部输入应改用tryFrom()。