编程

PHP 8.2 新特性 — 支持在 trait 中声明常量

1258 2022-11-14 15:17:38

PHP Traits 使得 PHP 类和枚举可以在不使用继承链、接口和抽象类的方式下,重用代码。Trait 类似于类,它们都支持属性、方法、自动加载、魔术常量和其他PHP语法。 

Trait 用在 PHP 类中。当类中使用 Trait 时,trait 中所声明的所有方法和属性对 PHP 类都是可用的。

不过,在 PHP 8.2 之前,trait 中是不能声明常量的。在 trait 尝试声明常量会在编译时出现致命错误。

trait FooBar {
    const FOO = 'bar';
}
Fatal error: Traits cannot have constants in ... code on line ...

虽然 trait 不允许声明常量,它还是能访问 PHP 类中的常量的。这导致了不完整泄露实现,因为 trait 没办法保证使用 trait 的类会声明引用的常数,而且也没办法在代码上强制它实现。

trait VersionDependent {
    protected function ensureVersion(): void {
        if (self::CURRENT_VERSION < self::MIN_VERSION) {
            throw new Exception('Current version is too old');
        }
    }
}

上例中的 VersionDependent 假定使用这个 triat 的类会声明 CURRENT_VERSION 和 MIN_VERSION 常量,但是对 VersionDependent 而言,没办法确保这些常量会被声明。

自 PHP 8.2 起,可以在 trait 中声明常量。Trait 常量也可以用可见修饰符 final 声明(PHP 8.1 起)。以下的常量声明在 PHP 8.2 及以后的版本中将是有效的:

trait FooBar {
    const FOO = 'foo';
    private const BAR = 'bar';
    final const BAZ = 'baz';
    final protected const QUX = 'qux';
}

class Test {
    use FooBar;
}

echo Test::BAZ; // 'bar'

并不强制要求组合的类一定要去声明 trait 已经声明的常量。

不过,组合类中声明的常数必须和 trait 中声明的常数兼容。 

重写 triat 常量

直接组成的类中不允许重写

在合成的类中,允许声明一个和 trait 中完全一样(trait 名, 可见性修饰符, 值)的常量:

trait FooBar {
    const FOO = 'foo';
    private const BAR = 'bar';
    final const BAZ = 'baz';
    final protected const QUX = 'qux';
}

class ComposingClass {
    use FooBar;

    const FOO = 'foo';
    private const BAR = 'bar';
    final const BAZ = 'baz';
    final protected const QUX = 'qux';
}

在 ComposingClass 中声明的 4 个常量,与 FooBar trait 兼容,因为它们完全一样,它们都有同样的可见度final 标志,还有最重要的是,也一样。

如果合成类声明了不兼容的常量,PHP 会在编译时抛出致命错误:

trait FooBar {
    const FOO = 'foo';
    private const BAR = 'bar';
    final const BAZ = 'baz';
    final protected const QUX = 'qux';
}

class ComposingClass {
    use FooBar;

    const FOO = 'zzz'; // Value is different.
    protected const BAR = 'bar'; // Visibility is different
    const BAZ = 'baz'; // Final flag is removed
}
Fatal error: ComposingClass and FooBar define the same constant (FOO) in the composition of ComposingClass. However, the definition differs and is considered incompatible. Class was composed in ... on line ...
Fatal error: ComposingClass and FooBar define the same constant (BAR) in the composition of ComposingClass. However, the definition differs and is considered incompatible. Class was composed in ... on line ...
Fatal error: ComposingClass and FooBar define the same constant (BAZ) in the composition of ComposingClass. However, the definition differs and is considered incompatible. Class was composed in ... on line ...

允许在衍生类中重写

如果常数没被声明成 final,合成类的子类可以重写常数

trait VersionDependent {

    protected const CURRENT_VERSION = '2.6';
    protected const MIN_VERSION     = '2.5';

    protected function ensureVersion(): void {
        if (self::CURRENT_VERSION < self::MIN_VERSION) {
            throw new Exception('Current version is too old');
        }
    }
}

class Application {
    use VersionDependent;
}

class MockApplication extends Application {

    protected const CURRENT_VERSION = '2.6-testing';
    protected const MIN_VERSION     = '2.4';

}

上面代码段中声明的所有常量都是有效的。

final 常量不能重写

如果 trait 中的常量用 final 声明,尝试重写会导致致命错误:

trait Test {
    final protected const FOO = 'foo';
}
class BaseContainer {
    use Test;
}
class ApplicationContainer extends BaseContainer{
    protected const FOO = 'bar';
}
Fatal error: ApplicationContainer::FOO cannot override final constant BaseContainer::FOO in ... on line ...

不允许直接访问 trait 常量

PHP trait 本意是用在类和 Enum 中,因此,不允许直接访问常量。下面的代码段会导致错误。

trait Test {
    public const FOO = 'foo';
}
echo Test::FOO;
constant('Test::FOO');
Error: Cannot access trait constant Test::FOO directly in ...:6

另外,defined 函数用来返回是否常量存在,当传入的是 trait 时会返回 false:

defined('Test::FOO'); // false

Trait 冲突

当一个类中有多个 triat 组成,而这些 trait 声明了同一个常数时,每个声明的值、可见度、final 关键字都必须相匹配。

trait Foo {
    final protected const ALPHA = 1;
}

trait Bar {
    final protected const ALPHA = 1;
}

class Test {
    use Foo;
    use Bar;
}

虽然两个 trait 都声明了 ALPHA 常量,因为它们的值、可见度和 final 与否都完全一样,组成的这个 Test 类便是合法的。尝试组成常量不匹配的类会导致编译时致命错误:

trait Foo {
    protected const ALPHA = 1;
    protected const BETA = 1;
    protected const GAMMA = 1;
}

trait Bar {
    protected const ALPHA       = 2; // Different value
    public const BETA           = 1; // Different visibility
    final protected const GAMMA = 1; // Different finality
}

class Test {
    use Foo;
    use Bar;
}
Fatal error: Foo and Bar define the same constant (ALPHA) in the composition of Test. However, the definition differs and is considered incompatible. Class was composed in ... on line ...
Fatal error: Foo and Bar define the same constant (BETA) in the composition of Test. However, the definition differs and is considered incompatible. Class was composed in ... on line ...
Fatal error: Foo and Bar define the same constant (GAMMA) in the composition of Test. However, the definition differs and is considered incompatible. Class was composed in ... on line ...

向下兼容性影响

PHP 8.2 以前的版本,trait 中不允许声明常量,因此已有的应用不会因此在 PHP 8.2 中出现问题。不过,声明过常量的 triat 无法在老版的 PHP 中运行,会导致致命错误:

trait FooBar {
    const FOO = 'bar';
}
Fatal error: Traits cannot have constants in ... code on line ...

这个特性没有办法用补丁的方式加到老版本中。