PHP: 魔术方法与枚举

PHP 有一组以 __ 开头的"魔术方法"——你不用显式调用它们,PHP 在特定时刻自动触发。它们让对象的行为更智能、更像内置类型。再加上 PHP 8.1 引入的枚举,这节课让你的 PHP 功力再上一层。

1. __toString() — 把对象变成字符串

PHP
<?php
class User {
    public function __construct(
        public string $name,
        public string $email
    ) {}
    
    // echo 对象时自动调用
    public function __toString(): string {
        return "{$this->name} <{$this->email}>";
    }
}

$user = new User("小明", "xiaoming@example.com");
echo $user;  // 小明 <xiaoming@example.com>
// 不需要写 $user->name,直接 echo 对象
?>
💡 提示: __toString() 让你定义一个"这个对象该怎么转成字符串"。用在日志记录、调试输出、模板渲染中。


2. __get() 和 __set() — 访问不存在的属性

PHP
<?php
class UserModel {
    private array $data = [];
    private array $changes = [];
    
    // 读取不存在的属性时调用
    public function __get(string $name): mixed {
        return $this->data[$name] ?? null;
    }
    
    // 给不存在的属性赋值时调用
    public function __set(string $name, mixed $value): void {
        $this->changes[$name] = true;
        $this->data[$name] = $value;
    }
    
    public function getChanges(): array {
        return array_keys($this->changes);
    }
}

$user = new UserModel();
$user->name = "小明";         // 触发 __set()
$user->email = "x@x.com";    // 触发 __set()
echo $user->name;            // 小明 — 触发 __get()
echo $user->name;            // 小明
print_r($user->getChanges()); // ['name', 'email'] — 追踪哪些字段被改了
?>

(1) __isset() 和 __unset()

PHP
<?php
class Config {
    private array $data = ['app_name' => 'MyApp'];
    
    public function __isset(string $name): bool {
        return isset($this->data[$name]);
    }
    
    public function __unset(string $name): void {
        unset($this->data[$name]);
    }
}

$c = new Config();
var_dump(isset($c->app_name));  // true
var_dump(isset($c->missing));   // false
unset($c->app_name);
var_dump(isset($c->app_name));  // false
?>

3. __call() — 调用不存在的方法

▶ 示例:动态查询构建器

PHP
<?php
class QueryBuilder {
    private array $where = [];
    private ?string $orderBy = null;
    
    // 调用不存在的方法时触发
    public function __call(string $name, array $arguments): static {
        if (str_starts_with($name, 'whereBy')) {
            // whereByName("小明") → WHERE name = "小明"
            $field = lcfirst(substr($name, 7));
            $this->where[] = "{$field} = '{$arguments[0]}'";
        } elseif ($name === 'orderByDesc') {
            $this->orderBy = "{$arguments[0]} DESC";
        }
        return $this;
    }
    
    public function toSQL(): string {
        $sql = "SELECT * FROM table";
        if ($this->where) {
            $sql .= " WHERE " . implode(" AND ", $this->where);
        }
        if ($this->orderBy) {
            $sql .= " ORDER BY {$this->orderBy}";
        }
        return $sql;
    }
}

$q = new QueryBuilder();
$q->whereByName("小明")
  ->whereByStatus("active")
  ->orderByDesc("created_at");

echo $q->toSQL();
// SELECT * FROM table WHERE name = '小明' AND status = 'active' ORDER BY created_at DESC
?>
▶ 试一试
💡 提示: 框架(如 Laravel 的 Eloquent)大量使用 __call() 来实现动态方法。whereByName() 这种看起来存在但实际没有定义的方法就靠它实现。


4. __clone() — 对象被克隆时

PHP
<?php
class ShoppingCart {
    private array $items = [];
    
    public function __construct(
        public string $owner
    ) {}
    
    public function addItem(string $name, int $qty): void {
        $this->items[] = compact('name', 'qty');
    }
    
    public function getItems(): array {
        return $this->items;
    }
    
    // clone 时自动调用
    public function __clone(): void {
        $this->owner = "{$this->owner} 的副本";
        // items 数组默认浅拷贝,但可以在这里做深拷贝
    }
}

$cart1 = new ShoppingCart("小明");
$cart1->addItem("PHP教程", 2);

$cart2 = clone $cart1;
echo $cart2->owner;  // 小明 的副本
// items 也被复制了
print_r($cart2->getItems()); // 有 PHP教程
?>

5. __sleep() 和 __wakeup() — 序列化

PHP
<?php
class DatabaseConnection {
    private $connection;
    
    public function __construct(
        private string $dsn,
        private string $user,
        private string $pass
    ) {
        $this->connect();
    }
    
    private function connect(): void {
        $this->connection = "connected({$this->dsn})";
    }
    
    // serialize() 时调用:返回要序列化的属性名
    public function __sleep(): array {
        // 不序列化 $connection(资源类型不可序列化)
        return ['dsn', 'user', 'pass'];
    }
    
    // unserialize() 时调用:重建连接
    public function __wakeup(): void {
        $this->connect();
    }
}
?>

6. 枚举 enum(PHP 8.1)

函数中写 "pending" "approved" "rejected" 容易拼错也很难追踪。枚举是你最好的朋友:

PHP
<?php
enum OrderStatus: string {
    case PENDING  = 'pending';
    case APPROVED = 'approved';
    case SHIPPED  = 'shipped';
    case DELIVERED = 'delivered';
    case CANCELLED = 'cancelled';
}

// 使用枚举
function updateOrder(int $id, OrderStatus $status): void {
    echo "订单 {$id} 更新为 {$status->value}<br>";
}

updateOrder(1, OrderStatus::APPROVED);  // ✅
// updateOrder(1, "approved");          // ❌ TypeError!
// updateOrder(1, "approvved");         // ❌ 想拼错都难
?>

▶ 示例:枚举 + match 黄金搭档

PHP
<?php
enum UserRole: string {
    case ADMIN    = 'admin';
    case EDITOR   = 'editor';
    case MEMBER   = 'member';
    case GUEST    = 'guest';
    
    // 枚举方法
    public function label(): string {
        return match($this) {
            self::ADMIN  => '管理员',
            self::EDITOR => '编辑',
            self::MEMBER => '会员',
            self::GUEST  => '游客',
        };
    }
    
    public function canEdit(): bool {
        return match($this) {
            self::ADMIN, self::EDITOR => true,
            default => false,
        };
    }
}

$role = UserRole::ADMIN;
echo $role->label();  // 管理员
echo $role->canEdit() ? '可以编辑' : '不可编辑';  // 可以编辑
echo $role->value;    // admin
?>
▶ 试一试

(1) 枚举方法

PHP
<?php
// UserRole::cases() — 获取所有枚举值
print_r(array_map(fn($r) => $r->value, UserRole::cases()));
// ['admin', 'editor', 'member', 'guest']

// UserRole::from() — 从值创建枚举(不匹配抛异常)
$role = UserRole::from('admin');   // UserRole::ADMIN
// $role = UserRole::from('super'); // ValueError

// UserRole::tryFrom() — 安全创建(不匹配返回 null)
$role = UserRole::tryFrom('super'); // null
?>
💡 提示: PHP 8.1+ 当你的数据只有几个固定可选值时就用枚举。OrderStatus::class 有代码自动补全,"pending" 没有——这就是区别。

❓ 常见问题

Q __get__set 会拖慢性能吗?
A 略有开销,因为每次访问都要走函数调用。对于大量数据操作(百万级),给类定义明确的 public/private 属性更快。对于 ORM/Model 这类需要灵活操作的场景,__get/__set 刚好。
Q 什么时候用枚举,什么时候用类常量?
A PHP 8.1+ 优先用枚举——有类型安全(传错值直接报错)、有自动补全、有 match 友好的完整性检查。类常量适合"只是一组相关配置值"的场景。
Q 枚举类可以定义方法吗?
A 完全能!上面的 label()canEdit() 就是例子。枚举让代码从"传字符串"升级为"传带逻辑的类型"。

📖 小节

📝 作业

  1. 写一个 DynamicConfig 类,用 __get/__set/__isset/__unset 实现"像属性一样操作配置数组"。
  2. 写一个 OrderStatus 枚举(Pending→Paid→Shipped→Delivered→Cancelled),给每个状态写 canTransitionTo(OrderStatus $target) 方法定义允许的状态转换。
  3. 写一个简单 ORM 类,用 __call() 实现 whereBy字段名($value) 的动态查询方法。
Web-Tutorial.com

Web-Tutorial 技术团队

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

100%

🙏 帮我们做得更好

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

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