PHP 是动态类型语言,但现代 PHP 已经提供了相当完整的类型声明体系。参数、返回值、属性、联合类型、可空类型、接口和枚举等功能,让大型 PHP 项目可以建立比传统 PHP 代码更加明确的类型契约。
其中 declare(strict_types=1) 是很多开发者都会遇到的功能。
它的作用并不是把 PHP 变成 Java、C# 这类静态类型语言,也不是简单地打开一个“全项目严格检查开关”。它主要改变当前 PHP 文件中函数调用涉及的标量类型转换规则,让错误的数据类型更早暴露出来。
对于中大型 SaaS、API、后台系统以及长期维护的 PHP 项目,这种差异非常重要。
strict_types怎么开启
最常见的写法是:
declare(strict_types=1);
function add(int $a, int $b): int
{
return $a + $b;
}
declare(strict_types=1); 必须放在文件开头的声明区域。
它是文件级声明,不是 php.ini 中的全局 PHP 配置,也不存在一个可以在 composer.json 中直接设置 strict_types=1 的项目级开关。
因此,如果项目有多个 PHP 文件,每个需要使用严格标量类型调用规则的文件通常都应该明确声明:
declare(strict_types=1);
现代 PHP 项目通常可以制定编码规范,要求新 PHP 文件默认使用这个声明。
strict_types到底改变了什么
这里最容易产生误解。
PHP 的类型声明本身一直可以用于约束函数参数和返回值。例如:
function calculate(int $value): int
{
return $value * 2;
}
问题在于 PHP 对标量类型存在一定的类型转换行为。
在没有严格类型声明的情况下,某些调用可能发生标量类型转换。
例如:
function calculate(int $value): int
{
return $value * 2;
}
calculate("10");
在弱类型模式下,字符串 "10" 可能被转换为整数 10。
如果调用方传入的是:
calculate("hello");
由于这个字符串不能正常转换为整数,运行时仍可能产生 TypeError,并不是所有错误都可以通过弱类型模式“自动转换解决”。
启用严格模式以后:
declare(strict_types=1);
calculate("10");
这里就会因为参数类型不匹配而抛出 TypeError。
这意味着 strict_types 的核心价值并不是“所有类型错误突然都能被发现”,而是减少 PHP 在标量参数调用过程中替你进行的隐式转换。
它不是全局严格类型系统
假设项目有两个文件:
// A.php
declare(strict_types=1);
foo("123");
以及:
// B.php
foo("123");
不能简单理解成“只要 A.php 开启了 strict_types,整个项目以后都是严格模式”。
strict_types 是调用文件级别的行为控制。
这也是为什么大型项目应该把它纳入代码规范,而不是试图寻找一个所谓的全局开关。
第三方 Composer 包是否使用 strict_types,也应该尊重其自身代码设计。不能因为自己的项目启用了严格模式,就认为所有依赖库的代码都会自动变成严格模式。
类型声明本身比strict_types更重要
很多开发者讨论严格类型时,只盯着这一行:
declare(strict_types=1);
但真正构成现代 PHP 类型安全基础的是完整的类型声明。
例如:
function findUser(int $id): ?User
{
// ...
}
这里同时表达了几个信息。
参数必须是整数。
返回值可以是 User。
也可以返回 null。
再例如 PHP 8 的联合类型:
function formatId(int|string $id): string
{
return (string) $id;
}
它明确告诉调用者,这个函数设计上允许整数或者字符串。
这比在代码内部写大量“猜测输入类型”的逻辑更加清晰。
strict_types并不会让PHP变成静态类型语言
这是理解现代 PHP 类型系统非常重要的一点。
PHP 仍然是动态类型语言。
strict_types=1 不会让 PHP 在程序运行之前像传统静态类型语言那样完成整个项目的类型检查。
PHP 的类型声明主要在运行时参与验证。
例如:
declare(strict_types=1);
function saveUser(int $id): bool
{
// ...
}
如果运行时传入错误类型,PHP 可以抛出 TypeError。
而 PHPStan、Psalm 等静态分析工具,则是在程序运行之前分析源代码中的类型关系。
两者解决的问题不同。
可以把它们理解为两层防线。
PHP 原生类型声明负责运行时契约。
PHPStan 或 Psalm 负责静态分析。
两者结合以后,大型项目的类型问题可以在更多阶段被发现。
strict_types对数组没有原文描述的那种作用
原文举例说,如果函数要求数组参数,传入字符串以后严格模式可以避免“将字符串转换为空数组”。
这个例子本身就不准确。
PHP 不会因为普通弱类型模式就把任意字符串自动转换成空数组,然后让:
function process(array $data)
悄悄接受它。
数组、对象等类型和标量类型的行为并不能简单套用 strict_types 的规则。
因此不能把 strict_types 描述成“任何类型不匹配都会从自动转换变成 TypeError”。
应该准确理解为,它主要改变函数调用中的标量类型强制转换行为。
返回值也应该明确声明
现代 PHP 项目应该尽可能为函数和方法声明返回类型。
例如:
function getUserName(int $id): string
{
return "Brian";
}
如果某个分支错误地返回了数组:
return [];
运行时就会产生类型错误。
这比函数返回什么完全取决于调用方“猜测”要可靠得多。
对于数据库查询、服务层、Repository、API Client 等核心代码,明确返回类型尤其有价值。
例如:
function getUser(int $id): ?User
{
// 找不到用户返回 null
}
调用者看到函数签名,就可以知道“找不到用户”属于正常结果,而不是等到运行程序以后才发现。
typed properties同样重要
类型安全不应该只停留在函数参数。
现代 PHP 还支持类型化属性:
class User
{
public int $id;
public string $email;
public bool $active;
}
这可以让对象状态更加明确。
如果业务对象中的 id 应该永远是整数,那么把它声明成 int 比使用没有类型限制的公共属性更加清晰。
当然,类型声明并不能保证业务逻辑一定正确。
一个 int 类型的用户 ID 仍然可能指向不存在的用户。
类型系统解决的是数据类型契约,而不是业务规则。
strict_types不能替代输入验证
例如:
function deleteUser(int $id): void
{
// ...
}
即使启用了严格类型,也不能因此认为:
“只要 $id 是 int,就可以安全删除用户。”
攻击者可能提交一个完全合法的整数,但这个整数对应的是另一个租户的用户。
因此 SaaS 系统仍然必须检查:
用户是否已经登录?
用户属于哪个租户?
这个对象是否属于当前租户?
当前账户是否具有删除权限?
是否需要重新认证?
是否需要审计日志?
类型安全和授权安全是完全不同的层次。
strict_types也不能替代数据库安全
同样:
function findUser(int $id): ?User
{
// ...
}
并不能保证 SQL 安全。
数据库查询仍然应该使用参数化查询:
$stmt = $pdo->prepare(
'SELECT * FROM users WHERE id = :id'
);
$stmt->execute([
'id' => $id
]);
类型声明和 SQL 参数绑定解决的是不同问题。
即使 $id 是 int,也不能因此形成“所有 SQL 注入问题都被 PHP 类型系统解决”的错误认识。
第三方库不需要因为strict_types而强行修改
Composer 项目经常同时包含自己的代码和大量第三方包。
如果自己的文件使用:
declare(strict_types=1);
并不意味着应该直接修改 vendor 目录里的第三方代码。
升级第三方包时,Composer 会重新安装这些文件,手工修改也会丢失。
如果遇到类型兼容问题,首先应该查看库本身支持的 PHP 版本、API 类型声明以及调用方式。
必要时,在自己的应用边界上进行明确的数据转换。
例如数据库、HTTP 请求、JSON 数据等外部输入本身就经常需要转换和验证。
严格类型并不意味着:
“所有输入进来以后都不能转换。”
正确的做法是让转换发生在明确的位置,而不是让类型转换散落在整个业务代码中。
外部输入本来就需要转换
假设 URL 中存在:
?id=123
PHP 从 HTTP 请求获得的查询参数通常是字符串。
业务层可能需要整数。
这时可以在输入边界进行明确验证和转换:
$id = filter_input(
INPUT_GET,
'id',
FILTER_VALIDATE_INT
);
if ($id === false || $id === null) {
throw new InvalidArgumentException('Invalid user ID');
}
然后再把已经验证过的整数传给业务函数:
$user = findUser($id);
这种设计比让业务层到处依赖隐式类型转换更加清晰。
严格类型并不是要求“永远不要转换类型”,而是鼓励开发者明确知道什么时候、为什么以及在哪里发生类型转换。
大型项目应该采用渐进式迁移
如果一个运行多年的 PHP 项目突然给所有文件加上:
declare(strict_types=1);
然后把所有参数和返回值一次性改成严格类型,很可能会产生大量问题。
原因并不是 strict_types 有问题,而是旧代码长期依赖了 PHP 的动态特性。
例如:
function getPrice($value)
{
return $value * 1.13;
}
这个函数可能被不同地方调用:
getPrice(10);
getPrice("10");
getPrice(null);
getPrice(false);
如果直接加入:
function getPrice(float $value): float
再配合严格模式,就可能暴露大量原来被隐式转换掩盖的调用问题。
这种情况下更适合渐进式迁移。
新代码首先使用严格类型。
修改旧模块时逐步增加参数和返回类型。
对数据库、HTTP、JSON、CLI 参数等外部输入建立明确的数据边界。
然后使用 PHPStan 或 Psalm 持续发现剩余类型问题。
PHPDoc仍然有价值
即使项目已经大量使用 PHP 原生类型声明,PHPDoc 仍然没有失去作用。
例如数组的结构:
/**
* @param array{id:int,email:string,active:bool} $user
*/
function processUser(array $user): void
{
// ...
}
原生 PHP 可以知道 $user 是数组,但不能完整表达所有复杂数组结构。
PHPStan、Psalm 等静态分析工具可以利用 PHPDoc 提供更加详细的信息。
因此现代 PHP 项目通常是原生类型声明与 PHPDoc、静态分析工具结合使用,而不是三选一。
strict_types不是性能优化功能
原文把 strict_types 与 PHP 7 JIT、类型推断以及性能损耗联系起来,这种说法容易让读者产生错误认识。
使用 strict_types=1 的主要目的不是优化 PHP 程序性能。
它的核心价值是明确类型契约、减少不必要的隐式转换以及让类型错误更早暴露。
在正常 SaaS 项目中,不应该把性能优化重点放在“strict_types 会不会让代码快几个百分点”。
真正影响 PHP 应用性能的通常是数据库查询、网络请求、缓存命中率、PHP-FPM worker、CPU、内存、I/O、序列化、第三方 API 以及应用本身的算法和架构。
如果项目存在实际性能问题,应该使用 profiling 工具测量,而不是根据是否启用 strict_types 猜测性能。
strict_types也不是安全机制
类型声明可以减少一部分逻辑错误,但不能把它当成安全防护系统。
它不能阻止 SQL 注入。
不能代替 CSRF Token。
不能代替 XSS 防护。
不能代替权限控制。
不能代替 SaaS 租户隔离。
不能代替身份认证。
不能阻止攻击者提交合法类型但恶意的数据。
例如:
function transferMoney(int $from, int $to, float $amount): void
这个函数的类型声明非常清晰。
但如果没有检查当前用户是否拥有 $from 账户的控制权,那么即使所有参数类型都正确,系统仍然可能存在严重的越权漏洞。
一个更合理的PHP项目规范
对于新开发的 PHP 8.x 项目,可以考虑把严格类型作为默认编码规范:
declare(strict_types=1);
然后逐步建立完整类型体系。
函数参数尽可能声明类型。
返回值尽可能声明类型。
类属性使用 typed properties。
可以使用联合类型和可空类型表达真实业务情况。
核心领域对象尽量避免大量 mixed。
公共 API 和服务层应该明确输入输出。
数据库和 HTTP 输入在边界处完成验证和转换。
再通过 PHPStan 或 Psalm 对整个项目进行静态分析。
这样形成的是一个完整的类型治理体系,而不是单独依赖 strict_types。
对于旧项目,则不应该为了追求形式上的“全项目严格模式”而一次性重构所有代码。
更实际的方式是从新模块、核心服务层和高风险业务开始。
随着测试覆盖率提高,再逐步增加类型声明。
如果某个旧函数确实需要接受多种类型,就应该明确表达这种需求,例如:
function normalizeId(int|string $id): int
{
// ...
}
而不是为了满足某种编码规范,强行把真实业务需求隐藏起来。
现代 PHP 的类型系统真正有价值的地方,不是让 PHP 变成一门完全静态类型语言,而是让开发者逐渐减少“这个变量应该是什么类型,全靠运行时才知道”的情况。
strict_types=1 是其中的一部分,但不是全部。
当原生类型声明、typed properties、联合类型、接口、返回类型、PHPDoc、PHPStan 或 Psalm、自动化测试以及明确的数据边界结合起来以后,PHP 项目才会逐渐形成可维护的类型契约。
对于小型脚本,这种规范可能显得有些正式;对于长期运行的 SaaS、支付系统、后台管理系统和大型 API 项目,明确类型带来的维护收益通常远远超过最初增加的代码工作量。
严格类型真正改变的,是开发者处理数据的方式:不是等 PHP 替你猜测这个值应该是什么,而是在代码边界明确告诉程序,这个函数接受什么、返回什么,以及哪些转换必须由开发者主动完成。
分享 Facebook |
X |
Whatsapp |
Linkedin
捐助(Paypal): https://www.paypal.me/observeccp
订阅中国观察电报 Telegram : https://t.me/s/ObserveCCP
责任编辑:雨轩 来源:中国观察 转载请注明作者、出处並保持完整。
今日热点:
近期焦点:
推荐阅读: