
简介面向Web开发初学者与需要快速上手中后端配合的开发者这份教程围绕PHP与MySQL的协同应用展开讲解如何使用PHP的MySQLi或PDO扩展建立数据库连接、执行SQL查询、处理结果集与错误并通过表单提交、动态内容生成等场景展示两者如何共同构建交互式Web应用。资源包内主要包含php_mysql.rar基础教程或示例代码、Source.zip完整Web应用源码以及lj笔记关键点与技巧总结压缩包整体约69.87MB结构清晰便于对照学习。已有228人学习该资源适合希望从环境搭建走向真实项目实践的学习者能够帮助理解从数据库操作到页面渲染的完整流程为后续独立开发打下扎实基础。1. PHP与MySQL数据库WEB开发教程压缩包背后是一整套请求链路第一次打开这种命名方式的教程压缩包多数人先被文件夹结构劝退一堆.php、一份.sql、还有读不懂的config.php。其实这四个词拆开就说明了一切PHP 负责在服务器上把页面动态生成出来MySQL 负责把数据稳定存进去WEB开发是最终目标而.zip只是它最不值得关心的部分。这套组合适合三类人正在做课程设计的学生、要接手老项目维护的初级开发、以及准备把 CRUD 写规范一点的后端新人。真正卡住你的往往不是 PHP 语法而是环境——解释器、数据库、Web 服务器三者版本不匹配教程里的代码一行都跑不起来。把链路理清教程才读得进去。2. PHP与MySQL配合工作的环境从解释器到数据库的握手2.1 WEB开发里PHP和MySQL各管哪一段浏览器发起一个HTTP GET请求到服务器Nginx 或 Apache 发现扩展名是.php就把这个请求转交给 PHP 解释器执行。PHP 脚本内用mysqli或PDO创建到 MySQL 的连接执行SELECT/INSERT/UPDATE拿到结果集后拼进 HTML 模板再通过 HTTP 响应返回给浏览器。MySQL 全程不直接面对浏览器它只跟 PHP 打招呼。常见理解误区是“Apache 连数据库”“Nginx 连数据库”。连接的建立者是 PHP 脚本Web 服务器只负责找到 PHP 并启动它。这意味着排查问题时要分清三段Web 服务器到 PHP 是否通PHP 到 MySQL 是否通MySQL 本身是否健在。后面所有排错都是按这条线走。2.2 本地开发环境集成包与手动配置如何取舍Windows 上最常见做法是用集成环境比如 phpStudy、XAMPP、WAMP。它们把 Apache/Nginx、PHP、MySQL 的管理界面和开关集成在一起十分钟就能把“mysql安装配置教程”里最费力的一步跳过去。但集成环境有个隐患默认版本组合未必匹配。装出来的 phpStudy 里 PHP 是 8.2、MySQL 是 8.0而教程写法停留在 PHP 5 时代的mysql_*函数这种情况在旧压缩包里出现过太多次。对比项集成环境phpStudy/XAMPP手动配置裸装各组件上手速度快默认端口大多已调好慢端口、扩展、服务名都要自己处理版本自由度有限一般只内置几个版本完全可控可以精确到小版本排错难度低面板自带启动日志高日志分散在三个目录是否适合学原理一般开关掩盖了细节适合每步都在建立认知我一般建议新手先用集成环境把教程跑通再用手动方式装一次因为生产环境没有面板。2.3 生产环境配置Nginx PHP-FPM MySQL生产环境更常见的是Linux Nginx PHP-FPM MySQL这组合就是常说的 LNMP。Nginx 不内置 PHP 执行模块它把.php请求通过fastcgi协议转发给 PHP-FPM 监听的地址。一段最简配置如下server { listen 80; server_name example.com; root /var/www/html; index index.php index.html; location ~ \.php$ { include fastcgi_params; fastcgi_pass 127.0.0.1:9000; fastcgi_index index.php; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; } }fastcgi_pass指向127.0.0.1:9000这是 PHP-FPM 默认监听的地址。注意SCRIPT_FILENAME必须显式传给 FPM否则 PHP 收到请求却不知道执行哪个文件直接返回空白页。fastcgi_param的赋值用到了 Nginx 变量$document_root和$fastcgi_script_name前者是站点根目录后者是请求的 PHP 文件路径。PHP-FPM 的监听方式可以在php-fpm.conf或pool.d/www.conf里改; 监听 TCP 端口 listen 127.0.0.1:9000 ; 或监听 Unix Socket ; listen /run/php/php8.2-fpm.sockUnix Socket 比 TCP 少了网络协议栈开销同一台机器上性能更好如果 Nginx 和 PHP-FPM 不在同一台机器才用 TCP。改完配置要systemctl reload php8.2-fpmreload 比 restart 温和不会中断正在处理的请求。2.4 装完先验证三条命令确认三件事环境装完别急着写页面先用命令确认底座是活的php -v # 查看 PHP 版本确认 CLI 可用 php -m | grep -i pdo # 确认 PDO 扩展已加载输出 pdo 和 pdo_mysql mysql -V # 查看 MySQL 客户端版本php -m列出的是 PHP 编译时和运行时加载的所有模块grep -i pdo做大小写不敏感匹配。看到pdo_mysql出现在输出里才算打通了 PHP 与 MySQL 通信的最底层依赖。此时可以顺手写一个info.php放在站点根目录内容是?php phpinfo(); ?然后在浏览器访问它。phpinfo()页面里的“PDO drivers”这一项会列出可用的驱动比命令行更直观。验证完立刻删掉这个文件它会把 PHP 的完整环境信息暴露给任何能访问到它的人。3. 教程里数据库端的第一步建表、字符集与索引设计3.1 一张用户表的完整建表语句多数教程 zip 里都会附带一个.sql文件但直接导入不是好习惯。先读懂它再手工敲一遍印象完全不同。以最常见的用户表为例CREATE TABLE user ( id INT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 自增主键, username VARCHAR(50) NOT NULL COMMENT 登录名, email VARCHAR(100) NOT NULL DEFAULT COMMENT 邮箱, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, status TINYINT NOT NULL DEFAULT 1 COMMENT 1正常 0禁用, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COLLATEutf8mb4_unicode_ci COMMENT用户表;INT UNSIGNED把整数取值上限翻倍Web 站点的用户量在相当长时间内用不完。VARCHAR(50)的 50 是字符数而非字节数。DATETIME搭配DEFAULT CURRENT_TIMESTAMP插入时就不用手动写当前时间。ENGINEInnoDB是必须的MyISAM 不支持事务而增删改查里的“改”和“删”在新版数据恢复场景下需要事务保证完整性。很多教程老代码里写的是ENGINEMyISAM那是十年前的默认选择。现在只要涉及并发写入一律 InnoDB。3.2 字符集用 utf8mb4而不是 utf8MySQL 里的utf8是历史包袱它最多只能存 3 字节的字符遇见 emoji 或生僻字就直接入库失败或变成?。utf8mb4才是真正的完整 UTF-8 编码能放下 4 字节字符。正确做法是建库时就定死CREATE DATABASE web_tutorial DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;扩容时再改库表字符集的代价远大于一开始就选对。utf8mb4_unicode_ci和utf8mb4_general_ci的差别在排序规则精度和速度上前者按 Unicode 标准排序更准后者排序略快但处理一些特殊字符时不按语言习惯来。新项目直接选unicode_ci没必要为了那一点点性能差牺牲正确性。3.3 从WEB开发查询习惯反推索引设计写 PHP 代码之前先想一个问题上线后用户会按什么条件查这张表查询路径决定索引建在哪。给user表加一个手机号登录场景的索引ALTER TABLE user ADD COLUMN phone VARCHAR(20) NOT NULL DEFAULT AFTER email; ALTER TABLE user ADD UNIQUE KEY uk_phone (phone);然后考虑“统计某天之后注册的所有用户”SELECT id, username, created_at FROM user WHERE created_at 2024-06-01 00:00:00 AND status 1;这条 SQL 的WHERE涉及created_at和status两个条件。最左前缀原则是联合索引的地基MySQL 可以把最左边的索引列用于范围匹配但一旦遇到范围条件、BETWEEN后续索引列就基本用不上了。上面这条语句里status放在联合索引的第二位就浪费了。ALTER TABLE user ADD INDEX idx_status_created (status, created_at);这样status 1可以先精确匹配再对created_at做范围扫描效果最好。索引不是越多越好每条索引都占用写入成本包含id的自增主键索引本来就在聚簇索引里。3.4 MySQL Workbench 里真正要看的三个面板用 MySQL Workbench 连接上后左侧的SCHEMAS面板看库表结构选中表点右键Table Inspector能看到索引明细和存储引擎查到一条慢 SQL 时用EXPLAIN按钮或手工执行EXPLAIN SELECT ...看执行计划关注type和rows两列。type ALL表示全表扫描数据量大时这是灾难type ref或range表示用到了普通索引或索引范围扫描可接受rows是预估扫描行数和实际返回行数差距过大时说明统计信息过期或索引用错了这三个面板覆盖了数据库课程设计里“验证查询性能”的绝大多数需求比对着命令行看SHOW INDEX直观得多。4. PHP连接MySQL的写法从 mysql_* 到预处理再到事务4.1 三种API的选择与历史老教程里最常见的连接函数是mysql_connect()。这套 API 在 PHP 7.0 已经被移除现在下载到的 zip 里还写它代码就是一堆红色报错。替代方案有两个mysqli和PDO。API面向对象预处理语句多数据库支持适用场景mysql_*否已移除无仅 MySQL只存在于古董教程mysqli是支持仅 MySQL老项目改造、追求 MySQL 专有特性PDO是支持MySQL、PostgreSQL、SQLite 等新项目首选可换库选择标准不复杂写新代码用 PDO把pdo_mysql驱动作为底线维护已有老项目继续用mysqli不要中途混用两套 API。4.2 用PDO建立连接的推荐写法一份包含连接、异常处理、字符集和时区的标准写法?php $host 127.0.0.1; $port 3306; $dbname web_tutorial; $user web_user; $pass your_password; $dsn mysql:host$host;port$port;dbname$dbname;charsetutf8mb4; $options [ PDO::ATTR_ERRMODE PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE PDO::FETCH_ASSOC, PDO::ATTR_EMULATE_PREPARES false, ]; try { $pdo new PDO($dsn, $user, $pass, $options); } catch (PDOException $e) { error_log($e-getMessage(), 3, /var/log/php_errors.log); exit(数据库连接失败); }$dsn里的charsetutf8mb4不是可选项它会同时影响连接后的会话字符集和排序规则和第 3 章建库时的字符集形成闭环。ATTR_ERRMODE ERRMODE_EXCEPTION让数据库错误变成异常脚本可以用 try/catch 统一处理这是现代 PHP 唯一值得用的错误模式。ATTR_EMULATE_PREPARES false关闭 PDO 内置的预处理模拟让真正的预处理交给 MySQL 端完成这一步直接影响 SQL 注入防护的有效性。MySQL 8 默认认证插件是caching_sha2_passwordPHP 7.4 以上版本才能直接连。如果代码报认证插件错误优先升级 PHP而不是把 MySQL 认证改回mysql_native_password。4.3 预处理语句才是WEB开发的安全底线数据库增删改查中最危险的不是写错 SQL而是把用户输入直接拼进 SQL。最典型的反例$sql SELECT * FROM user WHERE username . $_GET[username] . ;当用户输入admin OR 11时拼接出来的 SQL 直接变成SELECT * FROM user WHERE username admin OR 11OR 11让条件恒真查询结果返回全部用户。修正做法是预处理语句用占位符把 SQL 结构和用户输入拆开$stmt $pdo-prepare(SELECT * FROM user WHERE username ? AND status ?); $stmt-execute([$username, $status]); $user $stmt-fetch();?是位置占位符execute()传入数组按顺序绑定。prepare阶段 MySQL 已经完成语法解析用户输入只会被当作数据值无法再改变 SQL 结构注入也就无从谈起。ATTR_EMULATE_PREPARES false的意义就在这里关闭模拟后整个流程真正发生在 MySQL 端。4.4 把增删改查包成可复用函数教程 zip 里的代码常见问题是逻辑散落在每个脚本里连接、查询、关闭混在一起。把操作收敛到一组函数里是代码直接可维护的第一步function db(): PDO { static $pdo null; if ($pdo instanceof PDO) { return $pdo; } // 连接代码同上一节$dsn、$options 从配置文件读取 return $pdo; } function createUser(string $username, string $email): int { $pdo db(); $sql INSERT INTO user (username, email) VALUES (?, ?); $stmt $pdo-prepare($sql); $stmt-execute([$username, $email]); return (int)$pdo-lastInsertId(); } function updateUserStatus(int $id, int $status): bool { $pdo db(); $sql UPDATE user SET status ? WHERE id ?; $stmt $pdo-prepare($sql); return $stmt-execute([$status, $id]); } function findUserByUsername(string $username): ?array { $pdo db(); $sql SELECT id, username, email, status FROM user WHERE username ?; $stmt $pdo-prepare($sql); $stmt-execute([$username]); $row $stmt-fetch(); return $row ?: null; }把函数声明中的类型写清?array表示可能返回数组也可能返回null调用方拿到返回值后先判空再取字段比依赖返回false更不容易踩坑。这里隐藏着一个 PHP 运算符的经典坑从 MySQL 取出的字段即便数据库类型是INTPDO 在某些配置下返回的也可能是字符串1用判断没问题但 1恒为 false。习惯上用(int)$row[status]显式转型避免类型比较造成“数据没变但逻辑不对”的怪问题。事务的写法也同样收敛$pdo-beginTransaction(); try { createUser($username, $email); $pdo-exec(INSERT INTO user_log (action) VALUES (create_user)); $pdo-commit(); } catch (Throwable $e) { $pdo-rollBack(); error_log($e-getMessage()); }beginTransaction()之后所有的写操作都先进入 InnoDB 的回滚段直到commit()才真正落盘。任何一步抛异常就rollBack()保证要么两条 INSERT 都成功要么都不存在。catch (Throwable $e)比catch (Exception $e)捕获范围更广PHP 7 之后这类可直接抛出。5. WEB开发中MySQL连接失败的排查路径与验证方法5.1 先分两类错误连不上和登不上PHP 抛出的数据库连接异常错误信息五花八门但本质分两类。第一类是网络层根本连不到 MySQL报Connection refused或Connection time out第二类是能连上但账号密码不对或权限不足报Access denied for user。两类错误的排查方向完全不同。错误类型主要特征排查方向连接被拒绝Connection refused/Cant connect to MySQL serverMySQL 服务没启动 / 端口不对 / 防火墙拦截连接超时Connection timed out目标主机网络不通 / 安全组未放行 3306 端口登录失败Access denied for user xxxlocalhost用户名密码错误 / 账号未授权指定 host未知数据库Unknown database xxx库名拼写错误 / 大小写敏感策略先确认 MySQL 服务本身是存活的systemctl status mysqld # Linux 上查看服务状态 mysqladmin -h 127.0.0.1 -P 3306 -u root -p ping # 探测端口连通性mysqladmin ping返回mysqld is alive才说明服务活着。如果服务活着但 PHP 还是连不上检查 PHP 连接代码里的$host是否写成了localhost。localhost在某些 MySQL 客户端的解析下会走 Unix Socket而127.0.0.1走 TCP如果 PHP-FPM 进程和 MySQL 不在同一台机器或者 Socket 路径不匹配就会出现“明明服务活着就是连不上”的怪相。5.2 PHP的报错在哪里看PHP 解析阶段的错误和运行阶段的 PDO 异常看的文件不一样。解析错误在php.ini里由display_errors控制生产环境推荐关掉页面显示改成记日志display_errors Off log_errors On error_log /var/log/php_errors.log本地开发时可以把display_errors临时置On便于直接在浏览器看到报错但这么做会把路径、库表名等信息泄露给前端上线前必须改回Off。语法层面最轻量的检查php -l /var/www/html/index.php-l是lint的缩写只做语法检查不执行代码。输出No syntax errors detected说明这段脚本语法过关可以进入到运行时报错的排查阶段。PDO 异常已经在第 4 章用catch (PDOException $e)接住了所以看/var/log/php_errors.log是最快路径。5.3 乱码不是玄学是字符集没对齐页面上的中文显示成???或楼主基本就是字符集链条断裂。这条链条上有三处必须一致MySQL 库表字符集、PHP 连接 DSN 的charset、HTML 页面声明的编码。meta charsetUTF-8MySQL 侧可以用一条 SQL 确认当前会话字符集SHOW VARIABLES LIKE character_set%;关注character_set_server和character_set_database这两个值都应该是utf8mb4。PHP 侧检查第 4 章的$dsn是否带了charsetutf8mb4。三处对齐后仍有乱码再看文件本身编码Linux 上用file index.php可以查看文件编码UTF-8 显示为UTF-8 Unicode text如果显示ISO-8859之类用编辑器另存为 UTF-8 即可。5.4 用EXPLAIN验证一条慢查询教程里跑得动和跑得快是两回事。一条SELECT在数据量小时看不出问题上线后越跑越慢最直接的办法是 EXPLAINEXPLAIN SELECT id, username FROM user WHERE status 1 ORDER BY created_at DESC;执行计划里主要看三列typeALL就是全表扫描有索引没用上key实际使用的索引名为NULL表示没走索引rows预估扫描行数过大说明过滤性差上面这条 SQLstatus和ORDER BY created_at如果在两个独立索引上MySQL 通常只会选其中一个。解决方案是建联合索引(status, created_at)让过滤和排序都命中间一条索引避免ORDER BY触发文件排序Extra字段出现Using filesort时尤其要处理。这和第 3 章的索引设计法是一回事写 PHP 代码之前先用 EXPLAIN 验证这条 SQL 的执行路径。6. 把教程变成自己的骨架统一入口、PDO封装与日志验证教程 zip 里最常见的一类代码是每个 PHP 文件都复制一遍连接代码改库名要改几十个文件。把它收成一个骨架后续所有业务都能挂在上面。第一件事是做一个数据库单例封装final class Database { private static ?PDO $pdo null; public static function conn(): PDO { if (self::$pdo null) { $config require __DIR__ . /config.php; self::$pdo self::createConnection($config); } return self::$pdo; } private static function createConnection(array $config): PDO { $dsn sprintf( mysql:host%s;port%d;dbname%s;charset%s, $config[host], $config[port], $config[dbname], $config[charset] ?? utf8mb4 ); return new PDO($dsn, $config[user], $config[pass], [ PDO::ATTR_ERRMODE PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE PDO::FETCH_ASSOC, PDO::ATTR_EMULATE_PREPARES false, ]); } }static ?PDO $pdo null用静态属性保证同一请求内只建立一个连接避免每个查询都重复握手。final class禁止继承防止派生类改变连接行为。config.php独立出来库名、账号、密码集中在一个文件部署时只需改这一个地方。第二个技巧是统一异常出口。所有接口入口文件都写成同一结构try { $action $_GET[action] ?? list; $result match($action) { list listUsers(), detail getUserById((int)$_GET[id]), default throw new RuntimeException(invalid action), }; echo json_encode([code 0, data $result]); } catch (Throwable $e) { error_log([ . date(Y-m-d H:i:s) . ] . $e-getMessage(), 3, __DIR__ . /error.log); echo json_encode([code $e-getCode(), msg 请求失败]); exit; }match表达式是 PHP 8 之后的写法比一串if/elseif清晰。所有业务错误都抛异常由统一出口记日志并返回固定 JSON 结构前端只看code字段判断成功失败看到错误信息的具体内容从日志里查error.log。这一步能验证整条链路是否闭环浏览器请求某个actionMySQL 返回数据PHP 转成 JSON前端拿到结构化结果。本地验证最轻量方式是 PHP 内置开发服务器php -S 127.0.0.1:8080 -t public curl http://127.0.0.1:8080/index.php?actionlistpage1php -S不依赖 Nginx直接启动一个单线程开发服务器-t指定文档根目录。curl请求返回的 JSON 里code为 0说明骨架已通此时再故意传一个不存在的action观察响应变成业务错误码同时error.log里多出异常记录验证流程就算完整跑通了。本文还有配套的精品资源点击获取