rfc:typed_class_constants

PHP RFC: Typed Class Constants

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.

Vote

Add support for typed class constants?
Real name Yes No
Final result: 0 0
This poll has been closed.

Implementation

rfc/typed_class_constants.txt · Last modified: 2022/03/30 12:50 by mbniebergall