本文目录导读:

我来用一个新闻发布系统的案例,展示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架构的最佳实践。
版权声明:NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播