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而不是false,0在布尔判断里是假。实际开发中,判断子串是否存在,一定要用!== false或str_contains(PHP 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 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;第二步,写一个数据库连接类:
class Database { private static ?PDO $instance = null; public static function getInstance(): PDO { if (self::$instance === null) { $dsn = 'mysql:host=127.0.0.1;dbname=library;charset=utf8mb4'; 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容易翻车的地方。
如果你只学过单表查询,可能会写出先查所有图书,再在循环里逐条查分类的代码。性能极差,而且违反了基本的数据访问常识。正确的做法是使用JOIN:
SELECT 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上栽了跟头,那说明你的工程化意识还有提升空间。把错题重做一遍,比盲目刷十个新题更有效。