rfc:typed_class_constants

This is an old revision of the document!


PHP RFC: Typed class constants

Introduction

Despite the huge efforts put into improving the type system of PHP year after year, it is still not possible to declare constant types. This is less of a concern for global constants, but can indeed be a source of bugs and confusion for class constants:

By default, child classes can override class constants of their parents, so theoretically speaking, it's a mistake to make assumptions about class constant values and types, unless either their class or the constants themselves are final:

interface I {
    const TEST = "Test";  // We may naively assume that I::TEST is a string
}
 
class Foo implements I {
    const TEST = [];      // But it may be an array...
}
 
class Bar extends Foo {
    const TEST = null;    // Or null
}

However, it would be useful in some cases to restrict the type of class constants without making them final. And that's when typed class constants may help.

Proposal

This RFC proposes to add support for declaring class and interface constant types:

interface I {
    const string TEST = "Test";   // I::TEST really is a string now
}
 
class Foo implements I {
    const string TEST = "Test2";  // Foo::TEST must also be a string
}

Supported types

Class constant type declarations support all type declarations supported by PHP, with the exception of void, callable, never, as well as self and static.

The void, callable types are not supported due to the issues discussed in the typed properties v2 RFC. Similarly to the previous types, never is not applicable in the context of constants.

On the other hand, the static type is not supported due to technical limitations: if a class A has a class constant B of type static which references a global constant C of type A (or any other child class) then it's not possible to resolve the types since they reference each other.

class A {
    public const self CONST1 = C;
}
 
 
define("C", new A()); // Error: Undefined constant "C"

The same problem arises with the self type and when the class itself (A) is referenced.

Strict and coercive typing modes

The strict_types mode has no impact on the behavior since type checks are always performed in strict mode. This is consistent with the default value handing of typed properties.

Inheritance and variance

Class constants are covariant. This means that the type of a class constant is not allowed to be widen during inheritance. If the parent class constant is private, then the type may change arbitrarily. Besides the exceptions mentioned in the previous section, all other types are supported, including union, intersection, as well as DNF types. Some examples:

class Test {
	private const int A = 1;
	public const mixed B = 1;
	public const int C = 1;
	public const Foo|Stringable|null D = null;
}
 
class Test2 extends Test {
	// This is legal since Test::A is private
	public const string A = 'a';
 
	// This is legal since int is a subtype of mixed
	public const int B = 0;
 
	// This is illegal since mixed is a supertype of int
	public const mixed C = 0;
 
	// This is legal since Foo&Stringable is more restrictive than Foo|Stringable
	public const (Foo&Stringable)|null D = null;
}
 
class Foo implements Stringable {
    public function __toString() {
        return "";
    }
}

The reason why class constant types are covariant is that they are read-only. The change from int to mixed implies that reads from the class constant may now return values of any type in addition to integers.

Constant values

Constant values have to match the type of the class constant. The only exception is that float class constants also accept integer values, consistently with the handling of parameter/property types.

The following code illustrates legal and illegal constant values:

class Test {
	// this is legal
	public const string A = 'a';
	public const int B = 1;
	public const float C = 1.1;
	public const bool D = true;
	public const array E = ['a', 'b'];
 
	// this is legal
	public const iterable F = ['a', 'b'];
	public const mixed G = 1;
	public const string|array H = 'a';
	public const int|null I = null;
 
	// this is legal (special exception)
	public const float J = 1;
 
	// this is illegal
	public const string K = 1;
	public const bool L = "";
	public const int M = null;
}

If the constant value is a non compile-time evaluable initializer expression, it is not checked at compile-time. Instead, it will be checked during constant-updating, which will either occur when an object of the class is instantiated or when the class constant is being fetched. As such, the following code is legal:

class Test {
    public const int TEST1 = C;
}
 
define('C', 1);
 
// this prints 1
echo Test::TEST;

If the constant has an illegal type, a TypeError exception is thrown during the object new Test() instantiation or when the class constant Test::TEST is being fetched.

Reflection

The ReflectionClassConstant class is extended with two methods:

class ReflectionClassConstant implements Reflector {
	...
 
	public function getType(): ?ReflectionType {}
	public function hasType(): bool {}
}
  • getType() returns a ReflectionType if the class constant has a type, and null otherwise.
  • hasType() returns true if the class constant has a type, and false otherwise. The behavior matches that of getType()/hasType() for parameters/properties and getReturnType()/hasReturnType() for return types.

Backwards incompatible changes

None.

Impact on extensions

None.

To preserve backwards compatibility with extensions, a new function zend_declare_typed_class_constant() is introduced while keeping the original zend_declare_class_constant_ex() function intact.

Vote

Add support for typed class constants?
Real name Yes No
alcaeus (alcaeus)  
ashnazg (ashnazg)  
beberlei (beberlei)  
bmajdak (bmajdak)  
bukka (bukka)  
crell (crell)  
dams (dams)  
galvao (galvao)  
gasolwu (gasolwu)  
girgias (girgias)  
ilutov (ilutov)  
jbnahan (jbnahan)  
kalle (kalle)  
kguest (kguest)  
kocsismate (kocsismate)  
marandall (marandall)  
mcmic (mcmic)  
nicolasgrekas (nicolasgrekas)  
petk (petk)  
pierrick (pierrick)  
ramsey (ramsey)  
sergey (sergey)  
theodorejb (theodorejb)  
thorstenr (thorstenr)  
zeriyoshi (zeriyoshi)  
Final result: 25 0
This poll has been closed.
rfc/typed_class_constants.1676493139.txt.gz · Last modified: 2023/02/15 20:32 by kocsismate