Skip to content

Commit c082c6f

Browse files
committed
Separate public API from internal logic
For a while now, our public `Stringifier` interface has been leaking internal control flow. By returning `string|null`, we were forcing consumers to deal with a "null" state that only existed to support our internal chain of responsibility. It made the public API clunky and less type-safe than it should be. I’ve resolved this by splitting the responsibilities into two distinct roles. The public `Stringifier` now provides a clean, guaranteed contract: it always returns a `string`. This removes the burden from the caller to handle nulls that they shouldn't have seen in the first place. Internally, the logic now lives in the `Handler` interface. This is where returning `null` still makes perfect sense—it’s how the chain-of-responsibility negotiates which handler takes the lead. By moving existing stringifiers into the `Handlers/` namespace, we’ve made a clear distinction between the implementation details and the public-facing service. The result is a more predictable API that hides its complexity without sacrificing the flexibility of the underlying pattern.
1 parent 676f823 commit c082c6f

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

53 files changed

+647
-685
lines changed

README.md

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,13 +32,11 @@ echo Respect\Stringifier\stringify($value);
3232
### Using as an object
3333

3434
```php
35-
$stringify = Respect\Stringifier\Stringify::createDefault();
35+
use Respect\Stringifier\HandlerStringifier;
3636

37-
// with the `value` method
38-
echo $stringify->value($value);
37+
$stringifier = HandlerStringifier::create();
3938

40-
// with the `__invoke` method
41-
echo $stringify($value);
39+
echo $stringifier->stringify($value);
4240
```
4341

4442
### Examples

phpcs.xml.dist

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
<exclude-pattern>tests/fixtures/</exclude-pattern>
1919
</rule>
2020
<rule ref="Squiz.Arrays.ArrayDeclaration.ValueNoNewline">
21-
<exclude-pattern>tests/unit/Stringifiers/CallableStringifierTest.php</exclude-pattern>
21+
<exclude-pattern>tests/unit/Handlers/CallableHandlerTest.php</exclude-pattern>
2222
</rule>
2323
<rule ref="Squiz.Functions.GlobalFunction">
2424
<exclude-pattern>tests/integration/lib/helpers.php</exclude-pattern>
@@ -34,6 +34,6 @@
3434
</rule>
3535
<rule ref="SlevomatCodingStandard.Functions.StaticClosure.ClosureNotStatic">
3636
<exclude-pattern>tests/integration</exclude-pattern>
37-
<exclude-pattern>tests/unit/Stringifiers/CallableStringifierTest.php</exclude-pattern>
37+
<exclude-pattern>tests/unit/Handlers/CallableHandlerTest.php</exclude-pattern>
3838
</rule>
3939
</ruleset>

src/DumpStringifier.php

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?php
2+
3+
/*
4+
* This file is part of Respect/Stringifier.
5+
* Copyright (c) Henrique Moody <henriquemoody@gmail.com>
6+
* SPDX-License-Identifier: MIT
7+
*/
8+
9+
declare(strict_types=1);
10+
11+
namespace Respect\Stringifier;
12+
13+
use function print_r;
14+
15+
final class DumpStringifier implements Stringifier
16+
{
17+
public function stringify(mixed $raw): string
18+
{
19+
return print_r($raw, true);
20+
}
21+
}

src/Handler.php

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?php
2+
3+
/*
4+
* This file is part of Respect/Stringifier.
5+
* Copyright (c) Henrique Moody <henriquemoody@gmail.com>
6+
* SPDX-License-Identifier: MIT
7+
*/
8+
9+
declare(strict_types=1);
10+
11+
namespace Respect\Stringifier;
12+
13+
interface Handler
14+
{
15+
/**
16+
* Attempts to stringify the given value.
17+
*
18+
* @return string|null The stringified value, or null if this handler cannot handle the type
19+
*/
20+
public function handle(mixed $raw, int $depth): string|null;
21+
}

src/HandlerStringifier.php

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<?php
2+
3+
/*
4+
* This file is part of Respect/Stringifier.
5+
* Copyright (c) Henrique Moody <henriquemoody@gmail.com>
6+
* SPDX-License-Identifier: MIT
7+
*/
8+
9+
declare(strict_types=1);
10+
11+
namespace Respect\Stringifier;
12+
13+
use Respect\Stringifier\Handlers\CompositeHandler;
14+
15+
final class HandlerStringifier implements Stringifier
16+
{
17+
public function __construct(
18+
private readonly Handler $handler,
19+
private readonly Stringifier $fallback,
20+
) {
21+
}
22+
23+
public static function create(): self
24+
{
25+
return new self(CompositeHandler::create(), new DumpStringifier());
26+
}
27+
28+
public function stringify(mixed $raw): string
29+
{
30+
return $this->handler->handle($raw, 0) ?? $this->fallback->stringify($raw);
31+
}
32+
}
Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,10 @@
88

99
declare(strict_types=1);
1010

11-
namespace Respect\Stringifier\Stringifiers;
11+
namespace Respect\Stringifier\Handlers;
1212

13+
use Respect\Stringifier\Handler;
1314
use Respect\Stringifier\Quoter;
14-
use Respect\Stringifier\Stringifier;
1515

1616
use function array_keys;
1717
use function count;
@@ -20,19 +20,19 @@
2020
use function range;
2121
use function sprintf;
2222

23-
final class ArrayStringifier implements Stringifier
23+
final class ArrayHandler implements Handler
2424
{
2525
private const string LIMIT_EXCEEDED_PLACEHOLDER = '...';
2626

2727
public function __construct(
28-
private readonly Stringifier $stringifier,
28+
private readonly Handler $handler,
2929
private readonly Quoter $quoter,
3030
private readonly int $maximumDepth,
3131
private readonly int $maximumNumberOfItems,
3232
) {
3333
}
3434

35-
public function stringify(mixed $raw, int $depth): string|null
35+
public function handle(mixed $raw, int $depth): string|null
3636
{
3737
if (!is_array($raw)) {
3838
return null;
@@ -60,7 +60,7 @@ public function stringify(mixed $raw, int $depth): string|null
6060
continue;
6161
}
6262

63-
$items[] = sprintf('%s: %s', $this->stringifier->stringify($key, $depth + 1), $stringifiedValue);
63+
$items[] = sprintf('%s: %s', $this->handler->handle($key, $depth + 1), $stringifiedValue);
6464
}
6565

6666
return $this->quoter->quote(sprintf('[%s]', implode(', ', $items)), $depth);
@@ -69,10 +69,10 @@ public function stringify(mixed $raw, int $depth): string|null
6969
private function stringifyKeyValue(mixed $value, int $depth): string|null
7070
{
7171
if (is_array($value)) {
72-
return $this->stringify($value, $depth);
72+
return $this->handle($value, $depth);
7373
}
7474

75-
return $this->stringifier->stringify($value, $depth);
75+
return $this->handler->handle($value, $depth);
7676
}
7777

7878
/** @param mixed[] $array */

src/Stringifiers/ArrayObjectStringifier.php renamed to src/Handlers/ArrayObjectHandler.php

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,31 +8,31 @@
88

99
declare(strict_types=1);
1010

11-
namespace Respect\Stringifier\Stringifiers;
11+
namespace Respect\Stringifier\Handlers;
1212

1313
use ArrayObject;
14+
use Respect\Stringifier\Handler;
1415
use Respect\Stringifier\Helpers\ObjectHelper;
1516
use Respect\Stringifier\Quoter;
16-
use Respect\Stringifier\Stringifier;
1717

18-
final class ArrayObjectStringifier implements Stringifier
18+
final class ArrayObjectHandler implements Handler
1919
{
2020
use ObjectHelper;
2121

2222
public function __construct(
23-
private readonly Stringifier $stringifier,
23+
private readonly Handler $handler,
2424
private readonly Quoter $quoter,
2525
) {
2626
}
2727

28-
public function stringify(mixed $raw, int $depth): string|null
28+
public function handle(mixed $raw, int $depth): string|null
2929
{
3030
if (!$raw instanceof ArrayObject) {
3131
return null;
3232
}
3333

3434
return $this->quoter->quote(
35-
$this->format($raw, 'getArrayCopy() =>', $this->stringifier->stringify($raw->getArrayCopy(), $depth + 1)),
35+
$this->format($raw, 'getArrayCopy() =>', $this->handler->handle($raw->getArrayCopy(), $depth + 1)),
3636
$depth,
3737
);
3838
}
Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,21 +8,21 @@
88

99
declare(strict_types=1);
1010

11-
namespace Respect\Stringifier\Stringifiers;
11+
namespace Respect\Stringifier\Handlers;
1212

13+
use Respect\Stringifier\Handler;
1314
use Respect\Stringifier\Quoter;
14-
use Respect\Stringifier\Stringifier;
1515

1616
use function is_bool;
1717

18-
final class BoolStringifier implements Stringifier
18+
final class BoolHandler implements Handler
1919
{
2020
public function __construct(
2121
private readonly Quoter $quoter,
2222
) {
2323
}
2424

25-
public function stringify(mixed $raw, int $depth): string|null
25+
public function handle(mixed $raw, int $depth): string|null
2626
{
2727
if (!is_bool($raw)) {
2828
return null;
Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
declare(strict_types=1);
1010

11-
namespace Respect\Stringifier\Stringifiers;
11+
namespace Respect\Stringifier\Handlers;
1212

1313
use Closure;
1414
use ReflectionFunction;
@@ -19,9 +19,9 @@
1919
use ReflectionParameter;
2020
use ReflectionType;
2121
use ReflectionUnionType;
22+
use Respect\Stringifier\Handler;
2223
use Respect\Stringifier\Helpers\ObjectHelper;
2324
use Respect\Stringifier\Quoter;
24-
use Respect\Stringifier\Stringifier;
2525

2626
use function array_keys;
2727
use function array_map;
@@ -37,18 +37,18 @@
3737
use function strstr;
3838
use function substr;
3939

40-
final class CallableStringifier implements Stringifier
40+
final class CallableHandler implements Handler
4141
{
4242
use ObjectHelper;
4343

4444
public function __construct(
45-
private readonly Stringifier $stringifier,
45+
private readonly Handler $handler,
4646
private readonly Quoter $quoter,
4747
private readonly bool $closureOnly = true,
4848
) {
4949
}
5050

51-
public function stringify(mixed $raw, int $depth): string|null
51+
public function handle(mixed $raw, int $depth): string|null
5252
{
5353
if ($raw instanceof Closure) {
5454
return $this->buildFunction(new ReflectionFunction($raw), $depth);
@@ -170,14 +170,14 @@ private function buildParameter(ReflectionParameter $reflectionParameter, int $d
170170
private function buildValue(ReflectionParameter $reflectionParameter, int $depth): string|null
171171
{
172172
if (!$reflectionParameter->isDefaultValueAvailable()) {
173-
return $this->stringifier->stringify(null, $depth);
173+
return $this->handler->handle(null, $depth);
174174
}
175175

176176
if ($reflectionParameter->isDefaultValueConstant()) {
177177
return $reflectionParameter->getDefaultValueConstantName();
178178
}
179179

180-
return $this->stringifier->stringify($reflectionParameter->getDefaultValue(), $depth);
180+
return $this->handler->handle($reflectionParameter->getDefaultValue(), $depth);
181181
}
182182

183183
private function buildType(ReflectionType $raw, int $depth): string

src/Handlers/CompositeHandler.php

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
<?php
2+
3+
/*
4+
* This file is part of Respect/Stringifier.
5+
* Copyright (c) Henrique Moody <henriquemoody@gmail.com>
6+
* SPDX-License-Identifier: MIT
7+
*/
8+
9+
declare(strict_types=1);
10+
11+
namespace Respect\Stringifier\Handlers;
12+
13+
use DateTimeInterface;
14+
use Respect\Stringifier\Handler;
15+
use Respect\Stringifier\Quoters\StandardQuoter;
16+
17+
use function array_unshift;
18+
19+
final class CompositeHandler implements Handler
20+
{
21+
private const int MAXIMUM_DEPTH = 3;
22+
private const int MAXIMUM_NUMBER_OF_ITEMS = 5;
23+
private const int MAXIMUM_NUMBER_OF_PROPERTIES = self::MAXIMUM_NUMBER_OF_ITEMS;
24+
private const int MAXIMUM_LENGTH = 120;
25+
26+
/** @var array<Handler> */
27+
private array $handlers = [];
28+
29+
public function __construct(Handler ...$handlers)
30+
{
31+
$this->handlers = $handlers;
32+
}
33+
34+
public static function create(): self
35+
{
36+
$quoter = new StandardQuoter(self::MAXIMUM_LENGTH);
37+
38+
$handler = new self(
39+
new InfiniteNumberHandler($quoter),
40+
new NotANumberHandler($quoter),
41+
new ResourceHandler($quoter),
42+
new BoolHandler($quoter),
43+
new NullHandler($quoter),
44+
new DeclaredHandler($quoter),
45+
$jsonEncodableHandler = new JsonEncodableHandler(),
46+
);
47+
$handler->prependHandler(
48+
$arrayHandler = new ArrayHandler(
49+
$handler,
50+
$quoter,
51+
self::MAXIMUM_DEPTH,
52+
self::MAXIMUM_NUMBER_OF_ITEMS,
53+
),
54+
);
55+
$handler->prependHandler(
56+
new ObjectHandler(
57+
$handler,
58+
$quoter,
59+
self::MAXIMUM_DEPTH,
60+
self::MAXIMUM_NUMBER_OF_PROPERTIES,
61+
),
62+
);
63+
$handler->prependHandler(new CallableHandler($handler, $quoter));
64+
$handler->prependHandler(
65+
new FiberObjectHandler(new CallableHandler($handler, $quoter, closureOnly: false), $quoter),
66+
);
67+
$handler->prependHandler(new EnumerationHandler($quoter));
68+
$handler->prependHandler(new ObjectWithDebugInfoHandler($arrayHandler, $quoter));
69+
$handler->prependHandler(new ArrayObjectHandler($arrayHandler, $quoter));
70+
$handler->prependHandler(new JsonSerializableObjectHandler($jsonEncodableHandler, $quoter));
71+
$handler->prependHandler(new StringableObjectHandler($jsonEncodableHandler, $quoter));
72+
$handler->prependHandler(new ThrowableObjectHandler($jsonEncodableHandler, $quoter));
73+
$handler->prependHandler(new DateTimeHandler($quoter, DateTimeInterface::ATOM));
74+
$handler->prependHandler(new IteratorObjectHandler($handler, $quoter));
75+
76+
return $handler;
77+
}
78+
79+
public function prependHandler(Handler $handler): void
80+
{
81+
array_unshift($this->handlers, $handler);
82+
}
83+
84+
public function handle(mixed $raw, int $depth): string|null
85+
{
86+
foreach ($this->handlers as $handler) {
87+
$string = $handler->handle($raw, $depth);
88+
if ($string === null) {
89+
continue;
90+
}
91+
92+
return $string;
93+
}
94+
95+
return null;
96+
}
97+
}

0 commit comments

Comments
 (0)