Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Property hooks do not call parent's magic __get and __set methods when referencing parent property #17542

Open
ericnorris opened this issue Jan 22, 2025 · 7 comments

Comments

@ericnorris
Copy link
Contributor

Description

I was playing around with property hooks and noticed the following (in my opinion) inconsistency when combining property hooks with inheritance and __get and __set:

The following code (for __get, the same applies for __set in https://3v4l.org/Q2OAj):

<?php

class ParentClass {
    public function __get(string $name) {
        echo "__get($name) was called\n";
    }
}

class ChildClass extends ParentClass {
    public $foo {
        get => parent::$foo::get();
    }
}

(new ParentClass)->foo;
(new ChildClass)->foo;

Resulted in this output:

__get(foo) was called

Fatal error: Uncaught Error: Undefined property ParentClass::$foo in /in/Y27mB:13

But I expected this output instead:

__get(foo) was called
__get(foo) was called

I tried searching the RFC and existing PHP issues, but I haven't found this example - apologies if I missed it. Barring something saying that this shouldn't work, I would expect it to.

PHP Version

PHP 8.4.3

Operating System

No response

@bwoebi
Copy link
Member

bwoebi commented Jan 22, 2025

parent::$prop::get() was meant to explicitly call a hook function. There's no hook function, but a __get function, which is not quite the same. To actually call these, parent::__get($prop) would be the right thing to do.

@iluuu1994
Copy link
Member

I agree. It's honestly not something that we explicitly considered, but I agree that parent::__get($prop); is a reasonable workaround. /cc @Crell If you agree, we can close this issue.

@ericnorris
Copy link
Contributor Author

Thanks for the quick response! The manual currently says (emphasis mine):

Accessing parent hooks
A hook in a child class may access the parent class's property using the parent::$prop keyword, followed by the desired hook.
...
If there is no hook on the parent property, its default get/set behavior will be used.

It also gives an example of referencing the parent's property (not hook):

<?php
class Strings
{
    public string $val;
}

class CaseFoldingStrings extends Strings
{
    public bool $uppercase = true;

    public string $val {
        get => $this->uppercase
            ? strtoupper(parent::$val::get())
            : strtolower(parent::$val::get());
    }
}
?>

So I don't see how this is parent-hook-specific, since you can access plain properties. And if you can access a plain property, you should be able to access a private or non-existent one via __get.

That said, if the workaround approach is preferred, I think at the least the error messaging could be changed. My mental model is that you won't see a Undefined property error with __get, which is part of what made this surprising. If parent::$prop::get is invalid here, maybe we could throw an error specifically about that?

@iluuu1994
Copy link
Member

If there is no hook on the parent property, its default get/set behavior will be used.

Right. This sentence implies that the parent declares such a property, but without a hook for the given operation, in which case we'll resort to accessing the properties backing store. The idea of this fallback is to allow adding hooks to child properties in a backwards compatible way, i.e. by avoiding direct access of the backing value that would break if the parent were to add a hook itself (or more precisely, it would circumvent the parents hook). __get/__set are outside of the scope of what we intended for this fallback.

I don't think your expectation for this to work is unreasonable, but it wasn't documented, and thus is technically a language change. I personally don't think we need this fallback, and it might not be quite straight forward to implement. But if you feel strongly about this topic, feel free to ask for opinions on the internals mailing list.

If parent::$prop::get is invalid here, maybe we could throw an error specifically about that?

Can you make a concrete suggestion?

@ericnorris
Copy link
Contributor Author

This sentence implies that the parent declares such a property, but without a hook for the given operation, in which case we'll resort to accessing the properties backing store.

I can see where you're coming from, but to me it just implies "it will try to fetch the property normally", where normally means "it will follow the existing property access patterns", and hence why I'd expect __get to work.

I don't think your expectation for this to work is unreasonable, but it wasn't documented [...] if you feel strongly about this topic, feel free to ask for opinions on the internals mailing list.

I don't think I feel too strongly, but yeah I might raise it there; I think it'd be nice to either be consistent, or more strongly indicate that this will only work for existing properties or hooks. On that note...

If parent::$prop::get is invalid here, maybe we could throw an error specifically about that?

Can you make a concrete suggestion?

This is tough, because in a sense there is an "Undefined property", it's just that if you were accessing this undefined property outside of the class, it would work. It's only here that it doesn't, so maybe something like:

Error: ParentClass::$foo::get() must refer to a defined property in /in/Y27mB:13

..."defined" here could also be "named", "explicit", or anything that makes it clear that this property must actually exist in some form.

I think I'd be satisfied with this, because we're calling out that this language construct (ParentClass::$foo::get()) has a requirement to refer to real properties, so this isn't an "undefined property error", it's a ParentClass::$foo::get() error. Does that make sense?

@Crell
Copy link
Contributor

Crell commented Jan 22, 2025

Mm, yeah, we never really thought about this case. I would not be opposed to falling back to ParentClass::__get() if it's easy enough to do. If it would be hard, I don't think it's a large issue in practice to just leave it. Ideally, in a hook-filled world __get gets exponentially less common anyway.

@iluuu1994
Copy link
Member

I can see where you're coming from, but to me it just implies "it will try to fetch the property normally", where normally means "it will follow the existing property access patterns", and hence why I'd expect __get to work.

Just to clarify:

If there is no hook on the parent property, its default get/set behavior will be used.

This section explicitly refers to a declared parent property. So, it's not really a matter of interpretation. However, the case where the parent property is not declared is unspecified. It should have been explicitly declared whether this will error, or calling magic methods.

That said, I believe calling magic methods would also not be consistent, as accessing properties from the parent class will actually use the child property if declared there, rather than calling magic methods. E.g. https://3v4l.org/06HfX

class P {
    public function setProp() {
        $this->prop = 'foo';
    }
    
    public function __set($name, $value) {
        var_dump('__set', $name, $value);
    }
}

class C extends P {
    public $prop;
}

$c = new C();
$c->setProp();
var_dump($c);

Furthermore, if the parent property does not contain magic get/set, does that mean we're resorting to dynamic properties? That's would inevitably clash with non-virtual child properties. Similarly, what happens with __get/__set when the same property is accessed again within __get/__set? This also normally resorts to magic methods.

It's not obvious to me what the correct, consistent behavior is here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

4 participants