PHP 面试题库

收录真实面试常见问题,答案长度适合口头表达;拿不准时可返回相关主题复习。

47个问题 初级 高级
全部难度 初级 中级 高级
逐题查看 显示全部答案
报告错误

语言核心

25个问题
01 array_map() 与 array_filter() 对键的处理有什么不同? 中级 常见 查看答案 ▾ 收起 ▴

array_filter 始终保留输入键,因此筛选索引数组后可能留下空档。结果转成 JSON 时,这种非列表 PHP 数组会编码为对象。array_map 只有在恰好传入一个数组时才保留键;传入多个数组后,它按迭代位置对齐值,并返回连续整数键。我通常在处理阶段保留业务键,只在明确要求列表的边界调用 array_values。映射回调需要键时,我会使用清楚的 foreach,或显式传入键和值。

延伸阅读 数组函数
讲清楚了吗?
02 为什么 array_reduce() 通常应该显式接收初始值? 中级 常见 查看答案 ▾ 收起 ▴

初始值定义了处理第一项前的累加器,也是空输入的结果。PHP 省略它时,第一次回调收到 null;它不会像 JavaScript 的 reduce 那样采用数组首项。显式单位值还能稳定结果类型:加法用 0,乘法用 1,字符串拼接用空字符串,分组用空数组。如果不存在合理的空结果,我会先校验输入并抛出领域异常,而不是让意外的 null 流入回调。

延伸阅读 数组函数
讲清楚了吗?
03 什么情况下应使用 array_merge(),而不是数组联合运算符? 中级 常见 查看答案 ▾ 收起 ▴

后方字符串键配置应覆盖前方值,或需要拼接并重排整数键列表时,我会使用 array_merge。联合运算符对每个重复键保留左侧值,并保留整数键,因此适合左侧优先的默认值,或数字标识有意义的映射。两者都不是通用深合并:array_merge 是浅层操作,而 array_merge_recursive 可能把重复标量变成数组。选择前,我会先说明字符串键、整数键和嵌套数组的冲突政策,再分别写测试。

延伸阅读 数组函数
讲清楚了吗?
04 usort() 比较函数必须满足什么契约,调用方还需要知道什么? 中级 偶尔 查看答案 ▾ 收起 ▴

左值应排在前面时,比较函数返回负整数;排序意义上相等时返回 0;右值应排在前面时返回正整数。太空船表达式通常能清楚表达这项契约,布尔值无法覆盖三种情况。usort 通过引用修改数组,并把所有键替换为连续整数;需要保留原顺序时我会先复制,需要保留关联时则考虑 uasort。PHP 8 及以后会保留相等项的输入顺序,但输入顺序不确定时,确定性输出仍需要明确的决胜字段。

延伸阅读 数组函数
讲清楚了吗?
05 同一个 PHP 数组为何既能表示列表,也能表示映射? 初级 常见 查看答案 ▾ 收起 ▴

PHP 数组本质上是键为整数或字符串的有序映射。只有键按遍历顺序严格等于从 0 开始的连续整数时,它才具有列表形状,array_is_list 检查的正是这条规则。字符串键、非零起始键或中间空档都会让它成为映射形状,但运行时类型仍是 array。这项区别会影响边界:json_encode 把列表编码为 JSON 数组,把其他 PHP 数组编码为 JSON 对象。我会明确记录返回形状,并且只在键没有业务含义时使用 array_values 重排。

延伸阅读 数组
讲清楚了吗?
06 什么情况下应该使用 array_key_exists(),而不是 isset()? 初级 常见 查看答案 ▾ 收起 ▴

领域需要区分键缺失与键明确设为 null 时,我会使用 array_key_exists。它只检查键是否存在,因此普通值与 null 都算存在。isset 要求键存在且值不是 null,空合并运算符也采用相同的实际区分方式。例如在局部更新请求中,null 可能表示清空字段,缺失则表示保持不变,此时必须使用 array_key_exists。如果两种状态都表示采用默认值,isset 或 ?? 更简短,也能准确表达政策。

延伸阅读 数组
讲清楚了吗?
07 为什么 unset() 会改变 PHP 数组的 JSON 形状? 中级 常见 查看答案 ▾ 收起 ▴

unset 会删除一个键值对,但不会重新编号其余整数键。删除列表中间元素后会留下空档,之后的无键追加通常继续使用下一个整数键,而不会回填空档。这个值仍是 PHP 数组,但 array_is_list 已返回 false。由于 JSON 数组无法携带这些显式且不连续的键,json_encode 会把它表示为 JSON 对象。如果键只表示位置,我会在序列化边界调用 array_values;如果键是业务标识,则会有意保留它们。

延伸阅读 数组
讲清楚了吗?
08 复制 PHP 数组会复制什么,哪些修改仍可能共享? 中级 偶尔 查看答案 ▾ 收起 ▴

普通赋值让数组遵循值语义:修改副本的键、标量元素或嵌套数组,不会改变原数组。PHP 可能通过写时复制推迟物理复制,但这只是实现优化,不是应用代码可见的别名。对象元素有所不同,因为除非显式克隆对象,两个数组仍保存指向同一对象的句柄。使用 & 的显式引用赋值也会造成共享修改。我会分别测试容器独立性与对象身份,并在按引用 foreach 后立即 unset 循环变量。

延伸阅读 数组
讲清楚了吗?
09 PHP 的运行时值类型与声明类型有什么区别? 初级 常见 查看答案 ▾ 收起 ▴

运行时类型属于当前值,因此同一个变量可以先保存 int,再保存 string。类型声明约束参数、返回值、属性与类常量等选定位置,两者的集合有重叠,但并不相同。resource 是用户代码声明无法命名的运行时类型;iterable 描述数组或 Traversable 对象,不是独立的运行时值类型。mixed、void 与 never 也用于表达声明契约。我用 get_debug_type 生成诊断信息,用 is_* 函数执行明确的类型缩小,不会比较 gettype 的字符串结果。

延伸阅读 数据类型
讲清楚了吗?
10 declare(strict_types=1) 会强制什么,又不会处理什么? 中级 常见 查看答案 ▾ 收起 ▴

它是针对标量声明的文件级规则。对于用户定义函数的参数,采用调用发生文件中的设置,而不只是函数定义文件中的设置。严格模式会拒绝标量类型不匹配,唯一的放宽是 int 可以满足 float 参数;它也会收紧声明的返回值。严格模式不会解析查询字符串、验证数值范围、检查数组元素,也不会把 PHP 变成静态类型语言。我仍会在边界解析不可信输入,再把窄化且验证过的值传给使用严格声明的领域函数。

延伸阅读 数据类型
讲清楚了吗?
11 为什么对外部输入强制转换不等于验证? 初级 常见 查看答案 ▾ 收起 ▴

强制转换通过 PHP 转换规则保证结果类型,却不能证明来源表示完整且有意义。像 (int) ($input[“quantity”] ?? null) 这样的表达式,可能把字段缺失、格式错误与合法零值合并成同一个整数。验证会先定义允许的原始类型、语法、范围与缺失值政策,只有通过检查后才发生转换。这样既能保留有用错误,也能防止授权或计价逻辑使用由非法输入制造出来的貌似合理值。

延伸阅读 数据类型
讲清楚了吗?
12 array 类型声明能对数组内容保证什么? 中级 常见 查看答案 ▾ 收起 ▴

它只保证外层值是数组。原生 PHP 的 array 声明不会编码列表或映射形状、必需键与元素类型。PHPDoc 可以描述 array<string, int> 或具名数组形状,静态分析器也能检查配合良好的代码,但这些注解不会在运行时验证请求数据。我会在外部边界显式验证键和值。对于结构稳定的领域记录,我通常把验证后的数据映射到带类型属性的类,而不是让匿名数组贯穿整个应用。

延伸阅读 数据类型
讲清楚了吗?
13 什么情况下应选择纯枚举,而不是带值枚举? 初级 常见 查看答案 ▾ 收起 ▴

如果 case 只需在 PHP 内部彼此区分,而且领域中没有稳定的标量表示,我会选择纯枚举。每个 case 仍是带类型的单例,拥有 name,也能使用方法、接口和 cases()。如果值必须经过数据库、JSON API、队列消息或配置文件往返,我会选择带值枚举;此时字符串或整数值会成为外部契约。我不会只为标签或排序添加 backing value,展示文案与领域顺序应使用显式映射。

延伸阅读 枚举
讲清楚了吗?
14 from() 与 tryFrom() 有什么区别,分别适合用在哪里? 中级 常见 查看答案 ▾ 收起 ▴

两个方法都属于带值枚举,并且按 backing value 查找,不按 case 名称查找。from() 返回 case,找不到时抛出 ValueError,适合受数据库约束等内部不变量保护的数据。tryFrom() 返回 case 或 null,适合需要生成校验响应的客户输入。我在调用前仍会检查原始类型,也会区分字段缺失与未知值,不会把两者都压成默认 case。

延伸阅读 枚举
讲清楚了吗?
15 对 PHP 枚举使用 match,能否获得编译时穷尽性检查? 中级 常见 查看答案 ▾ 收起 ▴

PHP 本身不会在编译时证明枚举 match 已经穷尽。没有分支匹配且不存在 default 时,执行会抛出 UnhandledMatchError。这个运行时失败能避免新增 case 静默复用无关政策,但只有路径实际运行时才会出现。对于每个 case 都必须明确决定结果的逻辑,我会省略 default,让参数化测试遍历 cases(),并启用能更早报告遗漏的静态分析器。只有未来所有 case 都能安全共享同一行为时,default 才合适。

延伸阅读 枚举
讲清楚了吗?
16 修改枚举 case 或 backing value 时,哪些地方可能出问题? 高级 偶尔 查看答案 ▾ 收起 ▴

backing value 可能已经写入数据库行、JSON 载荷、消息、缓存键与测试夹具。PHP 序列化依赖枚举类型和 case 名称,因此重命名这些标识又会破坏另一批数据。新增 case 也可能破坏旧消费者:tryFrom 会对新值返回 null,缺少分支的 match 则只在运行时失败。我会把已发布值当作协议字段,先让消费者理解两种表示或明确未知值政策,再迁移存量数据,最后才让生产者停止写旧格式。

延伸阅读 枚举
讲清楚了吗?
17 PHP 诊断与 Throwable 对象有什么区别? 中级 常见 查看答案 ▾ 收起 ▴

PHP 诊断携带 E_* 整数级别、消息、文件和行号。符合条件的级别可以进入 set_error_handler(),其返回值决定 PHP 的普通展示或日志是否继续。Throwable 则是 Exception 与 Error 共享的对象层次,通过 try/catch 传播,也可能到达 set_exception_handler()。因此,Error 并不等于 E_ERROR。我会先判断失败来源,因为 E_ALL 掩码不会让错误处理器捕获 TypeError,catch 也不会收到普通 E_USER_WARNING,除非代码显式把它转换为 ErrorException。

延伸阅读 错误处理
讲清楚了吗?
21 为什么 catch (Exception) 可能漏掉 PHP 失败,什么情况下才应捕获 Throwable? 中级 常见 查看答案 ▾ 收起 ▴

Throwable 是 Exception 与 Error 两个分支共同实现的接口。应用抛出的 RuntimeException 属于 Exception,而 TypeError 等引擎失败属于 Error,所以 catch (Exception) 会漏掉后者。我会让局部捕获保持窄小,只列出真正能够恢复的失败。只有在事务、请求、任务或 CLI 边界中,所有逃出的失败都需要同一种回滚与终止政策时,我才捕获 Throwable。我不会机械地把每个 Exception 都改成 Throwable,因为那会把程序缺陷伪装成误导性的后备结果。测试还会让两个分支各有一个对象经过该边界。

延伸阅读 异常
讲清楚了吗?
25 PHP 中的表达式与语句有什么区别,为什么代码审查需要区分它们? 初级 常见 查看答案 ▾ 收起 ▴

表达式经过求值并产生一个值,语句则是完整的执行单位,例如赋值语句、return 或函数调用。区分二者很重要,因为 PHP 允许表达式内部产生副作用。赋值还会产生被赋的值,所以 if 中误写的单个等号是合法代码,不是语法错误。我通常让状态变化单独占一条语句,除非紧凑形式拥有清楚契约。审查时,我会用括号展开混合了赋值、比较与逻辑运算的表达式,确认结果类型,并测试两条分支。

延伸阅读 PHP 语言基础
讲清楚了吗?
26 现代 PHP 中怎样选择 if、match 与 switch? 初级 常见 查看答案 ▾ 收起 ▴

各分支提出不同问题或执行多条语句时,我使用 if,并把谓词按优先级排列。一个主题值应该产生唯一结果时,我使用 match;它执行严格比较、返回选中分支的值,而且不会贯穿。我主要在既有代码或故意按语句贯穿时保留 switch,同时记住 case 使用宽松匹配,非空分支通常需要 break。选择并非外观问题。我会测试重叠谓词的顺序、从 switch 迁移到 match 时字符串数字与整数的差别,以及没有 default 时的未匹配路径。

延伸阅读 PHP 语言基础
讲清楚了吗?
29 PHP 怎样在命名空间内解析非限定名称、限定名称与完全限定名称? 中级 常见 查看答案 ▾ 收起 ▴

我会先区分静态类式名称、函数、常量和动态字符串。非限定类名先使用当前文件的导入别名,否则属于当前命名空间。限定名称的首段可以由导入别名替换,否则相对于当前命名空间。开头反斜杠表示从全局根开始的完全限定名称,namespace\ 则明确表示相对名称。当前命名空间没有同名定义时,非限定函数与常量可以回退到全局空间,类却不能。动态字符串绕过这些静态导入规则,因此运行时需要完整名称时,我会传递 Type::class。

延伸阅读 命名空间
讲清楚了吗?
30 PHP 的命名空间导入与自动加载有什么区别? 初级 常见 查看答案 ▾ 收起 ▴

use 声明是编译期、文件级的记法。它告诉 PHP 怎样展开类、函数或常量的短名称,但不会包含文件、安装包或定义目标。PHP 解析出类式引用后,已注册的自动加载器才可能收到该完整名称并查找定义,Composer 通常用 PSR-4 完成这一步。排障时我会按顺序检查:先打印或查看准确的解析结果,再按 composer.json 推导预期路径;若配置有变化,就重新生成自动加载元数据,并通过 vendor/autoload.php 实际使用该类。

延伸阅读 命名空间
讲清楚了吗?
33 PHP 联合类型能保证什么,应用边界仍需完成哪些校验? 中级 常见 查看答案 ▾ 收起 ▴

像 int|string 这样的联合类型,只保证运行时值按照 PHP 调用点的类型规则满足至少一个已声明成员。它不会证明整数为正、字符串属于已知状态,也不会检查数组的必需键和元素类型。我会在 HTTP、CLI、数据库或消息数据进入系统时校验形状与领域约束,再将其规范化成更窄的内部表示。测试会覆盖每个允许成员、1 与 “1” 这类外观相似的值、边界值,以及必须拒绝的类型。

延伸阅读 PHP 8.0 新特性
讲清楚了吗?
37 __get() 与 __call() 何时运行,又不会拦截哪些操作? 中级 常见 查看答案 ▾ 收起 ▴

__get 会在代码读取未定义属性,或读取当前调用作用域无法访问的已声明属性时运行;__call 则是不可访问实例方法调用的对应后备入口。两者都不会包住对已声明公开成员的普通访问,所以必须覆盖全部访问的授权或审计不能只放在这里。我会从真实外部语法发起测试,而不是直接调用钩子,并覆盖公开、私有、缺失与拼写错误的成员。__set、__isset、__unset 和 __callStatic 也需要按同样的可见性规则分析。

延伸阅读 魔术方法
讲清楚了吗?
46 类使用 Trait 时,PHP 如何决定方法优先级? 中级 常见 查看答案 ▾ 收起 ▴

当前类声明的方法优先于导入的 Trait 方法,Trait 方法又优先于继承的父类方法。两个 Trait 提供同名方法时,除非 use 适配块用 insteadof 选择一个,否则会产生致命错误。我可以用 as 保留被排除的实现,并按需调整别名可见性。别名只是新增入口,不会重命名或删除原方法。审查变更时,我会为类、父类和嵌套 Trait 建立成员来源表,再测试适配后的每个最终公开入口。

延伸阅读 Trait
讲清楚了吗?

运行时行为

5个问题
18 set_error_handler() 回调应遵守什么契约? 中级 常见 查看答案 ▾ 收起 ▴

回调会收到注册掩码允许的诊断严重级别、消息、文件和行号。转换前应检查 error_reporting() & $severity,让抑制与当前政策保持有效。返回 false 会交回 PHP 默认处理;返回 true 则表示自定义路径完全接管展示与日志。我会让回调保持短小,避免依赖可能递归失败的组件,并把临时注册与 finally 中的 restore_error_handler() 配对。对于长生命周期工作进程或基于 Fiber 的应用,还要验证进程级处理器状态不会泄漏到其他请求或交错任务。

延伸阅读 错误处理
讲清楚了吗?
20 PHP 关闭处理器在致命终止后可以安全地做什么? 高级 偶尔 查看答案 ▾ 收起 ▴

关闭函数会在正常结束和许多终止路径上运行,因此必须先检查 error_get_last() 是否为 null,再用明确的致命级别允许列表筛选。它可以记录小型、幂等的事件,或释放简单进程资源,但不是恢复边界。此时事务可能不完整、内存可能耗尽、输出也可能已经开始。我不会继续领域写入、重新执行支付,也不会把通用 HTML 页面输出到 JSON 或流式响应中。重试应由能检查持久状态的外部工作进程或调用方决定。

延伸阅读 错误处理
讲清楚了吗?
23 finally 保证什么,其中不应该放什么? 中级 常见 查看答案 ▾ 收起 ▴

控制流通过正常完成、return、已捕获异常或继续传播离开 try/catch 结构时,finally 都会执行。我用它完成本作用域拥有的有限清理,例如释放锁、关闭流、回滚临时状态或恢复处理器。我不会在 finally 中 return,因为它会替换待返回值,还可能吞掉异常。我也会避免放入可能以不可预测方式失败的大段清理逻辑。资源取得与释放应相邻,测试则在取得资源后故意制造失败,证明清理会执行,而且不改变原始结果或异常。

延伸阅读 异常
讲清楚了吗?
27 为什么 PHP 的运算符优先级不等于求值顺序? 中级 偶尔 查看答案 ▾ 收起 ▴

优先级告诉解析器运算符与操作数如何分组,结合方向则处理相同优先级运算符的嵌套。两者通常都不承诺哪个子表达式先执行。当调用、自增、赋值或数组更新挤在同一表达式中时,这项区别会影响正确性;某个运行时上观察到的顺序不是可移植契约。我会把必须排序的副作用拆成有名称的独立语句。只有运算符明确规定条件求值时,我才依赖顺序,例如 &&、||、?? 的右侧或条件表达式选中的分支,而且不会把必须发生的写入藏在那里。

延伸阅读 PHP 语言基础
讲清楚了吗?
35 PHP 特性何时影响程序行为,它的错误又在什么时候被发现? 中级 常见 查看答案 ▾ 收起 ▴

特性只是元数据,因此写下 #[RequiresRole] 本身不会执行授权。消费者必须通过反射找到它,决定重复项如何组合,在需要时实例化对象,再应用策略。getAttributes 可以只返回描述符而不运行构造器;ReflectionAttribute::newInstance 才会实例化,并可能暴露非法目标、缺少参数或类未声明为特性等问题。我会通过真实消费者执行启动或集成测试,覆盖元数据缺失、重复、错误参数、错误目标,以及授权允许和拒绝两种结果。

延伸阅读 PHP 8.0 新特性
讲清楚了吗?

生产运维

1个问题
19 你会怎样配置并测试 PHP 生产环境的错误报告? 中级 常见 查看答案 ▾ 收起 ▴

我会保留 E_ALL,让警告、通知和弃用信息继续可观测;关闭 display_errors,避免内部细节进入响应;同时打开 log_errors,并使用受访问控制的目标。启动设置应由部署配置负责,不能只依赖请求代码。日志使用稳定事件码、关联 ID 和明确的字段允许列表,排除密码、令牌、Cookie、授权头与任意请求体。测试既要确认实际配置和内部上下文充足,也要确认公开响应不包含路径、堆栈、SQL 与秘密,还要覆盖日志目标自身失败的情况。

延伸阅读 错误处理
讲清楚了吗?

应用设计

5个问题
22 怎样在抽象层之间转换异常,同时不丢失原始原因? 中级 常见 查看答案 ▾ 收起 ▴

我会在理解底层失败的那一层捕获具体类型,补充当前操作语境,并把原始 Throwable 作为新异常构造器的第三个参数。这样,上层只依赖稳定的领域类型,getPrevious() 仍能保留驱动或服务原因。新消息描述本层失败的操作,不复制旧消息。我通常只在拥有关联与脱敏上下文的最外层记录整条链。每层都记录后再抛出会制造重复事件;遗漏 $previous 则只剩文案,破坏可编程访问的原因关系。

延伸阅读 异常
讲清楚了吗?
24 你会怎样设计并测试 PHP 的公开异常契约? 高级 偶尔 查看答案 ▾ 收起 ▴

PHP 异常是非检查型异常,函数签名不会强迫调用方确认它们。我会记录公共方法可能抛出的稳定应用类型,按调用方政策而不是消息文案选择类型,并在转换边界之后隐藏基础设施异常。测试覆盖成功、每种已记录失败类型、前一个异常关系,以及各条退出路径上的清理。我不会让客户端解析 getMessage(),也不会把 Exception::$code 自动当成 HTTP 状态。如果调用方需要结构化决策,异常会提供窄小的只读属性或子类型,而公开响应使用独立的稳定错误码。

延伸阅读 异常
讲清楚了吗?
28 怎样让 PHP 业务函数不依赖 Web 或 CLI 入口? 中级 常见 查看答案 ▾ 收起 ▴

我会让入口负责协议细节:Web 适配层读取指定的超全局字段,CLI 适配层读取 argv 并选择退出码。每个适配层先验证和规范化原始字符串,再把普通的带类型值传给业务函数。业务函数通过参数接收依赖与数据,返回含义稳定的领域结果,不读取全局变量、不打印响应,也不终止进程。这样,测试无需构造请求或启动命令就能直接调用它。最外层适配器再把领域失败转换成 HTTP 响应、标准错误消息或非零退出码。

延伸阅读 PHP 语言基础
讲清楚了吗?
31 怎样安全地构建一个根据外部键选择命名空间类的工厂? 高级 偶尔 查看答案 ▾ 收起 ▴

我不会把请求值拼进类名字符串,因为导入不会改写动态字符串,而且自动加载受攻击者影响的名称可能触发意外代码。我会建立从协议键到类常量的封闭映射,例如把 csv 映射到 CsvExporter::class;在调用 class_exists 或 new 前拒绝未知键,并把返回类型声明为共享接口。构造仍可能产生副作用,所以允许列表检查必须在前。测试覆盖每个允许键、大小写变体、分隔符、空输入与未知键,并断言映射类都能由 Composer 加载且实现该接口。

延伸阅读 命名空间
讲清楚了吗?
38 怎样设计安全的 __call() 代理? 高级 常见 查看答案 ▾ 收起 ▴

我会先建立从公开动态名称到固定操作的封闭映射。每个入口都在调用窄小服务接口前验证实参数量、运行时类型、授权与预期返回类型,未知名称统一抛出 BadMethodCallException。我不会把 method_exists 当作授权,也不会把调用方控制的名称直接转发给内部服务,否则服务新增公开方法就会静默扩大代理接口。如果名称集合稳定且很小,我会完全不用 __call,改为声明普通的带类型方法,从而改善发现、静态分析、重构和测试。

延伸阅读 魔术方法
讲清楚了吗?

工具链

1个问题
32 某个类能在开发者电脑加载,却在 Linux CI 中失败。怎样检查其命名空间与 PSR-4 映射? 中级 常见 查看答案 ▾ 收起 ▴

我会从失败调用所请求的完整类名开始,而不是看它的本地别名。找到 composer.json 中匹配的前缀后,我会移除前缀,把余下的命名空间分隔符转换成目录,再给类名添加 .php。目录、文件名、声明和引用的大小写都要逐一精确比较,因为大小写不敏感的文件系统可能在本地掩盖漂移。我还会检查单复数层级,确认入口载入了预期的 vendor/autoload.php,并在配置变化后运行 composer dump-autoload。最后要通过真实自动加载器复现,不能只对目标文件执行语法检查。

延伸阅读 命名空间
讲清楚了吗?

API 设计

1个问题
34 命名实参怎样改变 PHP 8 的 API 兼容性与数组展开行为? 中级 常见 查看答案 ▾ 收起 ▴

命名调用按形参的文本名称绑定,因此即使位置和类型不变,重命名公开形参也可能破坏调用方。位置实参必须在前,未知名称或重复绑定会抛出 Error。展开数组时,字符串键会被视为实参名,所以用 …$payload 转发不可信载荷,会让调用直接面对未知键与类型失败。我会先通过允许列表投影外部数据,再执行展开;同时保持公开形参名稳定,并保留按名称调用重要 API 的集成测试。

延伸阅读 PHP 8.0 新特性
讲清楚了吗?

迁移

1个问题
36 怎样把 switch 与嵌套空值检查安全迁移到 PHP 8 语法? 高级 偶尔 查看答案 ▾ 收起 ▴

我会先用不同类型的值、每个 case、贯穿路径和 default 固定旧行为。match 使用严格比较,只返回一个分支表达式,而且没有分支匹配时会抛出 UnhandledMatchError,因此机械替换 switch 可能同时改变三种行为。迁移嵌套空值检查时,我会找出每个独立可空步骤,并定义每种缺失的领域含义;这些步骤各自需要 ?->,而 ?? 提供的是另一层后备策略。必须执行的审计、授权和其他副作用,应移出可能被 nullsafe 链跳过的方法实参。

延伸阅读 PHP 8.0 新特性
讲清楚了吗?

对象生命周期

2个问题
39 PHP 对象被克隆时究竟发生什么? 中级 常见 查看答案 ▾ 收起 ▴

PHP 会先浅复制对象属性,再在新对象上调用 __clone。因此,对象类型的属性仍引用同一个嵌套实例,除非 __clone 主动替换它们。编写钩子前,我会按所有权分类属性:拥有的值对象可以克隆,服务可以继续共享或重新注入,有身份的对象可能禁止克隆,令牌与缓存则可能需要清空。测试会修改每一类嵌套可变值,同时断言应隔离与应共享的关系。数组尤其需要注意,因为复制数组不会克隆其中保存的对象。

延伸阅读 魔术方法
讲清楚了吗?
44 赋值、引用和克隆 PHP 对象有什么区别? 中级 常见 查看答案 ▾ 收起 ▴

普通对象赋值复制对象标识符,因此两个变量定位同一实例,通过任一名称修改都能从另一处观察到。它不会让两个变量本身成为别名:重新赋值其中一个变量不会改变另一个。显式 & 才会建立变量引用关系,仅为共享对象通常不需要它。clone 创建新的顶层身份,再浅复制属性,因此嵌套对象属性仍然共享,除非 __clone 主动替换。我会分别测试标量、数组与嵌套对象的修改,并且只在契约要求同一运行时实例时使用 ===。

讲清楚了吗?

持久化与安全

1个问题
40 __serialize() 与 __unserialize() 应建立什么契约? 高级 偶尔 查看答案 ▾ 收起 ▴

__serialize 应返回经过有意筛选且带版本的状态数组,而不是所有私有字段;令牌、连接、闭包、请求对象与缓存都应排除。__unserialize 要验证版本、必需键、嵌套类型与领域不变量,并初始化每个带类型属性,因为恢复过程不会走普通构造路径。我会保留固定旧载荷作为迁移测试,而不只测试新鲜往返。PHP 序列化是带自动加载和钩子行为的对象协议,因此绝不把攻击者控制的字节交给 unserialize。外部输入应使用纯数据格式并显式验证,可信存储中的字节也需要完整性保护。

延伸阅读 魔术方法
讲清楚了吗?

对象设计

3个问题
41 封装怎样帮助 PHP 类维护不变量? 中级 常见 查看答案 ▾ 收起 ▴

封装不只是把字段改成 private。我会先定义必须始终成立的状态,在构造器中接收所有必需值并拒绝非法组合,再只公开能够维护这些规则的操作。带类型属性能阻止部分错误赋值,却无法表达范围、字段关系或生命周期顺序。我也会避免返回可变内部对象,以免调用方绕过校验。测试会覆盖非法构造输入与每个公开修改操作,并通过可观察行为断言不变量,而不是使用反射读取私有字段。

讲清楚了吗?
42 在 PHP 中,什么时候选择接口、抽象类或组合? 中级 常见 查看答案 ▾ 收起 ▴

我从调用方需求出发。调用方只需要一种能力,而且无关实现都能提供时,我会定义小接口。多个实现确实共享状态、生命周期和稳定算法骨架时,抽象类才合适,因为 PHP 只能继承一个父类。若目标只是复用工作或替换一项策略,我会把行为放进协作者,并通过构造器组合。一个类可以实现多个接口,同时仍只继承一个父类。我不会为假想中的未来实现预建抽象,而是在真实替换边界出现时再引入。

讲清楚了吗?
45 PHP Trait 与接口有什么区别,什么时候会同时使用? 中级 常见 查看答案 ▾ 收起 ▴

Trait 通过向使用方类加入成员来复用实现;它不是可实例化对象,也不是可替换的能力类型。接口定义调用方可以向无关实现要求的公开操作,但在 PHP 8.3 中不提供方法体。多个类承诺同一接口,而且确实共享同一实现时,我会让各类实现接口并使用 Trait,同时用 Trait 抽象方法声明宿主需求。若行为需要可替换服务或独立生命周期,我会注入协作者,而不是把依赖藏进 Trait。

延伸阅读 Trait
讲清楚了吗?

继承

1个问题
43 怎样审查 PHP 子类能否安全替代父类? 高级 偶尔 查看答案 ▾ 收起 ▴

我不会只检查签名兼容性。子类必须接受父契约允许的全部输入,保持返回值语义,维护继承的不变量,也不能新增意外的必需副作用或失败。子类构造器必须建立全部父类状态,通常需要调用 parent::__construct。我会通过父类型对每个子类运行同一组父契约测试,而不是只调用子类特有方法。PHP 能检查兼容的参数逆变和返回值协变,却无法证明业务前置条件、异常政策或状态转换。如果替换关系需要许多例外,组合或更窄接口通常更清楚。

讲清楚了吗?

对象状态

1个问题
47 PHP 8.3 中的 Trait 属性会带来哪些状态与继承风险? 高级 偶尔 查看答案 ▾ 收起 ▴

Trait 声明的实例属性会成为每个使用方对象的状态,并与类属性共享命名空间,所以同名声明必须在结构上兼容。Trait 静态属性属于类,不属于单个对象,在常驻进程中可能跨请求或测试泄漏。PHP 8.3 中,只继承相关成员的子类会看到继承的存储,再次使用同一 Trait 的子类则获得与父类不同的存储。我会测试两个对象、两个无关类、只继承的子类和再次使用 Trait 的子类,也会避免用静态属性保存租户级数据。

延伸阅读 Trait
讲清楚了吗?