尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

PHP工程师综合练习卷:从语法陷阱到高并发与安全实战

PHP工程师综合练习卷:从语法陷阱到高并发与安全实战 其实这套《PHP工程师综合练习卷一》最早是我给自己团队做技术摸底用的我把它当一面镜子想看看组里那些平时写业务写得很溜的同学基础到底扎不扎实。批完卷子之后感触很深很多人简历上写着“精通PHP”可真到了卷子面前语法细节记不清、PDO不会用、安全漏洞看不出来短板藏都藏不住。这套卷子一共100分覆盖了PHP基础语法、面向对象、数据库与缓存、网络接口、安全防护、框架工程化六个大方向题型有单选、多选、判断、简答和编程题。如果你是一个正在准备面试的PHP工程师或者是一个想带新人、想给团队做技术考核的技术负责人这套卷子可以直接拿去用如果你刚学完PHP基础想检验一下自己到底学得怎么样它也能帮你暴露问题。我花了不少时间整理卷面设计和参考答案也把批卷过程中看到的高频错误记录了下来。这篇内容不光是题目清单更重要的是把每一类考点背后的设计逻辑和踩坑经验讲清楚让你知道为什么要考这些以及答题时应该往哪个方向想。1. 卷子整体设计与考核思路在出题之前我先把目标岗位需要的能力拆了一遍能用PHP写业务代码这只是及格线真正拉开差距的是语言底层机制是否清楚、数据库和缓存设计是否合理、安全意识有没有刻进肌肉记忆。所以这套卷子从一开始就不是按“知识点覆盖率”来出题的而是按“线上项目出问题的高发区”来倒推的。1.1 卷面结构满分100分120分钟怎么分配我按题型和考核方向做了下面的安排你可以直接照着这个结构复制成自己的考核模板题型题量分值考核方向建议用时单选题15题30分语法细节、函数用法、语言机制25分钟判断题10题10分弱类型陷阱、数组操作、运算符10分钟多选题5题15分面向对象、安全防护、数据库优化15分钟简答分析题3题15分设计思路、排错思路、方案选型25分钟编程题3题30分PDO封装、图片抓取替换、业务逻辑45分钟120分钟的时长是卡着时间设计的。前40分钟解决基础题中间20分钟做完简答最后40分钟集中写代码剩下20分钟检查。如果前面的选择题超过30分钟还没做完我就基本能判断这个人对基础语法不够熟——线上排查问题时不允许你翻着文档想半天函数名。难度配比上我控制为基础40%、进阶45%、综合15%。基础题保证认真复习过的人能拿分进阶题用来筛掉只会写简单CRUD的人综合题则让真正有项目经验的人有机会展示算法和业务建模能力。你如果自己刷这套题我建议按这个节奏计时不要做到哪儿算哪儿。1.2 出题思路这三个梯度到底在筛选什么先说基础题的筛选目标我默认“会用PHP”和“理解PHP”是两回事。比如我在单选题里特意放了0 abc这类弱类型比较又放了array_merge和运算符对字符串键名处理方式的区别。这些内容业务代码里天天会遇到但很多人从来没有停下来想过背后的规则。进阶题重点考察数据访问和接口安全。像PDO的预处理到底防的是哪种注入CORS预检请求在什么情况下会触发Redis队列消费失败后怎么保证不丢消息这些都是真实项目里每时每刻都在面对的问题。综合题其实是整张卷子的灵魂。我安排了图书管理系统的借还逻辑、HTML图片抓取替换、以及一个带核销码的扫码核销场景。这三道题都不需要背语法但要求你拿出工程思维考虑事务、幂等、异常分支和并发冲突。说实话见过太多候选人前面的题全对一到编程题就暴露真实水平因为写代码这件事装是装不出来的。2. 基础语法核心考点与常见陷阱很多工程师都有个误区觉得面向对象、设计模式才是高级内容语法细节无所谓。但我在实际项目里排查过太多诡异问题最后定位到的问题根源就是运算符优先级或者引用传值。所以这套卷子第一二大题专门用来检验这些最容易被忽视的地基。2.1 弱类型与运算符一不留神就翻车的判断题判断题我给了一道非常经典的题var_dump(0 abc)的结果是true还是false如果你只背过“PHP是弱类型语言”很容易凭直觉答true。实际上从PHP 8.0开始字符串和数字的比较规则已经改掉了——非数字字符串和0比较时不再是“字符串转成0”而是“数字转成字符串再比较”所以结果是false。但如果你线上还在跑PHP 5或PHP 7的老代码结果就是true。这就是为什么每当我看到项目还用着老版本都会提醒团队注意这种跨版本的隐蔽差异。另一道高频判断题是$a $b和array_merge($a, $b)的区别。前者是“数组的并集”字符串键名冲突时左边的值保留数字键名会重新编号后者是“合并”字符串键名冲突时右边的值覆盖左边。我让不少工程师口头解释过这个区别能一次说清楚的人不超过三成。你在刷这卷子的时候如果这道题犹豫了说明你写数组合并的时候大概率是靠猜的。除了这些我还考了isset和empty的边界差异、运算符在前缀后缀的返回值、list和each这种老函数的配合方式、static关键字在函数内的变量生命周期。说实话这些都很基础但没有一个考点是脱离实际业务场景的。举个最简单的例子排查一个“接口偶尔返回null”的bug时定位到最后发现是某个数组键名写错了而isset在键不存在时不会报错只会返回false——这种坑只有真被咬过才会长记性。2.2 数组、字符串与作用域工程里每天都要写的基础操作字符串操作我也是重点考的。PHP提供了substr、mb_substr、str_replace、preg_replace这一整套函数每个都有特定的使用场景。比如你在处理用户提交的姓名如果直接用substr去截取遇到中文多半会截出半个汉字必须用mb_substr并指定utf-8编码。再比如判断字符串是否包含某个子串老手会推荐str_containsPHP 8或者strpos()返回值要! false判断很多人记不住这个细节直接写if (strpos($str, a))当子串出现在第0位时就会漏判。字符串知识里我加了一个关于PHP序列化和json_encode的对比题。两者的区别在于序列化结果是PHP专有格式可以完整还原对象类型和私有属性JSON是通用格式跨语言互操作时必须用但它会丢掉对象的具体类信息。这道题的背景是缓存设计和接口对接——往Redis里存数据到底用哪种格式取决于你是只给自己用还是要给别人跨语言访问。我还考了mb_*函数族对编码的处理逻辑因为常见的乱码问题和邮件标题解析都离不开字符编码。数组和引用的结合点我也出了一道题。foreach遍历数组时用$value修改值需要在循环结束后unset($value)否则后续代码里再使用$value会意外修改数组的最后一个元素。这个坑我至少帮同事排查过三次线上bug每次都是因为变量污染。作用域相关的global和$GLOBALS也考了重点让答题者区分两者本质global是导入引用$GLOBALS是超全局数组直接访问原始变量改法不同效果也不同。3. 面向对象、工程化与本地部署选择题和判断题只占55分剩下45分都在考你能不能把代码组织得像一个工程。太多人写PHP是“脚本思维”一个文件从数据库连接写到前端输出改一个需求要满文件找代码。所以这一大块我重点考面向对象基础和工具链工程化能力。3.1 面向对象与设计模式不是背概念是看你会不会用面向对象的题目不考设计模式的定义而是考理解。比如我给了两段代码一段在类里直接new另一个具体类另一段通过构造函数注入接口。问你哪段更利于单元测试和扩展。这道题的答案显然是注入接口的写法——它的核心价值在于测试时可以传入Mock对象替换真实依赖不需要真的连接数据库或者调用外部服务。类相关的魔术方法也是高频考点。__get和__set是在访问不可访问属性时被自动调用的__call是在调用不存在的方法时触发__toString则决定了对象能不能直接被echo。我遇到过一个很实际的案例某个对象在日志里总是显示成“Object id #34”后来才知道是因为没实现__toString排查半天。这些魔术方法不是炫技它们在框架底层无处不在。单例模式我也考了而且特别指出线程安全和静态属性。很多人的答案只写了private static $instance加if (self::$instance null)但没有考虑new self()和new static()的区别也就是晚期静态绑定。在继承场景下new static()会实例化调用者子类new self()始终实例化当前定义类。一个单例如果写成new self()在子类中调用就会返回父类实例导致状态错乱。3.2 Composer、PSR规范与ThinkPHP框架工程化的第一题是Composer的autoload机制。我让答题者解释PSR-4是怎么把命名空间映射到目录的。核心就一句话App\Controllers\UserController对应app/Controllers/UserController.php命名空间前缀和目录路径一致类名对应文件名全小写目录通常是为了兼容Linux服务器大小写敏感的文件系统。很多人装完依赖会用vendor/autoload.php但从不关心背后的加载规则等到自己写包或者改命名空间时就会一脸懵。ThinkPHP框架在热词里出现的频率很高所以我专门出了一道关于控制器、模型、验证器分层的题。我给的场景是用户注册接口控制器只负责接收参数和返回结果模型层负责数据持久化验证器负责校验服务层负责业务逻辑组装。为什么这么分因为如果把SQL直接写在控制器里下一个需求变更时整个文件都会变成意大利面条。老版本的ThinkPHP升级到新版本时我提醒团队要特别注意路由配置、模型命名空间和数据库配置的变化这些是项目迁移时最容易踩的坑。3.3 Docker部署PHP环境把开发环境变成团队标准关于Docker部署我出的是一道方案设计题。要求用docker-compose跑起一个PHPMySQLRedis的环境。参考配置大概是这样的version: 3.8 services: php: image: php:8.2-fpm volumes: - ./app:/var/www/html depends_on: - mysql - redis nginx: image: nginx:1.25 ports: - 8080:80 volumes: - ./app:/var/www/html - ./nginx/conf.d:/etc/nginx/conf.d depends_on: - php mysql: image: mysql:8.0 environment: MYSQL_ROOT_PASSWORD: root MYSQL_DATABASE: demo volumes: - db_data:/var/lib/mysql redis: image: redis:7 volumes: db_data:很多人看这段配置觉得很简单但实际操作时坑不少。第一PHP容器要自己装扩展比如pdo_mysql和redis否则代码一运行就报Class PDO not found。第二MySQL 8默认认证插件是caching_sha2_password老版本PHP的pdo_mysql不一定支持需要在环境变量或初始化脚本里处理。第三明文密码放配置文件里仅供本机开发线上环境必须用环境变量或密钥管理这是铁律。部署完成后还有个容易被忽略的步骤验证整个链路是否连通。我的方法是先进入PHP容器执行php -m确认扩展再在宿主机上用curl测试Nginx转发到FPM是否正常最后写一段最简单的数据库连接脚本确认应用层能连上MySQL。这套验证流程走完了环境才算真正可用。4. 数据库、缓存与队列高并发项目的基本功数据库题目我放在了编程题之前的简答区因为这部分最能看出一个人是否有真实项目经验。网上随便搜一篇PHP面试题考的都是SQL语法但真正拉开差距的是能不能讲清楚为什么用预处理、为什么索引会失效、队列消费失败怎么处理。4.1 手写PDO访问封装满分答案长什么样编程题第一题我让答题者写一个基于PDO的数据库访问封装类。这题看起来简单但下面几个细节做对的人很少开启异常模式、关闭本地模拟预处理、设置默认抓取模式为关联数组。我期望的类大致长这样final class Database { private static ?Database $instance null; private PDO $pdo; private function __construct(array $config) { $dsn sprintf( mysql:host%s;port%s;dbname%s;charset%s, $config[host], $config[port], $config[dbname], $config[charset] ?? utf8mb4 ); $this-pdo new PDO($dsn, $config[username], $config[password], [ PDO::ATTR_ERRMODE PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE PDO::FETCH_ASSOC, PDO::ATTR_EMULATE_PREPARES false, ]); } public static function getInstance(array $config): self { if (self::$instance null) { self::$instance new self($config); } return self::$instance; } public function pdo(): PDO { return $this-pdo; } }几个参数我单独解释一下。ATTR_ERRMODE设为ERRMODE_EXCEPTION后SQL出错时会抛PDOException而不是返回一个错误码让你挨个判断ATTR_EMULATE_PREPARES设为false是让MySQL服务端真正做预处理而不是在客户端模拟拼接这一步是防SQL注入的关键ATTR_DEFAULT_FETCH_MODE设为FETCH_ASSOC保证查询结果默认是关联数组写起来比混合数组舒服得多。有一部分人把单例构造函数写成public还有人把pdo()方法写成了getConnection()这些都不算错但我在工程上更偏好统一的命名规范方便团队协作。最怕的是有人在类里用mysqli实现一遍——不是说不能用而是既然卷子写了“PDO封装”就要按题目要求来审题本身就是考核项。4.2 SQL与索引为什么加了索引查询还是慢数据库简答题我出了三个具体场景。第一个场景一个订单表有几百万数据按status字段查询很慢加了索引还是慢问为什么。答案是区分度太低status如果只有两三个枚举值索引扫描会返回大量数据优化器可能直接选择全表扫描。解决办法是改成联合索引比如(status, created_at)让筛选在时间维度上进一步收敛。第二个场景是一个慢查询SQL在WHERE条件里对create_time做了DATE()函数运算导致索引失效。我见过太多人踩这个坑——写WHERE DATE(create_time) 2024-01-01表面上看没问题但函数处理了索引列之后B树就没法走范围查询了。正确写法是WHERE create_time 2024-01-01 00:00:00 AND create_time 2024-01-02 00:00:00。第三个场景是分页性能。当页数足够大时LIMIT 1000000, 10会导致MySQL扫描并丢弃前面的一百万行。常见优化是延迟关联先查主键列表再关联回原表取数据或者记下上一页的最大ID做游标分页。这也是为什么现在很多中后台列表不做传统页码而是用“加载更多”的游标模式。4.3 缓存穿透处理与Redis队列消费模型缓存相关的题我考了缓存穿透、击穿和雪崩的区别。穿透是指查询一个根本不存在的数据每次都会打到底层数据库击穿是指热点key过期瞬间大量请求同时打到数据库雪崩是指大量key同一时间过期导致数据库压力骤增。三者的解决方案完全不同穿透要加空值缓存或布隆过滤器击穿可以考虑互斥锁或者热点key不设过期时间雪崩的处理方式是把过期时间加随机抖动。队列题目我设计了消息推送场景要求答题者用Redis实现一个简单的任务队列。参考答案是生产者用LPUSH写入任务消费者用BRPOP阻塞读取这里的关键点是BRPOP会阻塞等待消息而不是空转轮询消耗CPU$redis new Redis(); $redis-connect(127.0.0.1, 6379); while (true) { $task $redis-brpop(task_queue, 5); if (!$task) { continue; } try { // 处理任务 echo 处理任务: {$task[1]}\n; } catch (Throwable $e) { // 记录失败日志必要时进入重试队列 $redis-lpush(task_queue_failed, $task[1]); } }很多人在答题时只写了LPUSH和RPOP忽略了消息处理失败的问题。一旦任务执行异常消息已经被弹出就相当于丢数据了。所以我在参考答案里特意要求把失败任务重新入队或者记录到失败队列后续有专门的补偿任务去重试。消息幂等性也要考虑——消费者端用一个唯一任务ID判断是否已经处理过避免重复操作造成脏数据。5. 接口、跨域与安全线上项目的高频考点PHP项目大多要对外提供接口所以网络协议和安全防护也是这套卷子的重头戏。很多前端工程师会误以为PHP只能处理后端业务逻辑但实际上面向接口的细节非常多包括签名校验、跨域资源共享、JSONP、支付回调、防注入。这些知识点线上项目里每天都要用到。5.1 接口设计与签名校验不止是返回JSON简答题里有一个接口对接场景A系统要调用B系统提供的PHP接口需要一个防篡改的签名机制。我期望的标准答案是把所有请求参数除了签名本身按字典序排序拼接成字符串后加上密钥做HMAC-MD5或HMAC-SHA256生成签名服务端用同样的算法重新计算并比较。过程中还要加入timestamp参数防止重放攻击超过5分钟的请求直接拒绝。这道题考的不只是签名算法还有对安全边界的理解。有人只说了用MD5加密这在技术上是不完整的——MD5必须配合密钥使用否则攻击者可以自己生成任意参数的签名。有人说了加盐但没有说明盐应该怎么存。更关键的一点是签名校验的返回值应该统一为JSON结构并且明确错误码这样对接方才能快速定位是参数问题还是签名问题。接口设计还有一个容易被忽略的点幂等性。支付、发货、核销这类关键操作必须保证用户重试时不会造成重复扣款或重复发货。常见做法是客户端生成一个requestId服务端在Redis里用SETNX做去重已处理过的请求直接返回上次的结果。我在编程题里专门让答题者处理这个场景后面会详细讲。5.2 CORS与JSONP跨域问题的两种解法跨域相关的热词里JSONP和CORS都是高频出现的关键词。JSONP的原理是动态创建script标签利用浏览器允许跨域加载JavaScript文件的特性把数据包装成回调函数的参数返回。它有个明显的限制——只支持GET请求而且callback参数如果不可信很容易造成XSS注入。所以实际项目里除非是在对接老系统否则我不推荐JSONP。CORS是更规范的做法靠服务端返回响应头来控制Access-Control-Allow-Origin指定允许的源Access-Control-Allow-Methods指定允许的方法。很多人以为只要在PHP代码里加上这几个Header就完事了忽略了预检请求preflight的概念。当请求方法是PUT、DELETE或者Content-Type是application/json时浏览器会先发一个OPTIONS请求来探测服务端是否允许处理不好就出现“前端明明发了请求后端却说没有收到”的诡异现象。正确姿势是在PHP框架的路由层统一处理OPTIONS请求直接返回204。还有一个我特别想强调的点CORS和CSRF是两码事。就算服务端允许某个域名跨域访问如果应用自身没有做CSRF Token校验攻击者照样可以在其他站点通过提交表单的方式发起恶意请求。所以安全设计上跨域配置和CSRF防护要同时考虑不能因为“已经限制了跨域”就掉以轻心。5.3 注入、XSS、上传与SSRF安全题怎么答才拿分安全部分的题目我重点考了文件上传和图片抓取这两个真实场景。文件上传的经典漏洞是攻击者上传一个shell.php再用Web访问直接执行命令。很多人的防护只做了后端检查$_FILES[file][type]或后缀名但这两者都可以被伪造完全不可信。正确做法是用finfo_file()检测文件真实MIME类型再用getimagesize()验证图片尺寸把文件落到非Web目录或者使用随机文件名并禁止执行权限。图片抓取替换功能在热词里也有出现。这个功能的需求是从外部HTML中提取图片地址下载到本地替换成自己的域名。很多人的第一版代码如下$content file_get_contents($url); file_put_contents($localPath, $content);很明显的问题是file_get_contents可以读取任意URL如果传入的是file:///etc/passwd这种本地路径整个服务器文件都能被读走。这种漏洞叫SSRF服务端请求伪造在真实项目中会导致内网探测。我在参考答案中要求域名必须做白名单校验只允许抓取指定的站外域名限制协议只能为http或https同时还要过滤内网IP地址段防止攻击者构造http://127.0.0.1之类的内网地址。SQL注入的简答题我要求不仅说出什么是注入还要写出修复方案。最标准的答案就是PDO预处理加参数绑定配合关闭ATTR_EMULATE_PREPARES。在安全知识结构里我还顺带考察了PHP伪协议的风险比如php://filter常用于读取源码phar://可能触发反序列化漏洞。应对思路很明确凡是用户可控的路径都禁止直接传给文件函数或include语句必须层层校验和过滤。另外关于“后量子加密”这个热点目前PHP生态里落地成熟的扩展还不多建议先从OpenSSL扩展的现有算法入手跟进平时保持关注就好。6. 综合实战题解三道题暴露真实水平编程题我出了三道分值30分。这三道题不考偏题怪题全部来自我经历过的真实业务需求。每次我批到有人能把这三题写得干净利落基本上就能判断这个人可以直接上手项目不用太担心埋雷。6.1 实战一图书管理系统的借还与逾期计算第一题是给一个图书管理系统写后端接口核心需求就四个字借书、还书。但我在判断条件里加了一个隐藏考点——同一本书已经被借出时必须返回明确错误还书时要根据应还日期计算是否逾期。借书接口的代码要点是事务和行锁。第一步用SELECT ... FOR UPDATE锁定图书记录第二步判断状态是否为“可借”第三步插入借阅记录第四步更新图书状态为“已借出”。这四个步骤必须放在同一个数据库事务里否则并发时两个人同时借同一本书就会出现超借。我在参考答案里特意写了PDO::beginTransaction()加try/catch任何一步失败都rollBack()。逾期费用的计算比例是每天0.1元很多人直接date_diff算天数后乘单价但没有考虑“同一天还书不算逾期”的边界。还书接口同样要校验借阅记录是否存在防止重复还书导致金额被累加两次。这道题总分10分能完整处理事务和边界条件的人基本能拿到7分以上。6.2 实战二HTML内容图片抓取与替换第二题是处理一个富文本编辑器提交的内容把里面所有远程图片抓取到本地然后把src替换成本地地址。这个需求在内容管理系统中太常见了尤其是发布文章时用户直接从别的网站复制粘贴带图的文本图片还是挂在别人服务器上随时可能失效。参考答案大致如下function replaceRemoteImages(string $html, array $domainWhiteList []): string { $pattern /img\s[^]*src[\]([^\])[\][^]*/i; return preg_replace_callback($pattern, function ($matches) use ($domainWhiteList) { $url $matches[1]; // 跳过已经是本地地址的图片 if (str_starts_with($url, /) || str_starts_with($url, data:)) { return $matches[0]; } $host parse_url($url, PHP_URL_HOST); if ($domainWhiteList !in_array($host, $domainWhiteList)) { return $matches[0]; } $content file_get_contents($url); if ($content false) { return $matches[0]; } $ext pathinfo(parse_url($url, PHP_URL_PATH), PATHINFO_EXTENSION); $ext in_array($ext, [jpg, jpeg, png, gif, webp]) ? $ext : jpg; $filename date(Ymd) . / . uniqid() . . . $ext; $localPath /data/uploads/ . $filename; if (file_put_contents($localPath, $content) ! false) { return str_replace($url, /static/uploads/ . $filename, $matches[0]); } return $matches[0]; }, $html); }如果是在真实生产中我建议不要用正则解析HTML优先考虑用DOMDocument和DOMXPath对标签结构的处理更可靠。另外还要考虑懒加载图片src在>
返回列表