本文目录导读:

我来用一个新闻发布系统的案例,展示PHP中MVC架构的模型层设计和实现。
项目目录结构
news-system/
├── app/
│ ├── Models/
│ │ ├── BaseModel.php
│ │ ├── Category.php
│ │ └── News.php
│ ├── Controllers/
│ │ ├── NewsController.php
│ │ └── CategoryController.php
│ └── Views/
│ ├── news/
│ │ ├── index.php
│ │ ├── show.php
│ │ └── form.php
│ └── categories/
├── config/
│ └── database.php
├── public/
│ └── index.php
└── core/
├── Database.php
└── Router.php
核心数据库连接类
<?php
// core/Database.php
class Database {
private static $instance = null;
private $connection;
private function __construct() {
try {
$config = require_once __DIR__ . '/../config/database.php';
$dsn = "mysql:host={$config['host']};dbname={$config['dbname']};charset={$config['charset']}";
$this->connection = new PDO($dsn, $config['username'], $config['password'], [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false
]);
} catch (PDOException $e) {
die("数据库连接失败: " . $e->getMessage());
}
}
public static function getInstance() {
if (self::$instance === null) {
self::$instance = new self();
}
return self::$instance;
}
public function getConnection() {
return $this->connection;
}
}
基础模型层(BaseModel)
<?php
// app/Models/BaseModel.php
require_once __DIR__ . '/../../core/Database.php';
abstract class BaseModel {
protected $db;
protected $table;
protected $primaryKey = 'id';
public function __construct() {
$this->db = Database::getInstance()->getConnection();
}
/**
* 查找所有记录
*/
public function findAll($orderBy = null, $limit = null) {
$sql = "SELECT * FROM {$this->table}";
if ($orderBy) {
$sql .= " ORDER BY {$orderBy}";
}
if ($limit) {
$sql .= " LIMIT {$limit}";
}
$stmt = $this->db->prepare($sql);
$stmt->execute();
return $stmt->fetchAll();
}
/**
* 根据ID查找单条记录
*/
public function findById($id) {
$sql = "SELECT * FROM {$this->table} WHERE {$this->primaryKey} = :id";
$stmt = $this->db->prepare($sql);
$stmt->execute([':id' => $id]);
return $stmt->fetch();
}
/**
* 根据条件查找
*/
public function findBy($conditions) {
$where = [];
$params = [];
foreach ($conditions as $key => $value) {
$where[] = "{$key} = :{$key}";
$params[":{$key}"] = $value;
}
$whereClause = implode(' AND ', $where);
$sql = "SELECT * FROM {$this->table} WHERE {$whereClause}";
$stmt = $this->db->prepare($sql);
$stmt->execute($params);
return $stmt->fetchAll();
}
/**
* 创建记录
*/
public function create($data) {
$columns = implode(', ', array_keys($data));
$placeholders = ':' . implode(', :', array_keys($data));
$sql = "INSERT INTO {$this->table} ({$columns}) VALUES ({$placeholders})";
$stmt = $this->db->prepare($sql);
$stmt->execute($data);
return $this->db->lastInsertId();
}
/**
* 更新记录
*/
public function update($id, $data) {
$sets = [];
foreach ($data as $key => $value) {
$sets[] = "{$key} = :{$key}";
}
$setClause = implode(', ', $sets);
$sql = "UPDATE {$this->table} SET {$setClause} WHERE {$this->primaryKey} = :id";
$data[':id'] = $id;
$stmt = $this->db->prepare($sql);
return $stmt->execute($data);
}
/**
* 删除记录
*/
public function delete($id) {
$sql = "DELETE FROM {$this->table} WHERE {$this->primaryKey} = :id";
$stmt = $this->db->prepare($sql);
return $stmt->execute([':id' => $id]);
}
/**
* 获取记录总数
*/
public function count() {
$sql = "SELECT COUNT(*) as total FROM {$this->table}";
$stmt = $this->db->query($sql);
$result = $stmt->fetch();
return $result['total'];
}
/**
* 分页查询
*/
public function paginate($page = 1, $perPage = 10, $conditions = []) {
$offset = ($page - 1) * $perPage;
$where = '';
$params = [];
if (!empty($conditions)) {
$whereParts = [];
foreach ($conditions as $key => $value) {
$whereParts[] = "{$key} = :{$key}";
$params[":{$key}"] = $value;
}
$where = "WHERE " . implode(' AND ', $whereParts);
}
$sql = "SELECT * FROM {$this->table} {$where} LIMIT :limit OFFSET :offset";
$stmt = $this->db->prepare($sql);
foreach ($params as $key => $value) {
$stmt->bindValue($key, $value);
}
$stmt->bindValue(':limit', $perPage, PDO::PARAM_INT);
$stmt->bindValue(':offset', $offset, PDO::PARAM_INT);
$stmt->execute();
return [
'data' => $stmt->fetchAll(),
'total' => $this->countWhere($conditions),
'perPage' => $perPage,
'currentPage' => $page
];
}
/**
* 条件统计
*/
protected function countWhere($conditions = []) {
if (empty($conditions)) {
return $this->count();
}
$whereParts = [];
$params = [];
foreach ($conditions as $key => $value) {
$whereParts[] = "{$key} = :{$key}";
$params[":{$key}"] = $value;
}
$where = "WHERE " . implode(' AND ', $whereParts);
$sql = "SELECT COUNT(*) as total FROM {$this->table} {$where}";
$stmt = $this->db->prepare($sql);
$stmt->execute($params);
$result = $stmt->fetch();
return $result['total'];
}
}
新闻模型(News Model)
<?php
// app/Models/News.php
require_once 'BaseModel.php';
class News extends BaseModel {
protected $table = 'news';
protected $primaryKey = 'news_id';
/**
* 获取新闻及其分类信息(联表查询)
*/
public function getNewsWithCategory($id) {
$sql = "
SELECT n.*, c.name as category_name, c.slug as category_slug
FROM {$this->table} n
LEFT JOIN categories c ON n.category_id = c.id
WHERE n.{$this->primaryKey} = :id
";
$stmt = $this->db->prepare($sql);
$stmt->execute([':id' => $id]);
return $stmt->fetch();
}
/**
* 获取最近新闻
*/
public function getRecentNews($limit = 5) {
$sql = "
SELECT n.*, c.name as category_name
FROM {$this->table} n
LEFT JOIN categories c ON n.category_id = c.id
WHERE n.status = 'published'
ORDER BY n.published_at DESC
LIMIT :limit
";
$stmt = $this->db->prepare($sql);
$stmt->bindValue(':limit', $limit, PDO::PARAM_INT);
$stmt->execute();
return $stmt->fetchAll();
}
/**
* 搜索新闻
*/
public function search($keyword, $categoryId = null) {
$sql = "
SELECT n.*, c.name as category_name
FROM {$this->table} n
LEFT JOIN categories c ON n.category_id = c.id
WHERE (n.title LIKE :keyword OR n.content LIKE :keyword)
AND n.status = 'published'
";
$params = [':keyword' => "%{$keyword}%"];
if ($categoryId) {
$sql .= " AND n.category_id = :category_id";
$params[':category_id'] = $categoryId;
}
$sql .= " ORDER BY n.published_at DESC";
$stmt = $this->db->prepare($sql);
$stmt->execute($params);
return $stmt->fetchAll();
}
/**
* 为新闻添加标签
*/
public function addTags($newsId, $tags) {
// 先删除原有标签
$deleteSql = "DELETE FROM news_tags WHERE news_id = :news_id";
$stmt = $this->db->prepare($deleteSql);
$stmt->execute([':news_id' => $newsId]);
// 添加新标签
$insertSql = "INSERT INTO news_tags (news_id, tag_id) VALUES (:news_id, :tag_id)";
$stmt = $this->db->prepare($insertSql);
foreach ($tags as $tagId) {
$stmt->execute([
':news_id' => $newsId,
':tag_id' => $tagId
]);
}
}
/**
* 获取新闻的标签
*/
public function getTags($newsId) {
$sql = "
SELECT t.*
FROM tags t
INNER JOIN news_tags nt ON t.id = nt.tag_id
WHERE nt.news_id = :news_id
";
$stmt = $this->db->prepare($sql);
$stmt->execute([':news_id' => $newsId]);
return $stmt->fetchAll();
}
/**
* 验证新闻数据
*/
public function validate($data) {
$errors = [];
if (empty($data['title'])) {
$errors[] = '标题不能为空';
}
if (strlen($data['title']) > 200) {
$errors[] = '标题不能超过200个字符';
}
if (empty($data['content'])) {
$errors[] = '内容不能为空';
}
if (empty($data['category_id'])) {
$errors[] = '请选择分类';
}
return $errors;
}
}
分类模型(Category Model)
<?php
// app/Models/Category.php
require_once 'BaseModel.php';
class Category extends BaseModel {
protected $table = 'categories';
/**
* 获取分类及其新闻数量
*/
public function getCategoriesWithCount() {
$sql = "
SELECT c.*, COUNT(n.news_id) as news_count
FROM {$this->table} c
LEFT JOIN news n ON c.id = n.category_id
GROUP BY c.id
ORDER BY c.name ASC
";
$stmt = $this->db->query($sql);
return $stmt->fetchAll();
}
/**
* 获取树形分类结构
*/
public function getCategoryTree($parentId = 0, $depth = 0) {
$categories = $this->findBy(['parent_id' => $parentId]);
$tree = [];
foreach ($categories as $category) {
$category['depth'] = $depth;
$category['children'] = $this->getCategoryTree($category['id'], $depth + 1);
$tree[] = $category;
}
return $tree;
}
/**
* 生成分类路径(面包屑导航)
*/
public function getCategoryPath($categoryId) {
$path = [];
$currentId = $categoryId;
while ($currentId) {
$category = $this->findById($currentId);
if ($category) {
array_unshift($path, $category);
$currentId = $category['parent_id'];
} else {
break;
}
}
return $path;
}
}
数据库表结构设计
-- 数据库配置文件:config/database.php
return [
'host' => 'localhost',
'dbname' => 'news_system',
'username' => 'root',
'password' => '',
'charset' => 'utf8mb4'
];
-- 分类表
CREATE TABLE categories (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(100) NOT NULL,
slug VARCHAR(100) UNIQUE NOT NULL,
description TEXT,
parent_id INT DEFAULT 0,
status TINYINT DEFAULT 1,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_parent (parent_id),
INDEX idx_status (status)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 新闻表
CREATE TABLE news (
news_id INT AUTO_INCREMENT PRIMARY KEY,VARCHAR(200) NOT NULL,
slug VARCHAR(200) UNIQUE NOT NULL,
summary TEXT,
content LONGTEXT NOT NULL,
category_id INT,
author VARCHAR(100),
status ENUM('draft', 'published', 'archived') DEFAULT 'draft',
featured_image VARCHAR(255),
published_at DATETIME,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (category_id) REFERENCES categories(id) ON DELETE SET NULL,
INDEX idx_status (status),
INDEX idx_published (published_at),
INDEX idx_category (category_id),
FULLTEXT INDEX idx_search (title, content)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 标签表
CREATE TABLE tags (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(50) NOT NULL,
slug VARCHAR(50) UNIQUE NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 新闻标签关联表
CREATE TABLE news_tags (
news_id INT,
tag_id INT,
PRIMARY KEY (news_id, tag_id),
FOREIGN KEY (news_id) REFERENCES news(news_id) ON DELETE CASCADE,
FOREIGN KEY (tag_id) REFERENCES tags(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
使用示例
<?php
// 控制器中使用示例
require_once __DIR__ . '/../app/Models/News.php';
class NewsController {
private $newsModel;
public function __construct() {
$this->newsModel = new News();
}
// 展示新闻列表
public function index() {
// 分页查询
$page = $_GET['page'] ?? 1;
$result = $this->newsModel->paginate($page, 10);
// 获取最新新闻
$recentNews = $this->newsModel->getRecentNews(5);
// 加载视图
include __DIR__ . '/../Views/news/index.php';
}
// 展示单条新闻
public function show($id) {
// 获取新闻详情(含分类信息)
$news = $this->newsModel->getNewsWithCategory($id);
if (!$news) {
header('HTTP/1.0 404 Not Found');
exit('新闻不存在');
}
// 获取新闻标签
$tags = $this->newsModel->getTags($id);
// 加载视图
include __DIR__ . '/../Views/news/show.php';
}
// 创建新闻
public function create() {
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// 验证数据
$errors = $this->newsModel->validate($_POST);
if (empty($errors)) {
// 创建新闻
$newsId = $this->newsModel->create([
'title' => $_POST['title'],
'slug' => $this->createSlug($_POST['title']),
'content' => $_POST['content'],
'category_id' => $_POST['category_id'],
'status' => 'published',
'published_at' => date('Y-m-d H:i:s')
]);
// 添加标签
if (!empty($_POST['tags'])) {
$this->newsModel->addTags($newsId, $_POST['tags']);
}
header('Location: /news/' . $newsId);
exit;
}
}
include __DIR__ . '/../Views/news/form.php';
}
private function createSlug($title) {
$slug = strtolower(trim($title));
$slug = preg_replace('/[^a-z0-9-]/', '-', $slug);
$slug = preg_replace('/-+/', '-', $slug);
return $slug;
}
}
模型层设计要点
- 单一职责原则:每个模型只负责自己的数据表操作
- 数据封装:隐藏数据库操作细节,提供简洁的接口
- 代码复用:通过BaseModel提供通用CRUD操作
- 业务逻辑:模型层可以包含表单验证、数据处理等业务规则
- 性能优化:支持关联查询、分页等高级操作
- 安全性:使用预处理语句防止SQL注入
这个设计模式使得数据操作集中管理,易于维护和测试,同时也符合MVC架构的最佳实践。