PHP 8.3: Typed Class Constants

Version8.3
TypeNew Feature

PHP 8.3 and later support declaring a type for PHP Class constants. This ensures type compatibility of the constants when child classes and interface implementation override them.

Prior to PHP 8.3, it was not possible to programmatically enforce type compatibility.

In PHP 8.3 and later, class constants can declare a type after the const keyword:

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

The necessity for types in class constants is in enforcing all subclasses that override class constants to not change the type of the constants.

When a class constant is declared with a type, PHP enforces it on the declaration itself and subclasses/implementations following covariance. Further, PHP does not coerce constant values, and is always considered strictly typed.

PHP fatal errors if a class constant is declared with a different type that the declaration:

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

The following are valid examples of class constants that use constant visibility modifiers (added in PHP 7.1), final class constants (added in PHP 8.1), and support for constants in traits (added in PHP 8.3). Further, constants declared in Enums also support adding types to them.


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';
}

Supported Types and Type Coercion

Class constants support standard PHP standalone types, nullable types, union types, intersection types, and DNF types.

The following types are not supported for class constant types:

  • void and never types: These types are only meant to be used as return types.
  • callable: This type is context-dependent, and is not supported in typed properties either. However, the Closure type is allowed.

The following declarations are not allowed:

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

Strict Typing Behavior

Regardless of the strict_types behavior declared in the PHP script, class constants are always evaluated strictly.

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

Similar to return types, class constant types can also be "narrowed", or kept the same in a sub-class or an implementation. This follows LSP.

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

If a class constant attempts to change or widen the declaration (which means they are incompatible), PHP emits a fatal error at compile time:

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

Omitting constant type declaration

One noteworthy point is that if a parent class declares a constant type, all sub-classes must also declare a compatible type. Omitting the class constant type is considered an incompatible type.

For example, in the snippet below, ChildClass::VALUE is declared without a type. Because ParentClass::VALUE constant is declared with a type, this causes a fatal error:

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

Declaring a type for non-provide class constants is a backward-incompatible change, because all sub-classes must also declare compatible types.

In PHP 8.3, Zip, SNMP, and Phar core extensions declare class constants, which is a breaking change.

Reflection API Changes

It is possible to retrieve the type of class constants using the Reflection API.

The ReflectionClassConstant class supports two additional methods in PHP 8.3:

class ReflectionClassConstant implements Reflector {
    //...

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

The hasType() method returns whether the constant is declared with a constant.

The getType() method returns null if the class constant is declared with no type, or a ReflectionType object if a type is declared.

Usage example

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

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

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

Related Changes

Backward Compatibility Impact

Class constant type syntax is a backward incompatible change. PHP code that use class constant types do not compile in PHP at all, and results in a Parse error.

Note that declaring a class constant type in a PHP class can be an API-incompatible change, because all sub-classes must also declare the constant type, or PHP emits a fatal error otherwise.


RFC Discussion Implementation