Skip to content

Feature request: Allow unsetting of backed property hooks #17922

Open
@acabal

Description

@acabal

Description

(I hope this is the right place to suggest this, if not I'm happy to move it elsewhere.)

In the new property hooks feature of PHP 8.4, I think it would be very useful to be able to unset() backed properties.

Consider the following object, a forums post with a user object representing the poster which is lazy-loaded with a getter:

<?php
class ForumsPost{
    public int $UserId;
    public User $User{
        get{
            if(!isset($this->User)){
                $this->User = Db::GetUser($this->UserId);
            }

            return $this->User;
        }
    }
}

$fp = new ForumsPost();
$fp->UserId = 1;
print($fp->User->UserId); // `User` is retrieved from the database and `1` is printed.

Now suppose we have already retrieved the User property, and later we change the UserId. Now the UserId property and the User property are now out of sync. What if we could add a setter hook for the UserId property, to unset() any cached User object at the moment when we set the UserId? For example:

<?php
class ForumsPost{
    public int $UserId{
        set(int $value){
            // Set the new `UserId`...
            $this->UserId = $value;

            // And unset any cached `User` we may have fetched earlier...
            unset($this->User); // Error! Not allowed in PHP 8.4, but natural to think about and very convenient!
        }
    }

    public User $User{
        get{
            if(!isset($this->User)){
                $this->User = Db::GetUser($this->UserId);
            }

            return $this->User;
        }
    }
}

$fp = new ForumsPost();
$fp->UserId = 1;
print($fp->User->UserId); // `User` is retrieved from the database and `1` is printed.

$fp->UserId = 2;
print($fp->User->UserId); // Expected to print `2`, but actually is an error because we cannot `unset()` the backed `User` property.

The current state of affairs means that if we want to return a backed property to a "not initialized" state, we have to use kludges like making it a nullable type and calling null "not initialized" by convention, or setting it to say an empty string and calling that "not initialized" by convention. Both of these are poor, semantically wrong solutions, especially considering it's already allowed to unset() plain object properties directly.

Additionally, the consumer of an object might find the following behavior surprising:

<?php
$fp = new ForumsPost();
unset($fp->UserId); // Success!
unset($fp->User); // Error! Huh, why? (`$User` is a property hook...)

The consumer of an object should not need to know the internal implementation of properties. In this example it is well-established in PHP that we can unset() object properties, but since plain properties and property hooks are indistinguishable to the consumer, unset() on a property can result in an error if the internal implementation is a property hook. This is surprising to the consumer, who doesn't and shouldn't care how the property is implemented internally.

In the RFC for property hooks, unset() is explicitly disallowed because "it would involve bypassing any set hook that is defined". But I'm not sure why unset()ing something would invoke the setter.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions