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 不能定义同名属性,否则致命错误。

📖 小节

📝 作业

  1. 写一个 Loggable Trait,包含 log($message) 方法(自动加时间戳前缀),让 User 和 Product 两个不相关的类都 use 它。
  2. 写一个父类 Database,用 static:: 实现 table() 方法,子类 User/Order 分别返回 users/orders。写静态方法 all() 返回 SELECT * FROM [table]
  3. 创建一个 AppConfig 类,用类常量定义 APP_NAME、MAX_UPLOAD_SIZE、DEFAULT_LANGUAGE。写一个静态方法 display() 输出全部配置。
Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏