ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Mac 上从零搭建 PHP + MySQL 环境并跑通网页操作数据库

Mac 上从零搭建 PHP + MySQL 环境并跑通网页操作数据库

1. 为什么要在 Mac 上折腾 PHP + MySQL 这套组合

很多刚接触后端开发的朋友,第一个念头往往是“都什么年代了还玩 PHP”,但现实情况是,大量中小型网站、内部管理系统、老旧项目维护,依然跑在 PHP + MySQL 这套组合上。尤其是你想快速做一个能读写本地数据库的小网页,PHP 的入门门槛低到令人发指——不需要编译,不需要复杂依赖,写完文件扔到目录里就能跑。Mac 自带 Apache 和 PHP 的历史虽然已经结束(新系统移除了内置 PHP),但这反而让环境搭建变得更清晰可控。

这篇文章要解决的核心问题就一个:在 Mac 上从零搭出一套能跑 PHP、能连本地 MySQL、能通过浏览器操作数据库的完整环境,并且跑通一个最小可用的例子。适合谁看?刚学后端的学生、想自己做个工具站的产品经理、需要维护老项目的运维、以及任何对“网页操作数据库”这件事好奇的开发者。你不需要有 PHP 基础,但需要会打开终端、能看懂基本的命令行输出。

我见过太多人卡在第一步:MySQL 装完了不知道密码怎么设,PHP 页面打开一片空白不知道去哪看报错,连接数据库时报“Access denied”却不知道改哪个配置。这些坑我都会在下面逐个拆掉。整套流程实测下来,从零到跑通大约需要 40 分钟,其中大部分时间花在下载和配置上,真正写代码的时间不到 10 分钟。

2. 环境搭建的整体思路与工具选型

2.1 为什么选这套方案而不是 Docker 或集成包

在 Mac 上搭 PHP + MySQL,常见路线有三条:一是用 MAMP/XAMPP 这类集成包,二是用 Docker 起容器,三是手动分别安装。集成包的优点是双击就能用,缺点是目录结构固定、版本切换麻烦、出了问题不好排查,而且很多集成包自带的 PHP 版本偏旧。Docker 方案最干净,隔离性好,但对新手来说多了一层网络和卷映射的概念,调试时容易懵。

我推荐手动安装,理由很直接:你能清楚知道每个组件装在哪、配置文件在哪、日志在哪。这对后续排查问题是决定性的。手动安装用 Homebrew 管理,升级卸载都方便,而且 Mac 上 Homebrew 的 PHP 和 MySQL 配方非常成熟,基本不会出幺蛾子。唯一需要注意的是 macOS 版本和 Homebrew 的兼容性,这个后面会讲。

2.2 组件版本选择与依赖关系

整套环境涉及三个核心组件:PHP(处理网页逻辑)、MySQL(存储数据)、Web 服务器。Web 服务器这里有个选择:用 Apache 还是 Nginx,或者直接用 PHP 内置的开发服务器。对于本地开发和“跑通一个小例子”这个目标,我强烈建议用 PHP 内置服务器,一条命令就能启动,不需要配置虚拟主机、不需要改 hosts 文件。等你需要部署到生产环境时再学 Nginx 配置也不迟。

版本方面,PHP 选 8.x 系列(8.1 或 8.2 都行),MySQL 选 8.0。这里有个关键点:PHP 连接 MySQL 的扩展在 PHP 7 之后发生了重大变化。老教程里用的mysql_connect()函数在 PHP 7 就被移除了,现在必须用mysqli或PDO。如果你照着五年前的教程写代码,会直接报“Call to undefined function”。所以版本选新不选旧,但代码写法要跟上。

组件推荐版本安装方式作用
Homebrew最新官方脚本包管理器
PHP8.2brew install php处理网页逻辑
MySQL8.0brew install mysql数据存储
内置服务器PHP 自带php -S本地 Web 服务

2.3 安装顺序与依赖处理逻辑

安装顺序有讲究:先装 Homebrew,再装 PHP,最后装 MySQL。为什么?因为 PHP 的 MySQL 扩展(mysqli、pdo_mysql)在编译时需要知道 MySQL 的客户端库位置。虽然 Homebrew 的 PHP 配方通常已经内置了这些扩展,但先装 MySQL 能让扩展链接更顺畅。另外,MySQL 安装后需要初始化数据目录和设置 root 密码,这一步如果放在 PHP 之后做,不影响 PHP 本身,但会影响你测试连接的时间点。

还有一个隐藏依赖:macOS 的命令行工具(Command Line Tools)。Homebrew 安装 PHP 时会用到编译工具链,如果没装会提示你安装。这个通常在你第一次运行brew install时自动触发,跟着提示走就行,大概下载 1-2 GB,取决于网速。

3. 手把手实操:从零到跑通第一个页面

3.1 Homebrew 安装与常见网络问题处理

打开终端,粘贴官方安装命令。这里我不贴具体命令了,因为官方脚本地址偶尔会变,你直接搜“Homebrew 官网”拿最新命令最稳妥。安装过程中会要求输入密码,这是正常的,因为要写入/usr/local或/opt/homebrew目录。

安装完成后,运行brew --version验证。如果提示“command not found”,大概率是环境变量没配好。Apple Silicon 芯片的 Mac 默认路径是/opt/homebrew/bin,Intel 芯片是/usr/local/bin。你需要把对应路径加到~/.zshrc文件里。具体操作:用文本编辑器打开~/.zshrc,加一行export PATH="/opt/homebrew/bin:$PATH"(Intel 芯片改成/usr/local/bin),保存后运行source ~/.zshrc生效。

提示:如果你在国内网络环境下遇到下载慢的问题,可以配置 Homebrew 的镜像源。具体方法搜“Homebrew 镜像配置”有很多现成教程,这里不展开。但要注意,镜像源只加速下载,不影响包本身的完整性。

3.2 PHP 安装与内置服务器启动验证

运行brew install php。这个命令会安装 PHP 以及一系列依赖,包括openssl、readline等。安装完成后,运行php -v查看版本。如果显示 PHP 8.x,说明成功。

接下来验证 PHP 能否解析网页。在任意目录下新建一个文件叫info.php,内容就一行:

<?php phpinfo(); ?>

然后在终端进入这个文件所在目录,运行:

php -S localhost:8000

你会看到终端输出“PHP 8.x Development Server started”。打开浏览器访问http://localhost:8000/info.php,如果看到一个紫色的表格页面,里面列出了 PHP 的所有配置信息,说明 PHP 环境完全正常。这个phpinfo()页面是排查问题的利器,后面连接数据库时如果报错,可以在这里搜索“mysqli”或“pdo_mysql”确认扩展是否加载。

注意:php -S启动的服务器只监听 localhost,外部设备访问不了。这是本地开发的安全设计,不要试图改成0.0.0.0除非你清楚风险。另外,这个服务器是单线程的,同时处理多个请求会排队,但本地开发完全够用。

3.3 MySQL 安装、初始化与 root 密码设置

运行brew install mysql。安装完成后,MySQL 默认不会自动启动。你需要运行brew services start mysql来启动服务并设置开机自启。如果想手动控制,可以用mysql.server start和mysql.server stop。

MySQL 8.0 安装后,root 用户默认没有密码(或者有一个随机生成的临时密码,取决于安装方式)。Homebrew 安装的 MySQL 通常 root 密码为空。运行mysql -u root如果能直接进入 MySQL 命令行,说明密码为空。这时候必须立刻设置密码,否则任何本地程序都能无密码连接你的数据库。

设置密码的命令:

ALTER USER 'root'@'localhost' IDENTIFIED BY '你的新密码'; FLUSH PRIVILEGES;

把“你的新密码”替换成你自己设的密码。设置完成后,退出再重新登录验证:mysql -u root -p,输入新密码能进去就对了。

提示:MySQL 8.0 默认的认证插件是caching_sha2_password,而老版本的 PHP MySQL 扩展可能不支持这个插件。如果你在 PHP 连接时报“Authentication plugin 'caching_sha2_password' cannot be loaded”,需要把 root 用户的认证方式改成mysql_native_password。命令是:ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY '你的密码';。不过 PHP 8.x 的 mysqli 扩展已经支持新插件,通常不需要改。

3.4 创建测试数据库和表

进入 MySQL 命令行后,创建一个用于测试的数据库和一张简单的表。这里我设计一个“用户留言板”的场景,表结构简单但足够演示增删改查。

CREATE DATABASE test_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE test_db; CREATE TABLE messages ( id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(50) NOT NULL, content TEXT NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP );

这里有几个细节值得说:字符集用utf8mb4而不是utf8,因为 MySQL 的utf8是残缺的,存不了 emoji 和某些生僻字,utf8mb4才是真正的 UTF-8。created_at字段用TIMESTAMP并设置默认值为CURRENT_TIMESTAMP,这样插入数据时不用手动写时间,数据库自动记录。id字段用AUTO_INCREMENT让数据库自动分配主键,避免手动管理 ID 冲突。

3.5 编写第一个 PHP 连接脚本

现在回到 PHP 这边。在之前info.php所在目录,新建一个db_test.php文件。内容如下:

<?php $host = 'localhost'; $user = 'root'; $pass = '你的密码'; $dbname = 'test_db'; $conn = new mysqli($host, $user, $pass, $dbname); if ($conn->connect_error) { die('连接失败: ' . $conn->connect_error); } $conn->set_charset('utf8mb4'); $sql = "INSERT INTO messages (name, content) VALUES ('测试用户', '这是第一条留言')"; if ($conn->query($sql) === TRUE) { echo "新记录插入成功,ID 为: " . $conn->insert_id; } else { echo "插入失败: " . $conn->error; } $conn->close(); ?>

把你的密码替换成你设置的 MySQL root 密码。保存后,浏览器访问http://localhost:8000/db_test.php。如果看到“新记录插入成功,ID 为: 1”,说明 PHP 已经成功连接 MySQL 并写入了数据。

这个脚本虽然简单,但包含了连接数据库的完整要素:主机地址、用户名、密码、数据库名、字符集设置、错误处理、关闭连接。其中set_charset('utf8mb4')这行很多人会漏掉,导致中文乱码。connect_error判断是必须的,否则连接失败时页面会显示一堆看不懂的警告。

3.6 读取数据并渲染成网页

插入数据只是第一步,真正“网页操作数据库”的核心是读取并展示。新建list.php:

<?php $conn = new mysqli('localhost', 'root', '你的密码', 'test_db'); $conn->set_charset('utf8mb4'); $result = $conn->query("SELECT * FROM messages ORDER BY created_at DESC"); echo "<h1>留言列表</h1>"; echo "<table border='1' cellpadding='10'>"; echo "<tr><th>ID</th><th>姓名</th><th>内容</th><th>时间</th></tr>"; while ($row = $result->fetch_assoc()) { echo "<tr>"; echo "<td>" . htmlspecialchars($row['id']) . "</td>"; echo "<td>" . htmlspecialchars($row['name']) . "</td>"; echo "<td>" . htmlspecialchars($row['content']) . "</td>"; echo "<td>" . $row['created_at'] . "</td>"; echo "</tr>"; } echo "</table>"; $conn->close(); ?>

访问http://localhost:8000/list.php,你会看到一个表格,里面显示刚才插入的那条留言。这里用htmlspecialchars()包裹输出内容,是为了防止 XSS 攻击——如果有人留言内容里写了<script>标签,不加这个函数就会直接执行脚本。这是安全底线,哪怕本地测试也要养成习惯。

4. 常见问题与排查技巧实录

4.1 连接数据库报错的几种典型情况

实际操作中,连接数据库这一步是报错重灾区。我把最常见的几种错误和排查路径整理成表,方便你对照。

错误信息可能原因解决方法
Connection refusedMySQL 服务没启动运行brew services start mysql
Access denied for user密码错误或用户名不对检查密码,确认是 root@localhost
Unknown database数据库名写错或没创建用SHOW DATABASES;确认
No such file or directorysocket 路径不对指定127.0.0.1代替localhost
Call to undefined functionPHP 扩展没加载检查 phpinfo 里有没有 mysqli

其中“No such file or directory”这个错误特别隐蔽。原因是 PHP 连接localhost时默认走 Unix socket 文件,而 MySQL 的 socket 文件路径可能和 PHP 预期的不一致。解决办法很简单:把连接地址从localhost改成127.0.0.1,强制走 TCP 连接。这个技巧我用了无数次,几乎百试百灵。

4.2 PHP 页面空白或显示源码的排查

有时候你访问 PHP 页面,浏览器直接显示 PHP 源代码,或者一片空白。显示源码说明 Web 服务器没有把 PHP 交给解析器处理——如果你用的是php -S内置服务器,这种情况几乎不会发生,因为内置服务器本身就是 PHP 进程。但如果用 Apache 或 Nginx,就需要检查 PHP 模块是否加载。

页面空白则通常是 PHP 报错但错误信息没显示。默认情况下,PHP 的错误显示是关闭的。你可以在 PHP 文件开头临时加上:

ini_set('display_errors', 1); error_reporting(E_ALL);

这样所有错误都会直接打印在页面上。排查完记得去掉这两行,生产环境绝对不能开。

4.3 中文乱码的根源与彻底解决

中文乱码是 PHP + MySQL 的经典问题,根源在三个环节的字符集不一致:数据库表的字符集、MySQL 连接的字符集、PHP 输出的字符集。任何一环不是utf8mb4,都可能乱码。

彻底解决的方案是三层都设对:建表时用CHARACTER SET utf8mb4,PHP 连接后立刻set_charset('utf8mb4'),HTML 输出时加<meta charset="utf-8">。这三步做到位,乱码问题基本绝迹。如果还乱码,用SHOW VARIABLES LIKE 'character%';检查 MySQL 服务端的默认字符集,必要时在 MySQL 配置文件里改。

提示:MySQL 8.0 的默认字符集已经是utf8mb4,所以新建数据库时不指定字符集通常也没问题。但显式指定是好习惯,尤其是团队协作时,避免因为环境差异导致问题。

4.4 端口占用与进程管理

php -S localhost:8000启动时如果提示“Address already in use”,说明 8000 端口被占用了。可能是之前启动的 PHP 服务器没关掉,也可能是其他程序在用。查看端口占用的命令:

lsof -i :8000

找到 PID 后kill -9 PID干掉进程,或者换个端口比如php -S localhost:8080。MySQL 默认端口是 3306,如果被占用,MySQL 会启动失败。检查方法类似,lsof -i :3306。不过 3306 被占用的概率很低,除非你装了多个 MySQL 实例。

5. 从示例到实用:几个值得马上加的改进

5.1 把数据库配置抽离成独立文件

上面的示例把数据库密码直接写在 PHP 文件里,这在本地测试没问题,但一旦代码要分享或部署,密码就泄露了。正确的做法是把配置抽到一个单独的文件,比如config.php:

<?php define('DB_HOST', '127.0.0.1'); define('DB_USER', 'root'); define('DB_PASS', '你的密码'); define('DB_NAME', 'test_db'); ?>

然后在其他文件里require 'config.php';使用常量。更进一步,可以把config.php加入.gitignore,再提供一个config.example.php作为模板。这是行业标准做法,花两分钟就能改好。

5.2 用 PDO 替代 mysqli 的理由

PHP 连接 MySQL 有两种主流方式:mysqli和PDO。上面的示例用的是mysqli,因为它直观、函数名好记。但如果你打算长期写 PHP,我建议尽早转向PDO。原因有三:PDO 支持多种数据库(MySQL、PostgreSQL、SQLite 等),换数据库时不用重写连接代码;PDO 的预处理语句更安全,能彻底杜绝 SQL 注入;PDO 的异常处理更现代,用 try-catch 捕获错误比判断返回值优雅得多。

用 PDO 重写连接部分:

try { $pdo = new PDO('mysql:host=127.0.0.1;dbname=test_db;charset=utf8mb4', 'root', '你的密码'); $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); } catch (PDOException $e) { die('连接失败: ' . $e->getMessage()); }

插入数据时用预处理:

$stmt = $pdo->prepare("INSERT INTO messages (name, content) VALUES (:name, :content)"); $stmt->execute([':name' => '测试用户', ':content' => 'PDO 插入的内容']);

这样即使content里包含 SQL 关键字,也不会被当成 SQL 执行。

5.3 加一个简单的表单实现真正的“网页操作”

目前插入数据是靠写死 SQL 语句,不够“网页”。加一个 HTML 表单,让用户输入姓名和内容,提交后写入数据库,这才算完整的交互闭环。新建form.php:

<?php require 'config.php'; $conn = new mysqli(DB_HOST, DB_USER, DB_PASS, DB_NAME); $conn->set_charset('utf8mb4'); if ($_SERVER['REQUEST_METHOD'] === 'POST') { $name = $_POST['name'] ?? ''; $content = $_POST['content'] ?? ''; if ($name && $content) { $stmt = $conn->prepare("INSERT INTO messages (name, content) VALUES (?, ?)"); $stmt->bind_param('ss', $name, $content); $stmt->execute(); echo "<p>留言成功!</p>"; } } ?> <form method="post"> 姓名:<input type="text" name="name" required><br> 内容:<textarea name="content" required></textarea><br> <button type="submit">提交</button> </form> <a href="list.php">查看所有留言</a>

这个表单用prepare+bind_param实现了参数化查询,ss表示两个参数都是字符串类型。提交后数据写入数据库,点击链接就能看到列表。到这里,一个完整的“网页操作本地 MySQL 数据库”的闭环就形成了。

5.4 开发时的实时刷新与调试习惯

用php -S开发时,每次改完 PHP 文件刷新浏览器就能看到效果,不需要重启服务器。但如果你改了php.ini配置,就需要重启。另外,建议在开发时打开错误显示,在php.ini里设置display_errors = On,或者在每个文件开头临时开启。生产环境则相反,必须关闭错误显示,把错误记录到日志文件。

还有一个实用技巧:在终端运行php -S localhost:8000时,所有请求日志会实时打印在终端里。如果某个请求返回 500 错误,终端会显示具体的错误信息,比在浏览器里看空白页面高效得多。养成看终端日志的习惯,排查效率翻倍。

6. 我个人在实际操作中的几点体会

这套环境我搭过不下十次,每次换新 Mac 或者帮别人配置都要走一遍。最大的感受是:Homebrew 的 PHP 和 MySQL 配方已经非常成熟,真正花时间的不是安装,而是配置和排错。尤其是 MySQL 8.0 的认证插件变化和 PHP 扩展的加载路径,这两个点卡住过很多人。

另一个体会是,不要怕报错,怕的是看不到报错。PHP 默认不显示错误,MySQL 连接失败也只给一个模糊的提示。所以开发阶段一定要把错误显示打开,把日志级别调低,让所有问题都暴露出来。等代码稳定了再关掉。这个习惯能帮你省下大量猜测的时间。

最后说一个细节:php -S内置服务器虽然方便,但它不支持.htaccess文件,也不支持 URL 重写。如果你后续要学框架(比如 Laravel),需要换成 Nginx 或 Apache。但那是下一步的事,现在先把基础跑通,把“网页读写数据库”这个核心逻辑吃透,后面的路会顺很多。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进