在PHP面向对象编程中,继承是构建类层级、复用代码和实现多态的核心手段。通过继承,一个类可以从另一个类获得属性和方法,同时还能扩展或修改这些成员。严格来说,PHP只支持单继承,也就是一个类只能有一个直接父类,这是为了避免多继承带来的方法冲突问题。不过PHP允许通过接口来实现多继承的效果,这一设计在实际项目中非常常用。

一、extends关键字与基础继承语法
在PHP中声明继承关系非常简单,只需要在子类定义时使用extends关键字,后跟父类名称。下面是一个典型示例,Animal作为父类定义了公共属性和方法,Dog通过继承获得这些成员。
<?php
class Animal {
public $name;
public function __construct($name) {
$this->name = $name;
}
public function speak() {
return $this->name . " makes a sound";
}
}
class Dog extends Animal {
public function fetch() {
return $this->name . " is fetching";
}
}
$dog = new Dog("Buddy");
echo $dog->speak();
echo $dog->fetch();
?>
上面的代码中,Dog类没有重新定义name属性和speak方法,但实例化后仍然可以调用它们。这说明子类已经继承了父类的公共成员。这里要注意,PHP只允许一个类直接继承另一个类,例如class A extends B,如果还想同时继承C,语法上是不允许的。这种单继承模型让类的职责更清晰,减少了方法来源的歧义。
除了单继承,PHP还提供了接口继承,一个类可以通过implements同时实现多个接口。接口定义方法签名,实现类提供具体逻辑,两者配合可以模拟出类似多继承的能力。理解这一层后才能明白为什么许多框架的基础类只继承一个抽象父类,却实现了多个接口。
二、访问控制与parent关键字
继承涉及三类访问修饰符:public、protected和private。子类可以无条件继承public和protected成员,并在类内部访问它们。而private成员虽然也会被继承,但在子类中不可见,无法直接读取或修改。换句话说,private的继承只是内存层面的保留,不是使用层面的开放。
下面的示例展示了protected属性的典型用法,Animal父类把$legs标记为protected,Dog子类可以在自己的方法中访问,但外部代码不能直接操作它。
<?php
class Animal {
protected $legs = 4;
public function getLegs() {
return $this->legs;
}
}
class Dog extends Animal {
public function describe() {
return "I have " . $this->legs . " legs";
}
}
$dog = new Dog();
echo $dog->describe();
echo $dog->getLegs();
?>
如果子类定义了与父类同名的构造方法,父类的构造函数不会自动执行。这时需要在子类构造方法中手动调用parent::__construct()来复用父类的初始化逻辑。这样做既能保留父类的属性赋值,又能追加子类特有的初始化步骤。很多开发者刚开始使用继承时,会误以为父类构造方法会自动被调用,导致子类对象初始化不完整。
parent关键字还可以用于调用被重写的普通方法。比如子类重写了speak(),但希望先执行父类版本再补充内容,就可以写成parent::speak()。注意parent后面使用的是双冒号,不能使用箭头操作符。
三、方法重写与多态实现
方法重写是继承最直接的收益之一。子类可以定义一个与父类同名的方法,改变其具体实现。当通过子类对象调用该方法时,PHP执行的是子类中的版本。这种运行时根据对象实际类型决定方法调用的机制就是多态。经典例子是父类Animal定义makeSound(),子类Dog和Cat分别给出不同的叫声。
<?php
class Animal {
public function makeSound() {
return "Some generic sound";
}
}
class Dog extends Animal {
public function makeSound() {
return "Woof";
}
}
class Cat extends Animal {
public function makeSound() {
return "Meow";
}
}
function playSound(Animal $animal) {
echo $animal->makeSound();
}
playSound(new Dog());
playSound(new Cat());
?>
在上面代码中,playSound()函数的参数类型被限定为Animal,但传入Dog或Cat实例都可以正常工作。这是因为子类对象满足父类类型约束,同时方法调用会分派到各自的重写实现。多态让代码可以面向抽象编程,不必为每个具体类型写重复的逻辑分支。
重写方法时需要保持签名兼容,PHP 8对返回类型和参数类型的要求更严格。例如父类方法声明了返回类型string,子类重写时就不能返回其他类型,否则会触发致命错误。建议在重写方法时保持参数个数一致,或者使用默认参数来避免调用方传入不匹配的数据。
四、继承的常见问题与设计建议
继承虽然好用,但深度过大的继承树会让代码变得脆弱。每一层子类都依赖父类实现,一旦顶层父类修改,所有后代类都会受到影响。因此实际项目中通常建议继承层级控制在两到三层以内,超过这个范围就要重新审视是否应该抽取接口或使用组合。
一个常见误区是仅仅为了复用某个方法而建立继承关系。如果两个类之间不存在明确的属于关系,比如Car和Engine,就不应该让Car继承Engine,而应该使用组合,把Engine作为属性注入。组合比继承更灵活,可以在运行时替换依赖对象,也更容易进行单元测试。
命名空间和自动加载也会影响继承的使用。当父类和子类位于不同命名空间时,子类必须使用完全限定名称或先通过use语句导入父类。PHP的自动加载机制通常是按类名和命名空间映射到文件路径,因此继承一个类时也需要确保父类文件能被正确加载。如果在大型项目中随意跨命名空间继承,容易造成自动加载失败或类冲突。
最后还要注意final关键字。用final修饰的类不能被继承,用final修饰的方法不能被重写。这是为了防止关键逻辑被意外修改,在设计框架底层类时非常有用。例如PHP内置的Closure类就是final的,不允许被继承。合理使用final能让类层级更有边界感。