2

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.

3 Answers 3

3

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:

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

4 Comments

I actually didn't know 7.2 enabled this, but I don't think it's related to what the OP is asking. The type hasn't been changed, or widened, in the declaration, just the default value has been assigned.
Imho a nullable type is a wider type than a non nullable type - doesn't matter, if there's a default value or not
But that behavior didn't change with the rfc you're referencing. A default value of null has always been permitted since the introduction of type hinting. (Your answer doesn't explain this behavior for 7.0 and 7.1)
@Philipp thanks for your answer, but I think I have to agree with Devon that the widening is not the reason for this behavior. Mainly as it is also present in 7.1. Being one of PHP backward compatibility features seems more likely. It is really annoying me though ;)
1

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.

Comments

0

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.

3 Comments

Thanks for your answer. It seems likely that this is one of the many things PHP would do to keep backward compatibility intact. I just wished it was better documented.
@RickVH, right. I think most languages permit null values in place of other types, so it's not uncommon. With Philip's pointing out of the rfc that was implemented in 7.2, it looks like your concern will be even worse since they can actually nullify the interface's type and allow types other than string/null to be passed, so going forward, the developer has even more responsibility if they want to stay true to the interfaces.
I agree. But this is good to know. I will set this post as the answer to my question. With a big thanks to @Phillip for pointing out that it will be getting worse.

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.