
PHP 大概是这个行业里被骂得最多、用得也最多的语言之一。打开招聘网站看一圈PHP 岗位大量存在GitHub 上各种免费网站源码、图书管理系统、轻量级聊天室源码里 PHP 也占了大半。更别提那些常年挂在这上面的热搜词了Windows 10 nginx php、php 图片生产、php 视频压缩、php redis 消费组、代码审计、上传漏洞、伪协议……这些东西平时分散在各种论坛和问答帖里新手想一次性搞清楚非常难。我干了十多年 PHP踩过的坑不算少。这篇东西我想把 PHP 开发从环境搭建到项目实战、从基础语法到工程化安全这条完整的线串一遍。不管你是刚准备入门的学生还是做了几年业务逻辑想补技术深度的开发又或者是要带 PHP 实训课的老师这篇文章都是按我实际排查问题的思路写的。能跑通、能落地、能防坑比什么都重要。1. 环境与工具链先把 PHP 开发环境跑起来1.1 Windows 下 Nginx PHP 的搭法很多人第一次接触 PHP 都是装一个集成包phpstudy、wamp就完事了。但我强烈建议至少手动配一次 Nginx PHP因为只有手动配过你才知道 Web 服务器和 PHP 解释器之间到底是什么关系。先说思路。Nginx 本身不认识 PHP 代码它收到一个xxx.php的请求时会把这个请求交给 PHP-FPMWindows 下就是 php-cgi 进程去处理然后拿到结果再返回给浏览器。这套“反向代理 FastCGI”的模型理解透了后面所有 502、504 问题你都能快速定位。具体步骤以 Windows 10 为例去 nginx.org 下载 Windows 版 Nginx解压到比如C:/nginx。去 windows.php.net 下载 PHP 8 的 Non-Thread-Safe 版本解压到C:/php。把C:/php/php.ini-development重命名为php.ini打开后确认这几项至少要保证extension_dir ext存在且没被注释。修改C:/nginx/conf/nginx.conf加一个处理 PHP 的 locationserver { listen 80; server_name localhost; root C:/www; index index.php index.html; location ~ \.php$ { root C:/www; fastcgi_pass 127.0.0.1:9000; fastcgi_index index.php; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; include fastcgi_params; } }启动 PHP 解析进程在命令行进入C:/php执行php-cgi.exe -b 127.0.0.1:9000窗口不要关。启动 Nginx双击C:/nginx/nginx.exe或者命令行执行start nginx。然后在C:/www下建一个index.php写上?php phpinfo(); ?浏览器访问http://localhost/index.php能看到 phpinfo 页面就算通了。这里有个新手特别容易踩的坑Nginx 的 root 路径一定要和 fastcgi_param 里的 SCRIPT_FILENAME 对应上。很多人配完访问 php 文件直接下载、或者报 404八成就是$document_root和实际路径没配对。注意启动 php-cgi 的命令行窗口别关。关掉窗口 PHP 进程就没了Nginx 会立刻返回 502 Bad Gateway。后来我都是用 RunHiddenConsole 这类小工具把 php-cgi 转成后台进程省心很多。1.2 调试从 NetBeans 到 PhpStorm 的 Xdebug 配置搜索词里既有“如何用 netbeans 写 php”也有“idea php debug”。这两种情况我都经历过大学生的实训课和日常开发可以分开说。NetBeans 写 PHP 其实特别适合教学场景因为免费、配置简单、界面也不复杂。新建 PHP 项目时指定一下 PHP 解释器路径就是C:/php/php.exe就能直接运行和调试。如果是计算机程序设计PHP实训课我建议学生先用 NetBeans 打基础避免一开始就被 IDE 的配置劝退。但如果真要做日常开发、接口联调、追线上 bug我还是推荐 PhpStormidea 系列里的 PHP 版。它的 debug 配合 Xdebug 用起来是真的能救命。配置分三步在php.ini里加一段[xdebug] zend_extensionxdebug xdebug.modedebug xdebug.start_with_requestyes xdebug.client_host127.0.0.1 xdebug.client_port9003重启 php-cgi 进程用phpinfo()确认 Xdebug 已经加载。在 PhpStorm 里 Settings → PHP → Servers 配置好本地服务器端口 80。然后点右上角的电话图标Start Listening for PHP Debug Connections浏览器装上 Xdebug Helper 插件开启 Debug 模式刷新页面。然后你就可以在代码里打断点单步调试看变量面板了。Xdebug 默认端口在新版本里是 9003不是老的 9000很多人断点没反应就是端口写错了。实操心得Xdebug 开着的时候整个 PHP 响应会慢很多比平时慢 3 到 5 倍很正常。别慌这是它在做单步调试的底层通信关掉浏览器插件里的 Debug 开关就恢复正常了。1.3 用 Docker 打包 PHP 镜像“php 使用 docker 打包镜像”这个搜索词背后是真实的团队协作需求。以前新人入职最痛苦的事是“我本机能跑你本机跑不起来”环境不一样整个项目就崩了。用 Docker 把 PHP 运行环境固化下来这个问题基本解决。一个最简单的 PHP 8.2 FPM 镜像 Dockerfile 长这样FROM php:8.2-fpm RUN docker-php-ext-install pdo_mysql mysqli RUN apt-get update apt-get install -y libzip-dev unzip \ docker-php-ext-install zip WORKDIR /var/www/html COPY . .然后配一个 docker-compose把 Nginx、PHP、MySQL 一起编排起来services: nginx: image: nginx:alpine ports: - 80:80 volumes: - ./www:/var/www/html - ./nginx.conf:/etc/nginx/conf.d/default.conf depends_on: - php php: build: . volumes: - ./www:/var/www/html mysql: image: mysql:8.0 environment: MYSQL_ROOT_PASSWORD: root在项目根目录执行docker-compose up -d一套 Nginx PHP MySQL 的开发环境就起来了。Windows 下你需要先装 Docker Desktop跑起来之后所有命令都在容器里执行宿主机只需要装一个编辑器。给新手提个醒容器里的 PHP 时区默认是 UTC会导致日志时间、数据库写入时间差 8 个小时。解决办法是在 Dockerfile 里加一行RUN echo date.timezoneAsia/Shanghai /usr/local/etc/php/php.ini否则后面排查问题时时间对不上会非常痛苦。2. 语言核心从语法到类、错误处理与序列化2.1 基础语法与表单PHP 的第一行业务代码PHP 的基础语法和表单处理是所有搜索热词里最底层的部分。这里的知识没打好后面写登录、写接口都会很虚。PHP 代码必须放在?php ... ?标签里变量以$开头不需要声明类型结尾语句用分号。这些是常识但表单基础就需要一点门道了。一个标准的表单提交分两步前端 HTML 负责收集输入后端 PHP 负责接收和处理。form methodpost actionregister.php input typetext nameusername required input typepassword namepassword required button typesubmit注册/button /form?php if ($_SERVER[REQUEST_METHOD] POST) { $username trim($_POST[username] ?? ); $password $_POST[password] ?? ; if ($username || $password ) { exit(用户名和密码不能为空); } echo htmlspecialchars($username, ENT_QUOTES, UTF-8); }$_POST是 PHP 预定义好的超全局变量表单里 name 属性的值就是它的 key。这里有一个大多数教程不会强调但极其重要的点$_POST[username]在没有传值的时候会报 undefined index 警告所以一定要用?? 这种空合并运算符兜底。而输出用户输入时要用htmlspecialchars不然用户的输入里带一段script标签就直接把 XSS 漏洞引到页面上了。$_GET、$_POST、$_REQUEST这三兄弟的边界也要搞清楚GET 参数在 URL 上适合搜索、翻页这种不敏感的场景POST 参数在请求体里适合注册、登录、提交数据$_REQUEST是两者合并能不用就不用因为它会有安全隐患和优先级争议。2.2 运算符、类与接口设计写“能维护”的代码热词里有“php 运算符”、“php 类”、“php 接口数组对象”、“php 怎么修改 class”把这几个词放在一起看其实就是在问一段 PHP 代码怎么写才能既不报错又容易维护。运算符方面我日常用得最多的是这几个??空合并左边不存在就用右边、?:三元、?-空安全调用对象为空时不报错而是返回 null、太空船运算比较大小返回 -1/0/1。尤其??在处理数组参数时太好用了能把一堆 isset 判断压缩成一行$page $_GET[page] ?? 1;类方面PHP 的类结构其实很清晰public 表示对外公开protected 表示子类可访问private 表示只能本类使用。构造函数__construct在 new 的时候自动执行这是最常用的入口。热词问“php 怎么修改 class”实际操作里往往不是直接改源码而是用继承覆写class BaseUser { public function getRole(): string { return user; } } class AdminUser extends BaseUser { public function getRole(): string { return admin; } }这样既能扩展能力又不破坏原类的稳定性。如果改的是第三方库里的类更推荐用组合或者装饰器模式而不是直接去 vendor 目录里改别人的代码因为 composer update 一跑你改的代码全没了。接口数组对象这个问题在写 API 接口时尤其常见。PHP 的数组是最灵活的数据结构但对外返回接口时最好约定一个固定结构?php header(Content-Type: application/json); $data [ code 0, message ok, data [ id 1, name 张三, ], ]; echo json_encode($data, JSON_UNESCAPED_UNICODE);前端拿到这个结构后先判断code是否为 0再取data。这种约定比“每次接口都返回不同形状的数组”要舒服得多联调效率至少翻一倍。2.3 错误处理与序列化稳定运行的底线“php 错误处理”和“php 序列化中文”这两个热词一个是项目上线后的生存问题一个是跨语言通信的编码问题但都属于“写代码时不多想出问题时很痛苦”的类型。PHP 默认的错误处理方式是直接把错误打在页面上这在开发时很友好但生产环境必须关闭。正确做法是把错误转成异常统一记录到日志?php set_error_handler(function ($errno, $errstr, $errfile, $errline) { if (!(error_reporting() $errno)) { return false; } throw new ErrorException($errstr, 0, $errno, $errfile, $errline); }); set_exception_handler(function (Throwable $e) { error_log($e-getMessage() . at . $e-getFile() . : . $e-getLine()); http_response_code(500); echo 服务器开小差了请稍后再试; });这样所有错误都进了日志文件线上用户看到的只是友好提示不会暴露代码路径和 SQL 信息。这是我做维护项目时第一个要改造的地方。序列化中文的问题常见于 json_encode 之后中文变成了\u5f20\u4e09。这是因为 PHP 默认会把非 ASCII 字符转成 Unicode 转义形式解决办法很简单echo json_encode($data, JSON_UNESCAPED_UNICODE);加一个JSON_UNESCAPED_UNICODE参数中文就能以原样输出。很多人存数据库明明没问题接口一返回中文就乱码多半就是忘了这个参数或者忘了加charsetutf8mb4。再提一句序列化安全。PHP 的serialize()/unserialize()可以把对象转成字符串再还原这个机制本身没问题但unserialize()反序列化用户提交的数据是极其危险的操作可能直接触发对象注入攻击。凡是能改成 JSON 的通信一律用json_encode/json_decode别给反序列化漏洞留机会。3. 把 PHP 用到真实业务登录、小程序与经典项目3.1 用户登录、注册跳转与前后端交互“php 实现登录”、“注册成功后跳转”、“php div 弹窗”、“php 跨域 jsonp”这几个热搜词其实就是 Web 业务最经典的一条链路用户注册、登录、前端弹窗提示、后端跨域接口。注册成功跳转最标准的是后端 302 跳转?php // register.php 处理完注册逻辑 header(Location: login.php?registered1); exit;这里有个很多人犯过的错header()之前不能有任何输出否则跳转失效。如果你 echo 过一段调试信息再写 header 就会报“headers already sent”。所以处理完业务后直接写 header然后用exit终止脚本执行防止后续代码意外输出内容。登录逻辑也不能再用老一套的 md5 加密了。现在 PHP 内置了password_hash和password_verify安全性比 md5 高几个量级?php // 注册时存哈希 $hash password_hash($_POST[password], PASSWORD_DEFAULT); // 登录时验证 if (password_verify($_POST[password], $hash)) { $_SESSION[user_id] $user[id]; header(Location: index.php); exit; }会话安全要注意session 的 cookie 要设置 HttpOnly避免前端脚本读取到 session id。登录成功后不仅要重新生成 session id防止会话固定攻击还要设置合理的过期时间。跨域问题老项目喜欢用 JSONP原理是在页面里动态插入 script 标签通过回调函数拿数据。PHP 端这样处理?php $callback $_GET[callback] ?? ; $data [code 0, message ok]; echo $callback . ( . json_encode($data) . );但 JSONP 有几个硬伤只支持 GET、没法设置自定义请求头、而且外部可以随意调用。现在新项目基本都是用 CORS 了PHP 端只需输出几个响应头header(Access-Control-Allow-Origin: https://你的前端域名); header(Access-Control-Allow-Headers: Content-Type, Authorization); header(Access-Control-Allow-Methods: GET, POST, OPTIONS); if ($_SERVER[REQUEST_METHOD] OPTIONS) { http_response_code(204); exit; }关于 div 弹窗后端其实不用管弹窗具体怎么画只需要把结果按约定返回。前端根据code是 0 还是非 0 决定弹成功还是失败。接口契约清晰之后前端同事就不用天天追着你问“这次返回什么结构了”。3.2 微信小程序后端PHP 是怎么接活的“微信小程序的后端用 php 是如何实现的”也是个大热词。小程序后端和普通 Web 后端的核心差异在于“登录态验证方式”和“接口调用场景限制”。小程序登录不是传统的用户名密码而是拿微信给的临时凭证 code 去换 openid。整个流程是小程序端调用wx.login()拿到 code。小程序把 code 发给你的 PHP 后端。PHP 后端拿 code appid secret 请求微信接口jscode2session。微信返回 openid 和 session_key。PHP 后端用 openid 查用户表老用户直接登录新用户自动注册然后生成自己的 token 返回给小程序。PHP 里请求微信接口的简化代码?php $code $_POST[code] ?? ; $appid 你的小程序AppID; $secret 你的小程序AppSecret; $url https://api.weixin.qq.com/sns/jscode2session?appid{$appid}secret{$secret}js_code{$code}grant_typeauthorization_code; $response file_get_contents($url); $wxData json_decode($response, true); $openid $wxData[openid] ?? ;拿到 openid 之后不要把 openid 直接返回给小程序当身份凭证因为 openid 相当于用户的唯一标识泄露了理论上存在被冒用的可能。正确做法是自己生成一个 token比如bin2hex(random_bytes(32))存到数据库或者 Redis 里过期时间 7 天小程序后续请求在 header 里带Authorization: Bearer tokenPHP 每次校验 token 有效性。接口格式方面小程序端的 wx.request 对请求域名要求必须是 HTTPS且要在小程序后台配置合法域名。所以本地开发调试小程序时要么用开发者工具的“不校验合法域名”选项要么把前端开发环境指向线上 PHP 接口。这里还要强调一点小程序后端接口永远不要相信前端传上来的用户 ID。谁登录、发什么请求都从 token 里解析出来这才符合权限校验的基本逻辑。3.3 从图书管理系统到聊天室经典场景与文件处理很多 PHP 学习者的第一个完整项目是“图书管理系统”这真的是个特别好的练手项目。它麻雀虽小五脏俱全有用户表、图书表、借阅记录表有增删改查有用户角色权限有页面交互。核心表设计大概是这样usersid、username、password_hash、rolebooksid、title、author、isbn、total_count、available_countborrow_recordsid、user_id、book_id、borrow_at、return_at、status业务逻辑就是借书时检查 available_count 是否大于 0是则减一并插入借阅记录还书时反过来。这里就锻炼了事务处理的能力借书和扣库存必须同时成功或同时失败?php $pdo-beginTransaction(); try { $stmt $pdo-prepare(UPDATE books SET available_count available_count - 1 WHERE id ? AND available_count 0); $stmt-execute([$bookId]); if ($stmt-rowCount() 0) { throw new Exception(库存不足); } $pdo-prepare(INSERT INTO borrow_records (user_id, book_id) VALUES (?, ?))-execute([$userId, $bookId]); $pdo-commit(); } catch (Exception $e) { $pdo-rollBack(); throw $e; }“轻量级聊天室源码”和“音乐播放器 php”这类项目核心难点都是“服务端如何主动把新消息推给浏览器”。最简单的方案是前端 JavaScript 定时轮询 PHP 接口每秒或每 3 秒拉一次新消息。稍微好一点的是长轮询PHP 端用循环和usleep把请求挂住有新数据才返回。再进阶就是用 WebSocket但 PHP 这块需要引入 Swoole 或者 Workerman 这类常驻内存框架复杂度会明显上升。文件处理能力在真实项目里也经常遇到。“php 图片生产”对应的是 GD 库或 Imagick 做缩略图、水印、验证码“php 视频压缩”通常是 PHP 调用 FFmpeg 命令行工具做转码“excel 批量处理 php”一般用 PhpSpreadsheet 库做导入导出“php 生成 pdf 的数字签名”用 TCPDF 或 FPDI 配合证书做签名。它们的共同思路都是一样的PHP 不直接处理二进制细节而是调用成熟工具库或命令行工具然后做好队列任务和异步处理卡住主流程的现象一定要避免。4. 工程化与安全队列、代码审计与面试进阶4.1 Redis 消费组实现消息队列热词“php redis 消费组”和“php 队列”一起出现可以确定很多 PHP 项目已经走到了需要异步处理的阶段。最典型的场景就是用户注册后要发欢迎邮件、视频上传后要转码压缩、Excel 导入要处理几万行数据。这些活如果都在请求里同步做用户会一直转圈等着响应时间直接飙到几十秒。Redis 5.0 之后提供了 Stream 数据结构它天生就是做消息队列的。和传统 List 方案相比Stream 的消费组支持消息持久化、多消费者竞争消费、消息 ACK 确认还有 pending 列表能查谁没消费完生产可靠性高很多。生产者往队列里发消息?php $redis new Redis(); $redis-connect(127.0.0.1, 6379); $redis-xAdd(queue:order, *, [ user_id 123, order_id 8888, ]);消费者从消费组里取消息并处理?php $redis new Redis(); $redis-connect(127.0.0.1, 6379); $messages $redis-xReadGroup(order_consumer, worker-1, [queue:order ], 1, 5000); foreach ($messages[queue:order] as $id $msg) { // 处理订单消息 // TODO: 业务逻辑 // 处理成功后确认防止消息丢失 $redis-xAck(queue:order, order_consumer, [$id]); }命令参数的顺序很多新手记不住我建议把 XGROUP、XADD、XREADGROUP、XACK 这四条命令当成固定套路背下来先XGROUP CREATE queue:order order_consumer 0创建消费组然后生产者 XADD消费者 XREADGROUP处理完 XACK。注意XREADGROUP 的表示只读新消息不会读历史 pending 消息。消费者处理到一半崩了消息会一直留在 pending 列表里恢复后用 XAUTOCLAIM 重新认领。这种机制保证了“至少一次”投递处理逻辑要做好幂等比如按 order_id 加唯一索引。4.2 代码审计视角别让你的 PHP 变成攻击入口热词里“php 上传漏洞”、“一句话木马 php 文件上传”、“php 伪协议”、“[极客大挑战 2019]php”、“php 代码审计”这一串东西看着很“攻”但站在开发者的角度全部都是必须防的雷区。我见过太多刚入行的同学以为这些是“黑客技能”天天琢磨怎么绕验证、怎么过反爬。我明确说一句这些坑你不需要知道怎么踩但你必须知道别人会怎么踩你。写 PHP 项目的正确姿势是用安全审计的思路反推自己的代码哪里会被打穿。文件上传是重灾区。很多系统只在前端做了文件类型限制后端直接接收文件结果攻击者用抓包工具把文件名改成shell.php、把 Content-Type 改成image/png文件就传上去了。后端必须做三道校验后缀白名单、MIME 类型、文件内容头。而且上传文件要重命名存储比如用uniqid()生成新文件名禁止使用用户提供的原始文件名。上传目录还要禁止脚本执行Nginx 里加一句location ~ \.php$ { deny all; }就能挡掉大部分问题。伪协议也是安全重点。php://filter可以被用来读取源码php://input可以提交原始数据如果代码里有用户可控的include攻击者可以直接构造php://filter/convert.base64-encode/resourceconfig.php把配置文件源码读走。防这个问题的核心就是哲学家的一句话永远不要让用户输入控制文件路径。必须用文件包含功能时做一个白名单映射表用户传的是一个 index代码再根据 index 找到真正允许的文件路径。一句话木马、代码审计这些词只建议往“防守”方向理解。在做代码审计时我会重点检查四类入口检查点风险说明整改建议文件上传会被上传可执行文件白名单校验、重命名存储、目录禁脚本文件包含伪协议、目录穿越白名单映射、关闭 allow_url_include命令执行eval、shell_exec 入口尽量不用必须用则参数白名单反序列化对象注入、RCE不用 unserialize 处理外部数据至于“过 geevisit 验证”这类绕过安全验证的需求我劝一句这类东西往往直接踩在平台规则和法律法规的红线上真出了问题后悔都来不及。技术可以用来做好系统没必要去干这种灰色的活儿。4.3 PHP 面试基础知识以及一个十年老问题热搜词里有一句特别扎眼“干了 php 十年 一个算法都没接触到”。这句话在 PHP 圈子里流传很广也确实戳中了很多人的痛处写 PHP 十年天天 CRUD好像真的碰不到算法。但我要说句公道话。没接触到算法不一定是 PHP 的错很可能是你一直停留在业务脚本层面。数组的键值查找、数据库索引底层结构、分页查询的 offset 深分页问题、大文件导入的内存优化这些全都是数据结构和算法的应用。只是它们被框架封装好了你感觉不到而已。针对“php 面试基础知识”核心考点其实很固定PHP 变量底层结构zval 是怎么存字符串、数组的垃圾回收机制引用计数什么时候失效循环引用怎么处理数组底层PHP 数组本质上是有序哈希表所以它既能当列表又能当字典常驻内存与进程模型传统 PHP-FPM 为什么每次请求结束就销毁所有变量Swoole 为什么能常驻设计模式单例、工厂、观察者这些在框架源码里到处都是网络协议HTTP 状态码、TCP 三次握手、HTTPS 加密过程消息队列、缓存、高并发下的数据库优化如果你准备面试不要只背概念。拿一个真实的业务场景比如秒杀系统把 Redis 预扣库存、消息队列异步下单、数据库最终一致性这几条链路讲清楚比背十条定义都有说服力。关于算法和数据结构我的建议是至少要把数组、链表、哈希表、栈、队列这几种结构的原理吃透再搞懂时间复杂度概念。也不用刻意刷难题先把排序算法、二分查找、二叉树遍历这些基础的搞明白。就算你将来不做算法岗这些知识也能让你在排查性能问题、设计数据存储时多一种思路。5. 常见问题与排查技巧实录5.1 高频问题的快速定位表这套问题排查表是我多年实际维护 PHP 项目时攒下来的每一个都真实踩过。新手遇到同类型问题对着表格查就行。现象常见原因快速处理访问 PHP 返回 502 Bad Gatewayphp-cgi 没启动 / Nginx 连不上 9000 端口检查 php-cgi 进程netstat 看 9000 是否监听接口中文变成 \uXXXXjson_encode 默认转义 Unicode加 JSON_UNESCAPED_UNICODE 参数前端跨域请求报错后端没加 CORS 响应头或 OPTIONS 预检没处理加 Access-Control-Allow-* 头OPTIONS 直接返回 204JSON 反序列化字符串过长报错数据传输时被截断检查 post_max_size 和 memory_limitXdebug 断点不生效Xdebug 端口写错 / mode 不对改成 client_port9003确认 phpinfo 里有 xdebug上传文件后访问报 404上传目录没禁止脚本执行文件被安全软件删了存储改名目录配置 deny 规则Docker 里日志时间差 8 小时PHP 容器默认 UTC 时区php.ini 设置 date.timezoneAsia/ShanghaiRedis 消息队列入队了但不消费消费组没创建 / 消费者未 ACK检查 XGROUP 状态看 pending 列表Windows 下 9000 端口被占用其他程序占用了端口netstat -ano 找到进程改 php-cgi 端口5.2 我在实操中攒下的几条心得第一生产环境 PHP 错误显示必须关。display_errors Off然后log_errors On。这行配置能防止攻击者通过报错信息摸清你的目录结构、数据库表名、框架版本。很多时候不是你的代码防不住是报错信息把底牌全掀了。第二写接口前先和后端同事或者自己定一套响应结构再动手写代码。我就吃过亏接口先返回了[data ...]后来业务要扩展又改成[code 0, data ...]前端换来换去联调效率被白白拖累。第三给一个中间件级别的全局异常处理。PHP 项目规模一大最可怕的不是有异常而是异常散落在各个业务代码里有的打了日志、有的直接吞掉、有的把错误堆栈打印在页面上。统一在入口处 catch 所有 Throwable才是长久之计。第四不管公司用不用 Docker至少自己学会用 Docker 跑一个 PHP 环境。环境迁移、给别人复现 bug、部署上线这一套能力能让你在排查问题时少很多扯皮。再说回安全那条线。写 PHP 代码时请默认所有输入都是恶意的。用户提交的每个字符串、每个文件、每次请求都要先过滤再使用。不是我悲观是线上环境真的什么牛鬼蛇神都有。最后再分享一个小技巧遇到搞不定的 PHP 问题先打开phpinfo()把扩展列表、配置文件路径、关键参数截图保存。排查问题第一步永远是把环境信息拿到手而不是靠猜。这一步能省掉你 80% 的“试一下看看”时间。