ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

PHP工程师二星级自测:从字符串截取到安全防护的全面避坑指南

PHP工程师二星级自测:从字符串截取到安全防护的全面避坑指南 有次跟同行聊招人他说了句让我印象很深的话来面试的都说自己会PHP但一上手综合题就露馅。不是不会写是写得太想当然。substr切中文切出乱码、和分不清、PDO预处理以为加个占位符就万事大吉……这些恰恰是“二星级”该考的东西。这份《PHP工程师综合练习卷(二星级)》就是这么来的。它的定位很明确不考冷门函数不考背概念专挑日常开发中天天碰、但大多数人没有细想过的知识点。一星级考“知不知道”二星级考“熟不熟”三星级才考“设计得好不好”。适合谁做写过一两年业务代码、感觉自己什么都会、但一被追问就含糊的PHP工程师也适合准备面试、想系统查漏补缺的人。我自己的习惯是先别翻文档拿编辑器把答案敲一遍错了再反推原因。这套卷子不是让你刷完就扔的而是每道题背后都藏着一次线上事故或一次代码评审的教训。1. 先说清楚二星级到底在考什么1.1 练习卷的定位从“会不会”到“熟不熟”很多刚入行的朋友有个误区觉得PHP嘛语法简单写几个页面就是会了。但真到项目里麻烦的从来不是语法而是那些“看起来能用、实际上经不起追问”的地方。二星级练习卷出的题全部是这种类型日常在用但你可能没搞懂内部原理。比如字符串截取谁不会用substr但遇上中文、Emoji、多字节编码它就会给你颜色看。再比如错误处理try...catch谁都会写但E_WARNING和E_ERROR的差异、日志该怎么配、线上为什么不能开display_errors很多人答不上来。我判断一个工程师处在什么水平不看他会多少框架、写过多少接口就看他对这些基础知识的“边界感”。知道一个函数擅长什么更知道它在什么情况下会出问题这才叫熟。1.2 出题范围与自测方法整套练习卷覆盖六个方向基础语法与字符串处理、面向对象设计、数据库访问与封装、错误处理与安全、框架与工程化、部署与调试。每个方向都是从真实项目里抽出来的高频问题再套上“题目 解析 避坑经验”的格式。自测的时候别急着看答案。我建议你准备一个空目录起一个临时PHP文件把题目当小需求写一遍。写完再对照解析错了就反推是我不知道这个语法还是我知道但理解错了这两种错法对应的补课方式完全不同。这套卷子不是用来背的是用来暴露盲区的。你错得越多说明这次自测越有价值。2. 字符串、数组和运算符最容易答错的基础题2.1 substr 的这一排坑你踩过几个先看题目echo substr(HelloWorld, 0, 5); // 输出 echo substr(HelloWorld, -5); // 输出 echo substr(你好世界, 0, 3); // 输出前两题没什么悬念Hello和World。第三题开始分人了——在没开启mbstring的情况下substr(你好世界, 0, 3)不是输出前三个汉字而是输出一个乱码字符加上半个汉字。原因很简单substr是按字节切的一个UTF-8汉字占3个字节切3个字节刚好把一个汉字劈成两半。这种问题最常见的出现场景是截取商品标题、生成文章摘要、导出Excel时的单元格内容。线上环境只要标题里混入中文和英文截取位置就会变得不可控。凡是涉及多字节字符的截取一律用mb_substrecho mb_substr(你好世界, 0, 3, UTF-8); // 你好世第四个参数指定编码别省略。你没法保证运行环境的默认编码设置符合预期显式指定才是可靠做法。这套“显式指定”的思路在后边的PDO配置里还会用到。2.2 和 、strpos 的经典陷阱这道题可以说伴随了每个PHP工程师的成长$str abcdef; var_dump(strpos($str, a)); // 输出什么 var_dump(strpos($str, a) false); // 输出什么 var_dump(strpos($str, a) false); // 输出什么strpos($str, a)返回的是子串首次出现的位置也就是0。于是第一个var_dump输出int(0)第二个var_dump输出true——因为0 false在PHP里是成立的。这就是经典的“字符串在开头就判定找不到”的bug。正确姿势是永远用!if (strpos($str, a) ! false) { // 存在 }而且要注意PHP 8对字符串和数字的比较做了修正。PHP 7时代abc 0会返回truePHP 8之后只有数字开头的字符串才会做数值比较abc 0直接变成false。这意味着你从老项目升级到PHP 8凡是依赖旧比较结果的地方都要重新过一遍。2.3 数组函数高频“工具人”盘点PHP的数组函数有一百多个二星级不要求全背但有几个高频的必须熟练。array_column是从二维数组里取一列的利器。做下拉选项、列表展示、按ID匹配数据时一行代码就能搞定不需要自己写循环$users [ [id 1, name 张三], [id 2, name 李四], ]; $names array_column($users, name, id); // 输出 [1 张三, 2 李四]第三个参数把某个字段当键这在按ID快速查找时特别实用。array_map和array_filter则让“遍历处理过滤”不用再写三遍foreach。比如批量格式化时间、过滤掉空值$data [2024-01-01, , 2024-03-15, null]; $filtered array_filter($data, fn($v) !empty($v));至于网上流传的“PHP语言经典程序100题”我的看法是不推荐逐题硬刷但值得把涉及数组和字符串的那部分过一遍。它的价值不在背题而在于让你看到同一个函数在不同边界条件下的行为差异——这恰恰是二星级考察的核心。3. 面向对象从“会写类”到“会设计类”3.1 自定义方法的第一步可见性与单一职责很多新手的类是这样的一个index()方法里写了两百行从查数据库到拼HTML全干了。问他为什么不分方法他说“也能跑啊”。二星级要求你能答出为什么不行。核心原因有两个一是一个方法里塞太多职责任何一个环节出错你都得通读整个方法才能定位二是没法复用比如另一个页面需要同样的查询逻辑你只能复制粘贴改一处忘另一处。正确的做法是把“自定义方法”当公共接口来设计class UserService { private $db; public function __construct(PDO $db) { $this-db $db; } public function findById(int $id): ?array { $stmt $this-db-prepare(SELECT * FROM users WHERE id ?); $stmt-execute([$id]); $user $stmt-fetch(PDO::FETCH_ASSOC); return $user ?: null; } public function create(array $data): int { $stmt $this-db-prepare(INSERT INTO users (name, email) VALUES (?, ?)); $stmt-execute([$data[name], $data[email]]); return (int)$this-db-lastInsertId(); } }可见性也不是随便写的。private用于内部辅助方法protected留给子类扩展public才是对外暴露的接口。如果你把所有方法都设成public相当于把类的内部结构全亮给别人后续改内部实现会因为外部依赖太多而寸步难行。3.2 self、static、parent晚期静态绑定题这是面试高频题也是二星级练习卷里最值得反复做的一道。class A { public static function test() { return self::getName(); } protected static function getName() { return A; } } class B extends A { protected static function getName() { return B; } } echo A::test(); // 输出 A echo B::test(); // 输出什么答案是A。原因在于self在编译时就绑定了定义方法的那个类也就是A。不管你是通过B::test()调用还是通过A::test()调用self::getName()永远指向A::getName()。如果你想要“调用时动态决定”用static关键字这叫晚期静态绑定class A { public static function test() { return static::getName(); } // ... } echo B::test(); // 输出 B两种写法没有绝对对错关键是明确需求工厂方法、单例模式、ORM这类需要子类扩展的场景几乎都是static而普通工具类的内部调用self更可控。3.3 接口与对象ArrayAccess 与数据形态转换题目如何让一个对象像数组一样通过$obj[key]访问属性答案是实现ArrayAccess接口。这个接口规定了四个方法offsetExists、offsetGet、offsetSet、offsetUnset。这种设计把“读取规则”封装在类内部外部调用方不需要关心数据是存数组还是存数据库。class User implements ArrayAccess { private $data []; public function offsetExists($offset): bool { return isset($this-data[$offset]); } public function offsetGet($offset): mixed { return $this-data[$offset] ?? null; } public function offsetSet($offset, $value): void { $this-data[$offset] $value; } public function offsetUnset($offset): void { unset($this-data[$offset]); } }接口在PHP里的意义就是“契约”你定义好规则所有实现方都必须遵守。比如支付接口不管对接微信还是支付宝对外暴露的方法都叫pay()、refund()调用方不用关心底层实现。数据形态转换也是二星级的常考点。对象转数组用get_object_vars($obj)数组转对象用(object)$arr。但我在实际项目里更推荐内部模块之间传对象对外API统一返回数组。原因很现实——数组在PHP里天生就是“可序列化、可JSON化、可数组函数处理”的万能格式而对象还得考虑序列化时要不要带上私有属性。明确边界能让代码好维护很多。4. 数据库访问PDO封装到你手上的时候4.1 PDO 和 mysqli 怎么选老生常谈但很多人其实不清楚为什么团队规范里写“一律用PDO”。对比一下维度PDOmysqli支持数据库MySQL、PostgreSQL、SQLite、SQL Server等仅MySQL预处理统一API写法一致面向过程和面向对象两套写法异常处理可设置ERRMODE_EXCEPTION需要手动检查命名参数支持:name只支持?占位符如果你的项目未来可能换数据库——哪怕只是从MySQL换到MariaDB——PDO都能把迁移成本压到最低。而且PDO的异常模式是真的好用$pdo new PDO($dsn, $user, $pass, [ PDO::ATTR_ERRMODE PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE PDO::FETCH_ASSOC, PDO::ATTR_EMULATE_PREPARES false, ]);第四个参数里ATTR_EMULATE_PREPARES false是重点后面细说。4.2 预处理为什么能防注入很多人以为PDO预处理就是把SQL写漂亮点这是误解。预处理的原理分两步第一步把SQL模板发给数据库服务器服务器完成解析、编译——这时候SQL结构已经定了不可能再被改变第二步把参数值单独发给服务器让服务器用这些值去填充模板。参数值再“狡猾”也只是数据不会成为SQL结构的一部分。$stmt $pdo-prepare(SELECT * FROM users WHERE email ?); $stmt-execute([$_POST[email]]);如果手动拼SQL$sql SELECT * FROM users WHERE email {$_POST[email]};当输入 OR 11时拼接出来的SQL会变成WHERE email OR 11整表数据全查出来了。这就是最基础的SQL注入。但注意PDO::ATTR_EMULATE_PREPARES默认是true这时候PDO在本地模拟预处理实际上是先把SQL语句和参数拼接在一起再发给MySQL安全性就打了折扣。生产环境强烈建议设为false让MySQL自己做真正的预处理。4.3 从图书管理系统到题库保存真实CRUD中的数据组织练习卷里有一类经典题目“用PHP写一个图书管理系统”。很多人一听就头疼其实它考的就是CRUD的数据组织。图书管理系统核心表一般就三张图书表、分类表、借阅记录表。图书表存id、title、category_id、author、stock借阅记录表存id、book_id、user_id、borrow_at、return_at。列表页要显示图书和分类名时一条LEFT JOIN就能解决。另一个类似的题目是“保存题库信息”。这里值得讨论的是一道选择题的选项怎么存方案A是把选项存成JSON字段$question [ title PHP中哪个函数用于数组排序, options [A: sort, B: explode, C: implode, D: substr], answer A, ];方案B是建关联表question表和question_option表一对多。我的判断标准很简单如果选项内容固定、不会拿选项去做SQL筛选和统计JSON字段最省事如果后续要做“哪些题选A的多”“按选项标签筛选题目”这类分析就必须拆关联表。二星级要求的是能说清楚“为什么这么选”而不是照搬某个方案。分页查询也是CRUD里的硬核技能。标准写法是先把查询条件组装成数组再拼SQL$conditions []; $params []; if (!empty($_GET[keyword])) { $conditions[] title LIKE ?; $params[] % . $_GET[keyword] . %; } $where $conditions ? WHERE . implode( AND , $conditions) : ; $countSql SELECT COUNT(*) FROM books $where; // 然后执行 count 和 limit 查询注意条件数组和参数数组必须同步维护顺序不能乱。我在实际项目里见过不少因为这里顺序错位导致数据对不上的事故。5. 错误处理与安全二星级和初级的真正分水岭5.1 错误处理不是 try catch 一句的事入门时写PHP报错了就看页面上的红字。但到了二星级你必须理解PHP的错误体系E_ERROR是致命错误脚本直接终止E_WARNING是警告脚本继续跑但结果可能是错的E_NOTICE是提示很多是未定义变量这类问题E_DEPRECATED是弃用提示老代码升级时全是这玩意儿。线上环境的第一条铁律是关闭display_errors开启log_errors。ini_set(display_errors, 0); ini_set(log_errors, 1); ini_set(error_log, /var/log/php_errors.log);把错误直接暴露给用户等于把服务器路径、数据库结构、框架版本信息全送出去了。正确做法是让错误进日志用户看到的只是一个友好提示页。除了系统错误业务逻辑的异常要用try...catch接住但别什么都吞掉。我见过有人catch (Exception $e)之后什么都不干问题出在哪全被藏起来了。至少应该error_log($e-getMessage())或者接入集中日志系统。ThinkPHP老项目里经常出现的“页面错误!请稍后再试”就是典型的框架级错误页。这种提示对用户友好但对排查问题很不友好——你得去runtime/log目录翻当天的日志找到具体的异常堆栈才能定位是SQL写错了还是某个类没加载。5.2 伪协议与文件包含LFI漏洞的入口在CTF题里很常见但真实项目里同样会踩。文件包含漏洞LFI的核心是用户可以控制include的文件路径导致服务器去读取或执行预期之外的文件。PHP伪协议是这类攻击的放大器// 读取源码输出base64编码后的内容 php://filter/readconvert.base64-encode/resourceindex.php // 把POST内容当作代码执行 php://input // 直接用data协议构造代码 data://text/plain,?php phpinfo();?举个例子。如果你的代码里写了$page $_GET[page]; include $page . .php;攻击者传入php://filter/readconvert.base64-encode/resourceconfig就能把数据库配置文件的内容读走传入php://input再用POST提交一段PHP代码就能直接在服务器上执行命令。防御方式对include的文件名做白名单校验绝对不要直接用用户输入拼接路径。这也是为什么框架里会提供路由机制把“访问哪个控制器方法”限定在可控范围内。5.3 反序列化漏洞为什么 __wakeup 会成为突破口PHP的serialize和unserialize把对象变成字符串、再变回对象本身没毛病。但危险在于unserialize反序列化时会自动触发类里的魔术方法。最常被利用的是__wakeup()和__destruct()。__wakeup在反序列化时自动调用__destruct在对象销毁时自动调用。如果这些方法里有敏感操作——比如写文件、执行SQL、调用某个命令——攻击者构造出精心设计的序列化字符串就能在服务器上触发这些操作。CTF里的“[极客大挑战 2019]php”考的就是这个通过反序列化触发某个类的__wakeup或__destruct实现文件读取或者命令执行。题目里还会配合过滤flag关键字要求你绕过字符检测。防御思路有三个层次不要反序列化不可信数据。如果只是传数据用json_encode/json_decode就够了别用serialize/unserialize。反序列化时限制允许的类PHP 8提供了unserialize($data, [allowed_classes false])可以禁止实例化任何对象。对类里的魔术方法保持警惕不要在里面写危险操作。顺带提一句“PHP序列化中文”这个问题serialize会把中文转成\u形式这属于正常输出不影响反序列化结果。真正容易出问题的是序列化字符串里的类名在被重命名、移动命名空间后反序列化失败——这属于版本迭代时要考虑的兼容性问题不是bug。6. 框架、调试与环境本地写得好线上跑不了等于零6.1 ThinkPHP3.2.3 为什么到现在还有人讨论先说结论ThinkPHP 3.2.3是2015年前后的框架版本新的项目我强烈不建议用它。但现实是存量项目比新项目多得多很多公司到现在还在维护着基于TP3.2.3的老系统。这类老代码的典型问题是I()函数全局获取输入、SQL语句直接拼接、页面报错直接暴露框架版本和路径。你在工单里看到“页面错误!请稍后再试”十有八九就是这个版本。从那类项目里能学到什么不是学它的写法而是学它的教训框架版本要跟着官方安全公告走不要因为“能跑”就一直不升级。新一代框架比如ThinkPHP 6、Laravel都已经把路由、中间件、依赖注入做成了标配代码结构清晰很多。如果你还要用PHP做后台管理系统搭配Layui这类前端框架后端用ThinkPHP或Laravel提供API体验比老一套好太多。6.2 用 PHPStorm 跑通项目的正确姿势很多初学者在PHPStorm里打开项目直接点运行结果发现页面出不来就开始怀疑IDE坏了。其实PHPStorm只是一个编辑器它要“运行”PHP项目必须知道三件事PHP解释器在哪、项目根目录是哪、用内置服务器还是外部服务器。先配置CLI解释器Settings - PHP - CLI Interpreter选择本机的php.exe或/usr/bin/php。如果你的环境是Docker也可以直接在Docker标签页里选择容器中的PHPPHPStorm会自动从容器里拉取配置这一步就解决了“本地PHP版本和线上不一致”的问题。要快速调试接口不用非得配置完整的Nginx用PHPStorm自带的内置服务器就够了Run - Run index.php或者直接在命令行php -S localhost:8080 -t public-t指定Web根目录。这种方式适合写小接口、测逻辑。要测Xdebug断点就需要配置Xdebug扩展和PHPStorm的Server映射这一步稍微繁琐但一旦配好调试时的效率提升是质的飞跃。顺带提一句NetBeans。现在用的人不多了但它在很早期就提供了很好的PHP支持而且是免费开源的。如果你实在不想为IDE花钱NetBeans也是一个不差的备选。6.3 nginx php-fpm 与 Docker 打包环境问题的根治方案“在我电脑上是好的”这句话本质是环境不一致。二星级工程师应该对环境部署有清晰的认知。nginx本身不认识PHP它把请求通过fastcgi_pass转发给php-fpm处理。核心配置长这样server { listen 80; server_name example.com; root /var/www/html/public; index index.php index.html; location ~ \.php$ { fastcgi_pass 127.0.0.1:9000; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; include fastcgi_params; } }fastcgi_pass可以指向127.0.0.1:9000TCP方式也可以指向unix:/run/php/php8.2-fpm.sockSocket方式。Socket方式少一层网络开销同一台机器上性能更好但权限配置麻烦一些。最常见的坑是权限问题php-fpm进程默认以www-data用户运行如果代码目录是root所有就会报403或无法写入日志。解决方法是把项目目录所有者改成www-data或者调整php-fpm的user配置。与其每次在新机器上折腾一遍环境不如直接用Docker把环境固化下来。一个最简的PHP项目打包FROM php:8.2-fpm RUN docker-php-ext-install pdo_mysql COPY . /var/www/html WORKDIR /var/www/html然后配合docker-compose.yml把nginx、php、mysql、redis四个容器编排起来services: php: build: . volumes: - .:/var/www/html nginx: image: nginx:alpine ports: - 8080:80 volumes: - .:/var/www/html - ./nginx.conf:/etc/nginx/conf.d/default.conf:ro这样一套配置提交到仓库任何人拉下来跑一条docker-compose up -d环境和线上就完全一致了。现在很多公司内部用1Panel这类开源面板来管理PHP、MySQL、Redis环境本质上是把容器和面板管理结合起来对中小团队来说是个省心选项。7. 扩展视野队列、跨域与支付回调7.1 队列从 Redis list 到消息队列的演进凡是涉及“异步处理”的需求最终都会走到队列。PHP业务里最常见的场景是用户下单后要发通知、写日志、更新统计如果全在请求里同步执行接口响应会越来越慢。最简单的队列用Redis就能做生产者往列表左边塞数据消费者从右边阻塞取数据。// 生产者 $redis-lpush(task:email, json_encode([to userexample.com, subject 订单通知])); // 消费者命令行跑long-running进程 while (true) { $task $redis-brpop(task:email, 0); // 处理邮件发送 }brpop的第二个参数是超时时间0表示一直阻塞等待。这套方案简单直接适合任务量不大、不需要消息确认和重试的场景。但如果你的系统需要“消息被处理失败后重试”“多个消费者分担负载”“延迟执行”就要上RabbitMQ这类专业的消息队列了。它们的核心优势是完整的消息确认机制消费者处理完一条消息后必须显式ack否则消息会被重新投递。这套机制保证了消息不丢。二星级不需要你精通RabbitMQ的每个细节但至少要知道队列解决的是“请求与处理解耦”的问题选型时先看消息量和可靠性要求再看运维成本。7.2 JSONP 与 CORS跨域问题的两条路线前端页面在a.com后端接口在b.com浏览器默认会拦截跨域请求。二星级必须掌握两条主流解法。JSONP是历史最悠久的方案核心原理是script标签不受同源策略限制。后端返回的不是JSON而是一段JavaScript调用$callback $_GET[callback]; $data json_encode([status ok]); echo $callback . ( . $data . );;前端用script srchttp://b.com/api?callbackhandleResult加载handleResult函数就能拿到数据。隐患是JSONP只能支持GET请求而且callback参数如果不做白名单校验可能被注入恶意代码。更现代、更推荐的方案是CORS。直接在后端响应头里声明允许跨域header(Access-Control-Allow-Origin: https://a.com); header(Access-Control-Allow-Methods: GET, POST, PUT, DELETE); header(Access-Control-Allow-Headers: Content-Type, Authorization);需要注意如果接口需要携带CookieAccess-Control-Allow-Origin不能是*必须明确指定来源域名否则浏览器会拒绝带上凭证。这是配置CORS时最容易踩的坑。7.3 回调接口的幂等性微信支付通知里的门道支付回调是每个写PHP接口的人迟早要碰的东西。微信支付成功后会异步通知你的服务器但通知可能重复发送——可能是网络超时重试可能是微信服务端策略。所以回调接口的第一原则是幂等同样的通知发多少次结果都一致。一个标准的处理流程是接收通知数据验签。微信支付会用API密钥对参数做MD5或HMAC-SHA256签名你要用同样的规则重新计算一遍比对一致才继续处理。根据商户订单号查数据库判断这个订单是否已经处理过。如果已处理直接返回成功不再重复发货。更新订单状态时用带条件的UPDATE语句比如UPDATE orders SET status paid WHERE order_no ? AND status unpaid影响行数为0说明已经被处理过了。按微信要求的格式返回响应xmlreturn_code![CDATA[SUCCESS]]/return_code/xml。不管业务处理成不成功签名验证失败也要返回明确的错误码。我见过不少新人在这个接口上栽跟头要么忘了验签结果任何人都能伪造通知要么没做幂等通知重复两次货就发了两遍。这两条是支付回调的底线。还有一类需求是“接收邮箱里的支付消息”有些老系统没有接入支付回调而是靠定时去邮箱或支付平台拉取账单。实现思路是用IMAP协议连接邮箱PHP的imap_*函数可以读取收件箱、解析邮件内容再从中提取支付信息。这种方案属于“能用但很脆弱”能用回调还是优先回调。最后再分享一个小技巧每次做完这张卷子我都会把错题整理成一份自己的“踩坑清单”过两个月再回头做一遍。你会发现第二次错的题目往往和第一次不一样——第一次是不知道第二次可能是知道但没记住。这说明基础不是看一遍就牢固的是需要反复“锤打”的。如果想检验自己是不是真搞懂了有个笨但有效的办法把每道错题讲给旁边的人听讲到对方点头为止。讲解的过程会逼你把“好像知道”变成“确切知道”。PHP的上限不低但下限也真的不高——把基础打瓷实是性价比最高的事情。
返回列表