This is an old revision of the document!
PHP RFC: Typed Class Constants
- Date: 2020-07-06
- Author: Benas Seliuginas benas.molis.iml@gmail.com, Mark Niebergall mbniebergall@gmail.com
- Target version: PHP 8.2
- Status: Under Discussion
Introduction
Class constants provide a way to define static values on a class. Unlike class properties, class constants cannot be typed, and have no inheritance or ways to ensure extending or implementing classes have specific constants.
This RFC is proposing typed class constants along with inheritance to ensure concrete classes have contracted constants.
Proposal
Under this RFC, code like:
class Table { protected const TABLE_NAME = 'Test'; ... public function delete(): void { if (!is_string(static::TABLE_NAME)) { throw new Exception('Type of TABLE_NAME must be string'); } $this->database->delete(static::TABLE_NAME); } }
...might be written as:
class Table { protected const string TABLE_NAME = 'Test'; ... public function delete(): void { $this->database->delete(static::TABLE_NAME); } }
...without sacrificing any type-safety.
Supported types
Class constant type declarations support all type declarations supported by PHP, with the exception of void
, callable
, object
, and class names.
Class types (including self
, static
and parent
) are not supported because it is not useful and would be performance expensive. For PHP, all objects are mutable. Since constants should never change at runtime but objects can, object
and class types are not supported. Thus, the following examples are not allowed:
class Test { // this is illegal (because type is object) public const object A = 1; // this is illegal (because type is a class name) public const self B = 1; }
Meanwhile never
, void
, and callable
types are not supported due to the same issues as discussed in the typed properties v2 RFC.
The full list of proposed supported constant types are:
- array
- bool
- int
- float
- null
- string
Class constants will also support all union types, as long as each union type is one of the supported types. For example, these would all be valid:
- array|bool
- int|float
- string|null
Class constants will also support the nullable syntax:
- ?array
- ?bool
- ?int
- ?float
- ?string
The unsupported constant types will be:
- object
- callable
- iterable
- resource
Typed class constants will not support class keywords:
- self
- parent
- static
Strict and coercive typing modes
The strict_types
mode has no impact on behavior since class constants are immutable. The type check will always be performed when a constant is typed. This is consistent with the handling of typed property default values.
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.
class Test { private const int A = 1; public const mixed B = 1; public const int C = 1; } class Test2 extends Test { // this is legal (because Test::A is private) public const string A = 'a'; // this is legal public const int B = 0; // this is illegal public const mixed C = 0; }
The reason why class constant types are covariant is that they are read only i. e. declared once. The change from int
to mixed
implies that reads from the class constant may now return values of any type in addition to integers.
Class constants will be allowed to be declared with no value in abstract classes and interfaces. Concrete classes extending the abstract class or implementing an interface with a declared constant with no value must set a value.
For example, an abstract and concrete class may use this pattern:
abstract class Bird { public const bool CAN_FLY; public const string FAMILY; protected bool $isExtinct; } interface Swim { public const string PROPULSION; } final class EmperorPenguin extends Bird implements Swim { public const bool CAN_FLY = false; public const string FAMILY = 'penguin'; pubic const string PROPULSION = 'wings and webbed feet'; protected bool $isExtinct = false; }
Constant values
Constant values have to match the type of the class constant. The only exception is that float class constants also accept integer constant values, consistent with the handling for 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; public const ?int J = null; public const ?int K = 2; // this is legal (special exemption) public const float L = 1; // this is illegal public const string M = 1; public const int N = null; }
If the constant value is a non compile-time evaluable initializer expression, the constant value 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 SomeThing { public const string NAME = 'Widget'; } class Test { public const int TEST = TEST; public const string SOME_THING_NAME = SomeThing::NAME; } define('TEST', 1); // this prints 1 echo Test::TEST;
If the constant held an illegal type, a TypeError
exception would be generated during the object new Test()
instantiation or when the class constant Test::TEST
is being fetched.
Reflection
The ReflectionClassConstant
class is extended by 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.