Johan Fredrik Varen
Johan Fredrik Varen

Reputation: 3914

Can parameter types be specialized in PHP

Say we've got the following two classes:

abstract class Foo {
    public abstract function run(TypeA $object);
}

class Bar extends Foo {
    public function run(TypeB $object) {
        // Some code here
    }
}

The class TypeB extends the class TypeA.

Trying to use this yields the following error message:

Declaration of Bar::run() must be compatible with that of Foo::run()

Is PHP really this broken when it comes to parameter types, or am I just missing the point here?

Upvotes: 4

Views: 3099

Answers (6)

Stefan Gehrig
Stefan Gehrig

Reputation: 83622


This answer is outdated since PHP 7.4 (partially since 7.2).


The behavior you describe is called covariance and is simply not supported in PHP. I don't know the internals but I might suspect that PHP's core does not evaluate the inheritance tree at all when applying the so called "type hint" checks.

By the way, PHP also doesn't support contravariance on those type-hints (a feature commonly support in other OOP languages) - most likely to the reason is suspected above. So this doesn't work either:

abstract class Foo {
    public abstract function run(TypeB $object);
}

class Bar extends Foo {
    public function run(TypeA $object) {
        // Some code here
    }
}

And finally some more info: http://www.php.net/~derick/meeting-notes.html#implement-inheritance-rules-for-type-hints

Upvotes: 8

bdsl
bdsl

Reputation: 322

The code shown in the question is not going to compile in PHP. If it did class Bar would be failing to honour the gurantee made by it's parent Foo of being able to accept any instance of TypeA, and breaching the Liskov Substitution Principle.

Currently the opposite code won't compile either, but in the PHP 7.4 release, expected on November 28 2019, the similar code below will be valid, using the new contravariant arguments feature:

abstract class Foo {
    public abstract function run(TypeB $object); // TypeB extends TypeA
}

class Bar extends Foo {
    public function run(TypeA $object) {
        // Some code here
    }
}

All Bars are Foos, but not all Foos are Bars. All TypeBs are TypeAs but not all TypeAs are TypeBs. Any Foo will be able to accept any TypeB. Those Foos that are also Bars will also be able to accept the non-TypeB TypeAs.

PHP will also support covariant return types, which work in the opposite way.

Upvotes: 0

ontrack
ontrack

Reputation: 3043

Although you cannot use whole class-hierarchies as type-hinting. You can use the self and parent keywords to enforce something similar in certain situations.

quoting r dot wilczek at web-appz dot de from the PHP-manual comments:

<?php
interface Foo 
{
    public function baz(self $object);
}

class Bar implements Foo
{
    public function baz(self $object)
    {
        // 
    }
}
?>

What has not been mentioned by now is that you can use 'parent' as a typehint too. Example with an interface:

<?php
interface Foo 
{
    public function baz(parent $object); 
}

class Baz {}
class Bar extends Baz implements Foo
{
    public function baz(parent $object)
    {
        // 
    }
}
?>

Bar::baz() will now accept any instance of Baz. If Bar is not a heir of any class (no 'extends') PHP will raise a fatal error: 'Cannot access parent:: when current class scope has no parent'.

Upvotes: -1

mario
mario

Reputation: 145482

One could always add the constraint in code:

public function run(TypeA $object) {
    assert( is_a($object, "TypeB") );

You'll have to remember or document the specific type limitation then. The advantage is that it becomes purely a development tool, as asserts are typically turned off on production servers. (And really this is among the class of bugs to be found while developing, not randomly disrupt production.)

Upvotes: 0

Neil
Neil

Reputation: 3041

This seems pretty consistent with most OO principals. PHP isn't like .Net - it doesn't allow you to override class members. Any extension of Foo should slide into where Foo was previously being used, which means you can't loosen constraints.

The simple solution is obviously to remove the type constraint, but if Bar::run() needs a different argument type, then it's really a different function and should ideally have a different name.

If TypeA and TypeB have anything in common, move the common elements to a base class and use that as your argument constraint.

Upvotes: 2

Pekka
Pekka

Reputation: 449395

I think this is by design: It is the point of abstract definitions to define the underlying behaviour of its methods.

When inheriting from an abstract class, all methods marked abstract in the parent's class declaration must be defined by the child; additionally, these methods must be defined with the same (or a less restricted) visibility.

Upvotes: 0

Related Questions