RickVH
RickVH

Reputation: 23

php function implementation overrides non-nullable argument

I ran into this today with PHP (7.1 and 7.2 at least) with the following code:

namespace PlaceHolderX\Tests\PHPUnit\Unit;

use PHPUnit\Framework\TestCase;

final class BreakingClassesTest extends TestCase
{

    public function testBreak(): void
    {
        $tester = new SomeClassA();
        $tester->test();
        $this->assertNull($tester->get());
    }

}

interface InterfaceA {

    public function test(string $testString): void;

}

class SomeClassA implements InterfaceA
{
    /** @var null|string */
    private $testString;

    public function test(string $testString = null): void
    {
        $this->testString = $testString;
    }

    public function get(): ?string
    {
        return $this->testString;
    }
}

So I have an interface (InterfaceA) that has a method that requires a string. This argument is not nullable, cause if I wanted that I would have specified it as:

public function test(?string $testString): void;

But in the implementation class (SomeClassA) I can override the argument definition with a default value of null which results in a behavior I didn't intend with my interface.

So my main question is: Why is this possible? Of course, we will need to check this in code reviews, but it is something that is easy to miss.

I tried searching what causes this behavior but was not able to find an explanation. Maybe my search criteria are off.

Upvotes: 2

Views: 1066

Answers (3)

przemo_li
przemo_li

Reputation: 4053

Superclass should be replaceable by its subclasses. So subclass must be able to do everything superclass does, but it can also do more.

In your example, superclass/interface does not know how to handle null. But subclass does, and it's fine because users of superclass will pass only non-nulls as they think superclass contract is in effect.

Upvotes: 1

Philipp
Philipp

Reputation: 15629

In PHP7.2 there was parameter type widening implemented. This is some kind of contra variance. Sadly, PHP currently doens't support contra variance for parameters, but there is also and rfc in draft to support this.

The main idea is: If you have a child class, you could use "wider" parameter types in the child class. For return types, the oppsite is valid (covariance).

If this is a good or bad practice, depends on your needs. As far as I know, other languages behave the same way.

For further reading, there are the two rfc:

Upvotes: 3

Devon Bessemer
Devon Bessemer

Reputation: 35337

PHP allows you to set, or modify, default values in implementations as long as the type matches. One caveat is that all hinted types permit null as the default value.

If someone finds a specific explanation for this then I can update this answer, but prior to 7.1, the only way to declare an optional parameter was to assign a default value of null. The ?string syntax didn't exist, so this behavior may stem from that and still exists for backwards compatibility.

If you try to set a default value of say an integer, you'll see an error message that shows:

Fatal error: Default value for parameters with a string type can only be string or NULL

As of right now, it seems to be the developer's responsibility to ensure the default value of an implementation matches the interface declaration of nullable or not nullable.

Upvotes: 0

Related Questions