Interfaces in PHP don't make complete sense

Interfaces are one of the main features of object-oriented languages, they provide polymorphic behavior and are the base of highly extended patterns and techniques like the Repository Pattern and Dependency Injection, but do they really make sense in PHP? Let's dig into this.

Intro

First of all, I want to say interfaces are one of my favorite features in an object-oriented programming language, and PHP is not the exception, I love interfaces, and I'm not trying by any means to dismiss its usage, but the opposite! Interfaces are great and you should be using them, If you haven't worked with interfaces or don't know exactly how they work, here is an excellent article by Dayle Rees.

Object Orientation, Interfaces, and PHP, some history

History is important! We all know about one man called Christopher Columbus who brought civilization to a desert land not yet called America, knowing history is an essential part of understanding the present.

Interfaces were introduced in PHP 5.0 back in 2004, along with some other object-oriented features which aren't really important for these matters. This first implementation was a very rudimentary one, if you visit the interfaces page on php.net you will see a note saying that prior to PHP 5.3.9 implementing two interfaces that define methods with the same name will blow your code in one hundred pieces. PHP 5.3.9 was released the 10th of January 2012, almost 8 years after interfaces were introduced!

In a most decent version of PHP 5.x, let's say 5.4 and forth you have some decent behavior with interfaces, let's look at this in a real-world example (I love real world examples!):


interface ParserInterface
{
    public function parse($data);
}

Perfect, now we have an interface that will come very handy to parse data as its name suggest. Let's implement it!


class JSONParser implements ParserInterface
{
    public function parse($data)
    {
        $parsed = json_encode($data);

        if ($parsed == false) {
            throw new UnparseableException('Sorry we can\'t parse your data');
        }

        return $parsed;
    }
}

Let's imagine we already defined the UnparseableException class because it's not really important, and using the imagination is fun!

Sponge Bob SquarePants says Imagination

The first problem with interfaces: Lack of type hinting

I know, I know, this problem was addressed in PHP 7, but we're still talking about history, remember?

Right now we have an interface that allows us to parse data, pretty neat! But we have a problem. An interface is a contract, and should be strictly followed by all classes that implementing it, without type hinting we don't know what kind of data should we pass to the parse method, and despite the fact that I advocated for this relaxed style in the past (and still do!), this behavior is unwanted most of the time.

So, how do we solve this issue? Well, at the moment one simple workaround is adding some guard clauses and voilá!


class JSONParser implements ParserInterface
{
    public function parse($data)
    {
        if (!is_array($data)) {
            throw new UnparseableException('Unsupported type as argument. Please provide an array');
        }

        $parsed = json_encode($data);

        if ($parsed == false) {
            throw new UnparseableException('Sorry we can\'t parse your data');
        }

        return $parsed;
    }
}

It's not pretty, but it works! Sadly we have another problem

The second problem: Lack of return type

- "But this was already solved in PHP 7.0 and improved in PHP 7.2!"

Indeed, but we are still talking about PHP 5.x, be patient, we are getting there.

This problem is related to the previous one. As we already saw, an interface is a contract and must be enforced, we should remember this phrase because is !false. So, we have to question ourselves, are we really enforcing a contract if we don't even really know what will a method return? Spoiler alert: no.

Due to the lack of return type declaration in PHP 5.x we can even completely remove the return statement.


class JSONParser implements ParserInterface
{
    public function parse($data)
    {
        ...

        // return $parsed; Let's remove this YOLO
    }
}

So, how do we solve this? There is no way to make sure that all the clients implementing the interface will return the correct type (or even return at all), the only thing I can think of is testing our classes behavior before writing the code. TTD FTW!

Addressing these problems with PHP 7

Fortunately we don't have to live in the past anymore, we can rewrite our ParserInterface interface and JSONParser class to make them more object-oriented. Let's see.


interface ParserInterface
{
    public function parse(array $data): string;
}


class JSONParser implements ParserInterface
{
    public function parse(array $data): string
    {
        $parsed = json_encode($data);

        if ($parsed == false) {
            throw new UnparseableException('Sorry we can\'t parse your data');
        }

        return $parsed;
    }
}

With these simple changes we can get rid of the guard clause used to verify the argument type, it's a win-win!

But we still have a problem, this code is not very object-oriented, passing an array to the parse method feels kinda messy, it would be nicer if we can pass an object of type, let's say Format. Let's make it better.


interface ParserInterface
{
    public function parse(Format $data): string;
}


abstract class Format
{
    protected $value;

    public function __construct(array $value)
    {
        $this->value = $value;
    }

    public function getValue(): array
    {
        return $this->value;
    }
}


final class JSON extends Format
{
    const FORMAT = 'JSON';
}

In theory we should only implement ParserInterface in JSONParser, and everything will work just fine. Well, not exactly.


class JSONParser implements ParserInterface
{
    public function parse(JSON $JSONData): string
    {
        $parsed = json_encode($JSONData->getValue());

        if ($parsed == false) {
            throw new UnparseableException('Sorry we can\'t parse your data');
        }

        return $parsed;
    }
}

If we run this...


Fatal error: Declaration of JSONParser::parse(JSON $JSONData): string must be compatible with ParserInterface::parse(Format $data): string in...

What!? How can this be possible? Why is this failing? Well, time to see the current and most important issue with interfaces in PHP.

The current problem with interfaces in PHP: lack of covariance and contravariance

Covariance and contravariance are fancy terms to describe really simple concepts, if you use them your boss will think you're smarty pants and maybe will give you a raise, you lose nothing by trying.

Contravariance (or simply variance) is the ability to replace an object with its parent without breaking the implementation.

Covariance is the opposite, the ability to replace an object with one of its children, without breaking the implementation.

- What does this mean and how is it related to interfaces in PHP?

Pretty simple! PHP doesn't support contravariance and/or covariance, nor in interfaces, neither in classes. That's the reason why we obtain a fatal error when trying to implement ParserInterface and change the type hinting from Format (abstract class) to JSON (concrete class).

Just for demonstration purposes, let's see how we can implement the same logic using a more object-oriented language, in this case, C#.


public interface IParser <in T> where T : Format
{
    string Parse(T data);
}

public abstract class Format { protected Dictionary<string, string> value; public Format(Dictionary<string, string> value) { this.value = value; } public Dictionary<string, string> GetValue() { return value; } }
public class JSON : Format { public JSON(Dictionary<string, string> value) : base(value) { } public string FORMAT = "JSON"; }
public class JSONParser : IParser<JSON> { public string Parse(JSON JSONData) { // Let's suppose we're implementing the logic needed in here... } }

Here, we're defining an interface with a generic parameter, we can pass any Format's child class to it, and voilá, everything will work as expected.

If you don't totally understand the syntax in the C# implementation that's fine, after all, this post is about PHP, just remember that this works exactly as we want, nice!

So, the real question remains: How do we address this problem in PHP? Well, we can't... or can we?

PHP 7.4 FTW!

We already talked about the past and present of PHP, now let's talk about the future!

A new RFC has been approved to introduce covariance and contravariance support in PHP 7.4 (hallelujah!), without the need for generic, in/out notations, and all that weird stuff we need to make use of in other languages like C#.

You can see the details here, and if you are curious enough, the implementation here, so unless something exceptional happens we will be enjoying this feature in the next release.

So, assuming we have the PHP 7.4 runtime, our last implementation should work as expected!


class JSONParser implements ParserInterface
{
    public function parse(JSON $JSONData): string
    {
        $parsed = json_encode($JSONData->getValue());

        if ($parsed == false) {
            throw new UnparseableException('Sorry we can\'t parse your data');
        }

        return $parsed;
    }
}


## somewhere_in_your_application.php

$JSON = new JSON(['name' => 'Max', 'languages' => ['PHP', 'C#', 'JavaScript']]);
$parser = new JSONParser();

try {
    $JSONString = $parser->parse($JSON);
} catch (UnparseableException $exception) {
    // Let's suppose we are handling the exception properly
}

// $JSONString is equal to {"name":"Max","languages":["PHP","C#","JavaScript"]}!

Smooth!

Post-credits

I have been programming in PHP for more than 5 years, (I know, it's not that long, but that's my journey!) and until now, this is by far my favorite addition to the language, once you've worked in other languages that already implement this feature you start loving it. PHP 7.4 will be a great release overall, let's wait for it!

Before closing I have to say, the RFC defines a lot more than what we saw, so if you want to know in detail how covariance and contravariance are supposed to work in PHP 7.4, you should definitively check the RFC.

As always I want to remind the reader that the code written here is only for demonstration purposes and most of the time I end up using really bad examples; you could say - why the hell aren't you just using json_encode to parse your damn array!?- and well, you're right, but you get the point of the article, don't you?

I hope you have found this article useful. Thanks for staying this long!

PS: try the brand new tweet button and spread the word among your friends!