Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Implementing interfaces and errors with incompatible functions

Tags:

oop

php

I have this test script:

<?php
interface A
{
  function myfunction();
}

class B implements A
{
  function myfunction($var = "default")
  {
    echo $var;
  }
}

class C extends B
{
  function myfunction()
  {
    echo "myfunction";
  }
}

$c = new C();
$c->myfunction();

$b = new B();
$b->myfunction();

This runs fine and outputs myfunctiondefault.

Now when I remove the interface A and let B not implement A anymore, like so:

<?php
class B
{   
  function myfunction($var = "default")
  {   
    echo $var;
  }   
}   

class C extends B
{   
  function myfunction()
  {   
    echo "myfunction";
  }   
}   

$c = new C();
$c->myfunction();

$b = new B();
$b->myfunction();

I get this error:

PHP Strict standards:  Declaration of C::myfunction() should be compatible with that of B::myfunction() in /var/www/test/index.php on line 16

Strict standards: Declaration of C::myfunction() should be compatible with that of B::myfunction() in /var/www/test/index.php on line 16

myfunctiondefault

Why did this even work in the first time? I would expect the error in both situations...

like image 323
Veda Avatar asked May 09 '14 07:05

Veda


1 Answers

When using the interface, B and C are independently verified against the interface declaration. It doesn't matter that C extends B. C indirectly implements A, and C::myfunction is compatible with A::myfunction, so no problem there. B::myfunction is also compatible with A::myfunction, so no problem there either.

Without the interface, B::myfunction is the canonical declaration for that method, and since it accepts a parameter but the overriding C::myfunction does not, a STRICT warning is raised.

Basically, you want to make sure this code works:

if ($obj instanceof <classWhoseInterfaceIExpect>) {
    $obj-><interfaceIExpect>();
}

In more concrete terms:

if ($obj instanceof A) {
    $obj->myfunction();
}

Since A::myfunction is canonically declared as accepting no arguments, the above code will always work. However, without the interface:

if ($obj instanceof B) {
    $obj->myfunction('foo');
}

If B::myfunction is the canonical declaration which accepts an argument, but C delivers an implementation which does not, you've created a conflict. Hence the warning. This is to answer the question why it works the way it does, it's PHP's thinking explained.


Caveat: Yes, even if you use the interface, this will still create the same conflict:

interface A { ... }
class B implements A { ... }
class C extends B { ... }  // conflicting implementation to B

$obj = new C;

if ($obj instanceof B) {
    $obj->myfunction('foo');
}

C is both an instanceof A and B, yet it isn't compatible with both implementations of myfunction at the same time. This is your problem for creating two conflicting implementations. Whether PHP should warn here as well or not is debatable. PHP can't have your cake and eat it too. Its type system is there to help you catch certain errors as early as possible; it's certainly not perfect and can't protect you from shooting your own foot from all possible angles.

You basically shouldn't change method signatures ever if you want to avoid such issues. That, or have the change be cascading downwards; meaning that each extending class is compatible with the method signature of its direct parent class. So C::myfunction should accept at least one optional argument. Maybe PHP should catch this case, maybe it could be considered a bug that you're not getting a warning in all cases. Again, it's debatable and you should avoid getting into this situation to begin with.

like image 93
deceze Avatar answered Nov 01 '22 22:11

deceze