Table of Contents

PHP RFC: Readonly hooks

Introduction

Support for hooks on readonly properties was omitted from the original RFC, primarily to minimize complexity as there were questions around when it was safe to do. On further consideration, we believe that hooks on backed properties are sufficiently safe to support readonly, but not on virtual properties.

Proposal

We propose to allow both get and set hooks on readonly properties, if and only if it is a backed property.

The main concern is that readonly implies a property is immutable and idempotent. However, a get hook supports arbitrary code, so technically a developer could do something silly like:

class Dumb
{
    public readonly int $value { get => $this->value * random_int(1, 100); }
}

On the other hand, there is no shortage of dumb things that people can do with PHP already. The exact same silliness could be implemented using __get, for instance. However, there are valid uses for a readonly get hook, especially for ORMs and proxies. For example:

readonly class ProductFromDB
{
    public string $id;
 
    public function __construct(
        public string $name,
        public float $price,
        public Category $category,
    ) {}
}
 
// Generated code.
readonly class LazyProduct
{
    // Assigned via reflection or a closure.
    private DbConnection $dbApi;
 
    // Assigned via reflection or a closure.
    private string $categoryId;
 
    public Category $category {
        get {
            $this->category ??= $this->dbApi->loadCategory($this->categoryId);
        }
    }
}

That is, we feel, an entirely reasonable use of hooks, and would allow for lazy-load behavior per-property on readonly classes.

This is subtly different from the Lazy Proxy RFC, which operates on the whole object at once. We believe both use cases are valuable and should be supported.

A set hook, meanwhile, offers no issue for a backed readonly property. As long as it is backed we are able to determine if it is still uninitialized, and so a second set call would correctly fail as it should.

At this time, Ilija is still investigating whether it is more sensible to put the uninitialized check on the wrapping property, when visibility is checked, or only on the backing property, so the error would be thrown from within the hook body. In practice it will not make much difference to the developer, so we consider either case acceptable depending on what ends up being easier to do.

On balance, we believe the advantages and use cases for a lazy readonly property outweigh the potential for developers to do silly things. For that reason, we propose to allow both get and set hooks on backed readonly properties.

Backward Incompatible Changes

What breaks, and what is the justification for it?

Proposed PHP Version(s)

List the proposed PHP versions that the feature will be included in. Use relative versions such as “next PHP 8.x” or “next PHP 8.x.y”.

RFC Impact

Proposed Voting Choices

Include these so readers know where you are heading and can discuss the proposed voting options.

Patches and Tests

Implementation

After the project is implemented, this section should contain

  1. the version(s) it was merged into
  2. a link to the git commit(s)
  3. a link to the PHP manual entry for the feature
  4. a link to the language specification section (if any)

References