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

资讯详情

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

PHP综合练习卷:从基础语法、PDO封装到反序列化与安全部署

PHP综合练习卷:从基础语法、PDO封装到反序列化与安全部署 说实话我一直觉得PHP是最容易被低估的一门语言。最近在给团队整理一份PHP工程师综合练习卷刷题的过程中发现很多写了三五年业务代码的人一碰到综合练习就露馅——不是不会写功能而是基础边界不牢底层逻辑含糊。这份练习卷覆盖了基础语法、面向对象、数据访问、Web安全、环境部署几个大方向正好对应了PHP工程师从初级到中级最核心的能力线。这篇文章我会把练习卷里的典型题目和实际项目中对应的坑位一起拆开讲适合正在准备面试的PHP开发者也适合那些用PHP做过几个项目、想系统性查漏补缺的工程师。不是单纯对答案而是把每道题背后的“为什么”说透。1. 基础语法与字符串函数substr、运算符和序列化三类高频失分点综合练习卷的卷面往往不会上来就整复杂的架构题而是先用一批基础题试水。可恰恰是这批题失分率最高。字符串函数、运算符、序列化这三类基本占据了基础部分的大半江山我挑三个最典型的题型展开。1.1 substr函数从0开始的习惯最容易被边界测试打脸练习卷里关于substr的经典题通常长这样给定一个字符串让你截取中间几位、末位几位或者判断截取失败时返回什么。看起来简单真正动手写的时候边界条件能让你怀疑人生。$str Hello, PHP Engineer; echo substr($str, 7, 3); // 输出 PHP echo substr($str, -8); // 输出 Engineer负数从尾部倒数 echo substr($str, 0, -9); // 输出 Hello, PHP负数长度表示剔除尾部第一个坑是起始位置。很多从其他语言转过来的同学以为substr的起始位置是“1开始”但PHP里永远是从0开始。第二个坑是负值参数。start为负时从字符串尾部向前数length为负时表示“去除末尾N个字符”这两个方向搞反了结果就差十万八千里。第三个坑在返回类型上如果起始位置超出字符串长度substr不会返回空串在旧版本里会返回falsePHP 8里则返回空字符串。生产环境中如果你直接拿返回值做字符串拼接false被隐式转换成空串还算运气好碰上严格类型声明就直接TypeError了。比这更常见的是中文截取问题。UTF-8编码下一个汉字占三个字节直接substr可能把一个汉字的字节序列截断输出乱码。处理多字节字符串应该用mb_substr$name 你好PHP工程师; echo mb_substr($name, 0, 4, UTF-8); // 正常输出你好PHP这道题对应的实际场景是文章摘要截取、列表页标题截断。我建议在项目里封装一个统一函数内部走mb_substr同时处理超长省略号避免每个业务方自己各写一套导致风格混乱。1.2 运算符PHP 8前后比较规则变化是新的失分点运算符题里比较运算符是重灾区。最典型的一道var_dump(0 abc); // PHP 7 输出 truePHP 8 输出 false var_dump(1 01); // 输出 true字符串被转成数字比较 var_dump(1 01); // 输出 false严格比较类型和值很多人记住了一句口诀“0 任意非数字字符串都是true”这在PHP 7里确实成立原因是字符串与数字比较时非数字字符串被强制转换成0。但PHP 8改了这条规则——只有数字开头的字符串才会被转换为数字纯字母字符串不会再被转成0。这个变化直接导致一批老代码在升级时行为漂移练习卷里放这道题本质上是考察你有没有跟踪语言版本变化的习惯。再有一个必考点是运算符优先级。下面这个表达式不少人会算错$a true; $b false; $c $a || $b ? 1 : 0; // 结果是多少这里||的优先级低于?:所以实际执行的是$a || ($b ? 1 : 0)结果是true。如果想让||先执行必须加括号($a || $b) ? 1 : 0。在代码评审中我见过不少因为这种优先级问题出现的逻辑漏洞尤其在做权限判断时少一对括号可能导致整个条件判断与预期相反。我的建议很简单不要依赖记忆里的优先级凡是有混合运算符的表达式一律用括号把执行顺序明确写出来。这在练习卷里是得分点在代码评审里是可维护性。1.3 序列化与反序列化格式、中文和对象注入序列化题通常会给你一个数组或对象让你写出serialize之后的字符串再问反序列化的结果。先看一个数组$data [name PHP, version 8.3]; echo serialize($data); // a:2:{s:4:name;s:3:PHP;s:7:version;d:8.3;}格式本身不复杂a代表数组冒号后面是元素个数s代表字符串d代表浮点数。中文序列化的表现值得一提字符串长度是按字节数统计的比如工程师在UTF-8下是9个字节序列化里会写成s:9:工程师;但实际显示还是原样的中文。对象序列化之后会包含类名和属性列表反序列化时会触发魔术方法比如__wakeup()和__destruct()。练习卷里那道反序列化题目往往就是构造一个恶意对象让__destruct()在脚本结束时执行某个危险动作。这个原理放在后面安全题型里细说这里先记住一个结论永远不要对用户可控的输入直接调用unserialize。如果只是持久化数据优先用json_encode和json_decode如果必须序列化对象要做好类白名单校验或者用allowed_classes参数限制反序列化的类范围。$obj unserialize($userInput, [allowed_classes false]); // 强制转成 __PHP_Incomplete_Class2. 面向对象与数据访问从类方法调用到PDO封装的一次串讲面向对象的题在综合练习卷里不会只考语法它会把类设计、数据访问、接口返回风格串在一起。这里头最经典的一组题就是“类中自定义方法并调用”“PDO封装类”和“接口返回数组对象”。2.1 类中自定义方法并调用访问控制、静态方法和依赖传递练习卷里这类题通常给你一个半成品类让你补全方法并完成调用。核心考点有三个。第一个是访问修饰符。public、protected、private的区别要分清楚。外部调用只能走public方法protected是给子类用的private是本类专属。实际项目中常见的问题是把所有方法都写成public结果类的内部实现细节暴露无遗后续重构时牵一发而动全身。正确的做法是对外只暴露必要的方法内部辅助方法一律private或protected。第二个是静态方法的正确姿势。static方法用::调用不需要实例化。但要注意静态方法里不能直接使用$this因为它不属于某个具体对象。练习卷里有个经典坑在静态方法里调用实例方法直接报错Using $this when not in object context。第三个是依赖传递。一个类A的方法内部需要用到类B的能力最原始的做法是在方法里new B()但这样A和B就硬绑定了测试时很难替换。练习卷的加分答案是构造函数注入或者用一个简单的容器把依赖传进来class OrderService { private Notification $notifier; public function __construct(Notification $notifier) { $this-notifier $notifier; } public function createOrder(array $data): void { // 业务逻辑 $this-notifier-send(订单创建成功); } }这段代码好在哪里OrderService只依赖Notification这个接口约定至于发邮件、发短信还是发站内信调用方决定。练习卷里设计类时只要你能写出这样的依赖方向得分就不会低。2.2 PDO访问封装类为什么安全性和易用性可以兼得数据库访问在练习卷里几乎必考因为它是PHP项目的命脉。很多老项目用mysqli拼SQL跑起来没问题但一旦出现用户输入拼接SQL注入风险就摆在那里。更合理的做法是用PDO的预处理机制。下面是一个最小可用封装类class Database { private PDO $pdo; public function __construct(string $dsn, string $user, string $pass) { $this-pdo new PDO($dsn, $user, $pass); $this-pdo-setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); $this-pdo-setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC); $this-pdo-setAttribute(PDO::ATTR_EMULATE_PREPARES, false); } public function query(string $sql, array $params []): array { $stmt $this-pdo-prepare($sql); $stmt-execute($params); return $stmt-fetchAll(); } }三个setAttribute值得单独解释。ATTR_ERRMODE设为异常模式SQL报错时直接抛PDOException方便上层统一处理而不是返回false让你到处判断。ATTR_DEFAULT_FETCH_MODE设为FETCH_ASSOC查询结果默认是关联数组省得每次fetch时都传参数。ATTR_EMULATE_PREPARES设为false让MySQL使用真正的预处理协议而不是PHP端模拟这个选项在防注入上更彻底。练习卷喜欢在这个封装类基础上问你为什么用预处理能防注入原因是预处理的SQL模板和参数是分开发送的参数不会被拼接进SQL语法结构数据库自身会把它当作纯数据值处理。所以即使参数里写了 OR 11 --也只会被当成一个普通字符串不会改变SQL语义。2.3 接口返回数组还是对象一个影响前后端协作的细节这道题表面问的是“数组还是对象”实际考察的是你对PHP类型系统、JSON序列化、前后端协作模式三个层面的理解。先看结论PHP接口返回数据绝大多数场景应该返回数组再用json_encode($data)输出前端收到的是JSON对象。但练习卷会追一个细节接口里某个字段是空数组序列化之后前端可能收到[]也可能是{}。这是因为PHP的空数组被json_encode输出成[]而前端想要“对象”结构时就得让PHP返回(object)[]。这一坑在联调时非常容易出现。$a []; echo json_encode($a); // [] $b (object)[]; echo json_encode($b); // {}另一个相关考点是类型转换。(array)$obj可以把对象转数组(object)$arr可以把数组转对象。但要注意当对象转数组时私有属性会带上类名前缀的key如果不了解这个规则看到奇怪key会摸不着头脑。接口层判断要不要转对象建议规则是如果这个字段是“键值对字典”用对象如果是一组同构数据用数组。这样前端拿到的JSON语义更清楚也能避免[]与{}的歧义。3. 文件包含、伪协议、错误处理与反序列化漏洞综合卷里的安全题型安全题在PHP工程师综合练习卷里地位特殊因为它直接关系到线上系统能不能活下来。伪协议、错误处理、反序列化漏洞是我觉得最值得写进练习卷的三类题目。3.1 PHP伪协议练习从源码读取场景看过滤和限制PHP伪协议是文件操作和包含操作里的特殊流封装器。最常见的两个是php://filter和php://input。练习卷里会先放一个看似正常的业务代码$page $_GET[page]; include($page . .php);这个代码的意图是动态加载页面但用户传入pagephp://filter/readconvert.base64-encode/resourceconfig就能通过报错信息或页面输出读到config.php的源码base64编码后展示。这类题目在CTF里极其常见练习卷把它放进来是想让开发者理解凡是用户可控制的路径一旦进入文件函数都可能被滥用。防御思路要成体系不能靠单个函数。第一层是路径白名单只允许文件名匹配预先定义好的集合。第二层是配置层的收敛把allow_url_include设为Off关闭远程文件包含能力同时设置open_basedir限制PHP只能访问指定目录。第三层是函数禁用在disable_functions里把不用的危险函数禁用掉。这三层一起上才能有效降低风险。php://input这个伪协议也是练习卷常客。它可以读取请求体原始内容在接收POST的JSON数据时会用到$json file_get_contents(php://input); $data json_decode($json, true);这个用法本身是安全的但它容易和上面的不安全包含混在一起。练习卷的考点就是分辨读请求体用php://input没问题将请求体内容直接当成PHP代码去include才是问题。3.2 错误处理配置开发环境与生产环境完全不同的思路练习卷里关于错误处理的题经常是这样的场景线上页面突然弹出Fatal error: Uncaught Error: Call to undefined function连带把服务器绝对路径、PHP版本全部暴露出来。生产环境还开着display_errors这是很多事故的根源。正确的做法是区分环境; 开发环境 display_errors On error_reporting E_ALL log_errors On ; 生产环境 display_errors Off error_reporting E_ALL log_errors On error_log /var/log/php_errors.log开发环境要显示错误是因为需要快速定位生产环境关闭显示是因为任何报错信息都可能泄露内部细节同时会把不友好的HTML输出给用户。练习卷还会追问不显示错误了线上出问题怎么排查答案就是日志。所以log_errors必须打开并且要把日志路径配置到独立磁盘或专门的日志服务上防止日志把系统盘塞满。再进一步PHP还提供了自定义错误和异常处理函数。练习卷喜欢让你写一个简单的set_exception_handler回调把未捕获异常记录到日志同时输出一个统一的JSON错误响应。这在API项目里几乎是标配set_exception_handler(function (Throwable $e) { error_log($e-getMessage() . in . $e-getFile() . : . $e-getLine()); http_response_code(500); header(Content-Type: application/json); echo json_encode([code 500, msg 服务器开小差了]); });3.3 结合“[极客大挑战 2019]php”再看反序列化利用的防御重点[极客大挑战 2019]php 是一道经典的PHP反序列化CTF题综合练习卷里经常拿它当引申阅读。这道题的特点是先通过备份文件拿到源码再在源码里找到使用了unserialize的入口最终通过构造恶意对象触发危险方法链。它想说明的核心问题不是“CTF怎么打”而是正常情况下业务代码里为什么会存在反序列化入口大多数情况下是缓存、Session、RPC协议传递对象数据时选择了PHP原生的serialize格式。当序列化数据可以被用户控制时攻击者就能通过精心构造的字符串让__wakeup()、__destruct()等魔术方法在预期之外执行甚至从单个方法逐步扩大成整个应用被控制。练习卷在防御侧会要求你列出至少三条缓解措施。我会这样答第一所有接入层数据的反序列化一律使用allowed_classes白名单或干脆改用JSON格式阻断类实例注入。第二对魔术方法做审计尤其是__wakeup、__destruct、__toString里存在文件操作、命令执行、SQL拼接的类必须重点排查。第三在PHP配置中启用open_basedir和必要的disable_functions即使反序列化攻击成功也会因为文件系统和函数范围受限而难以扩大战果。这道题放在综合练习卷里根本目的是帮工程师建立一条安全底线反序列化不是不能用但要清楚它的危险边界在哪里并且控制好不可信数据与它之间的距离。4. 从练习卷到真实工程部署、经典项目和面试题的串联综合练习卷的最后一关考察的是项目落地和工程化能力。环境部署、经典项目、高频面试题三者本质上是同一件事的不同侧面。4.1 PHP环境部署LNMP、1Panel和Docker三条路线的取舍部署类题目问得最多的就是环境配置。传统的LNMP路线是Linux Nginx MySQL PHP-FPM练习卷会让你写一段Nginx配置把PHP请求转发给PHP-FPMserver { listen 80; server_name example.com; root /var/www/html; location / { index index.php index.html; try_files $uri $uri/ /index.php?$query_string; } location ~ \.php$ { include fastcgi_params; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; fastcgi_pass 127.0.0.1:9000; } }这段配置里最关键的是fastcgi_param SCRIPT_FILENAME这一行如果配错PHP文件内容会以纯文本形式返回浏览器一片源码。try_files这行的作用是把不存在的URI重写到入口文件这也是大部分PHP框架伪静态的基础。现在的部署方式演进得很快。自己用Linux命令一步步搭环境适合练基础日常开发和团队协作我推荐直接用1Panel这样的开源管理面板做离线部署PHP、MySQL、Redis都能可视化安装和启停对中小团队非常友好。再往前走一步就是Docker化。练习卷如果涉及Docker通常是一道简单的镜像打包题FROM php:8.3-fpm RUN docker-php-ext-install pdo_mysql mysqli COPY . /var/www/html WORKDIR /var/www/htmlDockerfile写起来不复杂难点在于理解镜像分层和容器编排。镜像里的PHP扩展如果因为缺少系统依赖装不上需要在RUN里先安装依赖库。这个坑在练习卷里不太会直接考但实际部署时非常常见提前有认知能省很多时间。4.2 图书管理系统与题库系统经典项目里最容易被追问的设计细节图书管理系统是PHP练习卷里的“常青树”。题目看似老套但面试官追问起来很细用户借书和还书怎么避免并发冲突一本书被两个人同时借走怎么办搜索功能怎么处理模糊匹配核心是表设计。读者表、图书表、借阅记录表三张表是基础。借书时不能只做INSERT还要先对图书表执行一条带条件的UPDATE把“可借”状态改成“已借出”并且用affected_rows判断是否成功$sql UPDATE books SET status 1 WHERE id ? AND status 0; $stmt $pdo-prepare($sql); $stmt-execute([$bookId]); if ($stmt-rowCount() 1) { // 借书成功写借阅记录 } else { // 说明书已经被借走 }这种原子操作可以避免两个请求同时通过查询确认“可借”然后都插入记录的问题。题库系统同样经典练习卷里“保存题库信息代码”这道题核心就是一个题目表的设计题目内容、选项JSON字段、答案、解析、题型、难度、分类ID。选项用JSON字段存比单独建一张选项表更简单也够用。批量导入题库时用INSERT ... ON DUPLICATE KEY UPDATE做幂等处理比逐条判断再插入效率高得多。4.3 队列、JSONP跨域和支付回调面试题背后的工程思维练习卷最后往往有一组“场景题”考察的是工程思维不是死记硬背。这里说三个高频的。第一个是PHP队列。面试题会问你的接口处理很慢怎么优化一个标准回答是引入消息队列把耗时任务异步化。PHP里最轻量的队列就是Redis List左侧生产、右侧消费// 生产者 $redis-lpush(task_queue, json_encode([type email, to userexample.com])); // 消费者脚本 while ($task $redis-rpop(task_queue)) { $data json_decode($task, true); // 发送邮件 }这题的得分点不是Redis函数用得多熟而是你能说清楚队列解决了什么削峰填谷、解耦、异步化。进一步还能延伸到失败重试和死信队列这就说明你有生产环境经验。第二个是跨域和JSONP。前端要跨域请求PHP接口JSONP是老方案原理是script标签不受同源策略限制PHP端把数据包装成回调函数调用$data [code 0, data [name PHP]]; $callback $_GET[callback] ?? ; echo $callback . ( . json_encode($data) . );;JSONP有个缺点它只支持GET请求而且callback参数如果不对服务端做正则校验可能被注入任意内容。现在主流方案是CORSPHP端设置响应头允许跨域即可。练习卷会追问两者区别知道JSONP的历史局限和CORS的现代做法就能答出层次。第三个是支付回调。微信支付、支付宝支付成功后会异步通知你的服务器。PHP接收通知时要做验签、校验订单金额、用transaction_id做幂等判断再把订单状态改成已支付。验签的逻辑因支付平台而异但核心思维是一致的不能信任任何外部的原始请求必须通过签名和订单状态校验之后才能更新业务数据。美团核销、小程序发货录入这类业务底层也是回调验签加幂等更新的套路一通百通。我个人在实际操作里有个体会这份练习卷刷完之后最有价值的不是记住哪道题的正确答案而是把零散的知识点串成了一条能力线。基础语法是表达能力面向对象和数据访问是骨架和血液安全是底线部署和工程思维才是把项目真正跑起来的保障。如果你也想自测建议给自己定一个目标动手把这套题里的代码都敲一遍再选一个类似图书管理系统的项目重写一版最后用Docker把环境复现出来。这个过程走完你会明显感觉到日常写代码的时候心里更有底。
返回列表