PHP: Trait与静态成员
PHP 有两个其他主流语言没有(或不这样设计)的特性:Trait 和后期静态绑定。这节课让你掌握这两个 PHP 独有的利器,写出更优雅的代码。
1. Trait — PHP 的代码复用魔法
PHP 是单继承——一个类只能有一个爹。但有时候,不相关的类需要共享同一段代码(比如"记录时间戳"、"生成唯一 ID"),Trait 就是为这个而生的。
PHP
<?php
// 定义一个 Trait
trait HasTimestamp {
private string $createdAt;
private string $updatedAt;
public function touch(): void {
$now = date("Y-m-d H:i:s");
if (!isset($this->createdAt)) {
$this->createdAt = $now;
}
$this->updatedAt = $now;
}
public function getCreatedAt(): string {
return $this->createdAt ?? '未知';
}
public function getUpdatedAt(): string {
return $this->updatedAt ?? '未知';
}
}
trait HasUuid {
public function generateUuid(): string {
return uniqid() . bin2hex(random_bytes(8));
}
}
// 在完全不同的类中使用 Trait
class User {
use HasTimestamp, HasUuid; // 使用多个 Trait
public function __construct(
public string $name
) {
$this->touch();
}
}
class Article {
use HasTimestamp;
public function __construct(
public string $title,
public string $content
) {
$this->touch();
}
}
$user = new User("小明");
echo $user->name . " 创建于 " . $user->getCreatedAt() . "<br>";
echo "UUID: " . $user->generateUuid() . "<br>";
$article = new Article("PHP Trait 教程", "主要内容...");
echo $article->title . " 最后更新于 " . $article->getUpdatedAt() . "<br>";
?>
💡 提示: Trait vs 继承:当多个不相关的类(User ≠ Article)需要共享同一段逻辑时用 Trait。如果子类之间有 is-a 关系(Dog is-a Animal),用继承。
2. Trait 冲突解决
当多个 Trait 有同名方法时:
PHP
<?php
trait Logger {
public function log(string $msg): void {
echo "[LOG] {$msg}<br>";
}
}
trait FileLogger {
public function log(string $msg): void {
echo "[FILE] {$msg}<br>";
}
}
class App {
use Logger, FileLogger {
// 用 FileLogger 的 log 替换 Logger 的 log
FileLogger::log insteadof Logger;
// 给被替换的方法起个别名,还能调用
Logger::log as logSimple;
}
}
$app = new App();
$app->log("启动程序"); // [FILE] 启动程序
$app->logSimple("启动程序"); // [LOG] 启动程序
?>
▶ 示例:实用 Trait 集合
PHP
<?php
trait Arrayable {
public function toArray(): array {
return get_object_vars($this);
}
public function toJson(): string {
return json_encode($this->toArray(), JSON_UNESCAPED_UNICODE);
}
}
trait Validatable {
public function validate(array $rules): array {
$errors = [];
foreach ($rules as $field => $rule) {
$value = $this->$field ?? null;
if (str_contains($rule, 'required') && empty($value)) {
$errors[$field] = "{$field} 不能为空";
}
}
return $errors;
}
}
class Product {
use Arrayable, Validatable;
public function __construct(
public string $name,
public float $price,
public string $description = '',
) {}
}
$p = new Product("机械键盘", 299);
echo $p->toJson() . "<br>";
print_r($p->validate(['name' => 'required', 'price' => 'required']));
?>
3. static 属性与方法
static 属于类本身而非实例。所有对象共享同一个 static 属性:
PHP
<?php
class Counter {
private static int $count = 0;
public function __construct() {
self::$count++; // self:: 访问静态成员
}
public static function getCount(): int {
return self::$count;
}
}
echo Counter::getCount(); // 0(用 类名::方法 调用)
$a = new Counter();
$b = new Counter();
$c = new Counter();
echo Counter::getCount(); // 3(所有实例共享同一个 $count)
?>
实例属性 $this-> |
静态属性 self::$ |
|
|---|---|---|
| 属于 | 每个对象 | 类本身 |
| 内存 | 每个对象一份 | 全局一份 |
| 访问 | $obj->prop |
Class::$prop |
4. self:: vs static::(后期静态绑定)
这是 PHP 最精妙也最容易搞混的概念之一:
PHP
<?php
class ParentClass {
public static function who(): string {
return 'Parent';
}
public static function test(): string {
return self::who(); // self:: 总指向当前类(编译时绑定)
}
public static function testLate(): string {
return static::who(); // static:: 指向实际调用的类(运行时绑定)
}
}
class ChildClass extends ParentClass {
public static function who(): string {
return 'Child';
}
}
echo ChildClass::test(); // "Parent" ← self:: 绑定了 ParentClass
echo ChildClass::testLate(); // "Child" ← static:: 绑定了 ChildClass
?>
self:: |
static:: |
|
|---|---|---|
| 绑定时机 | 编译时 | 运行时 |
| 指向 | 定义该方法的类 | 调用该方法的实际类 |
| 适用场景 | 工具方法、常量 | 需要子类重写的静态方法 |
▶ 示例:后期静态绑定实战 — 简易 ORM
PHP
<?php
abstract class Model {
// static:: 让子类能返回自己的表名
public static function table(): string {
// 默认:类名 → snake_case → 复数
return strtolower(static::class) . 's';
}
public static function find(int $id): string {
return "SELECT * FROM " . static::table() . " WHERE id = {$id}";
}
}
class User extends Model {}
class Product extends Model {
public static function table(): string {
return 'products'; // 自定义表名
}
}
echo User::find(1); // SELECT * FROM users WHERE id = 1
echo Product::find(5); // SELECT * FROM products WHERE id = 5
// static::table() 在每个子类中返回自己的表名
?>
5. 类常量 const
类常量不随对象变化,适合定义配置值和约定:
PHP
<?php
class HttpStatus {
public const OK = 200;
public const NOT_FOUND = 404;
public const INTERNAL_ERROR = 500;
public static function getMessage(int $code): string {
return match($code) {
self::OK => 'OK',
self::NOT_FOUND => 'Not Found',
self::INTERNAL_ERROR => 'Internal Server Error',
default => 'Unknown',
};
}
}
echo HttpStatus::OK; // 200
echo HttpStatus::NOT_FOUND; // 404
echo HttpStatus::getMessage(404); // Not Found
// PHP 8.1+ final const
class Config {
final public const APP_NAME = 'MyBlog';
// 子类不能重写 final 常量
}
?>
💡 提示: 使用类常量比
define() 更好——它们有命名空间、自动完成、不会污染全局空间。如果一组值属于同一种概念(HTTP状态码、用户角色),用类常量组织。
❓ 常见问题
Q Trait 和抽象类怎么选?
A 不相关的类共享代码 → Trait。有 is-a 关系 + 共享属性 → 抽象类。一个类可以 use 多个 Trait,但只能继承一个抽象类。
Q
self:: 和 static:: 的区别到底什么时候重要?A 当你写会被继承的静态方法时。比如框架的 Model 基类需要知道"到底哪个子类在调用这个静态方法"。写工具类(永不被继承的静态方法)时
self:: 就够了。Q Trait 能定义属性吗?能定义常量吗?
A 能定义属性和方法,但不能定义常量(PHP 现在还不支持)。多个 Trait 不能定义同名属性,否则致命错误。
📖 小节
- Trait 解决单继承限制,让不相关的类共享代码
use Trait1, Trait2 { ... }冲突用insteadof+asself::$prop访问静态属性/方法(属于类,所有实例共享)self::编译时绑定(指向定义处),static::运行时绑定(指向调用方)- 类常量
public const KEY = value优于全局 define
📝 作业
- 写一个
LoggableTrait,包含log($message)方法(自动加时间戳前缀),让 User 和 Product 两个不相关的类都 use 它。 - 写一个父类 Database,用
static::实现table()方法,子类 User/Order 分别返回 users/orders。写静态方法all()返回SELECT * FROM [table]。 - 创建一个
AppConfig类,用类常量定义 APP_NAME、MAX_UPLOAD_SIZE、DEFAULT_LANGUAGE。写一个静态方法display()输出全部配置。