第1章:面向对象编程(OOP):构建可复用的代码蓝图
章节介绍
章节学习目标
通过本章的学习,你将能够:
- 理解面向对象编程(OOP)的三大核心特性(封装、继承、多态)及其优势。
- 熟练地在PHP中定义类(Class)、创建对象(Object),并理解它们之间的关系。
- 掌握类属性与方法的定义,以及通过访问控制修饰符(public, protected, private)实现封装。
- 学会使用构造函数和析构函数管理对象的生命周期。
- 运用继承机制扩展已有类的功能,并理解方法重写与
parent关键字的使用。 - 了解抽象类与接口在实现多态和制定契约中的作用。
- 初步认识魔术方法,并能在简单场景下使用
__construct,__toString等方法。
在整个教程中的作用
本章是本模块乃至从"入门"迈向"实战"的思维转型关键点 。在此之前,我们编写的都是"过程式"的脚本------代码从上到下执行,数据与操作数据的函数分离。面向对象编程(OOP)则提供了一种全新的、更贴近现实世界模型的编程范式。它将数据(属性)和对数据的操作(方法) 捆绑在一起,形成一个独立的单元------对象。掌握OOP,意味着你将能够:
- 构建更复杂、更易维护的系统:通过类和对象来映射现实中的实体(如用户、文章、订单)。
- 提高代码的复用性:通过继承,可以轻松创建新类,复用已有类的代码。
- 增强代码的灵活性与可扩展性:多态和接口允许你编写更通用、更松耦合的代码。
- 为学习现代化PHP框架铺平道路:所有主流的PHP框架(如Laravel, ThinkPHP, Yii)都深度建立在OOP思想之上。本章是理解和运用这些框架的基石。
与前面章节的衔接
你已经掌握了PHP的基础语法、数组、函数以及MySQL的基本操作。你将发现,OOP中的"方法"本质上是定义在类内部的函数,"属性"则是与类绑定的变量。本章将带你以新的视角重新组织这些已知的代码元素,构建结构更清晰、职责更分明的程序。
本章主要内容概览
我们将从"类"与"对象"这一基本概念出发,逐步深入OOP的各个核心机制。首先,我们会创建一个基础的User类,定义其属性和方法。然后,通过继承创建更特殊的Admin类。接着,探讨抽象类和接口如何定义规范。最后,简要介绍一些有用的魔术方法。本章的实践将贯穿始终,最终你将拥有一个可用的、面向对象设计的User和Admin类,为后续构建完整的用户系统打下坚实基础。
核心概念讲解
1. 类(Class)与对象(Object):蓝图与实体
- 概念 :类 是对象的抽象"蓝图"或"模板",它定义了一类事物共有的属性 (是什么)和行为 (能做什么)。对象是类的一个具体"实例",是根据类这个蓝图创建出来的、实实在在存在的东西。
- 类比 :
汽车设计图是一个类 ,它规定了颜色、型号、引擎等属性和启动、加速等方法。根据这张设计图生产出来的每一辆具体的轿车(比如你的那辆白色轿车)就是一个对象。 - 在PHP中的关系 :使用
class关键字定义类,使用new关键字根据类创建对象。
php
// 定义"汽车"类(蓝图)
class Car {
// ... 属性和方法
}
// 创建具体的汽车对象(实体)
$myCar = new Car();
$yourCar = new Car();
`$myCar`和`$yourCar`是两个独立的对象,但它们都遵循`Car`类的结构。
2. 属性与方法:状态与行为
- 属性:也称为成员变量,用于描述对象的状态或特征(例如:用户的名字、文章的标题)。在类内部定义。
- 方法:也称为成员函数,用于定义对象的行为或能执行的操作(例如:用户登录、保存文章)。在类内部定义。
$this关键字 :在类的方法内部,$this是一个特殊的变量,它指向当前调用该方法的对象实例本身 。通过$this->属性名可以访问当前对象的属性。
3. 访问控制修饰符:实现封装
封装是OOP的一大支柱,它意味着将对象的内部细节隐藏起来,只通过一个受控的接口(通常是公共方法)与外部交互。这提高了安全性和代码的易维护性。PHP通过三个关键字实现:
public(公有):属性和方法可以在任何地方(类内部、外部、子类)被访问。这是最开放的权限。protected(受保护):属性和方法可以在声明它的类以及其子类(继承类)内部被访问,但不能在类的外部直接访问。private(私有) :属性和方法仅能在声明它的类内部被访问。即使是它的子类也无法直接访问。- 最佳实践 :属性应尽量声明为
private或protected,然后通过公共的getter(如getName())和setter(如setName($name))方法来提供对它们的访问和修改。这被称为"封装",它允许你在读取或修改属性时加入验证逻辑。
4. 构造函数与析构函数:对象的生命
- 构造函数
__construct():这是一个特殊的魔术方法。当使用new关键字创建一个对象时,该方法会自动被调用。它通常用于执行对象的初始化工作,例如为属性设置初始值或进行必要的资源分配。
php
public function __construct($name) {
$this->name = $name; // 创建对象时立即设置名字
echo "对象已创建。";
}
- 析构函数
__destruct():这也是一个魔术方法。当对象被销毁(例如,脚本执行结束,或明确地将对象变量设为null)时,该方法会自动被调用。它通常用于执行清理工作,如关闭数据库连接、释放文件句柄等。
5. 继承(Inheritance):代码复用的利器
- 概念 :允许一个类(子类、派生类)基于另一个类(父类、基类)来创建。子类将自动拥有 父类所有
public和protected的属性和方法,并可以添加自己新的属性和方法,或修改(重写)父类的方法。 - 语法 :使用
extends关键字。
php
class Admin extends User {
// Admin 类继承了 User 类的所有 public 和 protected 成员
// 可以在此添加管理员特有的属性和方法
}
- 方法重写:子类可以重新定义从父类继承来的方法,以提供更具体或不同的实现。这直接引出了"多态"的概念。
parent关键字 :在子类重写的方法中,如果需要调用父类被重写方法的原始版本,可以使用parent::方法名()。
6. 多态(Polymorphism)、抽象类与接口:面向抽象编程
- 多态 :"多态"意为"多种形态"。在OOP中,它指子类对象可以被当作父类对象来使用,并且同一个方法在不同的子类中可以有不同的行为。这通过方法重写来实现,增强了程序的灵活性和可扩展性。
- 抽象类(Abstract Class):
- 使用
abstract关键字声明。 - 不能被实例化 (即不能
new AbstractClass()),只能被继承。 - 可以包含抽象方法(使用
abstract关键字声明,没有方法体{},以分号;结束)和普通方法。 - 作用:为所有子类定义一个公共的模板或部分实现,并强制要求子类必须实现特定的抽象方法。
- 接口(Interface):
- 使用
interface关键字声明。 - 只能包含抽象方法(在PHP8之前)和常量。
- 类使用
implements关键字来实现接口,并且必须实现接口中定义的所有方法。 - 作用 :定义一套行为契约。一个类可以实现多个接口,从而承诺具备多种不同的能力。它比抽象类更纯粹地关注"能做什么",而不关心"是什么"。
7. 魔术方法(Magic Methods):PHP的语法糖
魔术方法是以双下划线__开头的方法,它们会在特定的时机被PHP自动调用。本章我们主要接触:
__construct(): 对象创建时调用。__destruct(): 对象销毁时调用。__toString(): 当对象被当作字符串处理时(如echo $obj;)调用,必须返回一个字符串。__get($name)/__set($name, $value): 当访问或设置一个不可访问(如private)或不存在属性时被调用,可用于实现动态属性或访问控制。
代码示例
示例1:基础类与对象、属性与方法
php
<?php
// 1. 定义一个基础的"用户"类 (User)
class User {
// 2. 定义属性(成员变量)
// public: 在任何地方都可访问
public $username;
// protected: 在本类和子类中可访问
protected $email;
// private: 仅在本类中可访问
private $passwordHash;
// 3. 定义方法(成员函数)
// 一个公共方法,用于设置用户信息
public function setInfo($username, $email, $password) {
$this->username = $username; // 使用 $this 访问当前对象的属性
$this->email = $email;
// 调用私有方法进行密码加密
$this->setPassword($password);
}
// 一个公共方法,用于获取用户信息(封装了protected属性email的访问)
public function getInfo() {
// 注意:这里无法直接返回 $this->passwordHash,因为它是私有的,且不应暴露。
return [
'username' => $this->username,
'email' => $this->email,
];
}
// 一个私有方法,用于内部处理密码
private function setPassword($password) {
// 使用 password_hash 安全地哈希密码(后续章节详解)
$this->passwordHash = password_hash($password, PASSWORD_DEFAULT);
echo "密码已设置并哈希。<br>";
}
// 一个验证密码的公共方法
public function verifyPassword($password) {
return password_verify($password, $this->passwordHash);
}
}
// 4. 使用类创建对象(实例化)
$user1 = new User(); // 创建一个 User 对象
$user2 = new User(); // 创建另一个 User 对象
// 5. 调用对象的方法来操作对象
$user1->setInfo('张三', 'zhangsan@example.com', 'mySecret123');
$user2->setInfo('李四', 'lisi@example.com', 'anotherSecret456');
// 6. 访问对象的属性(public属性可以直接访问)
echo "用户1的用户名是: " . $user1->username . "<br>"; // 输出:张三
// 7. 尝试访问 protected 和 private 属性(会导致错误)
// echo $user1->email; // Fatal error: Cannot access protected property
// echo $user1->passwordHash; // Fatal error: Cannot access private property
// 8. 通过公共方法安全地获取信息
print_r($user1->getInfo());
echo "<br>";
// 输出: Array ( [username] => 张三 [email] => zhangsan@example.com )
// 9. 验证密码
$isCorrect = $user1->verifyPassword('mySecret123');
echo $isCorrect ? '密码正确<br>' : '密码错误<br>'; // 输出:密码正确
$isCorrect = $user1->verifyPassword('wrongPassword');
echo $isCorrect ? '密码正确<br>' : '密码错误<br>'; // 输出:密码错误
?>
示例2:构造函数、析构函数与封装实践
php
<?php
class Product {
// 私有属性,通过公共方法访问(封装)
private $name;
private $price;
private $category;
// 构造函数:在创建对象时自动调用,用于初始化
public function __construct($name, $price, $category = '未分类') {
$this->setName($name);
$this->setPrice($price);
$this->category = $category;
echo "产品【{$this->name}】对象已创建。<br>";
}
// 析构函数:在对象销毁时自动调用
public function __destruct() {
echo "产品【{$this->name}】对象即将被销毁。<br>";
}
// Getter 方法:获取私有属性 name
public function getName() {
return $this->name;
}
// Setter 方法:设置私有属性 name,并可加入验证逻辑
public function setName($name) {
if (empty(trim($name))) {
throw new InvalidArgumentException('产品名不能为空。');
}
$this->name = htmlspecialchars(trim($name), ENT_QUOTES, 'UTF-8'); // 简单XSS防护
}
// Getter 和 Setter 方法 for price
public function getPrice() {
return number_format($this->price, 2); // 格式化输出
}
public function setPrice($price) {
if (!is_numeric($price) || $price < 0) {
throw new InvalidArgumentException('价格必须是非负数字。');
}
$this->price = floatval($price);
}
// 一个业务方法
public function getDescription() {
return "产品:{$this->name}, 价格:¥{$this->getPrice()}, 分类:{$this->category}";
}
}
// 测试代码
echo "脚本开始...<br>";
try {
$product1 = new Product('PHP教程书', 59.80, '图书');
echo $product1->getDescription() . "<br>"; // 输出:产品:PHP教程书, 价格:¥59.80, 分类:图书
// 使用 setter 修改属性
$product1->setPrice(55.00);
echo "打折后价格: " . $product1->getPrice() . "<br>"; // 输出:55.00
// 尝试设置非法值(会抛出异常)
// $product1->setPrice(-10);
// 第二个对象,生命周期更短
$product2 = new Product('马克杯', 25.50);
unset($product2); // 手动销毁对象,会立即触发析构函数
// 输出:产品【马克杯】对象即将被销毁。
} catch (Exception $e) {
echo '错误: ' . $e->getMessage() . '<br>';
}
echo "脚本结束...<br>";
// 脚本结束时,$product1 的析构函数也会被自动调用
?>
预期输出:
脚本开始...
产品【PHP教程书】对象已创建。
产品:PHP教程书, 价格:¥59.80, 分类:图书
打折后价格: 55.00
产品【马克杯】对象已创建。
产品【马克杯】对象即将被销毁。
脚本结束...
产品【PHP教程书】对象即将被销毁。
示例3:继承、方法重写与 parent 关键字
php
<?php
// 父类:普通员工
class Employee {
protected $name;
protected $baseSalary = 3000; // 基本工资
public function __construct($name) {
$this->name = $name;
}
public function work() {
return "{$this->name} 正在处理日常工作。<br>";
}
// 计算月薪的方法
public function calculateSalary() {
return $this->baseSalary;
}
public function getInfo() {
return "员工:{$this->name}, 月薪:{$this->calculateSalary()}<br>";
}
}
// 子类1:经理,继承自 Employee
class Manager extends Employee {
private $bonus = 2000; // 经理特有奖金
// 重写(Override)父类的 calculateSalary 方法
public function calculateSalary() {
// 使用 parent 调用父类的方法,然后加上奖金
$base = parent::calculateSalary();
return $base + $this->bonus;
}
// 经理特有的方法
public function holdMeeting() {
return "{$this->name} 经理正在主持会议。<br>";
}
// 重写 work 方法,展现多态
public function work() {
return "{$this->name} 正在统筹部门工作和分配任务。<br>";
}
}
// 子类2:销售,继承自 Employee
class Sales extends Employee {
private $commissionRate = 0.1; // 佣金比例
private $salesAmount = 0; // 销售额
public function setSalesAmount($amount) {
$this->salesAmount = $amount;
}
// 重写 calculateSalary 方法
public function calculateSalary() {
$commission = $this->salesAmount * $this->commissionRate;
return $this->baseSalary + $commission;
}
// 销售特有的方法
public function visitClient() {
return "{$this->name} 销售正在拜访客户。<br>";
}
}
// 测试代码
$emp = new Employee('小王');
echo $emp->work(); // 输出:小王 正在处理日常工作。
echo $emp->getInfo(); // 输出:员工:小王, 月薪:3000
$mgr = new Manager('张总');
echo $mgr->work(); // 输出:张总 正在统筹部门工作和分配任务。(调用了子类重写的方法)
echo $mgr->holdMeeting(); // 输出:张总 经理正在主持会议。
echo $mgr->getInfo(); // 输出:员工:张总, 月薪:5000(3000+2000)
$sal = new Sales('小李');
$sal->setSalesAmount(50000);
echo $sal->visitClient(); // 输出:小李 销售正在拜访客户。
echo $sal->getInfo(); // 输出:员工:小李, 月薪:8000(3000 + 50000*0.1)
// 多态的体现:将不同子类对象放入父类类型的数组中
$employees = [$emp, $mgr, $sal];
echo "<hr>所有员工汇报工作:<br>";
foreach ($employees as $employee) {
// 虽然 $employee 在数组中声明为 Employee 类型,但实际调用的是各自子类重写的 work() 方法
echo $employee->work();
}
?>
示例4:抽象类与接口
php
<?php
// 接口:定义"可支付"的契约
interface Payable {
public function getPaymentAmount(); // 必须实现的方法
}
// 接口:定义"可打印信息"的契约
interface Printable {
public function printInfo();
}
// 抽象类:为"文档"提供一个基础框架
abstract class Document {
protected $title;
protected $author;
public function __construct($title, $author) {
$this->title = $title;
$this->author = $author;
}
// 抽象方法:子类必须实现如何获取内容
abstract public function getContent();
// 普通方法:所有文档共享的打印标题的方法
public function printHeader() {
echo "标题: {$this->title} <br>";
echo "作者: {$this->author} <br>";
}
}
// 具体类:电子书,继承自 Document,并实现两个接口
class EBook extends Document implements Payable, Printable {
private $price;
private $fileSize;
public function __construct($title, $author, $price, $fileSize) {
parent::__construct($title, $author); // 调用父类构造函数
$this->price = $price;
$this->fileSize = $fileSize;
}
// 实现抽象方法
public function getContent() {
return "这是一本名为《{$this->title}》的电子书,大小 {$this->fileSize}MB。<br>";
}
// 实现 Payable 接口的方法
public function getPaymentAmount() {
return $this->price;
}
// 实现 Printable 接口的方法
public function printInfo() {
$this->printHeader(); // 调用继承来的方法
echo "价格: ¥" . $this->getPaymentAmount() . "<br>";
echo "内容摘要: " . $this->getContent();
}
}
// 具体类:发票,只实现 Payable 接口(它不是一个Document)
class Invoice implements Payable {
private $invoiceNumber;
private $amount;
public function __construct($number, $amount) {
$this->invoiceNumber = $number;
$this->amount = $amount;
}
// 实现 Payable 接口的方法
public function getPaymentAmount() {
return $this->amount;
}
public function getDetails() {
return "发票号:{$this->invoiceNumber}, 金额:¥{$this->amount}<br>";
}
}
// 测试代码
$ebook = new EBook('零基础学PHP', '某作者', 68.5, 12.3);
$ebook->printInfo();
echo "<hr>";
$invoice = new Invoice('INV20231001', 2000);
echo $invoice->getDetails();
echo "应付金额:¥" . $invoice->getPaymentAmount() . "<br>";
// 类型提示展示接口的作用
function processPayment(Payable $item) {
echo "正在处理支付,金额:¥" . $item->getPaymentAmount() . "<br>";
}
processPayment($ebook); // 可以传入 EBook
processPayment($invoice); // 也可以传入 Invoice
// processPayment(new Document(...)); // 错误!Document 没有实现 Payable 接口
?>
示例5:魔术方法 __toString 和简单的 __get/__set
php
<?php
class Config {
private $settings = [];
// 当尝试设置一个不存在的属性时调用
public function __set($name, $value) {
$this->settings[$name] = $value;
echo "动态设置了属性 '{$name}' 的值为 '{$value}'。<br>";
}
// 当尝试获取一个不存在的属性时调用
public function __get($name) {
if (array_key_exists($name, $this->settings)) {
return $this->settings[$name];
} else {
return "属性 '{$name}' 不存在。<br>";
}
}
// 当对象被当作字符串使用时调用
public function __toString() {
return "当前配置信息: " . json_encode($this->settings, JSON_UNESCAPED_UNICODE);
}
}
class SimpleUser {
private $data = ['name' => '访客', 'role' => 'guest'];
public function __toString() {
return "用户[名称:{$this->data['name']}, 角色:{$this->data['role']}]";
}
}
// 测试 __set 和 __get
$config = new Config();
$config->siteName = '我的PHP站'; // 触发 __set
$config->adminEmail = 'admin@example.com'; // 触发 __set
echo "站点名称: " . $config->siteName . "<br>"; // 触发 __get,输出:我的PHP站
echo "不存在的属性: " . $config->nonExistent . "<br>"; // 触发 __get,输出:属性 'nonExistent' 不存在。
echo $config . "<br><br>"; // 触发 __toString
// 测试 __toString
$user = new SimpleUser();
echo $user; // 输出: 用户[名称:访客, 角色:guest]
// 等价于 echo $user->__toString();
?>
实战项目:面向对象的用户与权限管理系统(V1.0)
项目需求分析和技术方案
- 目标:运用本章所学的OOP知识,构建一个结构清晰的用户系统模型。为后续章节集成数据库和会话控制打下基础。
- 核心实体:
User(用户基类):包含所有用户的公共属性(ID、用户名、邮箱、密码哈希)和行为(登录验证、信息获取)。Admin(管理员类) :继承自User,添加管理员特有的属性(如权限级别)和行为(如管理用户)。Guest(访客类) :继承自User,代表未登录用户,权限最低。
- 技术要求:
- 严格使用
private/protected修饰符进行封装。 User类使用构造函数初始化。Admin类重写User类的某些方法以体现多态。- 为
User类实现__toString()魔术方法,便于调试输出。
分步骤实现代码和详细说明
步骤1:创建基类 User
php
<?php
// User.class.php
class User {
// 私有属性,外部不能直接访问,实现封装
private $id;
private $username;
private $email;
private $passwordHash;
protected $role = 'user'; // 角色,protected允许子类访问和修改
// 构造函数:创建用户对象时必须提供基本信息
public function __construct($id, $username, $email, $password) {
$this->id = $id;
$this->setUsername($username);
$this->setEmail($email);
$this->setPassword($password); // 通过方法设置,以便进行哈希处理
echo "用户 '{$this->username}' 对象已创建。<br>";
}
// Getter 方法
public function getId() { return $this->id; }
public function getUsername() { return $this->username; }
public function getEmail() { return $this->email; }
public function getRole() { return $this->role; }
// Setter 方法(加入简单验证)
public function setUsername($username) {
if (strlen(trim($username)) < 3) {
throw new InvalidArgumentException('用户名至少3个字符。');
}
$this->username = trim($username);
}
public function setEmail($email) {
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
throw new InvalidArgumentException('邮箱格式不正确。');
}
$this->email = $email;
}
// 密码设置和验证
private function setPassword($password) {
if (strlen($password) < 6) {
throw new InvalidArgumentException('密码至少6位。');
}
$this->passwordHash = password_hash($password, PASSWORD_DEFAULT);
}
public function verifyPassword($password) {
return password_verify($password, $this->passwordHash);
}
// 用户行为
public function login($inputPassword) {
if ($this->verifyPassword($inputPassword)) {
return "用户 '{$this->username}' 登录成功。<br>";
} else {
return "密码错误,登录失败。<br>";
}
}
public function displayProfile() {
return "ID: {$this->id}, 用户名: {$this->username}, 邮箱: {$this->email}, 角色: {$this->role}<br>";
}
// 魔术方法:当对象被当作字符串时
public function __toString() {
return "[User] {$this->username} ({$this->email})";
}
}
?>
步骤2:创建子类 Admin
php
<?php
// Admin.class.php
require_once 'User.class.php';
class Admin extends User {
private $permissionLevel; // 管理员特有属性
// 构造函数,调用父类构造函数并初始化特有属性
public function __construct($id, $username, $email, $password, $permissionLevel = 1) {
parent::__construct($id, $username, $email, $password); // 必须先调用父类构造
$this->role = 'admin'; // 修改继承来的 protected 属性
$this->setPermissionLevel($permissionLevel);
}
public function getPermissionLevel() { return $this->permissionLevel; }
public function setPermissionLevel($level) {
if (!is_int($level) || $level < 1) {
throw new InvalidArgumentException('权限级别必须为正整数。');
}
$this->permissionLevel = $level;
}
// 重写父类方法,展现多态
public function displayProfile() {
$baseProfile = parent::displayProfile(); // 调用父类方法获取基础信息
return $baseProfile . "权限级别: {$this->permissionLevel}<br>";
}
// 管理员特有行为
public function manageUsers() {
return "管理员 '{$this->getUsername()}' 正在管理用户列表。<br>";
}
// 重写 __toString
public function __toString() {
return "[Admin] {$this->getUsername()} (Lv.{$this->permissionLevel})";
}
}
?>
步骤3:创建子类 Guest
php
<?php
// Guest.class.php
require_once 'User.class.php';
class Guest extends User {
// 访客的ID、用户名等是固定的或临时的
public function __construct() {
// 调用父类构造,传入访客的默认信息
parent::__construct(0, 'Guest', 'guest@example.com', ''); // 密码为空或不验证
$this->role = 'guest';
}
// 访客无法登录
public function login($inputPassword) {
return "访客无需登录,或请先注册。<br>";
}
// 重写 __toString
public function __toString() {
return "[Guest]";
}
}
?>
步骤4:项目测试与演示
php
<?php
// test_system.php
require_once 'User.class.php';
require_once 'Admin.class.php';
require_once 'Guest.class.php';
echo "<h3>用户系统测试</h3>";
try {
// 1. 创建普通用户
$user1 = new User(1001, 'alice_wonder', 'alice@example.com', 'AlicePass123');
echo $user1->displayProfile();
echo $user1->login('AlicePass123');
echo $user1->login('WrongPass');
echo "字符串表示: " . $user1 . "<br><br>"; // 触发 __toString
// 2. 创建管理员
$admin1 = new Admin(2001, 'sys_admin', 'admin@example.com', 'Admin@Secure456', 3);
echo $admin1->displayProfile(); // 调用的是 Admin 类重写的方法
echo $admin1->login('Admin@Secure456');
echo $admin1->manageUsers();
echo "字符串表示: " . $admin1 . "<br><br>";
// 3. 创建访客
$guest1 = new Guest();
echo $guest1->displayProfile();
echo $guest1->login('any'); // 调用 Guest 类重写的方法
echo "字符串表示: " . $guest1 . "<br><br>";
// 4. 多态演示:将不同类型的用户放入数组
$users = [$user1, $admin1, $guest1];
echo "<hr>所有用户自我介绍:<br>";
foreach ($users as $u) {
// 这里 $u 的类型是 User,但实际调用的是各自子类重写的 __toString 方法
echo $u . "<br>";
}
} catch (InvalidArgumentException $e) {
echo "创建用户时出错: " . $e->getMessage() . "<br>";
} catch (Exception $e) {
echo "发生未知错误: " . $e->getMessage() . "<br>";
}
?>
项目部署指南
- 在你的PHP开发环境(如XAMPP的
htdocs目录)中创建一个新文件夹,例如chapter01_project。 - 将上述四个PHP文件(
User.class.php,Admin.class.php,Guest.class.php,test_system.php)保存到该文件夹中。 - 通过浏览器访问
http:// localhost/chapter01_project/test_system.php。 - 观察页面输出,理解对象创建、方法调用和多态的效果。
项目扩展和优化建议
- 添加更多属性 :为
User类添加registrationDate(注册日期)、lastLogin(最后登录)等属性。 - 实现更多子类 :例如
Editor(编辑)类,继承User,拥有发布、编辑文章的权限,但没有管理员的管理权限。 - 引入接口 :定义一个
Authenticatable(可认证)接口,包含login()和verifyPassword()方法,让User和Admin类实现它。再定义一个Authorizable(可授权)接口,包含hasPermission($permission)方法,让Admin和Editor类实现。 - 完善验证 :在
setEmail等方法中使用更严格的正则表达式验证。
最佳实践
1. 行业标准和开发规范(PSR)
- PSR-1 基础编码规范 :类名使用
StudlyCaps(大驼峰),如UserManager;方法名使用camelCase(小驼峰),如getUserName();属性名同样使用camelCase。 - PSR-12 编码风格扩展规范:详细规定了代码缩进(4个空格)、大括号位置、关键字大小写等。遵循统一的代码风格能极大提高团队协作效率和代码可读性。
- 一个文件只定义一个类 :并将文件名命名为
类名.class.php或直接是类名.php。
2. 常见错误和避坑指南
- 忘记使用
$this:在类的方法内部访问属性或调用其他方法时,必须使用$this->。直接写$username访问的是局部变量或全局变量,而不是对象的属性。 - 混淆类与对象 :
User是一个类,$user = new User()中的$user才是对象。静态属性和方法属于类本身,而非对象。 - 过度使用继承("is-a"关系) :继承应严格用于"是一个"的关系(如
Admin是一个User)。对于"有一个"或"能做什么"的关系,应考虑使用组合或接口。例如,User有一个Profile(资料),应该将Profile作为User的一个属性对象,而不是让User继承Profile。 - 忽略访问控制 :将所有属性都设为
public是懒惰和危险的做法。务必思考每个成员的访问权限,优先使用private,然后按需放宽至protected,最后才是public。
3. 性能优化技巧
- 谨慎使用魔术方法 :
__get、__set、__call等魔术方法因为涉及动态解析,其性能开销远高于直接访问属性或调用方法。在性能关键的代码段中应避免过度使用。 - 惰性加载(Lazy Loading):如果对象的某个属性(如关联的大对象)创建或获取成本很高,可以考虑在第一次访问时才进行初始化。
php
class User {
private $profile = null;
public function getProfile() {
if ($this->profile === null) {
$this->profile = new Profile($this->id); // 仅在使用时才创建
}
return $this->profile;
}
}
4. 安全性考虑和建议(为后续章节铺垫)
虽然本章重点是OOP语法,但设计类时就必须将安全思想融入其中:
- 封装是安全的第一道防线 :通过将敏感数据(如
$passwordHash)设为private,防止它们被外部代码意外修改或直接读取。 - 在 Setter 方法中进行输入验证与过滤 :如示例所示,在
setEmail()、setUsername()中加入格式验证。这是防御XSS、SQL注入等攻击的起点。后续章节我们会使用filter_var和预处理语句进行更严格的防护。 - 密码永远不存储明文 :示例中使用了
password_hash()和password_verify()。这是PHP官方推荐的、安全的密码处理方式。绝对不要 使用md5()或sha1()来哈希密码。 - 面向对象的安全设计 :通过将权限检查(如
$permissionLevel)和行为(如manageUsers())封装在对象内部,可以更集中地管理安全逻辑。例如,在Admin类的manageUsers()方法开始处,可以检查$permissionLevel是否足够高。
练习题与挑战
基础练习题
- 【难度:★☆☆】 类与对象创建
- 题目 :请定义一个名为
Book的类,它应包含以下私有 属性:title(书名),author(作者),price(价格)。为该类创建:
- 题目 :请定义一个名为
- 一个公共的构造函数,用于初始化这三个属性。
- 公共的 Getter 方法 (
getTitle,getAuthor,getPrice)。 - 一个公共方法
getInfo(),返回例如 "《书名》 by 作者,价格:XX元" 的字符串。 - 要求 :实例化两个不同的
Book对象,并调用它们的getInfo()方法输出信息。 - 提示:注意构造函数参数与属性赋值的对应关系。
- 【难度:★☆☆】 封装与访问控制
- 题目 :接上题,为
Book类添加一个私有 属性discount(折扣,范围0-1,例如0.8代表8折)。添加setDiscount($rate)方法,该方法应检查传入的折扣率是否在0到1之间,如果不是则抛出异常。修改getPrice()方法,使其返回应用折扣后的价格(原价 * 折扣)。
- 题目 :接上题,为
- 要求 :创建一个
Book对象,设置一个合法的折扣和一个非法的折扣(使用try-catch捕获异常),并分别输出打折前后的价格。 - 提示 :在
setDiscount中使用if ($rate < 0 || $rate > 1) { ... }进行判断。
进阶练习题
- 【难度:★★☆】 继承与方法重写
- 题目 :基于第1题的
Book类,创建一个子类EBook(电子书)。EBook类新增一个私有 属性fileSize(文件大小,单位MB)。重写父类的getInfo()方法,在返回的信息末尾加上 ",文件大小:XX MB"。同时,EBook的getPrice()方法默认打9折(即价格是原价的90%)。
- 题目 :基于第1题的
- 要求 :分别实例化一个
Book对象和一个EBook对象,设置相同的书名、作者和原价,然后调用它们的getInfo()和getPrice()方法,观察输出差异。 - 提示 :在
EBook的构造函数中调用parent::__construct(),并在重写的getPrice()中使用parent::getPrice()* 0.9。
- 【难度:★★☆】 多态与抽象类
- 题目 :定义一个抽象类
Shape(形状),它有一个受保护 的属性name(形状名) 和一个抽象方法calculateArea()(计算面积)。创建两个子类Circle(圆形) 和Rectangle(矩形)。Circle类需要属性$radius(半径),面积公式为 π * r²。Rectangle类需要属性$width和$height,面积公式为 宽 * 高。为每个子类实现calculateArea()方法。
- 题目 :定义一个抽象类
- 要求 :创建一个
Shape类型的数组,放入一个Circle对象和一个Rectangle对象。遍历数组,调用每个元素的calculateArea()方法并输出结果。 - 提示 :抽象类不能实例化。使用
M_PI常量获取π的值。
综合挑战题
- 【难度:★★★】 综合应用:简单的购物车系统
- 题目:设计一个面向对象的微型购物车系统。
Product类:属性包括id,name,price。有构造函数和Getter。CartItem类:代表购物车中的一项。属性包括product(一个Product对象) 和quantity(数量)。有方法getTotalPrice()返回该项总价(单价*数量)。ShoppingCart类:代表整个购物车。属性是一个CartItem对象的数组$items。方法包括:addItem(Product $product, $quantity=1):添加商品。如果购物车中已有该商品,则增加其数量;否则新建一个CartItem加入数组。removeItem($productId):根据商品ID移除购物车中的对应项。getTotal():计算并返回购物车内所有商品的总价。displayCart():以清晰格式列出购物车中所有商品(名称、单价、数量、小计)和总价。- 要求 :编写测试代码,创建至少2种商品,添加到购物车中,进行增加数量、移除商品等操作,并最终调用
displayCart()输出购物车详情。 - 提示 :在
ShoppingCart::addItem中遍历$this->items,检查$item->product->getId()是否等于要添加商品的ID。
章节总结
本章重点知识回顾
本章我们系统地学习了PHP中面向对象编程(OOP)的核心知识,完成了从过程式思维到对象式思维的跨越:
- 核心概念 :理解了类 作为蓝图和对象 作为实例的关系,掌握了使用
new关键字实例化对象。 - 封装 :通过
public、protected、private三个访问控制修饰符,学会了如何隐藏对象的内部实现细节,并通过公共的Getter/Setter方法提供安全可控的访问接口。$this关键字是对象内部自我引用的关键。 - 对象生命周期 :使用构造函数
__construct()进行对象初始化,使用析构函数__destruct()进行资源清理。 - 继承 :使用
extends关键字实现代码复用,子类继承父类的属性和方法。掌握了方法重写 来修改继承的行为,并使用parent::调用父类方法。 - 多态:理解了子类对象可被视作父类类型,并通过重写的方法表现出不同行为,这增强了程序的灵活性。
- 抽象与接口 :学习了
abstract class用于定义部分实现的模板和强制规范,学习了interface用于定义纯粹的行为契约。它们是实现高级多态和松耦合设计的重要工具。 - 魔术方法 :初步接触了
__toString,__get,__set等魔术方法,它们为类提供了特殊的交互能力。
技能掌握要求
完成本章学习后,你应当能够:
- 独立设计并实现一个符合封装要求的类,正确使用访问控制修饰符。
- 根据"is-a"关系,使用继承来扩展已有类的功能。
- 在适当场景下,运用抽象类或接口来设计程序结构。
- 阅读并理解基于OOP编写的PHP代码。
进一步学习建议
- 巩固练习:务必亲手完成本章的练习题和实战项目,OOP的理解需要通过大量编码来内化。
- 阅读优秀代码:尝试在GitHub上寻找一些小型、结构清晰的PHP项目(如一个简单的博客引擎),阅读其源码,学习别人是如何组织类和设计对象间关系的。
- 预习下一章 :下一章我们将学习PDO ,这是与数据库交互的现代化、安全的方式。思考如何将本章创建的
User、Admin类与数据库表关联起来(例如,将属性值保存到数据库,或从数据库读取数据来创建对象)。这将是"对象关系映射(ORM)"思想的雏形。 - 探索更多OOP特性 :在掌握本章内容后,你可以自行了解静态属性和方法 、后期静态绑定 、Traits 等更进阶的OOP特性,它们将在你使用主流框架时经常遇到。
恭喜你完成了PHP编程中至关重要的一章!面向对象的思想将伴随你整个开发生涯。准备好,在下一章中,我们将让这些"对象"与数据库安全地对话。