资讯动态

原生PHP+MySQL实现MVC博客系统(无框架)

发布时间:2026/9/15 3:04:47 来源:尧图企业网站定制
简介这是一份基于PHPMySQL实现的MVC架构博客系统实战源码面向Web开发初学者与PHP后端入门者帮助理解经典三层架构在真实项目中的落地方式。资源包含610个文件以110个PHP核心逻辑文件Model/Controller/View模块、151个JS交互脚本、111个PNG图标及96个GIF动效素材为主辅以52个CSS样式文件和37个HTML页面模板整体压缩包7.37MB结构完整涵盖数据库配置、路由分发、用户登录、文章增删改查等全链路功能。已有142人学习下载适合通过可运行项目掌握PHP面向对象编程、MySQL数据操作PDO/Mysqli、前后端协同渲染及MVC目录规范models/views/controllers/config/public等标准分层。预览可见add_article.html.bak、edit.html.bak及多套CSS主题ui.css/default.css/green.css/black.css体现系统支持多皮肤切换与历史版本回溯能力。1. 用 PHP MySQL 实现 MVC 架构博客系统不是套模板而是亲手搭起请求流转骨架你在网上搜“php mysql 博客”十有八九点开的是带后台、含美化主题、甚至预装了评论和 SEO 插件的成品包。但真正卡住初学者的从来不是“怎么改首页颜色”而是——当用户访问/post/123时URL 怎么被拆解控制器怎么拿到123模型怎么查出对应文章并交给视图渲染这些环节之间没有魔法只有明确的职责边界和可追踪的数据流。本篇聚焦标题中明确出现的wangwei.zip_mvc php_php mysql 博客这一典型命名组合还原一个轻量级但结构清晰的 MVC 博客落地路径不依赖 Laravel 或 ThinkPHP 框架用原生 PHP 组织路由、控制器、模型与视图四层MySQL 存储文章、分类、标签三类核心数据所有代码可压缩进单个 zip 包如wangwei.zip解压即跑通基础读写。适合已会写单文件 PHP 页面、正尝试理解“为什么要有 MVC”、且需在本地环境Windows 10 Nginx PHP 8.1 MySQL 8.0快速验证逻辑的开发者。2. 拆解 MVC 三层职责为什么控制器不能直接 echo SQL 结果MVC 不是目录名堆砌而是对 HTTP 请求生命周期的分工契约。在wangwei.zip_mvc这类命名暗示的项目中“mvc” 后缀直指架构意图——必须让 Model 只管数据存取、View 只管 HTML 渲染、Controller 只做调度决策。若跳过这层设计直接在index.php里写mysqli_query(...)再echo h1.$row[title]./h1后续加个搜索功能或换数据库驱动时就得全局 grep 修改而 MVC 能把变更锁死在单一模块内。2.1 Model 层专注数据定义与操作拒绝业务逻辑污染Model 的核心任务是封装与数据库的交互细节对外暴露语义化方法如getArticleById($id)而非query(SELECT * FROM posts WHERE id ?)。它不处理分页计算、不判断用户权限、不拼接 URL——这些都该由 Controller 决策。以博客文章为例ArticleModel.php需定义?php // app/Model/ArticleModel.php class ArticleModel { private $pdo; public function __construct(PDO $pdo) { $this-pdo $pdo; } // 获取单篇文章含关联分类名 public function getArticleById(int $id): array|false { $sql SELECT p.id, p.title, p.content, p.created_at, c.name as category_name FROM posts p LEFT JOIN categories c ON p.category_id c.id WHERE p.id ?; $stmt $this-pdo-prepare($sql); $stmt-execute([$id]); return $stmt-fetch(PDO::FETCH_ASSOC); } // 获取文章列表支持分页 public function getArticles(int $limit 10, int $offset 0): array { $sql SELECT p.id, p.title, p.excerpt, p.created_at, c.name as category_name FROM posts p LEFT JOIN categories c ON p.category_id c.id ORDER BY p.created_at DESC LIMIT ? OFFSET ?; $stmt $this-pdo-prepare($sql); $stmt-execute([$limit, $offset]); return $stmt-fetchAll(PDO::FETCH_ASSOC); } }提示PDO 是 PHP 原生数据库抽象层比过时的mysql_*函数安全且支持预处理防注入。$limit和$offset参数为分页预留避免在 Controller 里拼接 SQL 字符串。2.2 View 层纯模板零 PHP 逻辑只做数据呈现View 文件应是纯粹的 HTML 简单变量输出禁止if判断权限、禁止foreach嵌套三层、禁止调用数据库方法。其存在意义是让设计师能直接编辑.html文件而不碰 PHP。例如app/View/article/show.php!-- app/View/article/show.php -- !DOCTYPE html html head title? htmlspecialchars($article[title]) ?/title /head body header h1? htmlspecialchars($article[title]) ?/h1 p分类? htmlspecialchars($article[category_name] ?? 未分类) ? | 发布时间? date(Y-m-d H:i, strtotime($article[created_at])) ?/p /header main ? nl2br(htmlspecialchars($article[content])) ? /main footer a href/返回首页/a /footer /body /html注意htmlspecialchars()是必加防护防止 XSSnl2br()将换行符转br适配纯文本内容。View 接收的$article数组由 Controller 注入自身不主动获取数据。2.3 Controller 层请求路由器与协调中枢不做数据加工Controller 是唯一接收 HTTP 请求、调用 Model 获取数据、选择 View 渲染的中间人。它不写 SQL、不拼 HTML、不校验表单校验应在 Model 或独立 Validator 中。以文章详情页为例app/Controller/ArticleController.php?php // app/Controller/ArticleController.php class ArticleController { private $articleModel; public function __construct(ArticleModel $articleModel) { $this-articleModel $articleModel; } // 处理 /post/{id} 请求 public function show(int $id) { // 1. 调用 Model 获取数据 $article $this-articleModel-getArticleById($id); // 2. 数据不存在则抛出 404 if (!$article) { http_response_code(404); include app/View/error/404.php; exit; } // 3. 注入数据到 View 并渲染 include app/View/article/show.php; } }关键点show()方法参数$id由 Router 解析后传入Controller 不解析 URLhttp_response_code(404)显式设置状态码而非仅显示错误页面exit防止后续代码执行。3. 构建请求入口与路由用 .htaccess 或 Nginx 规则捕获所有路径MVC 的起点是统一入口——所有请求/、/post/123、/category/php都导向index.php再由 PHP 自行解析路径。这需要 Web 服务器配置支持。3.1 Nginx 配置Windows 10 下 nginx.conf 的最小化 rewrite 规则在nginx/conf/nginx.conf的server块内添加location / { try_files $uri $uri/ /index.php?$query_string; } location ~ \.php$ { fastcgi_pass 127.0.0.1:9000; fastcgi_index index.php; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; include fastcgi_params; }说明try_files指令优先尝试静态文件失败则转发给index.php并将原始查询字符串如?page2透传fastcgi_pass指向 PHP-FPM 监听地址确保 PHP 脚本能被执行。3.2 PHP 路由器从 $_SERVER[REQUEST_URI] 提取控制器与动作index.php是整个 MVC 的门面它解析 URL、实例化对应 Controller 并调用方法?php // index.php require_once app/Config/Database.php; require_once app/Model/ArticleModel.php; require_once app/Controller/ArticleController.php; // 1. 初始化数据库连接 $pdo new PDO( mysql:hostlocalhost;dbnameblog;charsetutf8mb4, root, , [PDO::ATTR_ERRMODE PDO::ERRMODE_EXCEPTION] ); // 2. 解析请求路径如 /post/123 → [post, 123] $requestUri parse_url($_SERVER[REQUEST_URI], PHP_URL_PATH); $pathParts array_filter(explode(/, $requestUri)); // 3. 路由分发 if (count($pathParts) 2) { $controllerName ucfirst($pathParts[1]) . Controller; // post → PostController $action show; $id $pathParts[2] ?? null; if (class_exists(App\\Controller\\ . $controllerName)) { $controller new $controllerName(new ArticleModel($pdo)); if (method_exists($controller, $action) is_numeric($id)) { $controller-$action((int)$id); } else { http_response_code(404); include app/View/error/404.php; } } else { http_response_code(404); include app/View/error/404.php; } } else { // 首页路由/ → HomeControllerindex include app/View/home/index.php; }参数说明parse_url($_SERVER[REQUEST_URI], PHP_URL_PATH)安全提取路径排除查询参数干扰array_filter()去除空数组项因explode可能产生首尾空字符串is_numeric($id)防止非法路径如/post/abc导致 SQL 错误。3.3 MySQL 数据库初始化创建博客必需的三张表运行以下 SQL 创建posts文章、categories分类、tags标签表字符集设为utf8mb4支持 emoji-- 创建数据库 CREATE DATABASE IF NOT EXISTS blog CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; -- 切换数据库 USE blog; -- 文章表 CREATE TABLE posts ( id INT PRIMARY KEY AUTO_INCREMENT, title VARCHAR(255) NOT NULL, excerpt TEXT, content LONGTEXT NOT NULL, category_id INT DEFAULT 0, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, INDEX idx_category (category_id), INDEX idx_created (created_at) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 分类表 CREATE TABLE categories ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(100) NOT NULL UNIQUE, slug VARCHAR(100) NOT NULL UNIQUE ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 标签表多对多关系需中间表此处简化为文章字段存储逗号分隔 ALTER TABLE posts ADD COLUMN tags VARCHAR(255) DEFAULT ;优化点INDEX idx_category加速按分类查询INDEX idx_created加速按时间排序slug字段为未来 SEO 友好 URL 预留如/category/php。4. 实现增删改查闭环用表单提交驱动 Model 层写操作MVC 的价值在读写分离——View 提供表单Controller 接收 POST 数据并委托 Model 执行写入而非在 View 里嵌入INSERT INTO。以发布新文章为例4.1 View 表单声明式提交不包含任何 PHP 逻辑app/View/article/create.php仅含 HTML 表单!-- app/View/article/create.php -- form methodPOST action/article/store label标题input typetext nametitle required/labelbr label摘要textarea nameexcerpt/textarea/labelbr label内容textarea namecontent required/textarea/labelbr label分类select namecategory_id option value0请选择/option !-- 分类选项由 Controller 注入此处省略 -- /select/labelbr button typesubmit发布/button /form4.2 Controller 处理 POST验证、调用 Model、重定向ArticleController新增store()方法遵循 PRG 模式Post-Redirect-Get避免重复提交// app/Controller/ArticleController.php追加方法 public function store() { // 1. 验证必要字段 if (!isset($_POST[title]) || !isset($_POST[content]) || empty(trim($_POST[title]))) { http_response_code(400); echo 标题和内容不能为空; return; } // 2. 调用 Model 写入数据库 $result $this-articleModel-createArticle([ title trim($_POST[title]), excerpt $_POST[excerpt] ?? , content $_POST[content], category_id (int)($_POST[category_id] ?? 0) ]); // 3. 成功则重定向到新文章页失败则返回错误 if ($result) { header(Location: /post/ . $result); exit; } else { echo 保存失败请重试; } }4.3 Model 写操作参数绑定与事务保障ArticleModel追加createArticle()方法使用 PDO 事务确保数据一致性// app/Model/ArticleModel.php追加方法 public function createArticle(array $data): int|false { try { $this-pdo-beginTransaction(); $sql INSERT INTO posts (title, excerpt, content, category_id) VALUES (?, ?, ?, ?); $stmt $this-pdo-prepare($sql); $stmt-execute([ $data[title], $data[excerpt] ?? , $data[content], $data[category_id] ?? 0 ]); $insertId $this-pdo-lastInsertId(); $this-pdo-commit(); return (int)$insertId; } catch (PDOException $e) { $this-pdo-rollback(); error_log(Article create failed: . $e-getMessage()); return false; } }关键参数$this-pdo-beginTransaction()开启事务$this-pdo-lastInsertId()获取自增主键$this-pdo-rollback()在异常时回滚避免脏数据。5. 调试与排错定位 MVC 各层常见故障点当wangwei.zip_mvc解压后页面空白或报错需按请求链路逐层排查而非盲目 Google 错误信息。5.1 检查 Web 服务器是否正确转发请求在index.php开头插入调试代码?php file_put_contents(debug.log, URI: . $_SERVER[REQUEST_URI] . \n, FILE_APPEND); // ...后续代码访问/post/123后检查debug.log是否记录对应路径。若无记录说明 Nginx/Apache 未将请求转发给index.php需检查.htaccess或nginx.conf的 rewrite 规则。5.2 验证 PDO 连接与查询执行在ArticleModel的构造函数中添加连接测试public function __construct(PDO $pdo) { $this-pdo $pdo; // 测试连接 try { $this-pdo-query(SELECT 1); } catch (PDOException $e) { error_log(PDO connection failed: . $e-getMessage()); throw new Exception(Database connection error); } }若报错SQLSTATE[HY000] [1045] Access denied检查Database.php中的用户名密码若报错Connection refused确认 MySQL 服务已启动且监听localhost:3306。5.3 查看 PHP 错误日志定位语法与逻辑问题在php.ini中确保开启错误报告display_errors Off log_errors On error_log /path/to/php_error.log访问出错页面后直接查看php_error.log常见错误如Fatal error: Uncaught Error: Class App\Controller\PostController not found→ 检查index.php中类名拼写与文件路径是否匹配PostControllervsArticleControllerWarning: Undefined array key id→ Controller 方法参数未从路由正确传递检查index.php中$pathParts解析逻辑Notice: Trying to access array offset on value of type bool→ Model 查询返回false但 View 未判空直接使用$article[title]需在 Controller 中增加if (!$article)判断。5.4 MySQL 查询性能瓶颈诊断当文章列表加载缓慢用EXPLAIN分析查询EXPLAIN SELECT p.id, p.title, p.excerpt, p.created_at, c.name as category_name FROM posts p LEFT JOIN categories c ON p.category_id c.id ORDER BY p.created_at DESC LIMIT 10 OFFSET 0;若type列显示ALL全表扫描说明缺少索引若rows值远大于实际文章数需优化ORDER BY字段的索引。此时执行CREATE INDEX idx_posts_created ON posts(created_at);即可提升排序效率。6. 优化 URL 友好性与安全性从 /post/123 到 /post/123-我的第一篇php博客真实博客系统需兼顾 SEO 与用户体验URL 不应仅含 ID而应包含标题的 URL-safe 版本slug。这要求在 Model 层写入时生成 slug并在路由层支持/{id}-{slug}格式。6.1 自动生成 slug在 Model 中封装转换逻辑修改ArticleModel::createArticle()在插入前生成 slug// app/Model/ArticleModel.php更新 createArticle private function generateSlug(string $title): string { $slug iconv(UTF-8, ASCII//TRANSLIT, $title); // 中文转拼音近似 $slug preg_replace(/[^a-zA-Z0-9_]/, -, $slug); // 非字母数字转短横线 $slug trim($slug, -); return strtolower($slug); } public function createArticle(array $data): int|false { $slug $this-generateSlug($data[title]); // ...其他逻辑不变插入时增加 slug 字段 $sql INSERT INTO posts (title, excerpt, content, category_id, slug) VALUES (?, ?, ?, ?, ?); $stmt-execute([... , $slug]); // ... }6.2 路由层兼容新旧 URL 格式更新index.php中的路由解析支持/post/123-xxx和/post/123两种格式// index.php替换原有路由逻辑 if (count($pathParts) 2 $pathParts[1] post) { $path $pathParts[2] ?? ; // 匹配 /post/123 或 /post/123-xxx if (preg_match(/^(\d)(?:-.*)?$/, $path, $matches)) { $id (int)$matches[1]; $controller new ArticleController(new ArticleModel($pdo)); $controller-show($id); } else { http_response_code(404); include app/View/error/404.php; } }说明正则^(\d)(?:-.*)?$确保路径以数字开头可选后缀-xxx$matches[1]提取纯数字 ID忽略 slug 部分保持后端逻辑不变。6.3 View 层输出 SEO 友好链接在文章列表页app/View/home/index.php中生成链接时拼接 slug?php foreach ($articles as $article): ? a href/post/? $article[id] ?-? urlencode($article[title]) ? ? htmlspecialchars($article[title]) ? /a ?php endforeach; ?注意urlencode()处理标题中的空格与特殊字符如我的第一篇php博客→%E6%88%91%E7%9A%84%E7%AC%AC%E4%B8%80%E7%AF%87php%E5%8D%9A%E5%AE%A2确保 URL 合法。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价