Skip to content

Strange behavior of ReflectionClass::getProperty with trait and inheritance #15753

Closed

Description

@HypeMC

Description

The following code: https://3v4l.org/Zbnqa

<?php

class ParentClass
{
    protected readonly int $value;
}

trait Foo
{
    protected readonly int $value;
}

class ChildClassWithoutTrait extends ParentClass
{
    protected readonly int $value;
}

$reflector = new ReflectionClass(ChildClassWithoutTrait::class);
$instance = $reflector->newInstanceWithoutConstructor();

var_dump(
    $reflector->hasProperty('value'),
    $prop = $reflector->getProperty('value')
);

$prop->setValue($instance, 20);

var_dump($instance);

echo '--------------------------------------------------------------------------------------------------------', PHP_EOL;

class ClassWithTraitAndNoParent
{
    use Foo;
}

$reflector = new ReflectionClass(ClassWithTraitAndNoParent::class);
$instance = $reflector->newInstanceWithoutConstructor();

var_dump(
    $reflector->hasProperty('value'),
    $prop = $reflector->getProperty('value')
);

$prop->setValue($instance, 20);

var_dump($instance);

echo '--------------------------------------------------------------------------------------------------------', PHP_EOL;

class ChildClassWithTrait extends ParentClass
{
    use Foo;
}

$reflector = new ReflectionClass(ChildClassWithTrait::class);
$instance = $reflector->newInstanceWithoutConstructor();

var_dump(
    $reflector->hasProperty('value'),
    $prop = $reflector->getProperty('value')
);

$prop->setValue($instance, 20);

var_dump($instance);

Resulted in this output:

bool(true)
object(ReflectionProperty)#3 (2) {
  ["name"]=>
  string(5) "value"
  ["class"]=>
  string(22) "ChildClassWithoutTrait"
}
object(ChildClassWithoutTrait)#2 (1) {
  ["value":protected]=>
  int(20)
}
--------------------------------------------------------------------------------------------------------
bool(true)
object(ReflectionProperty)#2 (2) {
  ["name"]=>
  string(5) "value"
  ["class"]=>
  string(25) "ClassWithTraitAndNoParent"
}
object(ClassWithTraitAndNoParent)#1 (1) {
  ["value":protected]=>
  int(20)
}
--------------------------------------------------------------------------------------------------------
bool(true)
object(ReflectionProperty)#1 (2) {
  ["name"]=>
  string(5) "value"
  ["class"]=>
  string(11) "ParentClass"
}

Fatal error: Uncaught Error: Cannot initialize readonly property ParentClass::$value from scope ChildClassWithTrait in /in/Zbnqa:64
Stack trace:
#0 /in/Zbnqa(64): ReflectionProperty->setValue(Object(ChildClassWithTrait), 20)
#1 {main}
  thrown in /in/Zbnqa on line 64

Process exited with code 255.

But I expected this output instead:

bool(true)
object(ReflectionProperty)#3 (2) {
  ["name"]=>
  string(5) "value"
  ["class"]=>
  string(22) "ChildClassWithoutTrait"
}
object(ChildClassWithoutTrait)#2 (1) {
  ["value":protected]=>
  int(20)
}
--------------------------------------------------------------------------------------------------------
bool(true)
object(ReflectionProperty)#2 (2) {
  ["name"]=>
  string(5) "value"
  ["class"]=>
  string(25) "ClassWithTraitAndNoParent"
}
object(ClassWithTraitAndNoParent)#1 (1) {
  ["value":protected]=>
  int(20)
}
--------------------------------------------------------------------------------------------------------
bool(true)
object(ReflectionProperty)#1 (2) {
  ["name"]=>
  string(5) "value"
  ["class"]=>
  string(11) "ChildClassWithTrait"
}
object(ChildClassWithTrait)#n (1) {
  ["value":protected]=>
  int(20)
}

It seems that a combination of traits and inheritance somehow breaks the getProperty() method. Even though the hasProperty() method returns true for ChildClassWithTrait, the getProperty() method returns a ReflectionProperty for ParentClass.

PHP Version

all

Operating System

No response

Activity

  1. iluuu1994 commented on Sep 13, 2024

    @iluuu1994
    Member

    Simplified example:

    class ParentClass {
        protected readonly int $value;
    }
    
    trait Foo {
        protected readonly int $value;
    }
    
    class ChildClassWithTrait extends ParentClass {
        use Foo;
    
        public function __construct() {
            $this->value = 20;
        }
    }
    
    new ChildClassWithTrait();

    Given traits are described as "copy & paste" in the documentation, it seems to me that this should work.

  2. djmarland commented on Nov 21, 2024

    @djmarland

    We have also run into this issue, particularly when trying to set Attributes: https://3v4l.org/rZILt

    It seems properties from Traits are at the end of the properties list, so will not overwrite earlier properties from the parents.
    This means that the attribute values come from the parent instead of the trait used by the child.

  3. TimWolla commented on Mar 28, 2025

    @TimWolla
    Member

    I believe this one is already implicitly resolved with PHP 8.4, since readonly now implies protected instead of private.

  4. added a commit that references this issue on Apr 1, 2025
    011795b
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