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
).
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 somethingLoggable
, 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.