PHP 基本语法 07|类与对象:从属性、构造到继承
最小可用的类
<?php
class User
{
public string $name;
private int $age = 0;
public function __construct(string $name, int $age)
{
$this->name = $name;
$this->age = $age;
}
public function greet(): string
{
return "你好,{$this->name}";
}
}
$u = new User("张三", 28);
echo $u->greet();
三个必须记住的点:
- 属性必须声明可见性(
public/protected/private)。var $x是老写法,等价于public,别再用。 - 类内部访问属性用
$this->name,不是$name。$this->后面不带$符号。 - 对象是引用语义。
$b = $a之后两者指向同一个实例。
属性类型声明与"未初始化"状态
PHP 7.4 起属性可以带类型:
<?php
class Order
{
public int $id; // 有类型,但没默认值
public string $status = 'new'; // 有类型 + 默认值
public ?string $note = null; // 可空
}
带类型且没默认值的属性,处于"未初始化(uninitialized)"状态。读它会抛 Error,不是返回 null:
<?php
$o = new Order();
echo $o->status; // new 有默认值,没问题
echo $o->id; // ✗ Error: Typed property Order::$id must not be accessed before initialization
这个设计比"默认给 null"更安全,因为它逼你在构造函数里把所有必要字段都赋上。但它也意味着"声明类型就必须负责初始化",别漏。
用 unset($o->id) 可以让属性回到未初始化状态,用于"重置"语义。
构造函数与属性提升(PHP 8.0)
<?php
// 老写法:声明一遍,构造里再赋一遍
class PointOld
{
public float $x;
public float $y;
public function __construct(float $x, float $y)
{
$this->x = $x;
$this->y = $y;
}
}
// 新写法:构造器属性提升
class Point
{
public function __construct(
public float $x,
public float $y,
private string $label = "点",
) {}
}
参数前加可见性修饰符,PHP 会自动声明属性并赋值。这是 PHP 8 里最能省代码的一处改动,尤其对 DTO、值对象、依赖注入的构造器。
限制:提升的只能是"简单"声明(不能有 int|string 之外的复杂默认值表达式逻辑),且提升属性会参与构造函数签名(所以命名参数调用会受影响)。
readonly:一次写入,永久不变(PHP 8.1)
<?php
class Money
{
public function __construct(
public readonly int $amount,
public readonly string $currency = "CNY",
) {}
public function add(Money $other): Money
{
return new Money($this->amount + $other->amount, $this->currency);
}
}
$m = new Money(100);
// $m->amount = 200; // ✗ Error: Cannot modify readonly property
readonly 的规则:
- 只能在声明它的类作用域内初始化一次(通常在构造函数里)。
- 初始化之后任何写入(包括
++、unset)都会抛Error。 - 不能再和
static组合。
PHP 8.2 加了 readonly class,整个类所有属性都是 readonly:
<?php
readonly class Config
{
public function __construct(
public string $host,
public int $port,
) {}
}
这组特性最实际的价值:让值对象真正不可变,从而在并发、缓存、函数传参时都不用担心被意外改掉。
可见性
<?php
class Base
{
public string $open = "公开"; // 任何地方可访问
protected string $half = "受保护"; // 本类和子类
private string $secret = "私有"; // 仅本类
}
protected 的语义是"继承链内可见",private 则是"仅声明它的那个类"。一个常见的误解是子类能访问父类的 private —— 不能,即使子类里有个同名属性也是两回事。
静态成员与三种 self
<?php
class Registry
{
private static array $data = []; // 静态属性,属于类
public const VERSION = "1.0"; // 类常量
public static function set(string $k, mixed $v): void
{
self::$data[$k] = $v; // self 指当前类
static::$data[$k] = $v; // static 指"运行时的类"(后期静态绑定)
}
public static function get(string $k): mixed
{
return static::$data[$k] ?? null;
}
}
Registry::set("a", 1);
echo Registry::get("a");
三个关键字的区别:
| 关键字 | 指向 |
|---|---|
self |
定义这个方法的那个类(编译期确定) |
parent |
父类 |
static |
运行时实际调用的类(后期静态绑定) |
差别在继承时才显现:
<?php
class A
{
public static function who(): string { return self::class; }
public static function whoLate(): string { return static::class; }
}
class B extends A {}
echo B::who(); // A —— self 定死
echo B::whoLate(); // B —— static 跟随调用方
写工厂方法、模板方法时,用 static:: 才能让子类拿到"自己"。
类常量
<?php
class Status
{
public const PAID = 1;
private const SECRET = "x"; // PHP 7.1 起常量也能有可见性
}
echo Status::PAID;
PHP 8.3 起常量可以带类型:
<?php
class Config
{
public const string NAME = "app"; // 8.3+
}
常量在编译期就确定,不可修改,适合放状态码、配置键名。
继承
<?php
class Animal
{
public function __construct(protected string $name) {}
public function speak(): string
{
return "{$this->name} 叫了一声";
}
}
class Cat extends Animal
{
public function speak(): string
{
return parent::speak() . ":喵"; // 调父类实现
}
}
要点:
extends只能单继承。多继承靠接口和 trait(下一篇讲)。- 子类覆盖方法时,签名要兼容(参数类型不能更严格、返回类型不能更宽)。
parent::method()显式调父类;parent::__construct()处理父类初始化。- 子类继承父类的
public/protected成员,private不继承。
final 用来阻止继承或覆盖:
<?php
final class Immutable {} // 不能被继承
class A
{
final public function id(): int { return 1; } // 子类不能覆盖
}
对象的比较:== 和 ===
<?php
$a = new User("张三", 28);
$b = new User("张三", 28);
$c = $a;
var_dump($a == $b); // true 同类、属性值相同
var_dump($a === $b); // false 不是同一个实例
var_dump($a === $c); // true 同一个实例
== 比较属性值,=== 比较是否同一实例。这个语义和数组完全不同(数组的 == 是值比较、=== 也是值比较但要键类型一致),很容易混。
clone:浅拷贝
<?php
class Order
{
public array $items = [];
public function __clone()
{
// 深拷贝内部对象/数组
$this->items = array_map(fn($i) => clone $i, $this->items);
}
}
$a = new Order();
$a->items[] = (object)['sku' => 'A'];
$b = clone $a;
// 没有 __clone 时,$b->items 里的对象和 $a 是同一批
clone 是浅拷贝:值类型属性复制,对象属性仍共享引用。要让内部对象也独立,必须实现 __clone() 手工处理。
想"复制一份并改几个属性",PHP 没有一步到位的语法,标准做法是 clone 之后逐项赋值:
<?php
$b = clone $a;
$b->status = 'shipped';
要频繁做这件事时,更好的方式是提供一个 withXxx() 方法返回新实例(不可变对象的惯用法):
<?php
public function withStatus(string $s): static
{
$new = clone $this;
$new->status = $s;
return $new;
}
匿名类
<?php
$logger = new class ("app.log") {
public function __construct(private string $file) {}
public function write(string $m): void
{
file_put_contents($this->file, $m . PHP_EOL, FILE_APPEND);
}
};
$logger->write("hello");
匿名类适合测试替身、一次性小实现。它能继承、能实现接口、能写构造器,语法和普通类一样,只是没有名字。
动态属性:PHP 8.2 起被弃用
<?php
class User {}
$u = new User();
$u->nickname = "小张"; // PHP 8.2 起报 Deprecated: Creation of dynamic property
以前随手往对象上塞属性是常见做法,8.2 起会给出弃用警告。三种合法做法:
- 提前声明属性。
- 用
#[AllowDynamicProperties]标注(不推荐,只是压掉警告)。 - 实现
__get/__set魔术方法(下一篇讲)。
这个变更对老项目影响很大,升级到 8.2 时日志会被这条警告刷屏——但它是好事,动态属性是很多"字段名拼错、静默为 null"问题的根源。
对象与 null 安全
<?php
$user = getUser(); // 可能返回 null
echo $user?->name ?? "匿名"; // PHP 8.0 的 nullsafe
比 $user && $user->name ? ... : ... 干净得多,链式调用时收益更明显:
<?php
echo $order?->customer?->address?->city ?? "未填写";
坑清单
- 带类型属性不初始化就访问,抛
Error不是 null。 $this->后面不带$,写成$this->$name是动态属性名。self::在继承时不会跟着变,要跟随就用static::。- 子类
private和父类private同名时是两个属性。 clone是浅拷贝,内部对象仍共享。==比较对象是"属性值相等",容易误以为在比较同一对象。- 8.2 起动态属性被弃用,老代码升级要清理。
- 只读属性(
readonly)在构造函数之外赋值会抛错。