
1. 整体设计与出题思路1.1 这套练习卷是给谁准备的我自己在带团队和帮朋友筛简历的时候发现一个很有意思的现象很多自称“两年PHP开发经验”的候选人笔试题目一落到基础语法和工程实践的结合点上立刻就能看出差距。不是他们不会写代码而是很多人的PHP知识停留在“能跑就行”的层面缺乏对语言机制、常见坑点和工程约束的系统理解。所以这套“PHP工程师综合练习卷二星级”定位非常明确面向已经能独立写PHP代码、但还没有系统梳理过知识体系的开发者。它的难度介于“新手入门”和“高级架构”之间。换句话说你刚学完PHP基础语法做这套卷子可能会觉得有点吃力你有两三年实际项目经验做这套卷子会发现“哎这就是我平时踩过的坑”你如果是个架构师做这套卷子也能用来筛查团队里基础不扎实的同学。这套卷子的考察覆盖面比较广从语言基础、数组与字符串操作、面向对象设计到错误处理、数据库访问、安全防护、开发环境与调试工具再到简单的项目综合题都有涉及。题型上模仿了真实技术面试和笔试的常见组合选择题、程序阅读题、编程实现题、综合设计题。整套卷子的核心目的不是靠偏题怪题难倒你而是检验你“是否真的用过PHP写过东西并且踩过它该踩的坑”。1.2 为什么把难度定在“二星级”我在设计这套题目的时候刻意把难度压在二星级也就是“比入门多一点比进阶差一口气”。为什么不直接上三星、四星因为实战中我发现一个普遍问题很多人连基层的坑都没踩完就直接去研究高并发、分布式、消息队列结果面试时一写基础代码就露馅。二星级的核心导向是实用。它要求你能准确说出PHP中和的区别并且能举出实际例子。知道数组函数里array_map、array_filter、array_reduce各自的用途而不是只背名字。明白面向对象中public、protected、private的管理边界以及static和self的区别。能写出不依赖error_reporting(0)来掩盖问题的错误处理机制。能使用PDO预处理来处理数据库交互而不是把字符串变量直接拼进SQL。知道什么是XSS、SQL注入、CSRF并且能在代码层面给出基本防护。这些要求看起来都不高但恰恰是日常开发里最常用、也最容易出错的地方。把它做成一套综合练习卷就是让读者在练的过程中承认一个事实真正的PHP水平提升不是靠学新框架而是靠把基础打扎实。1.3 试卷结构布局按真实开发流程排序这套卷子在编排题目顺序的时候没有刻意打乱知识点而是模拟了一个PHP工程师从“接收需求”到“写代码”再到“排查问题”的完整过程第一部分是语言基础与运算符题相当于你刚打开IDE准备编码时最先碰到的东西。第二部分是数组、字符串、文件处理题对应业务开发时最频繁的数据操作。第三部分是面向对象与设计原则题考察代码组织能力和扩展性思维。第四部分是错误处理、异常与日志题这是区分“学生代码”和“工程代码”的分水岭。第五部分是数据库与PDO题直接关系到业务系统的数据安全。第六部分是安全、会话、接口题覆盖面广且紧贴Web开发实际场景。最后是综合实战题模拟一个精简CRM或文章管理系统的模块设计要求把前面的知识串起来。这种结构的好处是你做完之后会很清楚自己的短板落在哪一层。我也建议你按顺序做不要跳题。因为我设置的某些后续题目会依赖前面题目的思考结果。2. 核心考点拆解从语法到工程习惯2.1 变量、类型与运算符最基础也最容易被忽略说句得罪人的话很多PHP工程师写了三年代码照样会在变量类型比较上翻车。二星级练习卷第一部分我集中出了几道题目专门敲打这块。比如有一道题是这样的$a 0; $b 0; var_dump($a $b); // 结果是什么 var_dump($a $b); // 结果是什么第一反应是true和false的人说明你对PHP的松散比较机制有一定了解。但如果你能进一步解释清楚PHP在比较时会把字符串0转换为数字0所以0 0为真而要求类型和值都相等字符串0和整数0类型不同所以为假。那这道题就只能算是热身。真正容易出错的是这类$str abc; if ($str 0) { echo 相等; } else { echo 不相等; }如果你认为输出的是“不相等”那就踩坑了。PHP 7之前非数字字符串与数字比较时字符串会转换成数字abc转成0所以abc 0是true。PHP 8改变了这种比较逻辑非数字字符串与数字比较时返回false。这就是典型的版本差异坑。这一部分我还加入了运算符和字符串拼接的题目。比如$i 1; echo $i $i;这道题在不同PHP版本下的输出可能不同而且和表达式求值顺序有关。我出这道题的目的不是让你背答案而是提醒你在实际项目里不要在一条语句里对一个变量做多次自增自减因为可读性太差很容易产生难以排查的逻辑错误。2.2 数组与字符串业务开发的主战场数组和字符串操作我认为是PHP里最值得花时间练的基本功。练习卷第二部分我安排了一道关于array_map的题要求实现一个功能把数组中每个单词转为首字母大写。普通写法是 foreach 循环$words [hello, world, php]; foreach ($words as $word) { $word ucfirst($word); } unset($word); print_r($words);但如果用array_map一行就能搞定$words array_map(ucfirst, [hello, world, php]);这里有一个我特别想强调的点使用array_map时回调函数如果是PHP内置函数直接传字符串函数名就行如果数组有多个可以传多个数组参数。很多人在项目里喜欢无脑 foreach不是说不对而是有些场景用数组函数能让代码更清晰。但也要注意array_map会保留字符串键名吗不会它会重新索引数字键。如果你需要保留键那就得用array_walk。字符串相关题目里我放了一道经典的strpos与!的判断题$pos strpos(hello world, hello); if ($pos) { echo 找到; } else { echo 未找到; }输出是“未找到”因为strpos返回的是0而不是false0在布尔判断里是假。实际开发中判断子串是否存在一定要用! false或str_containsPHP 8。这种题目考的不是你记不记得函数名而是你是否真的知道“函数返回值的边界情况”。2.3 面向对象与魔术方法考的是封装思维练习卷面向对象部分的题我设计了一个典型的User类要求实现属性私有化、构造函数赋值、__get和__set魔术方法访问控制。这看起来简单但能看出候选人的封装习惯。有些同学写类时习惯把所有属性设成public认为这样省事。但实际上一旦属性暴露在外你就无法在赋值时做校验也无法在读取时做格式化。二星级的题目不会强制你写出完美的设计模式但会要求你展示基本的封装意识。一个参考答案class User { private string $name; private int $age; public function __construct(string $name, int $age) { $this-name $name; $this-age $age; } public function __get($name) { if ($name name) { return $this-name; } if ($name age) { return $this-age; } return null; } public function __set($name, $value) { if ($name age $value 0) { throw new InvalidArgumentException(年龄不能为负数); } $this-$name $value; } }这里涉及的知识点不只是魔术方法本身还有类型声明、构造函数、异常抛出。我还会追问一句如果让你给这个类增加一个计算出生年份的方法你会写在哪里这时候能答出“写在类内部而不是外部函数”的人才算真正理解类的作用是数据和行为的封装。2.4 错误处理与异常工程代码的分水岭很多初学者写PHP遇到错误第一反应是error_reporting(0)把错误关掉或者用抑制符。如果你在练习卷的“错误处理”部分也这么答我会在试卷上画一个大大的叉。真实项目中错误是客观存在的屏蔽错误解决不了任何问题只会让排查变得更加困难。我在练习卷里设计了一个需求假设你有一个从文件读取配置的方法文件可能不存在格式可能错误你该怎么处理好的做法是抛出异常并让上层决定如何处理function loadConfig(string $filePath): array { if (!file_exists($filePath)) { throw new RuntimeException(配置文件不存在: {$filePath}); } $content file_get_contents($filePath); $config json_decode($content, true); if (json_last_error() ! JSON_ERROR_NONE) { throw new RuntimeException(配置文件格式错误: . json_last_error_msg()); } return $config; }练习卷里还有一道关于自定义异常处理器的题set_exception_handler和set_error_handler分别处理什么它们的区别是什么我在实际项目里见过不少同事把set_error_handler当作异常捕获来用结果某些场景下根本捕获不到。你需要理解错误和异常在PHP中是两套机制set_error_handler捕获的是传统错误如警告、通知而set_exception_handler捕获的是未被 try/catch 处理的异常对象。3. 实操题详解本地环境与基础功能实现3.1 环境准备从零搭建PHP开发环境练习卷的实操题部分第一道就是“搭建本地开发环境并运行一个PHP项目”。这道题看起来基础但它能考验一个人对开发链路是否真正理解。我推荐初学者使用集成环境比如小皮面板phpStudy、XAMPP、Laragon或国产的1Panel这些工具都能一键部署Apache/Nginx PHP MySQL的组合。但你不要只满足于“装好了能用”要清楚自己安装了什么版本、目录结构是怎么安排的。以目前使用率较高的Nginx PHP-FPM组合为例你至少要明白Nginx 负责处理静态资源和HTTP请求转发遇到.php文件时通过fastcgi_pass把请求转发给PHP-FPM。PHP-FPM 是一个FastCGI进程管理器监听一个端口默认9000或Unix Socket真正执行PHP脚本。两者是独立的进程通过FastCGI协议通信。如果你只是用集成面板点一下“启动”不看日志、不关心进程那出了问题你会毫无头绪。我见过很多同事遇到502 Bad Gateway就懵了脑子里的第一个念头是“重装环境”。但其实90%的502都是PHP-FPM进程挂了或者配置错误导致的。实操步骤其实不复杂安装集成环境或者用Docker部署PHP容器。确认PHP版本执行php -v。确认PHP-FPM状态执行ps aux | grep php-fpm。在Nginx配置里添加一个server块配置root指向你的项目目录index加上index.php。测试配置nginx -t然后nginx -s reload。在项目根目录创建index.php写入?php phpinfo();。浏览器访问能看到PHP配置信息页说明环境OK。3.2 本地运行一个PHP综合小项目图书管理系统练习卷第二道实操题是“用PHP写一个基础的图书管理系统”。这是经典的入门项目也是检验综合能力的好载体。它涉及数据库设计、列表展示、增删改查、表单提交和安全性考虑一套流程跑下来能把二星级该覆盖的知识点全部串起来。我的建议是不要用框架直接用原生PHP做带PDO访问数据库。用框架反而会掩盖很多细节。第一步设计数据表CREATE TABLE books ( id INT AUTO_INCREMENT PRIMARY KEY, title VARCHAR(200) NOT NULL, author VARCHAR(100) NOT NULL, price DECIMAL(10,2) NOT NULL DEFAULT 0.00, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;第二步写一个数据库连接类class Database { private static ?PDO $instance null; public static function getInstance(): PDO { if (self::$instance null) { $dsn mysql:host127.0.0.1;dbnamelibrary;charsetutf8mb4; self::$instance new PDO($dsn, root, your_password, [ PDO::ATTR_ERRMODE PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE PDO::FETCH_ASSOC, ]); } return self::$instance; } }第三步实现列表页和新增页。列表页用PDO查询所有图书并输出为HTML表格。新增页接收POST请求用预处理插入数据$stmt Database::getInstance()-prepare( INSERT INTO books (title, author, price) VALUES (:title, :author, :price) ); $stmt-execute([ :title $_POST[title], :author $_POST[author], :price $_POST[price], ]);这里强调一下千万不要把$_POST的值直接拼进SQL字符串否则彻底绕过了PDO预处理的安全保护。有些同学虽然用了PDO但写法是$sql INSERT INTO books (title) VALUES ( . $_POST[title] . ); $db-query($sql);这还不如不用PDO因为结果上没有任何防护作用。3.3 使用PDO封装类处理多表查询练习卷中有一道题是“实现一个查询查出所有图书并附带每个图书分类的名称”。这个需求在实际业务中太常见了但也是新手写SQL容易翻车的地方。如果你只学过单表查询可能会写出先查所有图书再在循环里逐条查分类的代码。性能极差而且违反了基本的数据访问常识。正确的做法是使用JOINSELECT b.*, c.name AS category_name FROM books b LEFT JOIN categories c ON b.category_id c.id ORDER BY b.created_at DESC然后通过PDO执行并遍历$stmt Database::getInstance()-query($sql); $books $stmt-fetchAll(); foreach ($books as $book) { echo $book[title] . - . $book[category_name]; }这道题的目的不是考JOIN语法而是考察你是否具备“用一次查询解决问题”的思维习惯。实际开发中循环内查询是性能杀手之一在高并发环境下会被放大到难以忍受的程度。3.4 接口与JSON数据处理从数组到对象的边界练习卷的接口题部分我出了一道非常常见的任务写一个API接口接收客户端POST的JSON数据解析后存入数据库并返回JSON格式的结果。这题的常见坑点有没有用file_get_contents(php://input)读取原始请求体而是一直用$_POST结果拿到空数组。没做JSON解析错误判断直接json_decode得到null还以为是数据本身就是null。返回JSON时忘记设置Content-Type: application/json头。跨域情况没考虑导致前端无法访问。一个比较完整的实现是这样的header(Content-Type: application/json); $raw file_get_contents(php://input); $data json_decode($raw, true); if (json_last_error() ! JSON_ERROR_NONE) { http_response_code(400); echo json_encode([code 0, message 无效的JSON数据]); exit; } $title $data[title] ?? ; $author $data[author] ?? ; try { $stmt Database::getInstance()-prepare( INSERT INTO books (title, author) VALUES (:title, :author) ); $stmt-execute([:title $title, :author $author]); echo json_encode([code 1, message 添加成功, id (int)Database::getInstance()-lastInsertId()]); } catch (Throwable $e) { http_response_code(500); echo json_encode([code 0, message 服务器内部错误]); }这里用??空合并运算符给未传的字段设置默认值也是一种工程习惯。表单或接口传过来的数据不能默认它一定存在防御式编程在Web开发里永远不嫌多。4. 安全与常见问题踩坑记录和排查技巧4.1 你以为你防住了SQL注入其实没有PHP安全相关的题目我在练习卷里安排了三道SQL注入、XSS、CSRF。这三座大山是Web开发无论如何绕不过去的。先说SQL注入问得最多、也答得最乱。很多人说“我用PDO了所以不会SQL注入”。这句话只对了一半。PDO预处理在绝大多数场景下能防注入前提是你用的是预处理语句prepare execute并且把用户输入作为参数绑定。但如果你用PDO的query()方法执行拼接SQL防护作用就等于零。另外注意PDO::ATTR_EMULATE_PREPARES这个选项。默认情况下PDO可能使用模拟预处理也就是不真正把SQL和数据分开发送给MySQL服务器而是在客户端做转义后再拼接到SQL里。这虽然也能防大部分注入但不如关闭模拟、使用真正的预处理可靠。在连接数据库时可以加上PDO::ATTR_EMULATE_PREPARES false再说XSS。很多初学者以为只要输出时用htmlspecialchars就万事大吉。但实际项目中上下文不同转义方式也不同在HTML标签内输出需要转义 在JavaScript变量中输出需要额外处理反斜杠和换行在URL属性中输出需要urlencode。所以更稳妥的做法是存数据库时不转义输出时根据上下文转义。如果早期就想好所有输出位置会省很多事。4.2 经典报错实战为什么页面一片空白练习卷最后一部分是一些“案例分析题”。其中我特别喜欢的一道题是你打开一个PHP页面发现一片空白什么错误都没有你会怎么排查这道题的答案能反映一个人的调试经验。小白会打开文件反复看代码也看不出问题有经验的人会按这个顺序排查检查PHP错误显示是否被关闭。很多生产环境默认display_errors Off代码一旦有语法错误或致命错误页面直接白屏。查看错误日志。日志位置可以通过phpinfo()里的error_log配置项查看也可以临时用error_reporting(E_ALL); ini_set(display_errors, 1);把错误显示打开。检查PHP-FPM状态。如果PHP-FPM挂了通常不是白屏而是502。检查文件权限和目录权限。file_get_contents读取文件失败、日志目录不可写、Session目录满都可能造成白屏。我还遇到过一种奇葩情况代码文件里混入了BOM头UTF-8 BOM导致PHP在输出内容之前就发送了响应头后面header()设置就全部失效。排查起来特别费劲但在练习卷里提一句能让读者以后少踩一个坑。4.3 跨域与Cookie前后端分离必踩的几个坑关于接口调试练习卷里有一道跨域题。要求实现一个支持简单跨域的JSON接口。很多人照抄网上的代码加了几个Access-Control-Allow-*响应头但实际请求还是失败。常见的原因没有处理OPTIONS预检请求。当你的请求带有自定义头或非简单请求时浏览器会先发一个OPTIONS请求服务端需要返回200并带上允许的请求方法、请求头。Access-Control-Allow-Origin写成*但你的请求带上了Cookie*和credentials不能同时使用。没有设置Access-Control-Allow-Headers前端传了Content-Type: application/json被浏览器拦下来。一个处理预检请求的基本框架是header(Access-Control-Allow-Origin: https://your-frontend.com); header(Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS); header(Access-Control-Allow-Headers: Content-Type, Authorization); header(Access-Control-Allow-Credentials: true); if ($_SERVER[REQUEST_METHOD] OPTIONS) { http_response_code(204); exit; }5. 综合设计题框架与扩展方向参考5.1 给你的试卷加点“框架味”ThinkPHP基础题说句实话纯原生PHP的项目现在越来越少了企业招聘时普遍会问到框架。这套卷子最后我也放了几道框架相关的基础题主要以ThinkPHP为例因为它在国内的使用基数很大网上的教程和生态也齐全。关于ThinkPHP二星级练习卷不会让你写出一个完整的模块而是考察你对MVC和框架生命周期的理解。比如控制器、模型、视图分别负责什么你自己写代码时会把SQL查询逻辑放在控制器里吗框架的入口文件public/index.php是怎么找到对应控制器的路由规则大致是怎样的模板引擎里的变量输出和原生PHP直接输出有什么区别为什么说模板引擎能帮助规避一部分XSS风险这些问题没有标准答案但能反映你是否理解“框架是工具不是魔法”。如果你能准确说出“框架的请求生命周期从入口文件到路由分发再到控制器”那这道题基本就过关了。5.2 扩展方向从二星级往三星级进阶做完整套二星级练习卷之后你应该对自己的水平有一个相对客观的评估。如果大部分题目都能顺畅完成连附加题都能写出来那我觉得你可以适度挑战三星级的内容。我根据实战经验整理了几个进阶方向供你参考队列系统用Redis作为消息队列实现异步任务处理比如发送邮件、生成报表。接口幂等性设计如何保证重复请求不会造成重复数据。表单安全加固验证码、频率限制、Token自动失效。Docker部署把PHP、MySQL、Redis分别容器化用docker-compose编排实现一键部署。实时通信通过WebSocket或MQTT实现消息推送、视频会议信令部分。性能调优从慢查询日志、OpCache、Redis缓存三个层面优化一个高延迟接口。这些内容展开讲每一项都够写一篇独立文章但它们的共同基础仍然是这套二星级练习卷覆盖的PHP核心能力。基础不稳后面学什么都像空中楼阁这是我在实际带人过程中的最深体会。这套练习卷我前前后后发给过不少候选人做测试也发给过刚转行的朋友练手。我个人的建议是不要只看分数而要看自己错在哪类题上。如果你在数组函数和字符串判断上丢分最多说明你写业务代码时可能需要留意边界情况如果你在错误处理和PDO上栽了跟头那说明你的工程化意识还有提升空间。把错题重做一遍比盲目刷十个新题更有效。