rfc:fetch_property_in_const_expressions

PHP RFC: Fetch properties of enums in const expressions

Introduction

This RFC proposes to allow the use of ->/?-> to fetch properties of enums in constant expressions. The primary motivation for this change is to allow fetching the name and value properties in places where enum objects aren't allowed, like array keys (see https://github.com/php/php-src/issues/8344). There is currently no way to express this without repeating the value of the enum case.

enum A: string {
    case B = 'B';
    // This is currently not permitted
    const C = [self::B->value => self::B];
}

Proposal

This RFC proposes to allow the use of -> to fetch properties of enums inside all constant expressions. Using -> on all other objects will result in a runtime error.

Here are a few examples of code that will become valid if this RFC is accepted:

enum E: string {
    case Foo = 'foo';
}
 
const C = E::Foo->name;
 
function f() {
    static $v = E::Foo->value;
}
 
#[Attr(E::Foo->name)]
class C {}
 
function f(
    $p = E::Foo->value,
) {}
 
class C {
    public string $p = E::Foo->name;
}
 
// The rhs of -> allows other constant expressions
const VALUE = 'value';
class C {
     const C = E::Foo->{VALUE};
}

For the sake of completeness, this RFC also adds support for the nullsafe operator ?->.

Semantics

The semantics of -> in constant expressions are identical to outside of constant expressions, including access on non-object warnings, undefined property warnings, etc. One exception is that in constant expressions the operators may only be used on enums. The rationale for this decision is described in Supporting all objects.

enum E {
    case Foo;
}
 
class A {}
 
// Warning: Undefined property: E::$c
const C = E::Foo->c; // NULL
// Note that this will change to an error in PHP 9 https://wiki.php.net/rfc/undefined_property_error_promotion
 
// Warning: Attempt to read property "e" on null
const C = (null)->e; // NULL
 
// Warning: Attempt to read property "" on null
// Fatal error: Uncaught Error: Object of class A could not be converted to string
const C = (null)->{new A};
 
// Error: Fetching properties on non-enums in constant expressions is not allowed
const C = (new A)->foo;

Likewise, the semantics of the nullsafe operator ?-> are identical to outside of constant expressions. If the left hand side of the operator is null, the expression returns null, the right hand side is not evaluated and the entire chain is short-circuited.

const C = (null)?->prop; // NULL
const C = (null)?->prop1->prop2; // NULL
const C = (null)?->{new NotEvaluated}; // NULL
const C = (null)?->prop['foo']; // NULL

Supporting all objects

A previous version of this RFC allowed fetching properties on all objects, not just enums. There are two primary reasons support for all object was dropped.

  1. Order of evaluation
  2. Caching of constant expression values

Once we have a solution for these two problems we can extend support of -> in constant expressions to all objects.

Order of evaluation

This problem was previously described in New in initializers RFC - unsupported positions.

New expressions continue to not be supported in (static and non-static) property initializers and class constant initializers. The reasons for this are twofold:

For non-static property initializers, the initializer expression needs to be evaluated on each object creation. There are currently two places where this could happen: As part of object creation, and as part of the constructor call. Doing this as part of object creation can create issues for unserialization and any other process that is based on newInstanceWithoutConstructor() and does not want to implicitly execute potential side-effects.

Performing the initialization by injecting code in the constructor avoids the issue, but requires that constructor to actually be called. In particular, this would necessitate generating constructors for classes that do not explicitly declare them, and the disciplined invocation of such constructors from potential child constructors. The third option would be to introduce an additional initialization phase between creation and construction.

For static property initializers and class constant initializers a different evaluation order issue arises. Currently, these initializers are evaluated lazily the first time a class is used in a certain way (e.g. instantiated). Once initializers can contain potentially side-effecting expressions, it would be preferable to have a more well-defined evaluation order. However, the straightforward approach of evaluating initilizers when the class is declared would break certain existing code patterns. In particular, referencing a class that is declared later in the same file would no longer work.

As such support in these contexts is delayed until such a time as a consensus on the preferred behavior can be reached.

The -> operator suffers from the same problem as it may invoke the __get magic method with arbitrary side-effects. I incorrectly assumed this wasn't possible because __get can only be invoked in combination with new (as enums don't allow __get) which are disallowed in these contexts. This assumption was incorrect though as the LHS of the -> operator might be another constant which might be an object.

class Foo {
    public function __get(string $name) {
        echo "Side effect!\n";
    }
}
 
const FOO = new Foo();
 
class C {
    const C = FOO->bar;
}

Caching of constant expression values

The engine caches the result of default arguments between function calls to avoid re-evaluation if result is not ref-counted [1]. This works nicely because there is currently no way to produce a non-pure, non-ref-counted result in a constant expression. This would no longer be true if allowing -> on all objects in constant expressions.

class Foo {
    public function __get(string $name) {
        return rand(0, 100);
    }
}
 
function test($bar = (new Foo)->bar) {
    var_dump($bar);
}

We could remove the caching mechanism from function default arguments but that would negatively hurt performance for a small edge-case.

Similarly, property initializers are currently only evaluated once per class, and then the result is copied when an instance of the given class is created. Allowing side-effects and non-pure results means we must re-evaluate the property initializer once per instance.

[1] Ref-counted values consist of non-interned strings, arrays, resources and objects.

Allow all readonly properties

It was suggested that instead of restricting -> to be used on all non-enums to restrict access to just non-readonly properties. Unfortunately, this doesn't solve the caching problem described above as readonly properties can be initialized after object construction and thus becomes non-pure. The enum name and value properties are automatically initialized by the engine and thus are truly immutable.

class Foo {
    public readonly string $bar;
 
    public function init() {
        $this->bar = 'bar';
    }
}
 
const FOO = new Foo();
function test($bar = FOO->bar ?? 'default') {}
 
test();
FOO->init();
test();

Backward Incompatible Changes

There are no backwards-incompatible changes in this RFC.

Alternative approaches

Alternatively, arrays could be extended to allow enums or all objects as keys. This is the approach of the object keys in arrays RFC. While this RFC still might be worthwhile it comes with significant API changes in the engine and it does break the assumption in userland code that array keys are of type int|string.

Terminology

Note that the term “constant expression” in this RFC refers to any expression that isn't directly compiled to OpCodes but instead stored as a constant AST which is evaluated at run-time. Since allowing new in these expressions the result is no longer guaranteed to be pure or constant.

Vote

Voting opened on xxx and closes on xxx.

Add support for fetching properties of enums in constant expressions?
Real name Yes No
Final result: 0 0
This poll has been closed.
rfc/fetch_property_in_const_expressions.txt · Last modified: 2022/06/23 11:52 by ilutov