Skip to content

Commit 526354e

Browse files
committed
Change how to stringify Stringable objects
This library's goal is to convert anything to a string, and although it seems obvious to return the content of the `__toString()` method, that behavior does not tell the user much about what the input is. This commit sets the tone for this library: it's for engineers to represent anything as a string. However, the fact that we can convert an object to a string does not mean it is a string. This commit will also change how we verify those objects. Since PHP 8.0, any object that implements a `__toString()` method automatically implements the Stringable interface. Since we dropped support for versions that are less than 8.1, it makes sense to check that instead of checking if the method exists. Signed-off-by: Henrique Moody <[email protected]>
1 parent b2f1eff commit 526354e

File tree

5 files changed

+41
-58
lines changed

5 files changed

+41
-58
lines changed

src/Stringifiers/ClusterStringifier.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ public static function createDefault(): self
4444
new TraversableStringifier($stringifier, $quoter),
4545
new DateTimeStringifier($quoter, DateTimeInterface::ATOM),
4646
new ThrowableStringifier($stringifier, $quoter),
47-
new StringableObjectStringifier($stringifier),
47+
new StringableObjectStringifier($jsonParsableStringifier, $quoter),
4848
new JsonSerializableObjectStringifier($jsonParsableStringifier, $quoter),
4949
new ObjectStringifier($stringifier, $quoter),
5050
new ArrayStringifier($stringifier, $quoter, self::MAXIMUM_DEPTH, self::MAXIMUM_NUMBER_OF_ITEMS),

src/Stringifiers/StringableObjectStringifier.php

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,28 +10,33 @@
1010

1111
namespace Respect\Stringifier\Stringifiers;
1212

13+
use Respect\Stringifier\Quoter;
1314
use Respect\Stringifier\Stringifier;
15+
use Stringable;
1416

15-
use function is_object;
16-
use function method_exists;
17+
use function sprintf;
1718

1819
final class StringableObjectStringifier implements Stringifier
1920
{
2021
public function __construct(
21-
private readonly Stringifier $stringifier
22+
private readonly Stringifier $stringifier,
23+
private readonly Quoter $quoter
2224
) {
2325
}
2426

2527
public function stringify(mixed $raw, int $depth): ?string
2628
{
27-
if (!is_object($raw)) {
29+
if (!$raw instanceof Stringable) {
2830
return null;
2931
}
3032

31-
if (!method_exists($raw, '__toString')) {
32-
return null;
33-
}
34-
35-
return $this->stringifier->stringify($raw->__toString(), $depth);
33+
return $this->quoter->quote(
34+
sprintf(
35+
'%s { __toString() => %s }',
36+
$raw::class,
37+
$this->stringifier->stringify($raw->__toString(), $depth + 1)
38+
),
39+
$depth
40+
);
3641
}
3742
}
Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,10 @@
88

99
declare(strict_types=1);
1010

11-
namespace Respect\Stringifier\Test;
12-
13-
final class MyStringable
11+
final class ConcreteStringable
1412
{
15-
public const STRING_VALUE = self::class;
16-
1713
public function __toString(): string
1814
{
19-
return self::STRING_VALUE;
15+
return 'This is the return of __toString()';
2016
}
2117
}

tests/integration/stringify-object-stringable.phpt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ declare(strict_types=1);
55

66
require 'vendor/autoload.php';
77

8-
output(new Respect\Stringifier\Test\MyStringable());
8+
output(new ConcreteStringable());
99
?>
1010
--EXPECT--
11-
"Respect\\Stringifier\\Test\\MyStringable"
11+
`ConcreteStringable { __toString() => "This is the return of __toString()" }`

tests/unit/Stringifiers/StringableObjectStringifierTest.php

Lines changed: 22 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -10,66 +10,48 @@
1010

1111
namespace Respect\Stringifier\Test\Unit\Stringifiers;
1212

13+
use ConcreteStringable;
1314
use PHPUnit\Framework\Attributes\CoversClass;
1415
use PHPUnit\Framework\Attributes\Test;
1516
use PHPUnit\Framework\TestCase;
16-
use Respect\Stringifier\Stringifier;
1717
use Respect\Stringifier\Stringifiers\StringableObjectStringifier;
18-
use Respect\Stringifier\Test\MyStringable;
18+
use Respect\Stringifier\Test\Double\FakeQuoter;
19+
use Respect\Stringifier\Test\Double\FakeStringifier;
1920
use stdClass;
2021

22+
use function sprintf;
23+
2124
#[CoversClass(StringableObjectStringifier::class)]
2225
final class StringableObjectStringifierTest extends TestCase
2326
{
24-
#[Test]
25-
public function shouldNotConvertToStringWhenValueIsNotAnObject(): void
26-
{
27-
$raw = 'not-an-object';
28-
$depth = 1;
29-
30-
$stringifierMock = $this->createMock(Stringifier::class);
31-
$stringifierMock
32-
->expects($this->never())
33-
->method('stringify');
34-
35-
$stringableObjectStringifier = new StringableObjectStringifier($stringifierMock);
36-
37-
self::assertNull($stringableObjectStringifier->stringify($raw, $depth));
38-
}
27+
private const DEPTH = 0;
3928

4029
#[Test]
41-
public function shouldNotConvertToStringWhenValueIsNonStringableObject(): void
30+
public function itShouldNotStringifyRawValueWhenItIsNotAnInstanceOfStringable(): void
4231
{
43-
$raw = new stdClass();
44-
$depth = 1;
45-
46-
$stringifierMock = $this->createMock(Stringifier::class);
47-
$stringifierMock
48-
->expects($this->never())
49-
->method('stringify');
32+
$sut = new StringableObjectStringifier(new FakeStringifier(), new FakeQuoter());
5033

51-
$stringableObjectStringifier = new StringableObjectStringifier($stringifierMock);
52-
53-
self::assertNull($stringableObjectStringifier->stringify($raw, $depth));
34+
self::assertNull($sut->stringify(new stdClass(), self::DEPTH));
5435
}
5536

5637
#[Test]
57-
public function shouldConvertToStringWhenValueIsAnStringableObject(): void
38+
public function itShouldStringifyRawValueWhenItIsAnInstanceOfStringable(): void
5839
{
59-
$raw = new MyStringable();
60-
$depth = 0;
40+
$raw = new ConcreteStringable();
41+
42+
$stringifier = new FakeStringifier();
43+
$quoter = new FakeQuoter();
6144

62-
$expectedValue = MyStringable::STRING_VALUE;
45+
$string = $stringifier->stringify($raw->__toString(), self::DEPTH + 1);
6346

64-
$stringifierMock = $this->createMock(Stringifier::class);
65-
$stringifierMock
66-
->expects($this->once())
67-
->method('stringify')
68-
->with(MyStringable::STRING_VALUE, $depth)
69-
->willReturn($expectedValue);
47+
$sut = new StringableObjectStringifier($stringifier, $quoter);
7048

71-
$stringableObjectStringifier = new StringableObjectStringifier($stringifierMock);
49+
$actual = $sut->stringify($raw, self::DEPTH);
50+
$expected = $quoter->quote(
51+
sprintf('%s { __toString() => %s }', ConcreteStringable::class, $string),
52+
self::DEPTH
53+
);
7254

73-
self::assertSame($expectedValue, $stringableObjectStringifier->stringify($raw, $depth));
55+
self::assertSame($expected, $actual);
7456
}
7557
}

0 commit comments

Comments
 (0)