九:PHP 面向对象编程——类、对象与继承

一、回顾与本篇目标

上一篇你学会了 Session 和 Cookie,现在你的应用可以”记住”用户了——登录状态、购物车、偏好设置都能跨请求保持。

到目前为止,我们写的代码都是面向过程的:一个文件里按顺序写函数,调用函数完成任务。这种方式在小型项目中很直接,但随着项目变大,代码会越来越难维护——函数之间共享全局变量、同一个名字的函数可能冲突、数据和操作数据的方法分散在各处。

面向对象编程提供了一种更自然的代码组织方式。它把数据操作数据的方法打包在一起,形成对象。对象模拟现实世界中的实体——一个”用户”对象有姓名、邮箱等属性,也有登录、注销等行为。这种方式让代码更直观、更易维护、更可复用。

如果你之前学过 JavaScript 的类、Python 的类、或者 C++ 的类,PHP 面向对象的基本概念和它们大同小异——类、对象、继承、封装。PHP 的面向对象语法和 C++/Java 更接近,但比 C++ 简单(没有指针、没有手动内存管理)。

本篇的目标:

  1. 理解类和对象的基本概念
  2. 学会定义类、创建对象、访问属性和方法
  3. 掌握构造函数和析构函数
  4. 理解访问修饰符:public、private、protected
  5. 学会继承和多态
  6. 理解静态属性和方法
  7. 学会命名空间的使用

二、类和对象:从蓝图到实例

2.1 现实中的类比

在理解面向对象之前,先看一个生活中的例子:

  • 是一张设计图纸。图纸上画了房子的结构——哪里是客厅、哪里是卧室、门朝哪开。
  • 对象是按照图纸盖出来的实际房子。一张图纸可以盖很多栋房子——结构相同,但内部装修、住的人可以不同。
  • 属性是房子的特征——面积、层数、外墙颜色。
  • 方法是房子能做的事情——开门、关窗、开空调。

翻译成代码:

  • 类(Class):定义了一种数据类型——它包含哪些属性(变量)和哪些方法(函数)。
  • 对象(Object):类的具体实例。通过 new 关键字从类创建。

2.2 定义你的第一个类

<?php
// 定义一个 User 类
class User {
    // 属性(成员变量)
    public $name;
    public $email;
    public $age;

    // 方法(成员函数)
    public function introduce() {
        return "我叫{$this->name},邮箱是{$this->email},今年{$this->age}岁。";
    }
}

// 创建对象(实例化)
$user1 = new User();
$user1->name = '张三';
$user1->email = 'zhangsan@example.com';
$user1->age = 28;

$user2 = new User();
$user2->name = '李四';
$user2->email = 'lisi@example.com';
$user2->age = 22;

// 调用对象的方法
echo $user1->introduce() . '<br>';
echo $user2->introduce() . '<br>';
// 输出:
// 我叫张三,邮箱是zhangsan@example.com,今年28岁。
// 我叫李四,邮箱是lisi@example.com,今年22岁。
?>

逐行解释:

  • class User { ... }:定义一个名为 User 的类。类名习惯用大驼峰命名法(每个单词首字母大写)。
  • public $name;:声明一个公共属性 $name。属性的访问修饰符(publicprivateprotected)必须显式声明。注意 PHP 的属性声明中,$ 符号在变量名前面,不是类型前面。
  • public function introduce() { ... }:定义一个公共方法 introduce()
  • $this->name:在类的方法内部,$this 指向当前对象实例。用 $this->属性名(注意属性名前没有 $)访问当前对象的属性。这和 JavaScript 的 this.name、Python 的 self.name 是同一个概念。
  • new User():创建一个 User 类的实例(对象)。$user1$user2 是两个独立的 User 对象,它们的属性互不影响。
  • $user1->name = '张三':用箭头运算符 -> 访问对象的属性或方法。这和 C 语言结构体指针的 -> 符号类似。

2.3 和 JavaScript/Python/C++ 的对比

概念 JavaScript Python C++ PHP
定义类 class User { } class User: class User { }; class User { }
创建对象 new User() User() new User()User u; new User()
访问成员 obj.prop obj.prop obj.propptr->prop $obj->prop
当前对象 this self this(指针) $this

三、构造函数:对象创建时自动执行

构造函数是一个特殊的方法——在对象被创建时自动调用。通常用来初始化对象的属性。PHP 的构造函数名称固定为 __construct()(两个下划线开头)。

<?php
class User {
    public $name;
    public $email;
    public $age;
    private $createdAt;

    // 构造函数:创建对象时自动调用
    public function __construct($name, $email, $age) {
        $this->name = $name;
        $this->email = $email;
        $this->age = $age;
        $this->createdAt = date('Y-m-d H:i:s');
    }

    public function introduce() {
        return "{$this->name},{$this->age}岁,注册于{$this->createdAt}";
    }
}

// 创建对象时传入参数
$user1 = new User('张三', 'zhangsan@example.com', 28);
$user2 = new User('李四', 'lisi@example.com', 22);

echo $user1->introduce() . '<br>';
echo $user2->introduce() . '<br>';
?>

和 JavaScript/Python 的对比:

// JavaScript
class User {
    constructor(name, email) {
        this.name = name;
        this.email = email;
    }
}

# Python
class User:
    def __init__(self, name, email):
        self.name = name
        self.email = email

// PHP
class User {
    public function __construct($name, $email) {
        $this->name = $name;
        $this->email = $email;
    }
}

PHP 8 引入了构造器属性提升——一种更简洁的写法,一步完成属性声明、构造函数参数和属性赋值:

<?php
// PHP 8+ 简洁写法:构造器属性提升
class User {
    public function __construct(
        public string $name,
        public string $email,
        public int $age,
        private string $createdAt = ''
    ) {
        $this->createdAt = $createdAt ?: date('Y-m-d H:i:s');
    }
}

// 等价于之前的写法,但少了很多行
$user = new User('张三', 'zhangsan@example.com', 28);
echo $user->name;  // 张三
?>

在构造函数参数前加访问修饰符(publicprivateprotected),PHP 会自动声明这个属性并把参数值赋给它。这是现代 PHP 推荐的写法。

四、访问修饰符:控制可见性

PHP 提供了三个访问修饰符,控制属性和方法在哪些地方可以被访问

修饰符 类内部 子类 类外部 用途
public 对外暴露的接口(方法)或数据(属性)
protected 内部实现细节,但允许子类继承使用
private 仅当前类内部使用,子类也不能访问
<?php
class BankAccount {
    private $balance = 0;        // 私有:余额不能被外部直接修改
    
    public function deposit($amount) {
        if ($amount > 0) {
            $this->balance += $amount;
            return true;
        }
        return false;
    }
    
    public function withdraw($amount) {
        if ($amount > 0 && $amount <= $this->balance) {
            $this->balance -= $amount;
            return true;
        }
        return false;
    }
    
    public function getBalance() {
        return $this->balance;   // 通过公共方法访问私有属性
    }
}

$account = new BankAccount();
$account->deposit(1000);
$account->withdraw(300);
echo "余额:" . $account->getBalance();  // 700

// $account->balance = 99999;  // 报错!无法访问私有属性
?>

封装的核心思想:把内部数据(属性)设为 privateprotected,通过 public 方法来控制外部如何访问和修改这些数据。外部代码不能直接修改 $balance——只能通过 deposit()withdraw() 来操作,这两个方法内部可以做验证(存款金额必须大于 0、取款不能超过余额),确保数据始终有效。

五、继承:复用和扩展已有类

继承是面向对象编程的核心机制之一。一个类可以继承另一个类的属性和方法,然后添加自己的新功能或重写已有功能。

extends 关键字表示继承:

<?php
// 父类(基类)
class Person {
    protected $name;
    protected $age;

    public function __construct($name, $age) {
        $this->name = $name;
        $this->age = $age;
    }

    public function introduce() {
        return "我叫{$this->name},{$this->age}岁";
    }
}

// 子类(派生类):继承 Person
class Student extends Person {
    private $studentId;
    private $major;

    public function __construct($name, $age, $studentId, $major) {
        // 调用父类的构造函数
        parent::__construct($name, $age);
        $this->studentId = $studentId;
        $this->major = $major;
    }

    // 重写父类的方法
    public function introduce() {
        return parent::introduce() . ",学号{$this->studentId},专业{$this->major}";
    }

    // 新增方法
    public function study() {
        return "{$this->name}正在学习{$this->major}";
    }
}

$student = new Student('张三', 20, '2024001', '计算机科学');
echo $student->introduce() . '<br>';  // 我叫张三,20岁,学号2024001,专业计算机科学
echo $student->study() . '<br>';      // 张三正在学习计算机科学
?>

关键点:

  • class Student extends Person:Student 继承 Person 的所有 public 和 protected 属性和方法。private 成员不会被继承。
  • parent::__construct($name, $age):在子类的构造函数中调用父类的构造函数。parent 关键字指向父类。
  • 方法重写:子类定义了一个和父类同名的方法 introduce(),子类的版本会覆盖父类的版本。在子类的 introduce() 中,可以用 parent::introduce() 调用被覆盖的父类方法。
  • 新增方法:子类可以添加父类没有的新方法 study()

PHP 只支持单继承:一个类只能有一个直接父类。如果需要复用多个类的功能,使用 trait(后面会讲)或接口。

六、多态:同一个接口,不同的实现

多态的字面意思是”多种形态”。在面向对象编程中,多态指不同的子类可以用各自的方式响应同一个方法调用

<?php
// 抽象类:不能直接实例化,只能被继承
abstract class Animal {
    protected $name;

    public function __construct($name) {
        $this->name = $name;
    }

    // 抽象方法:子类必须实现
    abstract public function makeSound(): string;

    public function describe() {
        return "{$this->name}叫:{$this->makeSound()}";
    }
}

class Dog extends Animal {
    public function makeSound(): string {
        return "汪汪!";
    }
}

class Cat extends Animal {
    public function makeSound(): string {
        return "喵喵!";
    }
}

class Duck extends Animal {
    public function makeSound(): string {
        return "嘎嘎!";
    }
}

// 多态:用相同的方式处理不同类型的对象
$animals = [
    new Dog('旺财'),
    new Cat('咪咪'),
    new Duck('唐老鸭')
];

foreach ($animals as $animal) {
    echo $animal->describe() . '<br>';
}
// 旺财叫:汪汪!
// 咪咪叫:喵喵!
// 唐老鸭叫:嘎嘎!
?>

关键概念:

  • abstract class Animal:抽象类不能直接用 new 创建对象。它只定义子类应该有什么接口。
  • abstract public function makeSound():抽象方法没有方法体(不写花括号),只声明方法签名。所有继承 Animal 的具体子类必须实现这个方法。
  • 多态的核心foreach 循环中,$animal->describe() 调用的是 describe() 方法,但 describe() 内部又调用了 $this->makeSound()。由于 $this 指向的是实际的子类对象(Dog、Cat 或 Duck),所以实际执行的是对应子类的 makeSound() 方法。同一个 describe() 调用产生了不同的输出——这就是多态。

七、接口:定义行为契约

接口定义了一组方法签名(只有方法名和参数,没有方法体)。任何实现这个接口的类,必须实现接口中声明的所有方法

接口用 interface 关键字定义,类用 implements 关键字实现接口。一个类可以实现多个接口(弥补了 PHP 只支持单继承的限制)。

<?php
// 定义接口
interface Logger {
    public function log(string $message): void;
}

interface Notifier {
    public function send(string $recipient, string $message): bool;
}

// 一个类可以实现多个接口
class EmailService implements Logger, Notifier {
    public function log(string $message): void {
        echo "[日志] " . date('Y-m-d H:i:s') . " {$message}<br>";
    }

    public function send(string $recipient, string $message): bool {
        $this->log("发送邮件给 {$recipient}");
        // 实际发送邮件的逻辑...
        return true;
    }
}

class FileLogger implements Logger {
    public function log(string $message): void {
        $entry = "[" . date('Y-m-d H:i:s') . "] {$message}\n";
        file_put_contents('app.log', $entry, FILE_APPEND);
    }
}

// 面向接口编程:依赖抽象(接口),不依赖具体实现
function processNotification(Logger $logger, Notifier $notifier, string $to) {
    $logger->log("开始处理通知");
    $result = $notifier->send($to, "您有一条新消息");
    $logger->log("通知处理完成:" . ($result ? '成功' : '失败'));
}

$emailService = new EmailService();
processNotification($emailService, $emailService, 'user@example.com');
?>

接口 vs 抽象类:

特性 接口 抽象类
定义关键字 interface abstract class
使用关键字 implements extends
可以包含属性 ❌ 只能有常量
可以包含已实现的方法 ❌(PHP 8.0 后部分支持)
多继承 ✅ 可以实现多个接口 ❌ 只能继承一个抽象类
语义 “能做什么”(行为契约) “是什么”(类型层次)

简单来说:接口定义”能做什么”——能记录日志、能发送通知。抽象类定义”是什么”——是动物、是用户。实际开发中两者经常配合使用。

八、静态属性和方法

静态成员属于类本身,而不是属于某个具体的对象实例。它们通过类名直接访问,不需要创建对象。

<?php
class Counter {
    // 静态属性:所有实例共享,属于类本身
    private static int $count = 0;
    
    public function __construct() {
        self::$count++;  // 每次创建对象,计数器加 1
    }
    
    // 静态方法:通过类名直接调用
    public static function getCount(): int {
        return self::$count;
    }
}

// 还没有创建任何对象
echo Counter::getCount() . '<br>';  // 0

$c1 = new Counter();
$c2 = new Counter();
$c3 = new Counter();

// 通过类名直接调用静态方法
echo Counter::getCount() . '<br>';  // 3
?>

关键点:

  • static $count:声明静态属性。所有对象实例共享同一个 $count 变量,它属于类而不是对象。
  • self::$count:在类内部用 self:: 访问静态成员。self 指向当前类,相当于 $this 对于实例的对应物。
  • Counter::getCount():在类外部用 类名:: 访问静态方法。不需要创建对象。
  • :: 运算符:叫范围解析运算符(也叫 Paamayim Nekudotayim,希伯来语”双冒号”)。用来访问类的静态成员、常量、以及被覆盖的方法。

静态成员的典型用途:单例模式、工厂模式、工具函数集合、全局计数器、数据库连接管理。

九、魔术方法

PHP 提供了一些魔术方法(以 __ 两个下划线开头),在特定时机自动调用。除了已经学过的 __construct(),最常用的是 __toString()__get()/__set()

__toString():把对象转成字符串

<?php
class User {
    public function __construct(
        private string $name,
        private string $email
    ) {}

    // 当对象被当作字符串使用时自动调用
    public function __toString(): string {
        return "{$this->name} <{$this->email}>";
    }
}

$user = new User('张三', 'zhangsan@example.com');
echo $user;  // 张三 <zhangsan@example.com>
?>

__get() 和 __set():访问不存在或不可访问的属性时触发

<?php
class Config {
    private array $data = [];

    // 读取不可访问的属性时触发
    public function __get(string $name) {
        return $this->data[$name] ?? null;
    }

    // 设置不可访问的属性时触发
    public function __set(string $name, $value): void {
        $this->data[$name] = $value;
    }
}

$config = new Config();
$config->app_name = '我的应用';    // 触发 __set()
$config->version = '1.0.0';       // 触发 __set()
echo $config->app_name;            // 触发 __get() → 我的应用
echo $config->not_exist;           // 触发 __get() → 不输出(null)
?>

十、命名空间:防止类名冲突

当项目变大,可能会使用多个第三方库。如果两个库都有一个叫 User 的类,就会发生命名冲突。命名空间解决了这个问题——把类放在不同的”文件夹”下。

<?php
// 文件:Models/User.php
namespace App\Models;

class User {
    public function __construct(
        private string $name,
        private string $email
    ) {}

    public function getName(): string {
        return $this->name;
    }
}

// 文件:Controllers/UserController.php
namespace App\Controllers;

use App\Models\User;  // 导入其他命名空间中的类

class UserController {
    public function show(int $id) {
        $user = new User('张三', 'zhangsan@example.com');
        echo $user->getName();
    }
}

// 文件:index.php
require_once 'Models/User.php';
require_once 'Controllers/UserController.php';

$controller = new App\Controllers\UserController();
$controller->show(1);  // 输出:张三
?>

命名空间的规则:

  • namespace App\Models; 必须在文件的第一行(declare 语句除外)。
  • 命名空间用反斜杠 \ 分隔层级。这和文件路径的正斜杠 / 是相反的方向。
  • use 语句导入其他命名空间中的类,让代码中可以用简短的类名。
  • 如果没有指定命名空间,类属于全局命名空间

十一、综合演示:用户管理系统

下面用面向对象的方式重写用户管理系统,使用类来组织代码:

<?php
// ========== 数据库连接类 ==========
class Database {
    private static ?PDO $instance = null;

    public static function getConnection(): PDO {
        if (self::$instance === null) {
            $dsn = 'mysql:host=127.0.0.1;dbname=php_learning;charset=utf8mb4';
            self::$instance = new PDO($dsn, 'root', '');
            self::$instance->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
            self::$instance->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
        }
        return self::$instance;
    }
}

// ========== 用户模型类 ==========
class User {
    public function __construct(
        private ?int $id = null,
        private string $name = '',
        private string $email = '',
        private int $age = 0,
        private string $createdAt = ''
    ) {}

    // 根据 ID 从数据库加载用户
    public static function findById(int $id): ?self {
        $pdo = Database::getConnection();
        $stmt = $pdo->prepare("SELECT * FROM users WHERE id = :id");
        $stmt->execute(['id' => $id]);
        $data = $stmt->fetch();

        if (!$data) return null;

        return new self(
            $data['id'],
            $data['name'],
            $data['email'],
            $data['age'],
            $data['created_at']
        );
    }

    // 获取所有用户
    public static function findAll(): array {
        $pdo = Database::getConnection();
        $stmt = $pdo->query("SELECT * FROM users ORDER BY id DESC");
        $users = [];

        foreach ($stmt->fetchAll() as $data) {
            $users[] = new self(
                $data['id'],
                $data['name'],
                $data['email'],
                $data['age'],
                $data['created_at']
            );
        }
        return $users;
    }

    // 保存用户(新增或更新)
    public function save(): bool {
        $pdo = Database::getConnection();

        if ($this->id === null) {
            $stmt = $pdo->prepare(
                "INSERT INTO users (name, email, age) VALUES (:name, :email, :age)"
            );
        } else {
            $stmt = $pdo->prepare(
                "UPDATE users SET name = :name, email = :email, age = :age WHERE id = :id"
            );
            $stmt->bindValue(':id', $this->id, PDO::PARAM_INT);
        }

        $stmt->bindValue(':name', $this->name);
        $stmt->bindValue(':email', $this->email);
        $stmt->bindValue(':age', $this->age, PDO::PARAM_INT);
        $result = $stmt->execute();

        if ($this->id === null) {
            $this->id = (int)$pdo->lastInsertId();
        }
        return $result;
    }

    // 删除用户
    public function delete(): bool {
        if ($this->id === null) return false;
        $pdo = Database::getConnection();
        $stmt = $pdo->prepare("DELETE FROM users WHERE id = :id");
        return $stmt->execute(['id' => $this->id]);
    }

    // Getters
    public function getId(): ?int { return $this->id; }
    public function getName(): string { return $this->name; }
    public function getEmail(): string { return $this->email; }
    public function getAge(): int { return $this->age; }
    public function getCreatedAt(): string { return $this->createdAt; }

    // Setters
    public function setName(string $name): void { $this->name = $name; }
    public function setEmail(string $email): void { $this->email = $email; }
    public function setAge(int $age): void { $this->age = $age; }

    public function __toString(): string {
        return "{$this->name} ({$this->email})";
    }
}

// ========== 使用示例 ==========
// 创建新用户
$newUser = new User(null, '赵六', 'zhaoliu@example.com', 30);
$newUser->save();
echo "创建用户:{$newUser}<br>";

// 查找用户
$user = User::findById(1);
if ($user) {
    echo "找到用户:{$user},年龄:{$user->getAge()}<br>";
}

// 列出所有用户
$allUsers = User::findAll();
echo "共 " . count($allUsers) . " 个用户:<br>";
foreach ($allUsers as $u) {
    echo "  - {$u}<br>";
}
?>

代码设计要点:

  • 单一职责Database 类只负责数据库连接,User 类只负责用户相关的业务逻辑。
  • 静态工厂方法findById()findAll() 是静态方法,从数据库查询数据并创建 User 对象。这种模式叫 Active Record(Laravel 的 Eloquent ORM 就是基于这个模式)。
  • 封装:属性是 private,通过 gettersetter 方法访问。
  • 魔术方法 __toString():让 User 对象可以直接用 echo 输出。

十二、本篇动手练习

练习 1:定义一个产品类

新建 practice9-1.php,定义一个 Product 类,包含属性:namepricestock(库存)。包含方法:isInStock()(是否有库存)、sell($quantity)(卖出指定数量,库存不足返回 false)、restock($quantity)(进货)。所有属性设为 private,通过构造函数初始化。

练习 2:继承练习

新建 practice9-2.php,在练习 1 的基础上,创建 DigitalProduct(数字产品,如软件、电子书)和 PhysicalProduct(实体产品)两个子类。数字产品没有库存限制(isInStock() 永远返回 true),实体产品保留原有逻辑。数字产品增加一个 download() 方法,返回下载链接。

练习 3:接口练习

新建 practice9-3.php,定义 PaymentMethod 接口,包含 pay(float $amount): bool 方法。创建 AlipayWechatPayCreditCard 三个类实现这个接口。每个类的 pay() 方法输出不同的支付提示。写一个 checkout 函数,接收一个 PaymentMethod 参数和金额,调用 pay() 方法。

练习 4:完整的 MVC 雏形

新建一个项目文件夹,把第九节综合演示的代码扩展成一个简单的 MVC 结构。创建 User 模型类(已经写好了)、UserController 控制器类(包含 index() 列出所有用户、show($id) 显示单个用户、store($data) 创建用户、destroy($id) 删除用户),以及简单的视图文件(显示用户列表的 HTML)。

十三、本篇小结

这一篇你进入了 PHP 面向对象编程的世界:

  • 类和对象:类是蓝图,对象是按蓝图创建的实例。用 class 定义类,用 new 创建对象,用 -> 访问成员。类内部的 $this 指向当前实例。
  • 构造函数__construct() 在对象创建时自动调用。PHP 8 支持构造器属性提升,一行代码同时完成属性声明和赋值。
  • 访问修饰符public(任何地方可访问)、protected(类内部和子类可访问)、private(仅类内部可访问)。封装的核心是把内部数据设为私有,通过公共方法控制访问。
  • 继承class A extends B,A 继承 B 的公共和保护成员。用 parent:: 调用父类方法。PHP 只支持单继承。
  • 抽象类和接口:抽象类定义”是什么”,可以有已实现的方法。接口定义”能做什么”,只声明方法签名。一个类可以实现多个接口。
  • 静态成员:属于类而不是对象,用 :: 访问。静态属性在类内部用 self::$property,静态方法用 self::method()
  • 魔术方法__construct()__toString()__get()__set() 等,在特定时机自动触发。
  • 命名空间:防止类名冲突,用 namespace 声明,用 use 导入。

面向对象是现代 PHP 框架(Laravel、Symfony)的基础。理解了类、对象、继承、接口,你就能看懂这些框架的源码,写出结构清晰、可维护的大型项目。

下一篇预告

下一篇是《PHP 零基础入门》的终篇——《回顾与进阶——你的 PHP 学习路线图》。我们将总结本系列学过的所有内容,对比面向过程 vs 面向对象的写法,介绍 PHP 框架(Laravel)、Composer 包管理器,并给出后续的学习建议。

PHP 零基础入门,每周更新。

© 版权声明
THE END
喜欢就支持一下吧
点赞10 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容