在PHP面向对象编程中,__get和__set是两个重要的魔术方法,它们用来拦截对象对不可访问属性的读取和赋值操作。所谓不可访问,包括属性不存在,或者属性被声明为protected、private但在类外部直接访问。通过这两个方法,开发者可以把对象的内部状态保护起来,同时在访问层加入自定义逻辑。

一、__get方法的基本用法
__get方法在读取不可访问属性时自动调用,它接收一个参数,即被访问的属性名,需要返回对应的值。如果没有定义__get,直接读取私有属性会抛出致命错误。下面通过一个简单例子说明它的工作方式。
在下面的代码中,我们把$name设为私有,外部通过$obj->name读取时,PHP发现该属性不可见,就会转而调用__get方法。我们在方法里统一返回处理后的数据,这样外部无需关心内部存储结构。
<?php
class User
{
private $data = [
'name' => '张三',
'age' => 20
];
public function __get($key)
{
if (array_key_exists($key, $this->data)) {
return $this->data[$key];
}
// 属性不存在时返回null或抛异常
return null;
}
}
$user = new User();
echo $user->name; // 输出:张三
echo $user->age; // 输出:20
?>
上面的写法把真实数据放在私有数组里,外部看起来像在访问普通属性,其实经过了__get转发。这样做的好处是,如果以后我们在$data里改了键名,只要__get内部映射好,外部调用代码完全不用变。
需要注意的是,__get必须返回值,否则读取表达式会得到null。另外,如果类中已经声明了public的同名属性,那么__get不会被触发,因为该属性是可访问的,PHP不会走魔术方法通道。
二、__set方法的基本用法
__set在给不可访问属性赋值时触发,它接收两个参数:属性名和要写入的值。我们可以利用它做数据校验、类型转换或写入日志,防止不合法数据进入对象。
以下示例在赋值前检查年龄是否为整数且大于0,只有合法才写入私有数组,否则记录警告。这样就把数据边界控制在对象内部,调用方无法绕过规则直接改属性。
<?php
class User
{
private $data = [];
public function __set($key, $value)
{
if ($key === 'age') {
if (!is_int($value) || $value <= 0) {
throw new InvalidArgumentException('年龄必须是大于0的整数');
}
}
$this->data[$key] = $value;
}
public function __get($key)
{
return $this->data[$key] ?? null;
}
}
$user = new User();
$user->age = 25; // 正常
$user->name = '李四'; // 正常
// $user->age = -3; // 会抛异常
?>
从示例可以看出,__set让我们拥有了对属性写入的完全控制权。相比把属性声明为public再在别处做判断,这种集中式校验更不容易被遗漏。
和__get一样,如果目标属性是public,__set不会触发。另外,__set不需要返回值,它的职责是写入,不是返回结果。若在__set里什么都没做,这次赋值就等于无效,外部还以为成功了,这是常见坑点。
三、两者配合实现只读或动态属性
只定义__get而不定义__set,就能轻松实现只读属性:外部能读但不能写,一写就报错。反过来,只定义__set可实现只写属性,但实际业务中较少这样用。
另一种常见模式是动态属性容器,也就是上面例子里的$data数组。结合__get和__set,对象可以接受任意键值而不用提前声明属性,非常适合配置类、ORM模型等场景。
<?php
class Config
{
private $items = [];
public function __get($k)
{
return $this->items[$k] ?? null;
}
public function __set($k, $v)
{
$this->items[$k] = $v;
}
}
$cfg = new Config();
$cfg->host = '127.0.0.1';
$cfg->port = 3306;
echo $cfg->host; // 127.0.0.1
?>
这种动态容器写起来很灵活,但要注意:因为任意键都能写,若拼写错误也不会报错,可能埋下隐性bug。可以在__get里对未知键抛异常来及早暴露问题。
从架构角度看,用魔术方法包装属性比散落各处的public字段更利于维护。当业务规则变化,比如年龄上限从120改成150,只改__set一处即可,不用全局搜索赋值语句。
四、使用时的性能与误区
魔术方法本质是在运行时通过函数调用转发请求,比直接访问public属性稍慢。在高频循环里大量读写魔术属性,会有可感知的开销。普通业务对象不必过度担心,但底层热点代码要留意。
一个典型误区是认为__get能拦截所有属性读取。其实只有不可访问属性才会触发,如果子类声明了同名的public属性,父类里的__get就失效了。还有就是__get里再次访问$this->同属性名会造成无限递归,因为又会触发魔术方法。
<?php
class Bad
{
private $name;
public function __get($k)
{
// 错误:下面这行又会调用__get,导致递归溢出
return $this->name;
}
}
?>
正确做法是在__get里访问真正的私有存储(如$data数组),而不是同名属性。只要理清“可见属性”和“不可见存储”的边界,就能安全使用这对方法。
总结来说,__get和__set是PHP封装能力的核心工具。用它们可以隐藏实现、统一校验、支持动态字段,但也要避开递归陷阱和性能盲点。理解触发规则,才能写出既灵活又稳健的对象。
PHP魔术方法__get___set修改时间:2026-08-07 16:36:37