一、回顾与本篇目标
上一篇你学会了 Session 和 Cookie,现在你的应用可以”记住”用户了——登录状态、购物车、偏好设置都能跨请求保持。
到目前为止,我们写的代码都是面向过程的:一个文件里按顺序写函数,调用函数完成任务。这种方式在小型项目中很直接,但随着项目变大,代码会越来越难维护——函数之间共享全局变量、同一个名字的函数可能冲突、数据和操作数据的方法分散在各处。
面向对象编程提供了一种更自然的代码组织方式。它把数据和操作数据的方法打包在一起,形成对象。对象模拟现实世界中的实体——一个”用户”对象有姓名、邮箱等属性,也有登录、注销等行为。这种方式让代码更直观、更易维护、更可复用。
如果你之前学过 JavaScript 的类、Python 的类、或者 C++ 的类,PHP 面向对象的基本概念和它们大同小异——类、对象、继承、封装。PHP 的面向对象语法和 C++/Java 更接近,但比 C++ 简单(没有指针、没有手动内存管理)。
本篇的目标:
- 理解类和对象的基本概念
- 学会定义类、创建对象、访问属性和方法
- 掌握构造函数和析构函数
- 理解访问修饰符:public、private、protected
- 学会继承和多态
- 理解静态属性和方法
- 学会命名空间的使用
二、类和对象:从蓝图到实例
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。属性的访问修饰符(public、private、protected)必须显式声明。注意 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.prop 或 ptr->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; // 张三
?>
在构造函数参数前加访问修饰符(public、private、protected),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; // 报错!无法访问私有属性
?>
封装的核心思想:把内部数据(属性)设为 private 或 protected,通过 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,通过getter和setter方法访问。 - 魔术方法
__toString():让 User 对象可以直接用echo输出。
十二、本篇动手练习
练习 1:定义一个产品类
新建 practice9-1.php,定义一个 Product 类,包含属性:name、price、stock(库存)。包含方法: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 方法。创建 Alipay、WechatPay、CreditCard 三个类实现这个接口。每个类的 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 零基础入门,每周更新。













暂无评论内容