PHP魔术方法深入理解与实战
PHP魔术方法深入理解与实战
魔术方法是PHP中特殊的方法,以两个下划线开头,在特定情况下自动调用。理解魔术方法可以写出更灵活的代码。今天详细说说各种魔术方法的使用场景。
__construct和__destruct是最常用的魔术方法。对象创建时自动调用__construct,销毁时调用__destruct。
```php
class Connection
{
private ?PDO $pdo = null;
public function __construct(
private string $dsn,
private string $user,
private string $pass
) {
echo "连接对象创建\n";
}
public function connect(): PDO
{
if ($this->pdo === null) {
$this->pdo = new PDO($this->dsn, $this->user, $this->pass);
}
return $this->pdo;
}
public function __destruct()
{
$this->pdo = null;
echo "连接对象销毁\n";
}
}
?>
```
属性重载的魔术方法。__get、__set、__isset、__unset用于拦截属性访问。
```php
class DynamicProperties
{
private array $data = [];
private array $readonly = ['id', 'created_at'];
public function __get(string $name): mixed
{
echo "读取属性: $name\n";
return $this->data[$name] ?? null;
}
public function __set(string $name, mixed $value): void
{
if (in_array($name, $this->readonly)) {
throw new RuntimeException("只读属性: $name");
}
echo "设置属性: $name = " . json_encode($value) . "\n";
$this->data[$name] = $value;
}
public function __isset(string $name): bool
{
echo "检查属性是否存在: $name\n";
return isset($this->data[$name]);
}
public function __unset(string $name): void
{
echo "删除属性: $name\n";
unset($this->data[$name]);
}
}
$obj = new DynamicProperties();
$obj->name = "张三";
echo $obj->name . "\n";
echo isset($obj->name) ? "存在" : "不存在" . "\n";
unset($obj->name);
?>
```
方法重载的__call和__callStatic。当调用不存在的方法时触发。
```php
class QueryBuilder
{
private array $conditions = [];
private array $orders = [];
private ?int $limit = null;
public function __call(string $name, array $arguments): self
{
if (str_starts_with($name, 'where')) {
$field = lcfirst(substr($name, 5));
$this->conditions[] = [$field, $arguments[0]];
return $this;
}
if (str_starts_with($name, 'orderBy')) {
$field = lcfirst(substr($name, 7));
$this->orders[] = [$field, $arguments[0] ?? 'asc'];
return $this;
}
throw new BadMethodCallException("方法不存在: $name");
}
public function limit(int $limit): self
{
$this->limit = $limit;
return $this;
}
public function getQuery(): string
{
$sql = "SELECT * FROM table";
if (!empty($this->conditions)) {
$parts = array_map(fn($c) => "{$c[0]} = :{$c[0]}", $this->conditions);
$sql .= " WHERE " . implode(' AND ', $parts);
}
if (!empty($this->orders)) {
$parts = array_map(fn($o) => "{$o[0]} {$o[1]}", $this->orders);
$sql .= " ORDER BY " . implode(', ', $parts);
}
if ($this->limit !== null) {
$sql .= " LIMIT {$this->limit}";
}
return $sql;
}
}
$query = (new QueryBuilder())
->whereName('张三')
->whereAge(28)
->orderById('desc')
->orderByName()
->limit(10);
echo $query->getQuery() . "\n";
?>
```
__toString方法在对象被当作字符串使用时调用。
```php
class Money
{
public function __construct(
private float $amount,
private string $currency = 'CNY'
) {}
public function __toString(): string
{
$symbols = ['CNY' => '¥', 'USD' => '$', 'EUR' => '€'];
$symbol = $symbols[$this->currency] ?? $this->currency;
return $symbol . number_format($this->amount, 2);
}
public function add(Money $other): Money
{
if ($this->currency !== $other->currency) {
throw new InvalidArgumentException("货币不匹配");
}
return new Money($this->amount + $other->amount, $this->currency);
}
}
$price = new Money(99.99);
$tax = new Money(13.00);
$total = $price->add($tax);
echo "价格: $price\n";
echo "税费: $tax\n";
echo "总计: $total\n";
?>
```
__invoke让对象可以作为函数调用。
```php
class Logger
{
private string $logFile;
public function __construct(string $logFile = '/tmp/app.log')
{
$this->logFile = $logFile;
}
public function __invoke(string $message, string $level = 'INFO'): void
{
$line = sprintf("[%s] %s: %s\n", date('Y-m-d H:i:s'), $level, $message);
file_put_contents($this->logFile, $line, FILE_APPEND | LOCK_EX);
}
}
$log = new Logger();
$log("用户登录成功"); // 对象作为函数调用
$log("数据库连接失败", "ERROR");
echo "日志已写入\n";
?>
```
__clone魔术方法在clone对象时调用,控制深拷贝行为。
```php
class Order
{
public function __construct(
public string $id,
public array $items,
public \DateTime $createdAt
) {}
public function __clone(): void
{
// 深拷贝对象属性
$this->createdAt = clone $this->createdAt;
// 深拷贝数组中的对象
foreach ($this->items as &$item) {
if (is_object($item)) {
$item = clone $item;
}
}
unset($item);
}
}
$order1 = new Order('ORD-001', ['商品A', '商品B'], new \DateTime());
$order2 = clone $order1;
$order2->id = 'ORD-002';
echo "原始: {$order1->id}\n";
echo "克隆: {$order2->id}\n";
echo "时间不同: " . ($order1->createdAt !== $order2->createdAt ? '是' : '否') . "\n";
?>
```
__serialize和__unserialize(PHP7.4+)控制序列化行为。
```php
class SecureUser
{
public function __construct(
public string $name,
private string $password,
private string $token
) {}
public function __serialize(): array
{
return [
'name' => $this->name,
// 不序列化敏感信息
];
}
public function __unserialize(array $data): void
{
$this->name = $data['name'];
$this->password = '';
$this->token = '';
}
}
?>
```
魔术方法让PHP的对象系统更加灵活。属性重载可以实现动态属性,方法重载可以实现DSL风格的接口,序列化控制可以保护敏感数据。但魔术方法也有一些缺点,比如IDE自动补全可能不准确,增加理解难度。使用时要权衡灵活性和可维护性。
