rfc:write_once_properties

This is an old revision of the document!


PHP RFC: Write-Once Properties

Introduction

This RFC proposes to add support for a new property modifier that would allow properties to be initialized, but not modified afterwards. This feature would be useful in situations where one wants to guarantee that a property remains the same for the lifetime of an object - which is usually the case for Value Objects or Data Transfer Objects. Other languages, like Java and C# also have similar - but not exactly the same - concepts for a long time (final and readonly respectively).

Proposal

“Write-once” properties in PHP (the actual keyword is to be decided) could be initialized either by an explicit default value, or by an assignment operation. Contrary to how final properties in Java work, this RFC proposes to allow the initialization of object properties after object construction. The main purpose of choosing this approach is to make lazy loading possible - which is an important aspect for many PHP applications. In addition to object properties, class properties can also use the modifier in question with the same rules.

As soon as initialization is done, any other attempt to assign a value to “write-once” properties results in an exception. Besides assignment, the increment, decrement, and unset operations are also forbidden. As arrays are an immutable data structure in PHP, any attempt to mutate a property of array type (adding/removing/changing items) is forbidden. However, properties of object or resource types still remain mutable internally (see example below). In order to avoid possible problems, references on “write-once” properties are forbidden as well.

class Foo
{
    <keyword> public int $a = 1;
    <keyword> public string $b;
    <keyword> public array $c = ["foo"];
    <keyword> public object $d;
 
    public function __construct()
    {
        $this->b = "foo";
    }
}
 
$foo = new Foo();
 
$foo->a = 2;		// EXCEPTION: property a has already been initialized
$foo->b = "bar";	// EXCEPTION: property b has already been initialized
$foo->a++;		// EXCEPTION: incrementing/decrementing is forbidden
unset($foo->c);		// EXCEPTION: unsetting is forbidden
$foo->c[] = "bar";	// EXCEPTION: array values can't be modified
next($foo->c);		// EXCEPTION: internal pointer of arrays can't be modified as well
$var = &$this->c;	// EXCEPTION: reference isn't allowed
$this->c; = &$var;	// EXCEPTION: reference isn't allowed
 
key($foo->c);		// SUCCESS: internal pointer of arrays is possible to read
$foo->d = new Foo();	// SUCCESS: property d hasn't been initialized before
$foo->d->foo = "foo";	// SUCCESS: objects are still mutable internally

As untyped properties have an implicit default value (null) in the absense of an explicit one, their usefulness would be very limited. In order to avoid the introduction of unintiutive workarounds, this RFC proposes to disable the property modifier in question for them. Contrarily to untyped properties, typed properties are in uninitialized state by default, so they play well with the write-once semantics.

Furthermore, cloning objects with “write-once” properties is supported, in which case properties remain in the state (initialized/uninitialized) they were before. In other words, once a “write-once” property is initialized, it can't be changed after cloning. If you still want to modify them, you have to create a new object. Although this behaviour might be inconvenient in some cases, I decided not to address the problem in the current RFC in order to reduce its scope. The solution could be to add support for either object initializers or property mutation during the clone operation. As these features are neither a prerequisite for “write-once” properties, nor a trivial problem, it's better to properly discuss the question on its own.

At last, I'm proposing to add a new method for ReflectionProperty with which it would be possible to retrieve if a property has the modifier in question. Depending on the keyword choice, I'd suggest using the isImmutable(), isLocked(), isReadonly(), or isWriteonce() method names.

Alternative Approaches

As there are quite a few alternatives to implement a similar feature, I would like to highlight why the current one was chosen. Please find below a short evaluation about the various possibilities that were also considered, but got rejected.

Read-only semantics

This is the implementation that Java and C# both use. It has really clear rules: a final or readonly property has to be initialized before object construction ends by assigning value to them exactly once, and no further changes are allowed afterwards. On the plus side, we can always be sure that a property has a value, but the downside is that lazy initialization is not possible anymore with this approach. Apart from the (unnecessarily) strict behaviour, another problem is that this implementation is hardly applicable in PHP where object construction is a “fuzzy” term.

Write-before-construction semantics

According to this idea, a property could be assigned to multiple times before object construction ends, and no further changes would be allowed from that point on. Even though this approach makes it easier to deal with bigger class hierarchies (in which case it's likely that multiple constructors are involved in object creation, increasing the chance of assigning to the same property multiple times), it also has the same disadvantages as the read-only approach.

Property accessors

Although actually “write-once” properties and property accessors are orthogonal to each other, it's arguable whether we still needed “write-once” properties if we had property accessors. The case against having both features is that property accessors can alone prevent unwanted or unintended modifications while guaranteeing read access to the properties. The only problem with solely relying on property accessors is that they can't prevent changes in the private/protected scope (depending on visibility). Furthermore, there are quite a few easy, but admittedly esoteric ways to circumvent visibility protections (see my examples at the following link: https://3v4l.org/fNTRa). This is the reason why we currently don't have any way to ensure the immutability of a property - and property accessors wouldn't change this fact.

Open Questions

As there is no consensus about the name of the modifier, I'd like to put it to vote. You can find below the ideas that came up during discussion along with some pros/cons:

  • final: This keyword currently affects inheritance, but not mutability rules in PHP, thus a final property modifier would be confusing in this form
  • sealed: This keyword affects inheritance rules in other languages (e.g. in C#), thus it is also not a good candidate in our case
  • immutable: It's a descriptive name which sounds well, although it's also a little bit misleading since its usage with mutable data structures (objects, resources) are not restricted in any way
  • locked: Although locked sounds well, it's a little bit vague term that doesn't tell much about the feature. But at least it's not misleading.
  • writeonce: It's the most technically accurate name, however, it sounds exotic, and it might be confusing from the end-user perspective, as they are generally not expected to write these properties
  • readonly: Although it's not as technically accurate as “writeonce”, it represent the feature perfectly for end-users who are expected to only read these properties. C# also uses the same term for a similar purpose.

Considering the above, immutable, locked, writeonce, and readonly is going to be proposed as voting choices of the decision about the keyword.

Backward Incompatible Changes

There are no backward incompatible changes in this proposal except for the fact that immutable, locked, writeonce, or readonly would become a reserved keyword depending on the outcome of the secondary vote.

Future Scope

Adding support for “write-once” properties would lay the groundwork for immutable objects - for which I'm going to create a proposal should the current RFC be accepted. I also plan to address the problem with cloning mentioned in the proposal section.

Proposed Voting Choices

The primary vote (“Do you want to add support for the proposed property modifier?”) requires 2/3 majority, while the secondary one (“Which keyword to use”?) requires a simple majority.

References

Prior RFC proposing immutable properties: https://wiki.php.net/rfc/immutability

rfc/write_once_properties.1583273115.txt.gz · Last modified: 2020/03/03 22:05 by kocsismate