0

I've setup an abstract class that must execute methods from another instanciated class. So I setup an anonymous class that extends my abstract class and in the implemented method, I want to access the methods of that "other instanciated class", but for my IDE to help me to find the correct name of the methods and arguments, I must write somewhere the type.

abstract class blah
{
    protected object $reference;
    public function __construct(object $reference)
    {
        $this->reference = $reference;
        $this->hello();
    }
    abstract public function hello();
}

class world
{
    public function __construct()
    {
        new class($this) extends blah {
            public function hello()
            {
                $this->reference->display();
            }
        };
    }

    public function display()
    {
        echo 'hello world';
    }
}

new world();

I can't change the type from "object" to something else to allow my IDE to show the methods

public function __construct()
    {
        new class($this) extends blah {
            protected world $reference;
            public function hello()
            {
                $this->reference->display();
            }
        };
    }

this throws Fatal error: Type of blah@anonymous::$reference must be object (as in class blah)

I could copy the attribute "reference" to another variable and set a typehint to "world", but that's not the best

public function __construct()
    {
        new class($this) extends blah {
            protected world $reference;
            public function hello()
            {
                /** @var world $hello */
                $hello = $this->reference;
                $hello->display();
            }
        };
    }

what better solution do I have ?

edit: this seems to work, is it the best way ?

new class($this) extends blah {
            /** @var world $reference */
            protected object $reference;
            public function hello()
            {
                $this->reference->display();
            }
        };
1
  • Side note: PHP doesn't have implicit returns, or any ability to change the return value of a constructor, so the object created by new class ... here is immediately thrown away. Commented Apr 28, 2022 at 13:01

2 Answers 2

1

It's worth understanding why PHP is telling you that you can't change the property type.

Since this is a protected property, it can be read and written by either the parent or child class. For instance, the parent class might have methods like this, which would be inherited by the child class:

public function getReference(): object {
    return $this->reference;
}

public function setReference(object $newReference): void {
    $this->reference = $newReference;
}

public function setReferenceToDefault(): void {
    $this->reference = new SomethingBoring;
}

If you specify world as the type of $reference in a child class, the getReference method will work fine - any instance of world is an object, so the return type is OK. But the setReference() and setReferenceToDefault() methods would fail, because they try to assign something that isn't of type world.

Essentially, by declaring a protected property on the base class, you are setting a contract for all child classes, and PHP is enforcing it for you. The technical term for this is "contravariance": a child class can be more generous in what it accepts, but it can't reject values the parent would accept. The opposite is "covariance", which applies to returning values: the child class can be more specific about what it will return, but can't return something the parent class never would. When something is both input and output, you get both restrictions, so can't vary the type at all.

Since you don't actually use the property on the base class, this restriction isn't actually needed, so in this case one option is simply not to define the property or constructor on the base class. In other cases, you might want to use traits, which are "compiler assisted copy-and-paste": they don't assert any relationship between two classes, but they save you writing the same code more than once.

Sign up to request clarification or add additional context in comments.

3 Comments

but aren't class instances also objects ? The constructor of blah (__construct(object $reference)) expects objects and doesn't crash when I put $this which is an instance of world, so if it's allowed there, should it be allowed to the typed property "reference" to change to a class ?
It's tricky, object and world are the same on method parameter, but not the same on property type ?
It's not the constructor that's the problem, it's the fact that the parent class is free to do what it likes with that parameter, and the type is a contract the child class has to follow. I've edited my answer with an expanded example which is maybe a bit clearer.
0

If you want it only for IDE, then use documentation block to overwrite type declaration (if IDE supports it):

/**
 * @property world $reference
 */
new class($this) extends blah {
    public function hello()
    {
        $this->reference->display();
    }
};

E.g. PHPStorm, while not supporting that for anonymous class, supports for normal class description:

enter image description here

1 Comment

yeah this doesn't work for anonymous but on vscode + intelephense, this works on anonymous : /** @var world $reference */

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.