PHP二星级试卷全解析:覆盖PDO、数组、序列化等高频考点
前阵子群里有人甩出一份《PHP工程师综合练习卷(二星级)》问我二星级到底算个什么水平。我说能独立部署PHP环境、会写类、能用PDO操作数据库、能讲清楚错误处理和几类常见安全风险基本就够线了。但话又说回来简单不等于容易这套题恰好是很多“会用PHP但没系统学过PHP”的人最容易翻车的地方。它卡的位置不是零基础也不是架构师而是那种写过业务、但基本功还欠火候的初级到中级工程师。我最近把这套卷子从头到尾刷了一遍又翻了翻大家都在搜的PHP相关问题发现它几乎把日常开发里的高频考点全串起来了数组和字符串函数、魔术方法、序列化、PDO封装、错误级别、文件包含伪协议、跨域、队列、部署样样都有。下面我按自己的刷题顺序把每类题的出题意图、标准答案和背后的原理一次说透顺带附上我在真实项目里踩过的坑。这篇文章适合准备PHP面试的开发者也适合写过一段时间业务代码、想系统补一遍基础的同行。1. 试卷设计思路与考核重心1.1 二星级难度到底卡在哪网上各种练习卷的难度分级其实挺乱的有的写着“初级”却让你手写一个框架有的挂着“高级”结果还在考变量作用域。这份二星级卷子给我的感觉是定位很准一星级是能写Hello World三星级是能读懂流行框架源码而二星级正好卡在中间——能独立完成带数据库的小项目但一碰到类型转换、字符串边界、异常、安全这些细节就露馅。试卷里经常出现类似“$a 10; $b 5; echo $a $b;”这种看似送分的题。答案确实是15但面试官真想听的是你能不能解释清楚“PHP在做算术运算时把字符串转成了整型这个过程不会报错”这个底层机制。能讲明白这种题的人才算真正理解PHP弱类型的本质。二星级卡的也就是这个点它不要求你背手册但要求你不能只停留在“能跑就行”的层面。我把试卷和各类PHP技术文章对照后也自己画了个难度对照表方便你判断自己现在站在哪个阶段难度典型能力实际映射一星语法、变量、循环能写脚本处理小任务二星数组字符串、类、PDO、错误处理、安全基础能独立开发业务系统三星框架源码、Composer生态、设计模式、性能优化能设计应用架构1.2 核心考点与真实场景对照从题目分布来看这份卷子覆盖面其实高度贴近工作场景不是考纲里拍脑袋列出来的。基础语法和函数题对应你日常写业务逻辑时要处理表单数据、解析接口返回、生成报表面向对象题对应项目里封装Service、Model、工具类避免一个文件堆几千行数据库操作题对应所有需要存取数据的模块几乎没法回避错误与安全题对应线上环境偶发告警、接口被刷、文件包含被利用这类事故最后的工程化与部署题则对应团队协作、上线流程和容器化改造。这些考点和搜索引擎里的高频问题基本是重合的。你去搜“PHP面试题”“PHP错误处理”“PHP类”“PHP经典程序100题”“PHP源码”搜出来的内容目录差不多就是这份卷子的章节列表。所以刷题之前先别急着看答案花十分钟对照一下这个考点表标记出自己哪个板块是空白后面针对性补比从头到尾瞎刷效率高得多。1.3 正确刷题方式先写再查最忌讳的就是一上来翻答案看一遍觉得自己“会了”实际写的时候照样卡壳。我的建议是拿到题目后新建一个php文件不看任何资料先写一遍能跑通多少算多少写不出来的标个题号然后带着问题去查标准答案最后把卡壳的题重新默写一遍。一个周末基本能过完一轮印象绝对比直接看答案深刻。另外卷子里提到的每个函数都建议去官方手册翻一遍。不用全背但至少要清楚它的参数顺序、返回值和副作用。因为老同事和面试官都特别喜欢在“函数名看着眼熟参数顺序记反”这种细节上挖坑。比如strpos三个参数的顺序和in_array的参数顺序完全不一样写多了真的会混。二星级这关就是把这种基础肌肉记忆练出来。2. 基础语法与数据操作类题目剖析2.1 类型转换与isset/empty/is_null三兄弟这块是PHP面试的保留项目。先看一个经典组合题$a null; $b ; $c 0; $d 0; var_dump(empty($a), empty($b), empty($c), empty($d)); var_dump(isset($a), isset($b), isset($c), isset($d)); var_dump(is_null($a), is_null($b), is_null($c), is_null($d));我直接说答案。empty判断的是“空”null、空字符串、0、字符串0在PHP里都被视为空所以第一行全是bool(true)。isset只有在变量存在且不为null时才返回true$a是null所以是false后面三个都是true。is_null则只认null$a是true$b、$c、$d都是false。实际开发里那个典型的坑是后端用empty($_POST[name])判断前端有没有传姓名结果前端传了个合法值“0”直接被当空处理数据丢了。所以判断不同场景要选不同函数比如判断字符串是否传参应该用trim($value) ! 而不是empty。这几个函数不是随机挑一个用得结合业务类型来选。类型转换的题更阴:var_dump(10 5); // int(15) var_dump(true 1); // int(2) var_dump(abc 1); // int(1)PHP是弱类型语言算术运算会自动把两边转成数字。abc转换成数字时从头解析解析不到数字就得到0所以结果是1。很多新手以为这里会报错其实完全不会。这种“宽容”恰恰是线上诡异bug的来源之一。如果你要严格校验用户输入别依赖隐式转换老老实实先用is_numeric或filter_var判断。2.2 substr与中文处理PHP序列化中文的隐藏坑字符串题也差不多是必考的尤其是substr函数用法$str Hello, PHP; echo substr($str, 0, 5); // 输出 Hello echo substr($str, -3); // 输出 PHP echo substr($str, 7); // 输出 PHPsubstr的三个参数分别是“源字符串、起始位置、截取长度”起始位置从0开始负数表示从末尾倒数长度可以省略省略就截到结尾。这个函数本身不难难的是它的隐藏坑按字节截取。PHP里中文字符在UTF-8编码下通常占3个字节用substr截中文会直接截出半个字输出乱码。我之前做个导出功能字段是用户昵称超过10个字符要加省略号图省事直接substr上线后一堆乱码反馈。换成mb_substr($nickname, 0, 10, utf-8)才正常。所以处理多字节内容时记得用mb_系列函数。再看一个更隐蔽的“PHP序列化中文”问题。serialize一个包含中文的数组时中文会原样保留在结果里但serialize后字符串长度是按字节算的$data [name 张三]; $s serialize($data); echo $s; // a:1:{s:4:name;s:6:张三;}这里“张三”两个字在UTF-8下占6个字节所以长度标注是6。如果拿这个序列化串做缓存键、队列消息或签名必须保证两边编码一致否则长度对不上会反序列化失败。我见过有人把serialize结果直接存Redis另一台机器上脚本文件编码不同读取后反序列化直接报错。稳妥做法是序列化后再base64_encode一次既避免存储时的特殊字符问题也能杜绝编码带来的差异。2.3 PHP经典程序100题里的“送分题”其实不送分网上流传很广的“PHP经典程序100题”里面冒泡排序、斐波那契这类题目出现频率最高二星级卷子几乎必有一道。以冒泡排序为例标准写法是这样function bubbleSort(array $arr) { $len count($arr); for ($i 0; $i $len - 1; $i) { for ($j 0; $j $len - 1 - $i; $j) { if ($arr[$j] $arr[$j 1]) { [$arr[$j], $arr[$j 1]] [$arr[$j 1], $arr[$j]]; } } } return $arr; }题目本身不难难的是很多人边写边忘外层循环控制比较轮数内层循环控制每一轮的比较次数每一轮结束后最大的数会沉到最后所以内层循环的上界要不断减$i。如果交换时不引入临时变量很容易写出数据覆盖的bug。PHP里用[$a, $b] [$b, $a]这种方括号语法交换最省事也最不容易错。斐波那契也常考但很多人没想到考察点是“递归性能”。直接写return f($n-1)f($n-2)的递归输入到40就会明显变慢因为重复计算太多了。面试官想看到的进阶写法是迭代或带缓存function fib($n) { if ($n 1) { return $n; } $a 0; $b 1; for ($i 2; $i $n; $i) { [$a, $b] [$b, $a $b]; } return $b; }这些所谓的经典题本质是在考逻辑思维和边界条件处理背标准答案没有意义。把每一行的目的都搞清楚才算真的会。3. 面向对象与核心流程题拆解3.1 PHP类定义、属性和自定义方法的调用细节二星级卷子一定会有一道“编写一个类并调用其方法”。我一般建议按下面这种写法回答因为它能把知识点都覆盖到class OrderService { private $pdo; private $status pending; public function __construct(PDO $pdo) { $this-pdo $pdo; } public function setStatus(string $status): void { if (!in_array($status, [pending, paid, closed], true)) { throw new InvalidArgumentException(非法状态); } $this-status $status; } public function getStatus(): string { return $this-status; } public static function createDefault(): self { return new self(new PDO(sqlite::memory:)); } } $service OrderService::createDefault(); $service-setStatus(paid); echo $service-getStatus(); // 输出 paid这道题的考察点非常集中public/private/protected可见性、构造方法注入依赖、参数类型约束PDO $pdo、string $status、static关键字、self和$this的区别。实际项目里很多人图省事所有属性方法全写成public结果后面想收口权限时到处散落的调用根本改不动。自定义方法的调用也有不少细节。非静态方法要先实例化再通过对象调用静态方法用类名::方法名()比如OrderService::createDefault()。有人习惯用$this-method()去调静态方法PHP某几个版本能兼容但不推荐方向混了很容易踩坑。还有一点容易被忽略PHP方法名不区分大小写但变量名区分所以show()和SHOW()能调到同一个方法但变量$a和$A是两个变量。这类语言特性面试官偶尔会拿出来当陷阱题。3.2 魔术方法与序列化陷阱PHP的魔术方法几乎是必考项二星级不会让你背手册而是给你一个类问你访问不存在的属性或方法时会发生什么。看这个例子class User { private $data []; public function __get($name) { return $this-data[$name] ?? null; } public function __set($name, $value) { $this-data[$name] $value; } public function __toString() { return json_encode($this-data, JSON_UNESCAPED_UNICODE); } } $user new User(); $user-name 小明; echo $user-name; // 输出 小明 echo $user; // 输出 {name:小明}这里$user-name属性并不存在但__set和__get会把操作接住数据落到$data数组里。echo对象时则触发__toString否则PHP会报“Object of class User could not be converted to string”的致命错误。这种设计在框架的Model类里极其常见ThinkPHP早期的Model属性访问就是类似实现。序列化相关的坑更大。serialize对象时如果类里定义了__sleepPHP只序列化它返回数组里列出的属性返回空数组序列化结果里就没有对象属性。unserialize时触发__wakeup可以用于反序列化后重新初始化资源比如重新打开数据库连接、重新设置时区。但这里有个严重的安全问题如果unserialize的数据来自用户输入配合代码里已有的类方法POP链、文件包含和伪协议就可能形成远程代码执行漏洞这也是CTF比赛里“极客大挑战2019 PHP”这类题目的思路。二星级不要求你写攻击链但你必须明白永远不要反序列化不可信数据。3.3 接口返回数组、对象与JsonSerializable现在接口开发最常用的返回格式是JSON很多人的习惯是拼一个数组然后json_encode。但二星级题会专门考“PHP接口数组对象”这个点接口返回的数据到底是数组还是对象关键在json_decode的第二个参数$json {name:张三,age:20}; $obj json_decode($json); // 默认得到 stdClass 对象 $arr json_decode($json, true); // 传入 true 得到数组 echo $obj-name; // 张三 echo $arr[name]; // 张三第二参数为true时得到数组默认得到stdClass对象。写代码前必须先说清自己用的是哪种否则后面会报一堆“Cannot use object of type stdClass as array”或“Attempt to read property on array”的错误。如果想让对象在json_encode时输出自定义结构可以实现JsonSerializable接口class ApiResponse implements JsonSerializable { private $code; private $message; private $data; public function __construct($code, $message, $data) { $this-code $code; $this-message $message; $this-data $data; } public function jsonSerialize(): array { return [ code $this-code, message $this-message, data $this-data, ]; } } echo json_encode(new ApiResponse(0, ok, [id 1]), JSON_UNESCAPED_UNICODE); // {code:0,message:ok,data:{id:1}}这种写法比到处拼数组规范很多尤其在多人协作时能强制统一接口返回结构前端对接也舒服得多。4. 数据库操作与PDO封装题4.1 PDO连接MySQL的完整配置数据库题是二星级卷子的重头几乎必考“怎么用PDO连数据库”。还在用mysqli的老项目不少但面试官基本都会建议换PDO因为它同时支持多种数据库、预处理更顺手。一个完整可靠的连接写法$host 127.0.0.1; $port 3306; $dbname test; $username root; $password password; $dsn mysql:host$host;port$port;dbname$dbname;charsetutf8mb4; $options [ PDO::ATTR_ERRMODE PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE PDO::FETCH_ASSOC, PDO::ATTR_EMULATE_PREPARES false, ]; try { $pdo new PDO($dsn, $username, $password, $options); } catch (PDOException $e) { // 生产环境别直接输出 $e-getMessage() error_log($e-getMessage()); exit(数据库连接失败); }几个关键配置要能说清楚。第一charset必须用utf8mb4不要用utf8否则emoji和部分冷门汉字会存不进去。第二ERRMODE_EXCEPTION让SQL出错直接抛异常而不是只给warning否则问题藏在角落很难发现。第三ATTR_EMULATE_PREPARES设为false让MySQL使用真正的预处理对防注入更有效。4.2 封装一个PDO访问类的设计思路另一道典型题是“写一个PHP数据库PDO访问封装类”。网上现成版本很多但真正要考察的是设计思路不是让你抄源码。我通常这样设计一个最小可用的单例封装class Db { private static $instance null; private $pdo; private function __construct() { $settings require config/database.php; $dsn sprintf( mysql:host%s;port%s;dbname%s;charsetutf8mb4, $settings[host], $settings[port], $settings[dbname] ); $this-pdo new PDO($dsn, $settings[username], $settings[password], [ PDO::ATTR_ERRMODE PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE PDO::FETCH_ASSOC, ]); } public static function instance(): self { if (self::$instance null) { self::$instance new self(); } return self::$instance; } public function query(string $sql, array $params []): array { $stmt $this-pdo-prepare($sql); $stmt-execute($params); return $stmt-fetchAll(); } public function execute(string $sql, array $params []): int { $stmt $this-pdo-prepare($sql); $stmt-execute($params); return $stmt-rowCount(); } public function transaction(callable $callback) { $this-pdo-beginTransaction(); try { $result $callback($this-pdo); $this-pdo-commit(); return $result; } catch (Throwable $e) { $this-pdo-rollBack(); throw $e; } } }构造函数设为private外部无法new只能通过Db::instance()拿同一个实例保证一个请求生命周期内只有一个数据库连接减少握手开销。query方法用prepare加execute绑定参数而不是把变量拼进SQL。transaction方法接收闭包把一组SQL放进去执行一旦抛出异常就整体回滚。面试官常追问两个问题为什么用单例因为频繁new连接成本高为什么用预处理因为能防注入同时SQL解析结果可以被复用。4.3 预处理、事务与那些常见的PDO报错预处理是二星级必考点你得能说清楚它为什么能防注入。原理是客户端把SQL结构先发送给服务端解析数据参数再单独发送数据不会改变SQL结构