Home » Php » PHP 7.1: Is there a native interface that matches "string" and "Object that implements __toString()"?

PHP 7.1: Is there a native interface that matches "string" and "Object that implements __toString()"?

Posted by: admin July 12, 2020 Leave a comment

Questions:

We have objects that implement __toString():

class Foo {
    public function __toString()
    {
        return 'bar';
    }
}

Then we have functions that return either string or object that implement __toString(), like the example above.

While using return type string for strings works of course:

function getString():string {
    return 'Works!';
}

Returning object of type Foo does not work, because it’s not a string:

function getString(Foo $foo):string {
    return $foo; // Fatal TypeError!
}

Is there any PHP interface that we can use to type hint both string and objects that implement that unknown interface, e.g. Printable or Stringable (lol)?

The goal is to being able to return either string or objects that implement a specific interface (which would enforce the implementation of __toString).

How to&Answers:

The simple answer is “no” – there is no built-in “stringable” hint, although it has occasionally been suggested in the past.

As of PHP 8.0, there will be support for “union types”, so string|Stringable would represent “either a string or an instanceof Stringable“. However, there’s no such interface as Stringable, so this can’t be used to detect implementations of __toString.

Part of the reason for this, and the design question you need to ask, is what contract are you actually representing here? From the point of view of the calling code, there is no guarantee that the return value can actually be used as a string directly – there are many operations which will behave differently for a string and an object, or which will accept a string but not cast an object automatically. So the only thing that’s actually guaranteed by such a hint is that (string)$foo will give a string.

So you have a few options:

  • As Stefan says, always return an object, implementing some interface Stringable. If the function wants to return a string, it would first wrap it in a simple object.
  • Instead of a generic Stringable, come up with a more meaningful interface to return; maybe what you actually return is something Loggable, for instance. This way, wrapping the string can give you more concrete benefits: you can add other methods to the interface, and the calling code doesn’t need to do an “object or string” check before calling those methods.
  • If the calling code is only ever going to use the return value as a string, don’t return the object at all. Mark the function as returning string, and cast any objects to string before returning them.
  • If the calling code is expected to inspect the return value and do different things with different kinds of objects, don’t limit the return type at all. In PHP 8, you might want to add a union type, but only if there’s a short list of possibilities; a return type that’s too broad is barely useful as a contract anyway.

Answer:

You could box your string in a wrapper class that just takes the string in the c-tor and has a toString() method that returns it.

This wrapper class would implement your Stringable interface, as do your other classes.

Then you can provide a return-type hinted method that returns objects of type Stringable.

This method can decide whether it returns one of your objects or just a wrapper for a string, as all of them implement Stringable.

However, this is to be balanced against just returning mixed and checking the returned value type in your calling method.

Further, if your objects implement the magic method __toString, then all your calling methods could always apply a cast (string) on the returned value.

If the returned value is a string, then it just keeps being a string.

If it is one of your objects that implements the magic method, then you’ll get the string.

Yet you won’t have the return-type hint in that case.

Answer:

Strings and objects implementing __toString are not interchangeable, for instance $s[1] will fail on an object (unless it implements some other magic methods). An object might be a valid substitute in contexts where an argument is coerced to a string anyway, but that doesn’t apply to all possible operations you can do on a string. So, no, there’s no native interface for that, and you shouldn’t intermix these types for this reason either.