Accessing protected properties from objects that share the same ancestry.

I realized something odd about accessing protected properties the other day. It’s possible in PHP to access protected properties from other objects, as long as they are from the same class, as illustrated here:

<?php

class MyClass {

    protected $val;

    function __construct($newVal = 'default') {

        $this->val = $newVal;

    }

    function output(MyClass $subject) {

        echo $subject->val, "\n";

    }
}


$obj1 = new MyClass();
$obj2 = new MyClass("hello world");

$obj1->output($obj2);
// Output: hello world

?>

I always thought that protected strictly allows objects to access things from the current inheritence tree, but didn’t realize that this also extends to other instances of the same object.

This behavior works for properties and methods, and also when they are defined as private.

The other day, I realized though that this even works for objects of other classes, as long as you are accessing members that are defined in a class that also appears in the accessing class’ ancestry.

Sounds a bit complicated, so here’s the example:

<?php

class Ancestor {

    protected $val = 'ancestor';

}


class Child1 extends Ancestor {

    function __construct() {

        $this->val = 'child1';

    }

}

class Child2 extends Ancestor {

    function output(Ancestor $subject) {

        echo $subject->val, "\n";

    }

}

$child1 = new Child1();
$child2 = new Child2();

$child2->output($child1);
// Output: child1

?>

Interestingly, if the last example is modified so that the properties are not set in the constructors, but instead by overriding the property, this will break:

<?php

class Ancestor {

    protected $var = 'ancestor';

}


class Child1 extends Ancestor {

    protected $var = 'child1';

}

class Child2 extends Ancestor {

    function output(Ancestor $subject) {

        echo $subject->var, "\n";

    }

}

$child1 = new Child1();
$child2 = new Child2();

$child2->output($child1);
// Output: Fatal error: Cannot access protected property Child1::$var

?>

Because the third example throws an error, but the second does not, this makes me feel that I’ve simply stumbled upon an edge-case of the PHP engine, and that this feature is not by design. If it were designed as such, both should imho work.

After all if a certain behavior (in this case a property) works for an ancestor, the liskov substitution principle dictates it should also work for any sub-classes.

In addition, PHP normally only allows you to modify a property’s visibility to become more visible.

Still, I just ran into a case where the behavior of example #2 is super handy, but I’m not entirely sure if it’s a good idea to rely on this behavior, or to assume that this behavior is merely ‘undefined’ and could be altered without notice in a subsequent PHP version.

The PHP spec does not explictly disallow it though. This is the relevant text:

A member with protected visibility may be accessed only from within its own class and from classes derived from that class. source

Of course the PHP spec is likely not finished yet, and not sure if it’s vetted by the PHP team at all.

So I’m left not really knowing wether relying on this behavior is a good idea, but at the very least it’s immediately a good example of why having a correct and official PHP standard is a great idea.

Web mentions

Comments

  • Davey Shafik

    <p>Hi,</p><p>In your third example, it errors because you override the property in Child1, which Child2 does not derive from and therefore does not have access to it. If Child2 were to extend Child1, it would work fine.</p><p>If you were to then add a Child3 that also extended Child1, it could access the properties in Ancestor, Child1, and Child2.</p>
    • Evert

      Evert

      <p>Hi Davey, I understand why it happens, but what I took from it was that either:</p><p>A) Example #2 should not be allowed in the PHP engine and is a kind of bug, or undefined behavior, or at the very least behavior that I'm not supposed to use.</p><p>B) Example #3 shows a php bug, because it allows users to break the liskov substition principal.</p><p>I should not be allowed to change the contract of a class in a subclass.</p>
  • Matthew Weier O'Phinney

    <p>This is actually how both doctrine and zf2 implement proxy objects. It's particularly useful for hydrating a value or entity object.</p>
    • Evert

      Evert

      <p>Hmm well perhaps it's worth reporting a bug for example #3 then. If this is expected behavior in the PHP engine, it shouldn't be breakable by overriding the property value in a subclass :)</p>
      • Matthew Weier O'Phinney

        <p>Well, we don't override the properties; we use the fact that the object we're manipulating is of the same type in order to manipulate its properties. (This is also a key faculty for immutable objects; you clone, and then change the property in the cloned instance.)</p>
        • Evert

          Evert

          <p>Well, the problem arises when the Proxy object receives an instance of a subclass of what it's normally proxying. If this subclass does override a protected property with a new value, you'll get the fatal error.</p><p>Given that any class should be substitutable by one of it's subclass, I'd consider this a bug.</p>
          • Matthew Weier O'Phinney

            <p>Yes, definitely agree there.</p>
  • Petah

    <p>ReflectionProperty::setAccessible problem fixed.</p><p><a href="http://php.net/manual/en/reflectionproperty.setaccessible.php" rel="nofollow noopener" title="http://php.net/manual/en/reflectionproperty.setaccessible.php">http://php.net/manual/en/re...</a></p><p>/troll</p>
  • damonk

    <p>Ok really got a little confused until i tried it with Java. Normally in #example 3 the result of the method output should fallback to "ancestor" but it failed because of it was overridden by $var in Child1 class. I think that is just one of PHP style of implementing OOP, subclasses always overrides parent Classes. that is one thing that make it really DYNAMIC.</p>
  • Miles Johnson

    <p>Visibility is based around the scope level, not the object level. Any instance of MyClass will have the same scope, hence why you can modify properties of other objects of the same class name. I'm pretty sure Java and other OO languages work the same way.</p>