你能否用一个新闻发布系统案例展示PHP中MVC的模型层设计

wen PHP项目 47

本文目录导读:

你能否用一个新闻发布系统案例展示PHP中MVC的模型层设计

  1. 项目目录结构
  2. 核心数据库连接类
  3. 基础模型层(BaseModel)
  4. 新闻模型(News Model)
  5. 分类模型(Category Model)
  6. 数据库表结构设计
  7. 使用示例
  8. 模型层设计要点

我来用一个新闻发布系统的案例,展示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;
    }
}

模型层设计要点

  1. 单一职责原则:每个模型只负责自己的数据表操作
  2. 数据封装:隐藏数据库操作细节,提供简洁的接口
  3. 代码复用:通过BaseModel提供通用CRUD操作
  4. 业务逻辑:模型层可以包含表单验证、数据处理等业务规则
  5. 性能优化:支持关联查询、分页等高级操作
  6. 安全性:使用预处理语句防止SQL注入

这个设计模式使得数据操作集中管理,易于维护和测试,同时也符合MVC架构的最佳实践。

抱歉,评论功能暂时关闭!