
资料在文章末尾
前六篇写的都是面向过程的代码,这篇把博客系统整个推倒重来,用 OOP 的方式重构一遍。类、对象、封装、继承、多态、接口、Trait、命名空间、自动加载全部讲透,最后附完整可跑的博客系统源码。
前六篇的代码有个共同特征:变量存数据、函数处理逻辑、按顺序一个个调用。这种写法在功能少的时候很顺手,但项目一旦膨胀,几个问题会同时冒出来——全局变量满天飞、函数名容易撞车、同一段逻辑被复制到多处、改一个地方牵连一片。
面向对象编程(OOP)换了个思路:把程序看成一组互相协作的对象,每个对象有自己的属性(数据)和方法(行为),对应现实中的实体——用户、文章、订单。代码天然分块,扩展和维护都方便很多。
打个比方,面向过程像一个人从头到尾做一顿饭:买菜、洗菜、切菜、炒菜、装盘,所有步骤写在一张清单上。面向对象则像去餐厅点菜,你告诉厨师"一份宫保鸡丁",厨师(对象)自己知道怎么做(方法),用冰箱(另一个对象)里的食材(属性),最后给你成品,你不用管炒菜细节。
类是对象的模板,定义一类事物的共同属性和方法。比如"汽车"这个类,有颜色、品牌、速度等属性,有启动、加速、刹车等方法。对象是根据类创建的具体实例,"一辆红色的宝马 X5"就是汽车类的一个对象。
<?php// 定义类class Car { // 属性 public $color; public $brand; // 方法 public function start() { echo "汽车已启动!"; } public function drive() { echo "正在行驶..."; }}// 创建对象(实例化)$myCar = new Car();$myCar->color = '红色';$myCar->brand = '宝马';$myCar->start(); // 输出:汽车已启动!echo "我开的是一辆{$myCar->color}的{$myCar->brand}。";?>new 根据类生成对象,用 -> 访问对象的属性和方法。注意 PHP 的 -> 和 Java 的 .、C++ 的 :: 都不一样,新手容易写错。
类的方法内部,$this 代表当前对象本身,用来访问自身的属性和方法。
<?phpclass Car { public $color = '白色'; public function showColor() { echo "这辆车的颜色是:" . $this->color; }}$car = new Car();$car->showColor(); // 这辆车的颜色是:白色?>实战提醒:类内方法里访问本对象的属性必须用 $this->属性名,$属性名是访问不到的。$this是伪变量,不能在静态方法里用。
在 new 对象时自动调用,通常用来初始化属性。
<?phpclass Car { public $color; public $brand; public function __construct($color, $brand) { $this->color = $color; $this->brand = $brand; } public function info() { echo "品牌:{$this->brand},颜色:{$this->color}"; }}$car = new Car('黑色', '奔驰');$car->info(); // 品牌:奔驰,颜色:黑色?>有构造函数后,创建对象时可以直接传参设置初始状态,不用手动一个个赋值。
对象被销毁时(脚本结束或变量赋值为 null)自动调用,用来释放资源——关闭文件、断开数据库连接等。PHP 自己会做垃圾回收,所以析构函数实际用得不多,知道有这东西就行。
<?phpclass Car { public function __destruct() { echo "汽车对象被销毁了。"; }}$car = new Car();// 脚本结束时输出:汽车对象被销毁了。?>封装是 OOP 的核心原则之一,通过访问修饰符控制属性和方法的可见性。
• public:公开,任何地方都能访问。
• protected:受保护,只能在类本身和子类中访问。
• private:私有,只能在类本身内部访问,子类也不行。
<?phpclass User { private $password; // 外部无法直接读取或修改 public function setPassword($pwd) { // 可以在这里添加验证或加密逻辑 $this->password = password_hash($pwd, PASSWORD_DEFAULT); } public function checkPassword($pwd) { return password_verify($pwd, $this->password); }}$user = new User();$user->setPassword('secret123');var_dump($user->checkPassword('secret123')); // true// echo $user->password; // 报错,不能访问私有属性?>为什么要封装:直接暴露属性意味着任何代码都能改它,可能出现"年龄设为负数"这种无效状态。通过方法访问可以在入口加校验,保证数据安全。这是"把细节藏起来,只暴露必要接口"的设计思路,也是很多面试题的考点。
继承允许一个类(子类)继承另一个类(父类)的属性和方法,并可以增加或重写。比如父类"哺乳动物"有哺乳方法,子类"狗"自动拥有哺乳方法,还能增加"吠叫"方法。
<?php// 父类class Animal { public $name; public function __construct($name) { $this->name = $name; } public function eat() { echo "{$this->name} 正在吃东西。"; }}// 子类继承class Dog extends Animal { public function bark() { echo "{$this->name} 汪汪叫!"; }}$dog = new Dog('旺财');$dog->eat(); // 旺财 正在吃东西。(继承的方法)$dog->bark(); // 旺财 汪汪叫! (新增的方法)?>访问修饰符在继承中的表现:public 和 protected 会被子类继承,private 不会。子类可以通过 parent:: 调用父类的构造方法或普通方法。
<?phpclass Dog extends Animal { public function __construct($name, $breed) { parent::__construct($name); // 调用父类构造函数 $this->breed = $breed; }}?>踩坑提醒:PHP 只支持单继承,一个类只能继承一个父类。如果需要"多继承"的能力,用接口(implements 多个)或 Trait 解决。子类重写父类方法时,参数签名要兼容,否则会报致命错误。
多态指同一个方法在不同类中表现出不同行为。PHP 里主要通过方法重写实现:子类定义和父类同名的方法。
<?phpclass Animal { public function sound() { echo "动物发出声音。"; }}class Dog extends Animal { public function sound() { echo "汪汪!"; }}class Cat extends Animal { public function sound() { echo "喵喵!"; }}function animalSound(Animal $animal) { $animal->sound();}animalSound(new Dog()); // 汪汪!animalSound(new Cat()); // 喵喵!?>即使参数类型声明的是 Animal,实际执行的还是具体对象的方法。代码只关心"是一个动物",不关心具体是什么动物——这就是多态的威力,也是 Laravel 这类框架依赖注入的基础。
如果一个类只用于被继承,不希望被直接 new,可以定义为抽象类。抽象类中可以包含抽象方法(没有方法体,只定义签名),强制子类必须实现它们。
<?phpabstract class Shape { // 抽象方法没有大括号 abstract public function area(); // 普通方法可以有实现 public function describe() { echo "这是一个图形。"; }}class Circle extends Shape { private $radius; public function __construct($r) { $this->radius = $r; } // 必须实现area(),否则报错 public function area() { return pi() * $this->radius * $this->radius; }}$circle = new Circle(5);echo $circle->area(); // 78.5398...?>抽象类本身不能实例化(new Shape() 会报错),它像一份合同,子类必须遵守。
接口(interface)只定义方法签名,不包含任何实现。类通过 implements 实现接口,必须实现所有方法。一个类可以实现多个接口。
<?phpinterface Loggable { public function log($message);}interface Notifiable { public function sendNotification($user);}class UserService implements Loggable, Notifiable { public function log($message) { echo "日志:$message"; } public function sendNotification($user) { echo "给 $user 发送通知"; }}?>接口 vs 抽象类怎么选:
• 接口只能定义公共方法签名,不能有属性;抽象类可以有属性和已实现的方法。
• 一个类可以实现多个接口,但只能继承一个抽象类。
• 接口强调"能做什么",抽象类强调"是什么"。
PHP 单继承有时不够灵活,Trait 提供了一种在类之间横向共享代码的方式,弥补单继承的局限。
<?phptrait Logger { public function log($msg) { echo "[LOG] " . $msg; }}trait Authenticator { public function authenticate($user) { // ...验证逻辑 return true; }}class Admin { use Logger, Authenticator; // 使用多个 Trait public function doSomething() { $this->log('操作执行了'); }}?>如果多个 Trait 有同名方法,需要用 insteadof 或 as 解决冲突。
trait A { public function test() { echo "A"; } }trait B { public function test() { echo "B"; } }class C { use A, B { A::test insteadof B; // 使用 A 的 test B::test as testB; // 把 B 的 test 重命名为 testB }}静态成员属于类本身,而不是某个对象。用 static 关键字声明,通过 ::(范围解析操作符)访问。
<?phpclass Counter { public static $count = 0; public static function increment() { self::$count++; // self 代表当前类 }}Counter::increment();Counter::increment();echo Counter::$count; // 2?>静态方法里不能用 $this,因为它不属于任何对象。
后期静态绑定(进阶):在继承中, self::指向定义方法的类,static::指向运行时实际调用的类。这是高级特性,框架底层经常用,日常业务写得不多了解即可。
类中可以用 const 定义常量,属于类本身,通过 :: 访问,值不可变。
<?phpclass Config { const VERSION = '1.0'; const DEBUG = true;}echo Config::VERSION; // 1.0?>PHP 提供了一系列以 __ 开头的方法,在特定情况下自动调用。
__construct() | |
__destruct() | |
__get($name) | |
__set($name, $value) | |
__isset($name) | isset() 或 empty() |
__unset($name) | unset() |
__call($name, $args) | |
__callStatic($name, $args) | |
__toString() | echo $obj) |
__clone() | clone 时 |
<?phpclass User { private $data = []; public function __set($name, $value) { $this->data[$name] = $value; } public function __get($name) { return $this->data[$name] ?? null; } public function __toString() { return "用户:" . json_encode($this->data); }}$user = new User();$user->name = '张三'; // 触发 __setecho $user->name; // 触发 __get,输出 张三echo $user; // 触发 __toString,输出 用户:{"name":"张三"}?>项目变大后,类名冲突是常事——两个模块都有 User 类。命名空间解决这个问题。
<?phpnamespace Blog\Controllers;class UserController { // ...}// 使用$controller = new \Blog\Controllers\UserController();?>或者用 use 导入:
<?phpnamespace App;use Blog\Controllers\UserController;$controller = new UserController();?>没有指定命名空间的代码属于全局空间,访问全局类时要加 \ 前缀,比如 \PDO、\Exception。
传统方式每个文件都要 require 'User.php'; require 'Post.php';,文件一多就崩溃。自动加载让你遵循命名约定后,类在被使用时自动加载对应文件。
Composer 自动加载是现代 PHP 的标准方案。安装 Composer 后,在项目根目录创建 composer.json:
{ "autoload": { "psr-4": { "MyApp\\": "src/" } }}运行 composer dump-autoload 后,只要引入 vendor/autoload.php,就能自动加载 src/ 目录下遵循 PSR-4 规范的类。比如 MyApp\Models\User 会被映射到 src/Models/User.php。
不用 Composer 也能自动加载:本篇综合实战用 spl_autoload_register()写了一个简易 PSR-4 加载器,适合学习理解原理。生产环境还是用 Composer。
前六篇的博客系统是面向过程写的,这一节用 OOP 重构一遍。核心思路:把数据库操作封装成 Database 单例类,把用户和文章各自封装成 User / Post 模型类,页面只负责调用模型方法 + 渲染 HTML。
项目根目录├── Core/│ └── Database.php # 数据库单例类├── Models/│ ├── User.php # 用户模型│ └── Post.php # 文章模型├── register.php # 注册页面├── login.php # 登录页面├── home.php # 文章首页列表├── create_post.php # 发布文章├── view_post.php # 文章详情├── edit_post.php # 编辑文章├── delete_post.php # 删除文章├── welcome.php # 个人中心├── delete_user.php # 注销用户└── logout.php # 退出登录CREATE DATABASE IF NOT EXISTS blog CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;USE blog;CREATE TABLE users ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(255) NOT NULL, email VARCHAR(100) NOT NULL UNIQUE, avatar VARCHAR(255) NULL, create_time DATETIME DEFAULT CURRENT_TIMESTAMP) ENGINE=InnoDB;CREATE TABLE posts ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, user_id INT UNSIGNED NOT NULL, title VARCHAR(200) NOT NULL, content TEXT NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE) ENGINE=InnoDB;<?phpnamespace Blog\Core;class Database{ private static ?self $instance = null; private \PDO $pdo; // 私有构造,禁止外部new private function __construct() { $host = '127.0.0.1'; $dbname = 'blog'; $dbUser = 'root'; $dbPass = ''; $charset = 'utf8mb4'; $dsn = "mysql:host=$host;dbname=$dbname;charset=$charset"; $options = [ \PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION, \PDO::ATTR_DEFAULT_FETCH_MODE => \PDO::FETCH_ASSOC, \PDO::ATTR_EMULATE_PREPARES => false, ]; $this->pdo = new \PDO($dsn, $dbUser, $dbPass, $options); } // 获取单例实例 public static function getInstance(): self { if (self::$instance === null) { self::$instance = new self(); } return self::$instance; } // 获取PDO对象 public function getPdo(): \PDO { return $this->pdo; } // 禁止克隆 private function __clone() {}}单例模式要点:构造函数私有化(外部不能 new)、 getInstance()静态方法返回唯一实例、禁止克隆。这样整个应用只创建一个 PDO 连接,避免重复连接数据库。第六篇面向过程版每次都 new PDO,重构后只连接一次。
<?phpnamespace Blog\Models;use Blog\Core\Database;class User{ public ?int $id = null; public string $username = ''; public string $email = ''; public ?string $avatar = null; /** * 根据用户名查询用户(不含密码) */ public static function findByUsername(string $username): ?self { $pdo = Database::getInstance()->getPdo(); $stmt = $pdo->prepare("SELECT id, username, email, avatar FROM users WHERE username = :u"); $stmt->execute([':u' => $username]); $data = $stmt->fetch(); if (!$data) return null; $user = new self(); $user->id = $data['id']; $user->username = $data['username']; $user->email = $data['email']; $user->avatar = $data['avatar']; return $user; } /** * 根据ID查询完整用户(含密码用于校验) */ public static function findById(int $id): ?self { $pdo = Database::getInstance()->getPdo(); $stmt = $pdo->prepare("SELECT id, username, email, avatar, password FROM users WHERE id = :id"); $stmt->execute([':id' => $id]); $data = $stmt->fetch(); if (!$data) return null; $user = new self(); $user->id = $data['id']; $user->username = $data['username']; $user->email = $data['email']; $user->avatar = $data['avatar']; return $user; } /** * 注册新用户,返回用户ID */ public static function create(string $username, string $password, string $email, ?string $avatarPath = null): int { $pdo = Database::getInstance()->getPdo(); $hashPwd = password_hash($password, PASSWORD_DEFAULT); $stmt = $pdo->prepare( "INSERT INTO users (username, password, email, avatar) VALUES (:u, :p, :e, :avatar)" ); $stmt->execute([ ':u' => $username, ':p' => $hashPwd, ':e' => $email, ':avatar' => $avatarPath ]); return (int)$pdo->lastInsertId(); } /** * 校验密码是否匹配 */ public function verifyPassword(string $inputPwd): bool { if (!$this->id) return false; $pdo = Database::getInstance()->getPdo(); $stmt = $pdo->prepare("SELECT password FROM users WHERE id = :id"); $stmt->execute([':id' => $this->id]); $row = $stmt->fetch(); return $row && password_verify($inputPwd, $row['password']); } /** * 校验用户名/邮箱是否已存在 */ public static function isExists(string $username, string $email): bool { $pdo = Database::getInstance()->getPdo(); $stmt = $pdo->prepare("SELECT id FROM users WHERE username = :u OR email = :e"); $stmt->execute([':u' => $username, ':e' => $email]); return (bool)$stmt->fetch(); } /** * 删除当前用户账号(注销) */ public function delete(): void { if (!$this->id) return; $pdo = Database::getInstance()->getPdo(); $stmt = $pdo->prepare("DELETE FROM users WHERE id = :uid"); $stmt->execute([':uid' => $this->id]); }}<?phpnamespace Blog\Models;use Blog\Core\Database;class Post{ public ?int $id = null; public int $userId = 0; public string $title = ''; public string $content = ''; public string $createdAt = ''; public string $authorName = ''; /** * 获取全部文章(联表作者,倒序) * @return Post[] */ public static function getAll(): array { $pdo = Database::getInstance()->getPdo(); $sql = " SELECT p.*, u.username as authorName FROM posts p JOIN users u ON p.user_id = u.id ORDER BY p.created_at DESC "; $stmt = $pdo->query($sql); $list = []; while ($row = $stmt->fetch()) { $post = new self(); $post->id = $row['id']; $post->userId = $row['user_id']; $post->title = $row['title']; $post->content = $row['content']; $post->createdAt = $row['created_at']; $post->authorName = $row['authorName']; $list[] = $post; } return $list; } /** * 根据ID查询单篇文章 */ public static function find(int $id): ?self { $pdo = Database::getInstance()->getPdo(); $sql = " SELECT p.*, u.username as authorName FROM posts p JOIN users u ON p.user_id = u.id WHERE p.id = :pid "; $stmt = $pdo->prepare($sql); $stmt->execute([':pid' => $id]); $row = $stmt->fetch(); if (!$row) return null; $post = new self(); $post->id = $row['id']; $post->userId = $row['user_id']; $post->title = $row['title']; $post->content = $row['content']; $post->createdAt = $row['created_at']; $post->authorName = $row['authorName']; return $post; } /** * 保存:新增 / 更新 */ public function save(): self { $pdo = Database::getInstance()->getPdo(); if ($this->id) { // 更新 $stmt = $pdo->prepare("UPDATE posts SET title=:t, content=:c WHERE id=:id"); $stmt->execute([ ':t' => $this->title, ':c' => $this->content, ':id' => $this->id ]); } else { // 新增 $stmt = $pdo->prepare("INSERT INTO posts (user_id, title, content) VALUES (:uid, :t, :c)"); $stmt->execute([ ':uid' => $this->userId, ':t' => $this->title, ':c' => $this->content ]); $this->id = (int)$pdo->lastInsertId(); } return $this; } /** * 删除当前文章 */ public function delete(): void { if (!$this->id) return; $pdo = Database::getInstance()->getPdo(); $stmt = $pdo->prepare("DELETE FROM posts WHERE id = :id"); $stmt->execute([':id' => $this->id]); }}save() 方法的巧思:同一个 save()方法既能新增也能更新——判断$this->id是否存在来决定走 INSERT 还是 UPDATE。这是 ActiveRecord 模式的雏形,Laravel 的 Eloquent 也是这个思路。
<?php// 自定义PSR4自动加载器,无需composerspl_autoload_register(function ($className) { // 命名空间根 Blog\ 对应项目根目录 $prefix = 'Blog\\'; $baseDir = __DIR__ . DIRECTORY_SEPARATOR; // 匹配命名空间前缀 if (str_starts_with($className, $prefix)) { $relativeClass = substr($className, strlen($prefix)); // 命名空间转文件路径 Blog\Models\User → Models/User.php $file = $baseDir . str_replace('\\', DIRECTORY_SEPARATOR, $relativeClass) . '.php'; if (file_exists($file)) { require $file; } }});session_start();header("X-XSS-Protection: 1; mode=block");header("X-Frame-Options: DENY");use Blog\Models\User;// CSRFif (empty($_SESSION['csrf_reg'])) { $_SESSION['csrf_reg'] = bin2hex(random_bytes(32));}$errors = [];$fill = ['username' => '', 'email' => ''];$uploadDir = __DIR__ . '../uploads/avatar/';if (!file_exists($uploadDir)) @mkdir($uploadDir, 0755, true);$avatarPath = null;if ($_SERVER['REQUEST_METHOD'] === 'POST') { // CSRF校验 if (!hash_equals($_SESSION['csrf_reg'], $_POST['csrf_token'] ?? '')) { $errors['global'] = "非法提交请求"; } $username = trim($_POST['username'] ?? ''); $password = $_POST['password'] ?? ''; $password2 = $_POST['password2'] ?? ''; $email = trim($_POST['email'] ?? ''); $fill['username'] = $username; $fill['email'] = $email; // 基础校验 if (empty($username)) $errors['username'] = "用户名不能为空"; elseif (mb_strlen($username) < 3) $errors['username'] = "用户名至少3字符"; if (empty($password)) $errors['password'] = "密码不能为空"; elseif (strlen($password) < 6) $errors['password'] = "密码最少6位"; if ($password !== $password2) $errors['password2'] = "两次密码不一致"; if (empty($email)) $errors['email'] = "邮箱不能为空"; elseif (!filter_var($email, FILTER_VALIDATE_EMAIL)) $errors['email'] = "邮箱格式错误"; // 头像上传 if (isset($_FILES['avatar']) && $_FILES['avatar']['error'] !== UPLOAD_ERR_NO_FILE) { $file = $_FILES['avatar']; if ($file['error'] !== UPLOAD_ERR_OK) { $errors['avatar'] = "文件上传失败"; } else { $finfo = new finfo(FILEINFO_MIME_TYPE); $mime = $finfo->file($file['tmp_name']); $allow = ['image/jpeg','image/png']; if (!in_array($mime, $allow)) $errors['avatar'] = "仅支持jpg/png"; elseif ($file['size'] > 1048576) $errors['avatar'] = "头像不超过1MB"; else { $ext = $mime === 'image/png' ? 'png' : 'jpg'; $name = md5(uniqid(true).$username).".".$ext; $dest = $uploadDir . $name; move_uploaded_file($file['tmp_name'], $dest); $avatarPath = "uploads/avatar/".$name; } } } // 无错误校验账号是否存在 if (empty($errors)) { if (User::isExists($username, $email)) { $errors['global'] = "用户名或邮箱已被注册"; } else { $uid = User::create($username, $password, $email, $avatarPath); // 自动登录 $_SESSION['logged_in'] = true; $_SESSION['user_id'] = $uid; $_SESSION['username'] = $username; $_SESSION['avatar'] = $avatarPath; header("Location: welcome.php", true, 302); exit; } }}?><!DOCTYPE html><html lang="zh-CN"><head><meta charset="UTF-8"><title>用户注册</title><style>*{margin:0;padding:0;box-sizing:border-box;font-family:Microsoft YaHei;}body{background:#f4f6f9;padding:60px 20px;}.card{width:420px;margin:0 auto;background:#fff;padding:32px;border-radius:10px;box-shadow:0 2px 12px rgba(0,0,0,0.07);}h2{text-align:center;margin-bottom:24px;color:#2d3748;}.global-err{background:#fee;color:#dc2626;padding:10px;border-radius:6px;margin-bottom:16px;text-align:center;}.item{margin-bottom:16px;}label{display:block;margin-bottom:6px;color:#4a5568;}input{width:100%;padding:10px 12px;border:1px solid #cbd5e0;border-radius:6px;font-size:15px;}.err-text{color:#dc2626;font-size:13px;margin-top:4px;display:block;}button{width:100%;padding:11px;background:#2563eb;color:#fff;border:none;border-radius:6px;font-size:16px;cursor:pointer;}button:hover{background:#1d4ed8;}.link{text-align:center;margin-top:16px;font-size:14px;}.link a{color:#2563eb;text-decoration:none;}</style></head><body><div class="card"> <h2>新用户注册</h2> <?php if(!empty($errors['global'])): ?> <div class="global-err"><?=htmlspecialchars($errors['global'],ENT_QUOTES)?></div> <?php endif; ?> <form method="post" enctype="multipart/form-data"> <input type="hidden" name="csrf_token" value="<?=htmlspecialchars($_SESSION['csrf_reg'],ENT_QUOTES)?>"> <div class="item"> <label>用户名</label> <input type="text" name="username" value="<?=htmlspecialchars($fill['username'],ENT_QUOTES)?>" placeholder="至少3字符"> <?php if(!empty($errors['username'])):?><span class="err-text"><?=$errors['username']?></span><?php endif; ?> </div> <div class="item"> <label>密码</label> <input type="password" name="password" placeholder="最少6位"> <?php if(!empty($errors['password'])):?><span class="err-text"><?=$errors['password']?></span><?php endif; ?> </div> <div class="item"> <label>确认密码</label> <input type="password" name="password2"> <?php if(!empty($errors['password2'])):?><span class="err-text"><?=$errors['password2']?></span><?php endif; ?> </div> <div class="item"> <label>邮箱</label> <input type="email" name="email" value="<?=htmlspecialchars($fill['email'],ENT_QUOTES)?>"> <?php if(!empty($errors['email'])):?><span class="err-text"><?=$errors['email']?></span><?php endif; ?> </div> <div class="item"> <label>头像(选填,1MB内jpg/png)</label> <input type="file" name="avatar" accept="image/jpeg,image/png"> <?php if(!empty($errors['avatar'])):?><span class="err-text"><?=$errors['avatar']?></span><?php endif; ?> </div> <button type="submit">注册</button> </form> <div class="link">已有账号?<a href="login.php">去登录</a></div></div></body></html><?phpspl_autoload_register(function ($className) { $prefix = 'Blog\\'; $baseDir = __DIR__ . DIRECTORY_SEPARATOR; if (str_starts_with($className, $prefix)) { $relativeClass = substr($className, strlen($prefix)); $file = $baseDir . str_replace('\\', DIRECTORY_SEPARATOR, $relativeClass) . '.php'; if (file_exists($file)) { require $file; } }});session_start();header("X-XSS-Protection: 1; mode=block");header("X-Frame-Options: DENY");use Blog\Models\User;$error = '';$fillUser = '';if ($_SERVER['REQUEST_METHOD'] === 'POST') { $username = trim($_POST['username'] ?? ''); $password = $_POST['password'] ?? ''; $fillUser = $username; if (empty($username) || empty($password)) { $error = "用户名和密码不能为空"; } else { $user = User::findByUsername($username); if (!$user || !$user->verifyPassword($password)) { $error = "用户名或密码错误"; } else { $_SESSION['logged_in'] = true; $_SESSION['user_id'] = $user->id; $_SESSION['username'] = $user->username; $_SESSION['avatar'] = $user->avatar; header("Location: welcome.php", true, 302); exit; } }}?><!DOCTYPE html><html lang="zh-CN"><head><meta charset="UTF-8"><title>账号登录</title><style>*{margin:0;padding:0;box-sizing:border-box;font-family:Microsoft YaHei;}body{background:#f4f6f9;padding:60px 20px;}.card{width:380px;margin:0 auto;background:#fff;padding:32px;border-radius:10px;box-shadow:0 2px 12px rgba(0,0,0,0.07);}h2{text-align:center;margin-bottom:24px;color:#2d3748;}.err-box{background:#fee;color:#dc2626;padding:10px;border-radius:6px;margin-bottom:16px;text-align:center;}.row{margin-bottom:16px;}label{display:block;margin-bottom:6px;color:#4a5568;}input{width:100%;padding:10px 12px;border:1px solid #cbd5e0;border-radius:6px;font-size:15px;}button{width:100%;padding:11px;background:#2563eb;color:#fff;border:none;border-radius:6px;font-size:16px;cursor:pointer;}button:hover{background:#1d4ed8;}.reg-link{text-align:center;margin-top:16px;font-size:14px;}.reg-link a{color:#2563eb;text-decoration:none;}</style></head><body><div class="card"> <h2>账号登录</h2> <?php if($error): ?> <div class="err-box"><?=htmlspecialchars($error,ENT_QUOTES)?></div> <?php endif; ?> <form method="post"> <div class="row"> <label>用户名</label> <input type="text" name="username" value="<?=htmlspecialchars($fillUser,ENT_QUOTES)?>"> </div> <div class="row"> <label>密码</label> <input type="password" name="password"> </div> <button type="submit">登录</button> </form> <div class="reg-link">没有账号?<a href="register.php">注册</a></div></div></body></html><?phpspl_autoload_register(function ($className) { $namespacePrefix = 'Blog\\'; $rootPath = __DIR__ . DIRECTORY_SEPARATOR; if (strpos($className, $namespacePrefix) === 0) { $classPath = substr($className, strlen($namespacePrefix)); $file = $rootPath . str_replace('\\', DIRECTORY_SEPARATOR, $classPath) . '.php'; if (file_exists($file)) { require_once $file; } }});session_start();header("X-XSS-Protection: 1; mode=block");header("X-Frame-Options: DENY");use Blog\Models\User;// 未登录拦截if (empty($_SESSION['logged_in'])) { header("Location: login.php", true, 302); exit;}// CSRF令牌if (empty($_SESSION['csrf_del_user'])) { $_SESSION['csrf_del_user'] = bin2hex(random_bytes(32));}$error = '';$currentUser = User::findById($_SESSION['user_id']);if (!$currentUser) { header("Location: login.php", true, 302); exit;}// 提交注销逻辑if ($_SERVER['REQUEST_METHOD'] === 'POST') { $token = $_POST['csrf_token'] ?? ''; $inputPwd = trim($_POST['password'] ?? ''); // CSRF校验 if (!hash_equals($_SESSION['csrf_del_user'], $token)) { $error = "非法操作"; } elseif (empty($inputPwd)) { $error = "请输入登录密码确认注销"; } elseif (!$currentUser->verifyPassword($inputPwd)) { $error = "密码错误,无法注销账号"; } else { // 删除用户账号 $currentUser->delete(); // 清空session跳转登录页 $_SESSION = []; if (ini_get("session.use_cookies")) { $params = session_get_cookie_params(); setcookie(session_name(), '', time() - 86400, $params["path"], $params["domain"], $params["secure"], $params["httponly"] ); } session_destroy(); header("Location: login.php", true, 302); exit; }}?><!DOCTYPE html><html lang="zh-CN"><head><meta charset="UTF-8"><title>注销账号确认</title><style>*{margin:0;padding:0;box-sizing:border-box;font-family:Microsoft YaHei;}body{background:#f5f7fa;padding:80px 20px;}.card{width:480px;margin:0 auto;background:#fff;padding:36px;border-radius:10px;box-shadow:0 2px 12px rgba(0,0,0,0.08);text-align:center;}h2{color:#dc2626;margin-bottom:16px;font-size:20px;}.tip{margin-bottom:24px;color:#444;line-height:1.7;}.err{color:#dc2626;margin-bottom:16px;}.input-box{width:100%;padding:10px 12px;border:1px solid #cbd5e0;border-radius:6px;margin-bottom:20px;font-size:15px;}.btn-group{display:flex;gap:12px;width:100%;}.btn-col{flex: 1 1 50%;}.btn{display:block;width:100%;height:48px;line-height:48px;font-size:16px;text-align:center;border-radius:6px;border:none;text-decoration:none;cursor:pointer;color:#fff;}.btn-del{background:#dc2626;}.btn-cancel{background:#666666;}</style></head><body><div class="card"> <h2>永久注销账号</h2> <?php if($error): ?> <div class="err"><?=htmlspecialchars($error,ENT_QUOTES)?></div> <?php endif; ?> <div class="tip"> 注销后你的账号、头像、所有发布文章将被永久删除,数据无法恢复!<br> 请输入登录密码确认操作 </div> <form method="post"> <input type="hidden" name="csrf_token" value="<?=htmlspecialchars($_SESSION['csrf_del_user'],ENT_QUOTES)?>"> <input class="input-box" type="password" name="password" placeholder="输入你的登录密码" required> <div class="btn-group"> <div class="btn-col"> <button class="btn btn-del" type="submit">确认注销</button> </div> <div class="btn-col"> <a class="btn btn-cancel" href="welcome.php">取消返回</a> </div> </div> </form></div></body></html><?phpspl_autoload_register(function ($className) { $namespacePrefix = 'Blog\\'; $rootPath = __DIR__ . DIRECTORY_SEPARATOR; if (strpos($className, $namespacePrefix) === 0) { $classPath = substr($className, strlen($namespacePrefix)); $file = $rootPath . str_replace('\\', DIRECTORY_SEPARATOR, $classPath) . '.php'; if (file_exists($file)) { require_once $file; } }});session_start();if (empty($_SESSION['logged_in'])) { header("Location: login.php", true, 302); exit;}use Blog\Models\User;$userName = htmlspecialchars($_SESSION['username'], ENT_QUOTES);$avatar = '';$dbUser = User::findById($_SESSION['user_id']);if ($dbUser) { $avatar = $dbUser->avatar;}?><!DOCTYPE html><html lang="zh-CN"><head><meta charset="UTF-8"><title>个人中心</title><style>*{margin:0;padding:0;box-sizing:border-box;font-family:Microsoft YaHei;}body{background:#f4f6f9;padding-top:100px;text-align:center;}.box{width:460px;margin:0 auto;background:#fff;padding:40px 30px;border-radius:12px;box-shadow:0 2px 14px rgba(0,0,0,0.07);}h2{color:#2d3748;margin-bottom:20px;font-size:22px;}.avatar-wrap{margin:26px 0 32px;}.avatar-img{width:140px;height:140px;border-radius:50%;object-fit:cover;border:3px solid #2563eb;}.avatar-empty{width:140px;height:140px;border-radius:50%;background:#e2e8f0;display:inline-flex;align-items:center;justify-content:center;color:#666;font-size:14px;}.btn-wrap{display:grid;grid-template-columns: 1fr 1fr;gap:14px;}.btn{display:block;width:100%;height:46px;line-height:46px;font-size:16px;text-decoration:none;border-radius:8px;color:#fff;transition:opacity 0.2s ease;}.btn:hover{opacity:0.88;}.btn-blue{background:#2563eb;}.btn-gray{background:#6b7280;}.btn-red{background:#dc2626;}</style></head><body><div class="box"> <h2>欢迎你,<?=$userName?>!</h2> <div class="avatar-wrap"> <?php if(!empty($avatar) && file_exists($avatar)): ?> <img class="avatar-img" src="<?=htmlspecialchars($avatar,ENT_QUOTES)?>" alt="用户头像"> <?php else: ?> <div class="avatar-empty">暂无头像</div> <?php endif; ?> </div> <div class="btn-wrap"> <a href="home.php" class="btn btn-blue">浏览全部文章</a> <a href="create_post.php" class="btn btn-blue">发布新文章</a> <a href="logout.php" class="btn btn-gray">安全退出登录</a> <a href="delete_user.php" class="btn btn-red">永久注销账号</a> </div></div></body></html><?phpsession_start();$_SESSION = [];if (ini_get("session.use_cookies")) { $params = session_get_cookie_params(); setcookie(session_name(), '', time()-86400, $params["path"], $params["domain"], $params["secure"], $params["httponly"] );}session_destroy();header("Location: login.php", true, 302);exit;?><?phpspl_autoload_register(function ($className) { $prefix = 'Blog\\'; $baseDir = __DIR__ . DIRECTORY_SEPARATOR; if (str_starts_with($className, $prefix)) { $relativeClass = substr($className, strlen($prefix)); $file = $baseDir . str_replace('\\', DIRECTORY_SEPARATOR, $relativeClass) . '.php'; if (file_exists($file)) { require $file; } }});session_start();use Blog\Models\Post;$postList = Post::getAll();?><!DOCTYPE html><html lang="zh-CN"><head><meta charset="UTF-8"><title>全部文章</title><style>*{margin:0;padding:0;box-sizing:border-box;font-family:Microsoft YaHei;}body{background:#f5f7fa;padding:40px 20px;}.wrap{max-width:720px;margin:0 auto;}.top{display:flex;justify-content:space-between;align-items:center;margin-bottom:24px;}h1{color:#2d3748;font-size:24px;}.btn{padding:8px 16px;background:#2563eb;color:#fff;text-decoration:none;border-radius:6px;}.item{background:#fff;padding:20px;border-radius:10px;box-shadow:0 1px 8px rgba(0,0,0,0.06);margin-bottom:16px;}.title{font-size:18px;margin-bottom:8px;}.title a{color:#2563eb;text-decoration:none;}.meta{color:#666;font-size:14px;}.empty{text-align:center;padding:40px;color:#888;background:#fff;border-radius:10px;}</style></head><body><div class="wrap"> <div class="top"> <h1>文章列表</h1> <div> <?php if(!empty($_SESSION['logged_in'])): ?> <a href="create_post.php" class="btn">发布文章</a> <a href="welcome.php" class="btn" style="background:#666;margin-left:8px;">个人中心</a> <?php else: ?> <a href="login.php" class="btn">登录</a> <?php endif; ?> </div> </div> <?php if(empty($postList)): ?> <div class="empty">暂无文章</div> <?php else: ?> <?php foreach($postList as $p): ?> <div class="item"> <div class="title"> <a href="view_post.php?id=<?=$p->id?>"><?=htmlspecialchars($p->title)?></a> </div> <div class="meta">作者:<?=htmlspecialchars($p->authorName)?> · <?=$p->createdAt?></div> </div> <?php endforeach; ?> <?php endif; ?></div></body></html><?phpspl_autoload_register(function ($className) { $prefix = 'Blog\\'; $baseDir = __DIR__ . DIRECTORY_SEPARATOR; if (str_starts_with($className, $prefix)) { $relativeClass = substr($className, strlen($prefix)); $file = $baseDir . str_replace('\\', DIRECTORY_SEPARATOR, $relativeClass) . '.php'; if (file_exists($file)) { require $file; } }});session_start();header("X-XSS-Protection: 1; mode=block");header("X-Frame-Options: DENY");if (empty($_SESSION['logged_in'])) { header("Location: login.php", true, 302); exit;}use Blog\Models\Post;if (empty($_SESSION['csrf_post'])) $_SESSION['csrf_post'] = bin2hex(random_bytes(32));$errors = [];$tFill = $cFill = '';if ($_SERVER['REQUEST_METHOD'] === 'POST') { if (!hash_equals($_SESSION['csrf_post'], $_POST['csrf_token']??'')) $errors[] = "非法请求"; $tFill = trim($_POST['title']??''); $cFill = trim($_POST['content']??''); if(empty($tFill)) $errors[] = "标题不能为空"; if(mb_strlen($tFill)>200) $errors[] = "标题不能超过200字"; if(empty($cFill)) $errors[] = "内容不能为空"; if(empty($errors)){ $post = new Post(); $post->userId = $_SESSION['user_id']; $post->title = $tFill; $post->content = $cFill; $post->save(); header("Location: home.php", true, 302); exit; }}?><!DOCTYPE html><html lang="zh-CN"><head><meta charset="UTF-8"><title>发布文章</title><style>*{margin:0;padding:0;box-sizing:border-box;font-family:Microsoft YaHei;}body{background:#f5f7fa;padding:60px 20px;}.card{width:620px;margin:0 auto;background:#fff;padding:32px;border-radius:10px;box-shadow:0 2px 12px rgba(0,0,0,0.08);}h2{text-align:center;margin-bottom:24px;color:#2d3748;}.err{background:#fee;color:#dc2626;padding:10px;border-radius:6px;margin-bottom:16px;}.item{margin-bottom:16px;}label{display:block;margin-bottom:6px;color:#444;}input,textarea{width:100%;padding:12px;border:1px solid #cbd5e0;border-radius:6px;font-size:15px;}textarea{height:240px;resize:none;}button{width:100%;padding:12px;background:#2563eb;color:#fff;border:none;border-radius:6px;font-size:16px;cursor:pointer;}button:hover{background:#1d4ed8;}.back{display:block;text-align:center;margin-top:16px;color:#666;text-decoration:none;}</style></head><body><div class="card"> <h2>发布新文章</h2> <?php if(!empty($errors)): ?> <div class="err"><?=implode('<br>', array_map(fn($v)=>htmlspecialchars($v,ENT_QUOTES),$errors))?></div> <?php endif; ?> <form method="post"> <input type="hidden" name="csrf_token" value="<?=htmlspecialchars($_SESSION['csrf_post'],ENT_QUOTES)?>"> <div class="item"> <label>标题</label> <input type="text" name="title" value="<?=htmlspecialchars($tFill,ENT_QUOTES)?>"> </div> <div class="item"> <label>正文</label> <textarea name="content"><?=htmlspecialchars($cFill,ENT_QUOTES)?></textarea> </div> <button type="submit">发布</button> </form> <a class="back" href="home.php">返回列表</a></div></body></html><?phpspl_autoload_register(function ($className) { $prefix = 'Blog\\'; $baseDir = __DIR__ . DIRECTORY_SEPARATOR; if (str_starts_with($className, $prefix)) { $relativeClass = substr($className, strlen($prefix)); $file = $baseDir . str_replace('\\', DIRECTORY_SEPARATOR, $relativeClass) . '.php'; if (file_exists($file)) { require $file; } }});session_start();use Blog\Models\Post;$id = (int)($_GET['id'] ?? 0);$post = Post::find($id);if(!$post){ header("Location: home.php", true, 302); exit;}$isAuthor = !empty($_SESSION['logged_in']) && $_SESSION['user_id'] == $post->userId;?><!DOCTYPE html><html lang="zh-CN"><head><meta charset="UTF-8"><title><?=htmlspecialchars($post->title)?></title><style>*{margin:0;padding:0;box-sizing:border-box;font-family:Microsoft YaHei;}body{background:#f5f7fa;padding:40px 20px;}.box{max-width:700px;margin:0 auto;background:#fff;padding:36px;border-radius:10px;box-shadow:0 2px 12px rgba(0,0,0,0.08);}h1{text-align:center;margin-bottom:16px;color:#222;}.meta{text-align:center;color:#666;margin-bottom:24px;padding-bottom:16px;border-bottom:1px solid #eee;}.content{line-height:1.8;font-size:16px;color:#333;white-space:pre-wrap;}.btn-group{margin-top:30px;text-align:center;}.btn{display:inline-block;padding:8px 16px;margin:0 6px;text-decoration:none;border-radius:6px;color:#fff;}.edit{background:#059669;}.del{background:#dc2626;}.back{background:#666;}</style></head><body><div class="box"> <h1><?=htmlspecialchars($post->title)?></h1> <div class="meta">作者:<?=htmlspecialchars($post->authorName)?> | <?=$post->createdAt?></div> <div class="content"><?=nl2br(htmlspecialchars($post->content))?></div> <div class="btn-group"> <?php if($isAuthor): ?> <a href="edit_post.php?id=<?=$post->id?>" class="btn edit">编辑</a> <a href="delete_post.php?id=<?=$post->id?>" class="btn del">删除</a> <?php endif; ?> <a href="home.php" class="btn back">返回列表</a> </div></div></body></html><?phpspl_autoload_register(function ($className) { $prefix = 'Blog\\'; $baseDir = __DIR__ . DIRECTORY_SEPARATOR; if (str_starts_with($className, $prefix)) { $relativeClass = substr($className, strlen($prefix)); $file = $baseDir . str_replace('\\', DIRECTORY_SEPARATOR, $relativeClass) . '.php'; if (file_exists($file)) { require $file; } }});session_start();header("X-XSS-Protection: 1; mode=block");header("X-Frame-Options: DENY");if (empty($_SESSION['logged_in'])) { header("Location: login.php", true, 302); exit;}use Blog\Models\Post;$pid = (int)($_GET['id'] ?? 0);$post = Post::find($pid);if(!$post || $post->userId != $_SESSION['user_id']){ header("Location: home.php", true, 302); exit;}if(empty($_SESSION['csrf_edit'])) $_SESSION['csrf_edit'] = bin2hex(random_bytes(32));$errors = [];$tFill = $post->title;$cFill = $post->content;if ($_SERVER['REQUEST_METHOD'] === 'POST') { if (!hash_equals($_SESSION['csrf_edit'], $_POST['csrf_token']??'')) $errors[] = "非法请求"; $tFill = trim($_POST['title']??''); $cFill = trim($_POST['content']??''); if(empty($tFill)) $errors[] = "标题不能为空"; if(mb_strlen($tFill)>200) $errors[] = "标题超长"; if(empty($cFill)) $errors[] = "内容不能为空"; if(empty($errors)){ $post->title = $tFill; $post->content = $cFill; $post->save(); header("Location: view_post.php?id=$pid", true, 302); exit; }}?><!DOCTYPE html><html lang="zh-CN"><head><meta charset="UTF-8"><title>编辑文章</title><style>*{margin:0;padding:0;box-sizing:border-box;font-family:Microsoft YaHei;}body{background:#f5f7fa;padding:60px 20px;}.card{width:620px;margin:0 auto;background:#fff;padding:32px;border-radius:10px;box-shadow:0 2px 12px rgba(0,0,0,0.08);}h2{text-align:center;margin-bottom:24px;color:#2d3748;}.err{background:#fee;color:#dc2626;padding:10px;border-radius:6px;margin-bottom:16px;}.item{margin-bottom:16px;}label{display:block;margin-bottom:6px;color:#444;}input,textarea{width:100%;padding:12px;border:1px solid #cbd5e0;border-radius:6px;font-size:15px;}textarea{height:240px;resize:none;}button{width:100%;padding:12px;background:#059669;color:#fff;border:none;border-radius:6px;font-size:16px;cursor:pointer;}button:hover{background:#047857;}.back{display:block;text-align:center;margin-top:16px;color:#666;text-decoration:none;}</style></head><body><div class="card"> <h2>编辑文章</h2> <?php if(!empty($errors)): ?> <div class="err"><?=implode('<br>', array_map(fn($v)=>htmlspecialchars($v,ENT_QUOTES),$errors))?></div> <?php endif; ?> <form method="post"> <input type="hidden" name="csrf_token" value="<?=htmlspecialchars($_SESSION['csrf_edit'],ENT_QUOTES)?>"> <div class="item"> <label>标题</label> <input type="text" name="title" value="<?=htmlspecialchars($tFill,ENT_QUOTES)?>"> </div> <div class="item"> <label>正文</label> <textarea name="content"><?=htmlspecialchars($cFill,ENT_QUOTES)?></textarea> </div> <button type="submit">保存修改</button> </form> <a class="back" href="view_post.php?id=<?=$pid?>">取消返回</a></div></body></html><?phpspl_autoload_register(function ($className) { $prefix = 'Blog\\'; $baseDir = __DIR__ . DIRECTORY_SEPARATOR; if (str_starts_with($className, $prefix)) { $relativeClass = substr($className, strlen($prefix)); $file = $baseDir . str_replace('\\', DIRECTORY_SEPARATOR, $relativeClass) . '.php'; if (file_exists($file)) { require $file; } }});session_start();header("X-XSS-Protection: 1; mode=block");header("X-Frame-Options: DENY");if (empty($_SESSION['logged_in'])) { header("Location: login.php", true, 302); exit;}use Blog\Models\Post;$pid = (int)($_GET['id'] ?? 0);$post = Post::find($pid);if(!$post || $post->userId != $_SESSION['user_id']){ header("Location: home.php", true, 302); exit;}if(empty($_SESSION['csrf_del'])) $_SESSION['csrf_del'] = bin2hex(random_bytes(32));$err = '';if ($_SERVER['REQUEST_METHOD'] === 'POST') { if (!hash_equals($_SESSION['csrf_del'], $_POST['csrf_token']??'')) { $err = "非法操作"; } else { $post->delete(); header("Location: home.php", true, 302); exit; }}?><!DOCTYPE html><html lang="zh-CN"><head><meta charset="UTF-8"><title>确认删除</title><style>*{margin:0;padding:0;box-sizing:border-box;font-family:Microsoft YaHei;}body{background:#f5f7fa;padding:80px 20px;}.card{width:460px;margin:0 auto;background:#fff;padding:36px;border-radius:10px;box-shadow:0 2px 12px rgba(0,0,0,0.08);text-align:center;}h2{color:#dc2626;margin-bottom:16px;font-size:20px;}.tip{margin-bottom:24px;color:#444;line-height:1.7;}.err{color:#dc2626;margin-bottom:16px;}.btn-group{display:flex;gap:12px;}.btn-group form, .btn-group .wrap{flex:1;}.btn-group button, .btn-group a{ display:block;width:100%;height:44px;line-height:44px;padding:0;border-radius:6px;font-size:16px;text-decoration:none;border:none;cursor:pointer;text-align:center;color:#fff;}.confirm{background:#dc2626;}.cancel{background:#666;}</style></head><body><div class="card"> <h2>删除确认</h2> <?php if($err): ?><div class="err"><?=htmlspecialchars($err,ENT_QUOTES)?></div><?php endif; ?> <div class="tip">确定永久删除文章:<br><strong><?=htmlspecialchars($post->title)?></strong>?<br>删除后无法恢复</div> <div class="btn-group"> <form method="post"> <input type="hidden" name="csrf_token" value="<?=htmlspecialchars($_SESSION['csrf_del'],ENT_QUOTES)?>"> <button class="confirm" type="submit">确认删除</button> </form> <div class="wrap"> <a class="cancel" href="view_post.php?id=<?=$pid?>">取消</a> </div> </div></div></body></html>下面是博客系统 OOP 版的实际运行效果,从注册、登录到发布文章、编辑删除的完整流程截图:













• 类与对象:类是蓝图,new 创建对象。
• 属性与方法:$this 引用自身,构造函数初始化。
• 封装:public/protected/private 控制访问,保护数据。
• 继承:extends 扩展父类,减少代码重复。
• 多态:方法重写,同一操作不同表现。
• 抽象类与接口:定义规范,强制实现。
• Trait:横向代码复用,解决单继承局限。
• 静态成员与常量:类本身的数据和行为。
• 魔术方法:自动化对象行为。
• 命名空间:避免类名冲突,组织代码。
• 自动加载:Composer PSR-4 一键搞定。
• 综合实战:重构博客系统,代码分离清晰,可维护性飞跃。
到这里,现代 PHP 开发的基石就搭起来了。Laravel 这类框架底层正是大量运用了这些 OOP 概念——依赖注入、服务容器、中间件,都建立在类、接口、多态之上。理解了本篇,你就不只是"会用框架",而是能"看懂框架"。
声明:文字整理过程中借助了 AI 工具辅助排版,代码与知识点均经过实际测试验证。

本文配套资料包
资料下载

-END-
