1. 先聊两句:这套组合到底卡人卡在哪
我最近给一个老项目补环境,重新走了一遍 ThinkPHP 6.0.2 + PHPStudy + Composer 这套流程。说实话,网上相关的教程一抓一大把,但真正能一口气跑通的并不多。问题不是步骤难,而是教程之间互相矛盾:有的让你用集成环境自带的 PHP 版本,有的让你先改 PHPStudy 的端口,还有的直接甩一个几十 MB 的项目压缩包让你解压到根目录,结果访问一片空白,谁都不知道是哪一步出了错。
我写这篇不是想凑热闹,而是把这次实际搭建的完整过程,连同两年里在这套环境上反复踩过的坑一起整理出来。目标很明确:让一个从没配过 ThinkPHP 6 的人,照着操作能在半小时内看到默认欢迎页,并且知道每一个关键步骤背后“为什么一定要这样做”。适合三类人看:刚学 ThinkPHP 的新手、要接手旧项目跑本地环境的开发者,以及想用 PHPStudy 快速验证 TP6 功能模块的技术爱好者。
先说结论:这套环境真的不难,但有一个绕不开的命门——Composer 拉取依赖的速度和稳定性。只要把 Composer 的源和 PHP 版本选对,后面基本就是一路下一步。
2. 环境准备:PHPStudy 与 PHP 版本选择的讲究
2.1 别下错 PHPStudy 版本
PHPStudy 现在官方叫“小皮面板”,官网直接搜就能找到。它有 Windows 版和 Linux 版,Windows 下是一个桌面控制面板,Linux 下是网页面板,两者界面差异很大,教程不能互相套用。
我平时本地开发用 Windows 版,服务器上用 Linux 版。如果你是跟着这篇走本地开发,下载 Windows 版即可,版本号其实不用太追新,v8.x 或者最新的 8.1 都行,核心功能差别不大。
安装时有一个不少人忽略的点:安装路径不要带中文和空格。我见过有人装在D:\软件\phpstudy,后面 Composer 和 PHP 扩展加载各种报错,改回D:\phpstudy_pro才消停。这不是玄学,是 PHP 的某些扩展和工具链对路径里的特殊字符敏感,用纯英文路径省心很多。
另外安装完面板后,第一次启动会提示选择 Apache 还是 Nginx。这里不用太纠结,先选哪个都能跑,后面伪静态配置我会分别给写法。我自己倾向于 Nginx,占内存小、并发表现好,而且 ThinkPHP 官方生产环境文档也以 Nginx 为主。
2.2 ThinkPHP 6.0.2 对 PHP 版本的硬性要求
ThinkPHP 6.0 的官方写的是 PHP >= 7.2.5,也就是说 PHP 7.2.5 以上都能跑。但版本能跑和跑得舒服是两码事。
我在 PHPStudy 里装了 7.2、7.4、8.0 三个版本,实测下来 ThinkPHP 6.0.2 配 PHP 7.4 是最稳的。7.2 也能跑,但有些第三方包已经开始放弃对 7.2 的兼容,装依赖时容易碰到坑。8.x 在 TP6.0.2 早期版本上偶尔会有“函数未定义”这类兼容问题,因为 PHP 8 删掉了一批旧函数。虽然 6.0.2 有兼容处理,但没必要拿老框架去挑战新运行时。
给你们一个选版本的建议表:
| PHP 版本 | ThinkPHP 6.0.2 兼容性 | 我的推荐度 | 备注 |
|---|---|---|---|
| 7.2 | 可运行,依赖兼容性下降 | 不推荐 | 第三方包很多已放弃支持 |
| 7.4 | 稳定运行 | 强烈推荐 | 目前 TP6 本地开发最佳选择 |
| 8.0 | 大部分功能正常 | 可以但没必要 | 老项目改造时容易出幺蛾子 |
| 8.1+ | 风险较高 | 不推荐 | 建议直接用 ThinkPHP 8 |
2.3 PHP 版本怎么在面板里切换和确认
PHPStudy 装完默认可能给的是 PHP 8 或者更老的 5.x,需要在面板的“软件管理 - PHP”里看一下已经装了哪些版本。如果没有 7.4,点“安装”按需装一个,这个过程会下载安装包,稍微等一下就行。
确认版本有个很简单的办法:在 PHPStudy 的“设置 - 配置文件 - PHP”,能看到当前站点用的 PHP 版本;也可以在项目根目录写一个临时 PHP 文件,放一句<?php phpinfo();,访问后看第一行显示的版本对不对。
注意一点:你切换了 PHPStudy 里的 PHP 版本,并不代表命令行里的 PHP 也跟着变了。Windows 环境下,PHPStudy 的 PHP 是装在D:\phpstudy_pro\Extensions\php\7.4.3nts这种路径下的,跟系统 PATH 里的 PHP 是两回事。这一点正好是下一节 Composer 的问题源头。
2.4 开发环境里必须检查的三个开关
进 PHPStudy 面板之后,不需要改太多东西,但有三处我要建议你提前确认:
- MySQL 的端口如果不是 3306,ThinkPHP 的
.env文件里数据库配置要跟着改,不然连不上库。 - Nginx/Apache 的进程没有占用,特别是 80 端口被 IIS 或者其它进程抢走时,站点起不来。
- PHP 扩展里
fileinfo、curl、openssl这些必须打开。ThinkPHP 6 的依赖里有的会用到它们,缺了某些扩展,Composer 安装时候直接报错,弹出来的提示还不直观。
打开扩展的位置在面板的“PHP 扩展”栏,勾选后点“应用更改”,PHPStudy 会自动帮你改php.ini并重启服务,不用手动去翻配置文件,这点比我们自己改配置省力很多。
3. Composer 是第一个大坎:安装、换源与版本指定
3.1 Windows 下 Composer 的两种安装方式
Composer 在 Windows 下推荐有两种方式:
- 直接下载
Composer-Setup.exe安装包,一路下一步。它会帮你把composer.bat加入系统 PATH,以后在命令行直接敲composer就能用。 - 下载
composer.phar文件,放在项目目录或者一个固定目录,用php composer.phar的方式调用。好处是不污染系统,坏处是每次命令都要带php前缀。
我推荐第一种,省事。但安装过程中有一个关键选择:Composer 安装程序会让你指定 PHP 的路径。这时候如果你机器上装过多个 PHP,务必确保选的是 PHPStudy 里同一版本的php.exe。我见过有人系统里装了别家的 PHP(比如小皮之外的),Composer 用的是那个 PHP,依赖装好后跑起来却是另一个版本,最后项目直接白屏。
最简单的做法:在已安装的其他环境菜单里勾选“排除安装”,让 Composer 只认识一个 PHP。或者在 PHPStudy 的面板里找到当前站点用的 PHP 可执行文件,Composer 安装时手动指定到那个路径。
3.2 不换源,创建项目能等到你怀疑人生
Composer 默认的依赖源是 Packagist 的国外服务器,在国内网络环境下拉取速度非常不稳定。我最初用默认源创建一个 ThinkPHP 6.0.2 项目,卡在updating dependencies阶段十几分钟不动,最后直接超时失败。换成国内镜像后,同样的操作两三分钟完成。
切换全局镜像源的方法,命令行执行:
composer config -g repo.packagist composer https://mirrors.aliyun.com/composer/这条命令是把全局的 Composer 镜像源改成阿里云镜像。改动之后,以后所有composer create-project和composer install都会走这个源。
想验证是否成功,执行:
composer config -g -l可以看到repositories.packagist的值是我们刚设置的地址。
如果换了源仍慢,还可以临时指定源:
composer create-project topthink/think=6.0.2 tp6 --prefer-dist --no-interaction当然阿里云镜像偶尔有缓存延迟,遇到某个包版本拉取不到的时候,可以临时切到腾讯源比较一下:
composer config -g repo.packagist composer https://mirrors.cloud.tencent.com/composer/注意镜像源只影响 Composer 下载,不影响项目代码的运行,切换后重新composer update也不会破坏已有项目结构。
3.3 多 PHP 版本共存时 Composer 用错版本怎么办
这是我认为整套流程里最容易踩但又最没人提的一个坑。
PHPStudy 装了多个 PHP 版本时,Composer 默认使用PATH里那个 PHP。如果你在命令行里敲php -v显示的是 PHP 8,那 Composer 会用 PHP 8 去执行依赖解析。很多依赖在 PHP 8 下安装会出现版本兼容警告,甚至直接拒绝安装。
我当时碰到的情况是:项目要跑在 PHP 7.4,但 Composer 用的是 PHP 8,结果composer create-project时提示某些插件不被当前 PHP 支持。排查后发现是 PATH 顺序问题。
解决办法有两个:
方法一,临时指定 PHP 版本来执行 Composer:
D:\phpstudy_pro\Extensions\php\7.4.3nts\php.exe D:\composer\composer.phar create-project topthink/think=6.0.2 tp6方法二,直接把 7.4 的目录提到 PATH 最前面。右键“此电脑 - 属性 - 高级系统设置 - 环境变量”,在Path里把 PHP 7.4 的目录移到最上边。
我个人建议用方法一,因为改动最小、不容易影响系统里其它工具。
3.4 常见 Composer 报错现场还原
我把这两年收集到的 Composer 高频报错整理成一个表,方便你对着排查:
| 报错信息 | 实际情况 | 解决办法 |
|---|---|---|
Composer is not initialized | Composer.lock 缺失或者composer install没找到锁文件 | 首次用composer update生成 lock 后再 install |
Could not resolve host | 网络访问不了默认源 | 切国内镜像源重试 |
Your requirements could not be resolved | 依赖版本冲突 | 用composer create-project topthink/think=6.0.2指定版本,避免乱拉最新 |
The openssl extension is required | php.ini 里没开 openssl | PHPStudy 面板勾选 openssl 扩展并重启 |
App directory already exists | 目标目录已存在同名文件 | 换一个目录名,或删掉目标目录后重试 |
最后一行的App directory already exists是我看到新手问得最多的。这问题特别简单:你创建项目的目录不是空的,Composer 不敢覆盖。换一个新目录名就行,比如tp6,别直接建think再往里塞。
4. 创建 ThinkPHP 6.0.2 项目:命令、目录结构与首次访问
4.1 创建命令与版本锁定
准备工作都做完后,创建 TP6.0.2 项目的完整流程如下:
cd D:\www composer create-project topthink/think=6.0.2 tp6分解一下这条命令:
create-project是 Composer 的创建项目指令,等同于先git clone再composer install。topthink/think是 ThinkPHP 框架的主包名。=6.0.2是版本约束,锁定在 6.0.2 版本。tp6是要创建的目标目录名。
如果你不写=6.0.2,默认会拉取该包可用的最新 6.x 版本。作者标题里既然提到 6.0.2,说明项目需求是明确的,那就老老实实锁版本。
命令执行过程中会下载 ThinkPHP 核心和一组依赖包。下载完成后,你会在tp6目录下看到类似下面的结构:
tp6/ ├── app/ 应用目录 │ ├── controller/ 控制器目录 │ ├── model/ 模型目录 │ └── ... ├── config/ 配置目录 ├── route/ 路由定义目录 ├── runtime/ 运行时生成目录(缓存、日志) ├── vendor/ Composer 依赖包目录 ├── view/ 视图目录(部分版本可能没有,需自行创建) ├── public/ Web 根目录 │ └── index.php 入口文件 ├── .env 环境配置文件(默认没有,需复制 .env.example 或自己建) ├── composer.json 依赖声明文件 └── think 命令行入口(Linux/macOS)4.2 不能用根目录当站点根目录:public 的定位
刚接触 ThinkPHP 6 的人容易有的一个错误认知:以为项目根目录就是网站根目录。其实不是。
ThinkPHP 6 强制要求 Web 服务器把站点根目录指到public目录,原因有两条:
一是入口安全。TP6 的入口文件只有public/index.php。如果站点根目录是项目根目录,访问者直接访问http://你的域名/app/controller/User.php就能看到源码,安全性等于裸奔。而指到public后,根目录以上的文件都无法直接通过 URL 访问。
二是路由纯净。URL 重写时会把所有访问请求交给index.php处理,如果把根目录暴露出去,静态资源和入口会混在一起,伪静态规则容易冲突。
理解这一点,后面配置 PHPStudy 就不会迷糊。
4.3 首次访问:欢迎页出现才算第一步完成
先在项目里建立一个访问入口。如果创建后public下已经有.example这类示例文件,直接删掉就行,不影响。然后在命令行执行:
cd D:\www\tp6 php think run这条命令会调用 PHPStudy 里配置好的 PHP 内置服务器,默认监听127.0.0.1:8000。浏览器访问http://127.0.0.1:8000,正常情况下能看到 ThinkPHP 的默认欢迎页。
看到欢迎页,说明 Composer 拉取的框架代码没问题、PHP 扩展没问题、目录权限没问题。后面要做的就是用 PHPStudy 把域名和站点串起来。
另一种更快的方式是在 PHPStudy 面板里直接创建一个站点,把域名指向项目目录。但这就要先解决“运行目录指向 public”的事,所以我把这一个主题拆成了独立的一节来说。
5. 必踩的坑:PHPStudy 里运行目录不指向 public 会怎样
5.1 小皮面板里创建站点并指定运行目录
打开 PHPStudy 面板,在左侧菜单点“网站”,再点“创建网站”。填写以下几项:
- 域名:比如
tp6.test - 端口:默认 80,如果被占用可以改成 8000、8080 这类
- 网站目录:选到你的项目根目录,比如
D:\www\tp6 - 运行目录:这里最关键,要手动选择为
\public - PHP 版本:选择 PHP 7.4(对应你项目需要的版本)
截图省略,因为不同版本界面稍有差异,但关键点都一样:运行目录必须选public。
如果创建网站时没有设置运行目录,默认指向网站根目录。此时访问站点,可能出现的情况有两种:一是直接显示目录结构列表,把app、config都列出来,非常不安全;二是访问http://tp6.test/index.php能出页面,但访问http://tp6.test/是 403 或者 404。两种都说明目录指向错了。
5.2 本地域名解析:hosts 文件别忽略
PHPStudy 创建的站点域名tp6.test,本地浏览器访问时实际上查的是系统 DNS。如果系统配置的 DNS 服务器不认识这个域名,会解析失败。
开发环境的常规做法是在 hosts 文件里手动加一行解析记录。hosts 文件在C:\Windows\System32\drivers\etc\hosts,以管理员身份打开记事本,在最下面加:
127.0.0.1 tp6.test如果 PHPStudy 创建站点时勾选了“创建 hosts 解析”,它会自动帮你写进去;没勾照做,别忘了这一步。
5.3 设置之后仍然打不开的三个检查点
有次我帮一个同事排查,他运行目录也选了public,hosts 也配了,访问却还是 404。最后发现是下面三个点的问题:
PHPStudy 是 Nginx 环境,伪静态规则没有加上。ThinkPHP 6 默认需要 URL 重写功能,Nginx 下如果没配置伪静态,访问
http://tp6.test时index.php没被正确路由,自然打不开页面。关于这一点,下一节专门展开。项目目录没有给足读写权限。TP6 运行时会在
runtime目录写日志、缓存,如果 PHPStudy 进程对该目录没有写权限,会出现页面空白但错误日志里没有任何记录的情况。Windows 下大部分情况不会有这个权限问题,但 Linux 面板必须注意,经常是runtime目录权限为 755、所属用户不对导致的问题。端口冲突。站点的端口如果是 80,而系统里 IIS 或者其它 Web 服务也占用着 80,PHPStudy 的站点就起不来。解决办法是把 PHPStudy 的 Nginx 或者 Apache 停掉,重新启动,或者在创建站点时换个端口。
6. 伪静态与 URL 重写:让 URL 里没有 index.php
6.1 为什么要处理伪静态
ThinkPHP 6 默认的路由格式是:
http://tp6.test/index.php/index/hello如果你把index.php留在 URL 里,性能上没大影响,但两个问题:
- URL 不美观,对外分享的时候很长;
- 某些环境下的安全策略会把
index.php当成漏洞特征,虽然多数是误报,但没必要自己给自己找麻烦。
处理掉index.php的办法叫“伪静态”,实质是在 Web 服务器层面把所有不存在的文件请求重写到入口文件index.php。
6.2 Apache 环境下的伪静态配置
Apache 下最简单的方式是利用 ThinkPHP 自带的.htaccess文件。TP6 的public目录里自带了一个.htaccess,内容大致是:
<IfModule mod_rewrite.c> Options +FollowSymlinks -Multiviews RewriteEngine On RewriteCond %{REQUEST_FILENAME} !-d RewriteCond %{REQUEST_FILENAME} !-f RewriteRule ^(.*)$ index.php?/$1 [QSA,PT,L] </IfModule>把这段配置放到public/.htaccess里,并且确保 Apache 开启了mod_rewrite。PHPStudy 的 Apache 默认会开这个模块,一般不用手动改。
6.3 Nginx 环境下的伪静态配置
Nginx 环境没有.htaccess,需要在站点的 Nginx 配置里加上一段 location 规则。
在 PHPStudy 面板里,找到该站点对应的 Nginx 配置文件(“网站 - 操作 - 配置文件”),在server {}段里加:
location / { if (!-e $request_filename) { rewrite ^(.*)$ /index.php?s=$1 last; break; } }保存后,在面板里重启 Nginx。这里有个小细节:Nginx 的if (!-e $request_filename)意思是请求的文件不存在时,才进入重写,避免把真实的静态资源如 CSS、JS、图片也转给index.php。
6.4 配置完成后的验证方式
配置完伪静态后,验证三步:
- 访问
http://tp6.test,应该直接出现欢迎页或你写的控制器页面。 - 访问一个有控制器但不存在的路由,比如
http://tp6.test/nonexist,应该返回 TP6 的 404 页面,而不是 Nginx 默认的 404。 - 在
public目录放一个静态文件,比如public/robots.txt,能正常访问到文件内容,说明静态资源没有被重写规则误伤。
这三步走通,伪静态就基本到位了。
7. 冷门但真实:$_SERVER['REQUEST_URI'] 为空导致路由失效
这个坑我在热搜词里看到有人也搜了,很值得单说一节。因为它不是一个常见配置问题,而是环境层面的“幽灵问题”。
7.1 问题现象与排查链路
有次我搭好 TP6 项目后,访问首页正常,但一访问任何二级路由就白屏。打开调试模式看错误,显示路由解析失败,类不存在的提示。一开始我以为是路由定义写错,检查了半天没发现问题。后来在public/index.php里临时写:
var_dump($_SERVER['REQUEST_URI']); var_dump($_SERVER['REQUEST_METHOD']); die;发现$_SERVER['REQUEST_URI']居然是空字符串。
ThinkPHP 6 的路由解析依赖$_SERVER['REQUEST_URI']或者$_SERVER['PATH_INFO']来识别当前请求的地址。如果REQUEST_URI为空,框架无法正确解析控制器的路由,所以首页通过默认路径能打开,但二级路径全部失效。
7.2 为什么会出现这个情况
实际上,REQUEST_URI是由 Web 服务器注入到 PHP 环境变量里的。正常情况下 Nginx 会通过fastcgi_param REQUEST_URI $request_uri;传给 PHP-FPM。如果它为空,说明fastcgi_params或php-fpm接收方配置里缺少这一行,或者被覆盖了。
常见的触发场景有几个:
- 某些精简版的 Nginx 配置模板里漏了
fastcgi_param REQUEST_URI,而 ThinkPHP 恰好依赖它; - Apache 环境下
mod_rewrite规则和AcceptPathInfo的相互作用导致REQUEST_URI被改写为空; - 某些安全软件拦截了带有路径信息的请求,转发时把
REQUEST_URI置空。
7.3 兜底解决思路与代码示例
第一个解决路径是检查 Web 服务器配置:Nginx 的fastcgi.conf或者站点配置里是否包含fastcgi_param REQUEST_URI $request_uri;。没有就补上,然后重载配置。
第二个解决路径是代码层面兜底,在public/index.php入口文件的最上方,也就是框架加载之前加上判断:
if (empty($_SERVER['REQUEST_URI'])) { if (isset($_SERVER['HTTP_X_ORIGINAL_URL'])) { $_SERVER['REQUEST_URI'] = $_SERVER['HTTP_X_ORIGINAL_URL']; } elseif (isset($_SERVER['REQUEST_URI_FALLBACK'])) { $_SERVER['REQUEST_URI'] = $_SERVER['REQUEST_URI_FALLBACK']; } else { $_SERVER['REQUEST_URI'] = $_SERVER['SCRIPT_NAME']; if (isset($_SERVER['QUERY_STRING']) && $_SERVER['QUERY_STRING']) { $_SERVER['REQUEST_URI'] .= '?' . $_SERVER['QUERY_STRING']; } } }这段代码的原理是手动把缺失的变量用其它服务器变量拼装回来,保证框架读取路由时拿到的地址是完整可用的。它不是万能的,但能解决大多数 Web 服务器没有注入导致的空值问题。
如果用了这个兜底后问题还在,那我建议你把错误模式开大,看 TP6 日志里的完整请求参数是什么,顺着日志里缺少的 $_SERVER 键名继续补。
8. 进阶避坑笔记:关联删除、二级域名与安全底线
8.1 模型关联删除的三种实现
ThinkPHP 6 的模型关联删除,是很多人在实际业务里绕不开的需求。比如删一篇文章,要把它的评论、点赞关联数据一起删掉。三种方式我都用过:
方式一:使用with()关联后控制层手动循环删除。缺点是 N+1 次查询,数据量小还行,数据量大了性能难看。
方式二:使用模型事件,在模型类里定义一个删除监听:
protected static function onBeforeDelete($model) { // 删除前先清理关联数据 $model->comments()->delete(); }这种方式的优点是把逻辑内聚在模型里,不污染控制器代码,推荐业务层使用。
方式三:数据库外键级联。在数据表设计时给外键加上ON DELETE CASCADE,数据库层自己处理。这种方式性能最好,但外键约束在某些分表场景下比较麻烦,动手要谨慎。
选哪种,取决于你的业务复杂度。简单说:单条数据删除用事件,批量的用查询后再统一删,数据量大时优先考虑数据库设计。
8.2 二级域名设置的两个层面
“ThinkPHP 开启二级域名设置”这个关键词也很热。坦率地讲,二级域名设置有硬件层和业务层两个层面,经常被人搞混:
- 域名解析 + 服务器配置层面:需要把
admin.tp6.test解析到你的服务器,并在 Nginx 或 Apache 里新增一个站点或者 server 块。 - 框架路由层面:如果两个域名指向的是同一个 TP6 项目,可以在
route/route.php里用Route::domain()做域名分组:
Route::domain('admin.tp6.test', function () { Route::get('index', 'admin.Index/index'); });如果只是本地开发想模拟二级域名,PHPStudy 创建站点时直接填admin.tp6.test,并在 hosts 里加一行解析,效果一样,不用专门上外网服务器操作。
8.3 关于 thinkphp 漏洞的三个安全提醒
最后想多说一句安全相关的。搜“thinkphp 漏洞”的人不少,很多是运维或开发在自查。有一些基础操作可以大幅降低框架版本漏洞的风险:
本地开发可以用 6.0.2 这个版本,但上生产环境前一定要
composer update topthink/framework到 6.0.x 的最新版本。旧版本没有后续安全补丁,等于把一个已公开的问题留在线上。关闭调试模式。TP6 的
.env文件里默认是APP_DEBUG = true,生产环境务必改成false,并且开启APP_TRACE前先想想日志里会不会带出敏感信息。不要把 MySQL 的 root 密码写在项目里,尽量用最小权限账号,.env 文件不要让 Web 服务器目录直接可读。PHPStudy 运行目录指到
public后,.env在项目根目录无法被 URL 访问,但文件系统层面该设的权限还是要设。
我个人的习惯是:本地随便折腾,生产环境打死不偷懒,依赖锁版本、配置最小化、日志定期清理。任何框架版本都有生命周期,ThinkPHP 6.0 也不例外,别让一个“能跑”变成“不敢动”。
这套环境真正用顺之后,你会发现后面写业务逻辑的速度远大于配环境的效率。把这次记录里的每一个坑都避开,你剩下的时间就可以花在更有意思的功能实现上了。