Skip to content

Protected properties are not scoped according to their prototype #19044

Closed

Description

@bwoebi

Description

The following code (https://3v4l.org/7Xf05#v8.4.10):

<?php

abstract class P {
    abstract protected $foo { get; }
}

class C1 extends P {
    protected $foo = 1;
}

class C2 extends P {
    protected $foo = 2;
    
    static function foo($c) { return $c->foo; }
}

var_dump(C2::foo(new C2));
var_dump(C2::foo(new C1));

Resulted in this output:

int(2)

Fatal error: Uncaught Error: Cannot access protected property C1::$foo in /in/7Xf05:14
Stack trace:
#0 /in/7Xf05(18): C2::foo(Object(C1))
#1 {main}
  thrown in /in/7Xf05 on line 14

But I expected this output instead:

int(2)
int(1)

Doing the same with methods works flawlessly (https://3v4l.org/iQo81#v8.4.10):

<?php

abstract class P {
    abstract protected function foo();
}

class C1 extends P {
    protected function foo() { return 1; }
}

class C2 extends P {
    protected function foo() { return 2; }
    
    static function doFoo($c) { return $c->foo(); }
}

var_dump(C2::doFoo(new C2));
var_dump(C2::doFoo(new C1));

results in the expected

int(2)
int(1)

PHP Version

PHP 8.4.10

Operating System

No response

Activity

  1. bwoebi commented on Jul 5, 2025

    @bwoebi
    MemberAuthor

    @iluuu1994 This was very surprising - I just checked the RFC and it says that they work the same than abstract methods:

    abstract properties may also be declared protected, just as with abstract methods

    So I assume this is a bug in the implementation?

  2. changed the title [-]Protected properties do not follow the same scoping rules than methods[/-] [+]Protected properties are not scoped according to their prototype[/+] on Jul 5, 2025
  3. iluuu1994 commented on Jul 5, 2025

    @iluuu1994
    Member

    It looks like this is not unique to hooks or abstract properties. https://3v4l.org/OJWLS#v8.4.10

  4. iluuu1994 commented on Jul 5, 2025

    @iluuu1994
    Member

    Ah I see @bwoebi already got the solution. 🙂 Using prop_info->prototype looks like the right approach to me.

  5. bwoebi commented on Jul 5, 2025

    @bwoebi
    MemberAuthor

    @iluuu1994 Yeah, in the past there was not really a big point in repeating the property definition, so this was rather obscure and irrelevant. With property hooks you now can set protected abstract $prop { get; } on the parent class, so the child has to define it and then it breaks.

    Added that as a test case to the PR as well.

  6. added 2 commits that reference this issue on Jul 22, 2025
    9b85315
    b13347b
  7. jackweinbender commented on Mar 17, 2026

    @jackweinbender

    First, let me thank you maintainers for the work that you do on php.

    I wanted to flag one knock-on issue here (also related to #19406, I think) which recently bit us when upgrading from 8.3. Namely, this change also impacts whether magic __set and __get methods are called when sibling classes assign to shared protected properties that are redeclared in the child. For example:

    Simplified Example:

    class SharedParent {
        protected $id;
        public function __set($name, $value) {
            echo "Running Setter $name $value\n";
            $this->id = $value;
        }
    }
    
    class ChildA extends SharedParent {
        
        protected $id; // ChildA Redeclares the protected property
        
        public function set($txt) {
            $this->id = $txt;
        }
        public function setSibling($sibling, $txt) {
            $sibling->id = $txt;
        }
    }
    class ChildB extends SharedParent { 
        public function set($txt) {
            $this->id = $txt;
        }
        public function setSibling($sibling, $txt) {
            $sibling->id = $txt;
        }
    }
    
    $a = new ChildA();
    $b = new ChildB();
    
    $a->id = "testing";
    $b->id = "123";
    
    $a->set("testing");
    $b->set("123");
    
    $a->setSibling($b, "testing"); // does not run __set in 8.4.10
    $b->setSibling($a, "123"); // Runs __set in 8.4.11

    Results in 8.4.11:

    Running Setter id testing
    Running Setter id 123
    Running Setter id 123
    

    Results in 8.4.12:

    Running Setter id testing
    Running Setter id 123
    

    We ran into a situation where some of our model classes were altering properties of their siblings and expecting the __set methods to be called (marking the objects dirty). But this change made it so the setters weren't being called and, for us, objects not being saved to the DB (caught in testing, fortunately).

    I'm writing this mostly for posterity—from our testing this behaviour was stable since 5.2.0 (released 2006). We agree it was a bug, but for us, it was a load-bearing bug. Hoping this comment helps someone else out.

    And again, thanks for your hard work on improving php!

  8. iluuu1994 commented on Mar 17, 2026

    @iluuu1994
    Member

    @jackweinbender Thanks for your feedback. Issues like this one are always tough. It's a clear bug, but I understand that even bug fixes have BC concerns, especially for long-standing bugs (Hyrum's law). We generally already postpone such fixes to the next minor release, though I suppose in this case we deemed the risk low, given before hooked properties were a thing there weren't many reasons to override properties.

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

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