PHP 怎么开启严格类型

wen PHP项目 1

PHP 严格类型详解:从 declare(strict_types=1) 到现代工程实践

PHP 怎么开启严格类型

目录导读

  1. 为什么需要严格类型?—— 弱类型带来的“隐形坑”
  2. PHP 严格类型开关的三种写法(含代码示例)
  3. 严格类型对函数参数、返回值、属性赋值的影响
  4. 严格类型 vs 弱类型:一组对比实验(实测结果)
  5. 常见陷阱与最佳实践(含 Laravel/ThinkPHP 中的注意事项)
  6. 严格类型与联合类型、空类型、协变的协同
  7. 高频问答(FAQ):5 个开发者最关心的问题
  8. 什么时候该开,什么时候不该开?

为什么需要严格类型?—— 弱类型带来的“隐形坑”

PHP 最初作为一门“入门友好”的语言,其弱类型特性让无数新手快速上手,但也埋下了深水炸弹,考虑下面这段代码:

function calculateTotal($price, $quantity) {
    return $price * $quantity;
}
echo calculateTotal("10.5", 3); // 输出 31.5,看起来没问题
echo calculateTotal("abc", 3);  // 输出 0,并且产生一个警告(在 PHP 8 之前)

弱类型下,"abc" 被静默转换成 0,业务数据被污染却没有任何报错,这在金融计算、库存扣减、支付系统中是致命的,而 declare(strict_types=1) 就是为了解决这类“静默错误”而生。

根据 PHP 官方 RFC(Strict Types 提案,PHP 7.0 引入),严格类型模式是一种每个文件独立生效的编译指令,它改变了类型转换的默认策略。

PHP 严格类型开关的三种写法

标准声明(最常用)

<?php
declare(strict_types=1);
// 后续所有代码都运行在严格类型模式下

与命名空间/类配合

<?php
declare(strict_types=1);
namespace App\Services;
class OrderService {
    // ...
}

注意:declare 必须放在任何输出、namespace 之前(但可以放在 namespace 之前,也可以放在 namespace 之后,取决于 PHP 版本——官方推荐放在 namespace 之前)。

PHP 8 中的属性类型严格化(无需 declare 的局部场景)

严格类型声明只影响函数调用边界,不影响属性赋值,但你可以通过构造器参数提升(Constructor Promotion)结合严格类型实现类似效果:

<?php
declare(strict_types=1);
class Product {
    public function __construct(
        private int $id,
        private string $name,
    ) {}
}

严格类型对函数参数、返回值、属性赋值的影响

场景 弱类型模式 严格类型模式
函数参数 int $x foo("1") 自动转 int(1) 抛出 TypeError(除非是 int 兼容的字符串)
返回值 int return "1"; 自动转 int(1) 抛出 TypeError
属性赋值 int $x = 1 不检查类型(除非有 #[TypedProperty] 不检查(严格类型不影响属性赋值)
继承/接口实现 参数可收窄,返回值可放宽 同样遵循,但严格模式下拒绝隐式转换

特别注意:在严格模式下,floatint 不会自动截断。

function test(int $x) {}
test(1.5); // TypeError: 必须为 int,给定 float

这与弱模式下自动取整(51)截然不同。

严格类型 vs 弱类型:一组对比实验(实测结果)

我们写一个快速测试脚本:

<?php
// weak.php (未开启严格)
function add(int $a, int $b) { return $a + $b; }
var_dump(add(1.9, 2.1)); // int(4) — 两个 float 被取整
// strict.php
declare(strict_types=1);
function add(int $a, int $b) { return $a + $b; }
try {
    var_dump(add(1.9, 2.1));
} catch (TypeError $e) {
    echo "捕获错误: " . $e->getMessage();
}
// 输出:捕获错误: add(): Argument #1 ($a) must be of type int, float given

另一个关键差异:严格模式下,数字字符串 "10" 传给 int 参数时会被接受(因为 PHP 认为“数字字符串”是 int 的安全扩展),但 "10.5" 会被拒绝,这是很多人踩坑的点:

declare(strict_types=1);
function foo(int $n) {}
foo("10");  // 正常,PHP 8 中允许数字字符串转换为 int
foo("10.5"); // TypeError! 

常见陷阱与最佳实践(含 Laravel/ThinkPHP 中的注意事项)

陷阱 1:declare 必须写在文件最顶部

不是文件第一行就会报错?其实只要在可执行代码之前即可,但禁止放在 <?php 标签后面留有输出(BOM 或空行),某些 IDE 会自动添加 BOM,导致 declare 失效,建议:文件存为 UTF-8 无 BOM。

陷阱 2:框架文件与第三方库混用

Laravel 或 ThinkPHP 的多数核心文件没有开启严格类型,如果你的控制器开启了严格类型,调用框架未声明类型的函数时,行为可能不一致,最佳实践是:项目内新建的类文件统一开启严格类型,框架文件保持原样,同时可以在 phpstanpsalm 中配置强制所有自定义文件开启。

陷阱 3:declare 不继承

每个文件必须单独写,你不能在 autoload.php 里写一次就全局生效,这正是它的设计——避免老代码被意外破坏。

最佳实践清单:

  • ✅ 所有新增业务代码文件顶部加 declare(strict_types=1);
  • ✅ 严格模式下,函数尽量用 int|float 联合类型而不是仅 int,避免误拒数字字符串。
  • ✅ 在 phpstan.neonpsalm.xml 中设置 strict_types 检查到最高级别。
  • ❌ 不要对 index.php 等入口文件开启(它通常只是加载引导文件)。

严格类型与联合类型、空类型、协变的协同

PHP 8.0+ 引入了联合类型(Union Types),与严格模式搭配完美:

declare(strict_types=1);
function formatPrice(int|float $price): string {
    return number_format($price, 2);
}

空类型(nullable)也需注意:

function findUser(int $id): ?User {
    // 返回值只能为 User 或 null,严格模式下不能返回 false 或 0
}

协变与逆变:严格类型并不影响继承时的类型宽窄规则,子类可以返回更具体的类型,但参数不能更宽泛,错误示例:

declare(strict_types=1);
class A { public function test(int $x): int {} }
class B extends A {
    public function test($x): int {} // 参数变宽,PHP Fatal error
}

高频问答(FAQ):5 个开发者最关心的问题

Q1:declare(strict_types=1) 对性能有影响吗? A:编译阶段就确定,运行时零开销,但会稍微增加编译字节码大小,实测对性能影响可以忽略不计(<1%),反而弱类型频繁隐式转换开销更大。

Q2:我可以在运行时动态开启或关闭吗? A:不可以,这是编译期指令,每个文件独立生效,不过你可以用 eval('declare(strict_types=1); ...') 但极不推荐。

Q3:开启了严格类型,还能用 settype() 强制转换吗? A:完全可以,严格模式只拦截隐式转换,你仍然可以显式 (int)intval() 转换后再传参。

Q4:为什么 "10" 可以传给 int 参数,而 "10.0" 不行? A:PHP 8.0 起,数字字符串(numeric string)被特判为“安全的整数文本”,允许自动转 int,但带小数的字符串 "10.0" 会被视为 float 字符串,传入 int 参数时被拒绝,这是符合底层 zend_parse_parameters 逻辑的。

Q5:我的老项目有几百个文件,如何快速逐步开启严格类型? A:推荐使用 Rector 工具自动添加 declare(strict_types=1) 到所有文件,然后跑全量测试,如果测试失败,用 --dry-run 逐文件回滚,也可以结合 PHPStan 的 reportUnmatchedIgnoredErrors 找出违规点。

什么时候该开,什么时候不该开?

场景 建议
新项目、新模块 ✅ 必须开启
第三方包开发(Composer 包) ✅ 强烈建议,提高使用者信心
老项目维护(历史包袱重) ⏸ 逐文件迁移,先加测试覆盖
面试/教学代码 ✅ 开启并讲解,帮助理解类型系统
PHP 5.x 项目 ❌ 语法不支持,需先升级 PHP 7+

最后一句忠言:严格类型不是银弹,但它能把 PHP 从“草莽英雄”变成“可靠的技术债还清工具”,开启它,你的代码从“可能正确”变为“要么正确,要么快速失败”,这对大型团队协作、复杂业务系统、API 接口开发尤其重要,建议每一位 PHPer 都养成文件开头写 declare(strict_types=1); 的良好习惯。

抱歉,评论功能暂时关闭!