编程

PHP 8.3: 类常量显式类型

1034 2023-10-20 15:18:00

PHP 8.3 及之后的版本支持对 PHP 类常量进行类型声明。这样可以在子类和接口实现重写常量时,保证常量的类型兼容。

PHP 8.3 之前,无法以编程方式强制执行类型兼容性。

在 PHP 8.3 及其之后,类常量可以在 const 关键词之后声明类型:

class Test {
    const string TEST_CONSTANT = 'test';
}

类常量中类型的必要性在于强制所有重写类常量的子类不更改常量的类型。

当用类型声明类常量时,PHP 会在声明本身和协方差之后的子类/实现上强制执行它。此外,PHP 不强制常量值,并且总是被认为是严格类型化的。

如果类常量值使用与声明不同的类型声明的,则会出现PHP致命错误:

class Test {
    const string CONST = 1;
}
Fatal error: Cannot use int as value for class constant Test::CONST of type string

以下是使用可见性修饰符(在PHP 7.1 中添加)的类常量、final 类常量(在 PHP 8.1 中添加)、以及 trait 中支持常量(PHP 8.3 中添加)的有效示例。此外,枚举中的常量声明也支持添加类型。 

Classes with typed constants

class Test {
    // public constant with "string" type
    const string FOO = 'test';

    // protected constant with "string" type
    protected const string GARPLY = 'test';

    // final protected constant with "string" type
    final protected const string WALDO = 'test';
}

Traits with typed constants

trait TestTrait {
    // final protected constant with "string" type
    final protected const string WALDO = 'test';
}

Interfaces with typed constants

interface TestInterface {
    public const string TEST = 'test';
}

Enums with typed constants

enum TestEnum: string {
    public const string TEST = 'test';
}

支持类型及类型强制

类常量支持标准的 PHP 独立类型、nullable 类型、联合类型、交集类型和 DN F类型。

以下是类常量不支持的类型:

  • voidnever 类型:这些类型只能用作返回类型。
  • callable: 此类型依赖于上下文,类型化属性也不支持此类型。但是,允许使用 Clouse 闭包类型。

以下声明不被允许:

class Test {
    const void FOO = 'test';
    const never FOO = 'test';
    const callable FOO = 'test';
}
Fatal error: Class constant Test::FOO cannot have type void
Fatal error: Class constant Test::FOO cannot have type never
Fatal error: Class constant Test::FOO cannot have type callable

严格类型行为

无论 PHP 脚本中如何声明的 strict_types 行为,类常量总是严格计算的。

declare(strict_types=0);

class Test {
    const string CONST = 1;
}
Fatal error: Cannot use int as value for class constant Test::CONST of type string in /mnt/w/localhost/test/test.php on line 4

Variance

类似于返回类型,类常量类型也可以”变窄“,或者在子类或实现中保持一致。这遵循 LSP。

class ParentClass {
    public const string|int VALUE = 'MyValue';
}
class ChildClass extends ParentClass {
    public const string VALUE = 'MyValue';
}

如果类常量尝试修改或者将声明变宽(这意味着不兼容),PHP 会在编译时发出致命错误:

class ParentClass {
    public const string|int VALUE = 'MyValue';
}
class ChildClass extends ParentClass {
    public const string|int|float VALUE = 'MyValue';
}
Fatal error: Type of ChildClass::VALUE must be compatible with ParentClass::VALUE of type string|int

忽视类常量声明

一个值得注意的点是,如果父类声明了一个常量类型,那么所有子类也必须声明一个兼容的类型。忽视类常量类型被认为是不兼容的类型。

比如,在下面的代码中,ChildClass::VALUE 没有声明类型。因为  ParentClass::VALUE 常量声明了类型,这将导致致命错误:

class ParentClass {
    public const string|int VALUE = 'MyValue';
}
class ChildClass extends ParentClass {
    public const VALUE = 'MyValue';
}
Fatal error: Type of ChildClass::VALUE must be compatible with ParentClass::VALUE of type string|int

为不提供类常量声明的常量声明类型是向后不兼容的更改,因为所有子类也必须声明兼容的类型。

在 PHP 8.3 中,Zip、SNMP 和 Phar 核心扩展中声明常量,也是破坏性变更。

反射 API 变更

可以使用反射 API 检索类常量的类型。

ReflectionClassConstant 类支持 PHP 8.3中的两个附加方法:

class ReflectionClassConstant implements Reflector {
    //...

    public function getType(): ?ReflectionType {}
    public function hasType(): bool {}
}

hasType() 方法返回常量是否声明了类型。

如果类常量不使用类型声明,getType() 方法将返回 null;如果声明了类型,将返回 ReflectionType 对象。

示例

class Test {
    const string MY_CONST = 'MyConst';
}

$reflector = new ReflectionClassConstant('Test', 'MY_CONST');

$reflector->hasType(); // true
$reflector->getType(); // "string"

向后兼容性影响

类常量类型语法是向后不兼容的变更。使用类常量类型的 PHP 代码在 PHP 中不会编译,并且会导致解析错误。

请注意,在 PHP 类中声明类常量类型可能会是 API 不兼容的改变,因为所有子类也必须声明类常量类型,否则 PHP 会发出致命错误。