<?php
//
// The goal is to try to safely cast an object to string but only if it can reasonably
// be assumed to be stringable. (properly implements __toString)
//
// This is as close as I've been able to come to ensure that bad things do not get
// called and I only deal with proper string results.
//
// The only major edge case here is that without implementing reflection it is
// impossible to know if __toString was implemented as a static method. This
// is a bummer but I'm willing to call this one "good enough."
//
class HasValidToString { public function __toString() { return 'Object\'s __toString is valid'; } }
class HasNonPublicToString { protected function __toString() { return 'has non public __toString'; } }
class HasNullToStringReturnValue { public static function __toString() { } }
class HasNumberReturnValue { public function __toString() { return 5; } }
class HasObjectReturnValue { public function __toString() { return new HasValidToString(); } }
// Do to PHP's fuzzy nature on how to handle things like this, you'll get a warning that
// this method should not be static but it will happily let you call it on an object
// instance so even though you might say that this is expected to fail it actually
// won't. Chances are something else awful will happen, though. Short of dipping
// into Reflection I'm not sure how to guard against this such that we do not
// call into a static __toString method.
class HasStaticToString {
public static function __toString() {
return 'Object\'s __toString is static; should not be able to call it';
}
}
$isExpectedToPass = true;
$isExpectedToFail = false;
function safely_cast_object_to_string($object)
{
if (! method_exists($object, '__toString')) {
throw new InvalidArgumentException('Object has no __toString');
}
if (! is_callable([$object, '__toString'])) {
throw new InvalidArgumentException('Object\'s __toString is not callable');
}
$string = $object->__toString();
if (! is_string($string)) {
throw new InvalidArgumentException('Object\'s __toString does not return a string value');
}
return $string;
}
foreach ([
[new HasValidToString(), $isExpectedToPass],
[new HasNonPublicToString(), $isExpectedToFail],
[new HasNullToStringReturnValue(), $isExpectedToFail],
[new HasNumberReturnValue(), $isExpectedToFail],
[new HasObjectReturnValue(), $isExpectedToFail],
[new HasStaticToString(), $isExpectedToFail], // it actually passes...
] as $test) {
list ($sample, $expectation) = $test;
$actual = false;
$string = null;
$message = '';
try {
$string = safely_cast_object_to_string($sample);
$actual = true;
} catch (\InvalidArgumentException $e) {
$message = $e->getMessage();
}
printf(
"%26s [%s] %s\n",
get_class($sample),
$actual === $expectation ? 'PASS' : 'FAIL',
! is_null($string) ? $string : $message
);
}