Skip to content

Commit 942e830

Browse files
committed
Replace AutoQuoteModifier with focused Stringify and Raw modifiers
The `AutoQuoteModifier` was an attempt to handle both quoting and raw output in a single place, but in practice, it felt confused and didn’t solve the problem as intended. It tried to do too much, resulting in unwanted outcomes. This refactor breaks those responsibilities into single-purpose modifiers, which are much easier to reason about. The New Modifier Strategy: - `EnsureStringModifier` (formerly `StringifyModifier`): This now has a very narrow focus. It only stringifies non-string values, leaving existing strings untouched. - `StringifyModifier`: I’ve introduced a new version of this that always stringifies every value it receives, regardless of type. - `RawModifier`: This handles the `|raw` pipe specifically, providing a clean escape for unquoted scalar output. These modifiers are designed to complement one another. In Respect\Validation, where we want strings to be quoted by default, the `StringifyModifier` handles the all parameters, while the `RawModifier` provides the necessary "opt-out" via the `|raw` pipe. Assisted-by: OpenCode (GLM-4.6) Assisted-by: Gemini 3 (Thinking) Assisted-by: Claude Code (Opus 4.5)
1 parent c1df17a commit 942e830

16 files changed

+467
-295
lines changed

composer.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
"require": {
66
"php": "^8.5",
77
"respect/stringifier": "^3.0",
8-
"symfony/polyfill-ctype": "^1.33",
98
"symfony/polyfill-mbstring": "^1.33",
109
"symfony/translation-contracts": "^3.6"
1110
},

docs/modifiers/AutoQuoteModifier.md

Lines changed: 0 additions & 45 deletions
This file was deleted.
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
# EnsureStringModifier
2+
3+
The `EnsureStringModifier` converts non-string values to strings using a `Stringifier` instance. It serves as the terminal modifier in the default `PlaceholderFormatter` chain.
4+
5+
## Behavior
6+
7+
| Pipe | String | Other Types |
8+
| ------ | ------------------ | ------------------------- |
9+
| (none) | Returned unchanged | Converted via stringifier |
10+
| `\|*` | Throws exception | Throws exception |
11+
12+
- Uses `HandlerStringifier` by default for type conversion
13+
- Throws `InvalidModifierPipeException` if any pipe is used (e.g., `{{value|something}}`)
14+
15+
This modifier is typically used at the end of a modifier chain to ensure all values become strings.
16+
17+
## Usage
18+
19+
```php
20+
use Respect\StringFormatter\PlaceholderFormatter;
21+
use Respect\StringFormatter\Modifiers\EnsureStringModifier;
22+
23+
$formatter = new PlaceholderFormatter(
24+
[
25+
'name' => 'John',
26+
'active' => true,
27+
'data' => ['x' => 1],
28+
],
29+
new EnsureStringModifier(),
30+
);
31+
32+
echo $formatter->format('{{name}} is {{active}}');
33+
// Output: John is true
34+
35+
echo $formatter->format('Data: {{data}}');
36+
// Output: Data: ["x":1]
37+
```
38+
39+
## Examples
40+
41+
| Parameters | Template | Output |
42+
| ------------------------ | ------------ | --------- |
43+
| `['name' => 'John']` | `{{name}}` | `John` |
44+
| `['count' => 42]` | `{{count}}` | `42` |
45+
| `['price' => 19.99]` | `{{price}}` | `19.99` |
46+
| `['active' => true]` | `{{active}}` | `true` |
47+
| `['active' => false]` | `{{active}}` | `false` |
48+
| `['value' => null]` | `{{value}}` | `null` |
49+
| `['items' => [1, 2, 3]]` | `{{items}}` | `[1,2,3]` |
50+
| `['data' => ['a' => 1]]` | `{{data}}` | `["a":1]` |
51+
52+
## Custom Stringifier
53+
54+
You can provide a custom `Stringifier` to control how non-string values are converted:
55+
56+
```php
57+
use Respect\StringFormatter\PlaceholderFormatter;
58+
use Respect\StringFormatter\Modifiers\EnsureStringModifier;
59+
60+
$formatter = new PlaceholderFormatter(
61+
['data' => $value],
62+
new EnsureStringModifier($customStringifier),
63+
);
64+
```
65+
66+
See the [Respect\Stringifier documentation](https://github.com/Respect/Stringifier) for details on creating custom stringifiers.

docs/modifiers/Modifiers.md

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,26 +32,30 @@ You can specify a custom modifier chain when creating a `PlaceholderFormatter`:
3232

3333
```php
3434
use Respect\StringFormatter\PlaceholderFormatter;
35-
use Respect\StringFormatter\Modifiers\AutoQuoteModifier;
35+
use Respect\StringFormatter\Modifiers\RawModifier;
3636
use Respect\StringFormatter\Modifiers\StringifyModifier;
3737

3838
$formatter = new PlaceholderFormatter(
39-
['name' => 'John'],
40-
new AutoQuoteModifier(new StringifyModifier()),
39+
['name' => 'John', 'active' => true],
40+
new RawModifier(new StringifyModifier()),
4141
);
4242

4343
echo $formatter->format('Hello {{name}}');
4444
// Output: Hello "John"
45+
46+
echo $formatter->format('Hello {{name|raw}}');
47+
// Output: Hello John
4548
```
4649

4750
If no modifier is provided, the formatter uses `StringifyModifier` by default.
4851

4952
## Available Modifiers
5053

51-
- **[AutoQuoteModifier](AutoQuoteModifier.md)** - Quotes string values by default, `|raw` bypasses quoting
5254
- **[ListModifier](ListModifier.md)** - Formats arrays as human-readable lists with conjunctions
5355
- **[QuoteModifier](QuoteModifier.md)** - Quotes string values using a stringifier quoter
54-
- **[StringifyModifier](StringifyModifier.md)** - Converts values to strings (default)
56+
- **[RawModifier](RawModifier.md)** - Returns scalar values as raw strings with `|raw` pipe
57+
- **[EnsureStringModifier](EnsureStringModifier.md)** - Converts non-string values to strings (bypasses strings)
58+
- **[StringifyModifier](StringifyModifier.md)** - Always converts values to strings (default)
5559
- **[TransModifier](TransModifier.md)** - Translates string values using a Symfony translator
5660

5761
## Creating Custom Modifiers

docs/modifiers/RawModifier.md

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
# RawModifier
2+
3+
The `|raw` modifier returns scalar values as raw strings, converting booleans to `1`/`0`. Non-scalar values delegate to the next modifier.
4+
5+
> **Note:** This modifier is designed to be used with [StringifyModifier](StringifyModifier.md). When `StringifyModifier` quotes strings by default, `RawModifier` provides a way to output unquoted values using the `|raw` pipe.
6+
7+
## Behavior
8+
9+
| Pipe | String | Other Scalars | Non-Scalar |
10+
| ------- | --------------- | ------------------- | -------------------------- |
11+
| (none) | Delegates | Delegates | Delegates |
12+
| `\|raw` | Unquoted string | Converted to string | Delegates to next modifier |
13+
14+
With `|raw`, booleans are converted to `1`/`0`. All other values are delegated to the next modifier.
15+
16+
## Usage
17+
18+
The `RawModifier` is typically used with `StringifyModifier` to create a modifier chain that handles raw output:
19+
20+
```php
21+
use Respect\StringFormatter\PlaceholderFormatter;
22+
use Respect\StringFormatter\Modifiers\RawModifier;
23+
use Respect\StringFormatter\Modifiers\StringifyModifier;
24+
25+
$formatter = new PlaceholderFormatter(
26+
['firstname' => 'John', 'lastname' => 'Doe', 'active' => true],
27+
new RawModifier(new StringifyModifier()),
28+
);
29+
30+
echo $formatter->format('Hi {{firstname}}');
31+
// Output: Hi John
32+
33+
echo $formatter->format('Active flag: {{active|raw}}');
34+
// Output: Active flag: 1
35+
```
36+
37+
## Examples
38+
39+
Here are some examples demonstrating the behavior of `RawModifier` when used with `StringifyModifier`:
40+
41+
| Parameters | Template | Output |
42+
| ------------------------------------------- | ---------------- | --------------------------------------------- |
43+
| `['name' => 'John']` | `{{name}}` | `"John"` |
44+
| `['name' => 'John']` | `{{name\|raw}}` | `John` |
45+
| `['count' => 42]` | `{{count}}` | `42` |
46+
| `['count' => 42]` | `{{count\|raw}}` | `42` |
47+
| `['on' => true]` | `{{on}}` | `true` |
48+
| `['on' => true]` | `{{on\|raw}}` | `1` |
49+
| `['off' => false]` | `{{off}}` | `false` |
50+
| `['off' => false]` | `{{off\|raw}}` | `0` |
51+
| `['items' => [1, 2], 'list' => ['a', 'b']]` | `{{items\|raw}}` | `["a", "b"]` (delegated to StringifyModifier) |
Lines changed: 60 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,83 @@
11
# StringifyModifier
22

3-
The `StringifyModifier` converts values to strings using a `Stringifier` instance. This is the default modifier used by `PlaceholderFormatter`.
3+
The `StringifyModifier` always converts values to strings, regardless of their type.
4+
5+
> **Note:** This modifier pairs well with [RawModifier](RawModifier.md). Since `StringifyModifier` may quote strings, you can use `RawModifier` to provide a `|raw` pipe for unquoted output when needed.
46
57
## Behavior
68

7-
- Strings pass through unchanged
8-
- Other types are converted using the configured stringifier
9-
- Throws `InvalidModifierPipeException` if an unrecognized pipe is passed
9+
| Pipe | Any Value |
10+
| ------ | ---------------- |
11+
| (none) | Stringified |
12+
| Other | Throws exception |
13+
14+
All values are converted to strings using the stringifier. Unlike `EnsureStringModifier`, even strings are passed through the stringifier, which may or may not modify them.
1015

1116
## Usage
1217

18+
The `StringifyModifier` is the default modifier in `PlaceholderFormatter`. You can also create instances with custom stringifiers:
19+
1320
```php
1421
use Respect\StringFormatter\PlaceholderFormatter;
22+
use Respect\StringFormatter\Modifiers\StringifyModifier;
23+
use Respect\Stringifier\HandlerStringifier;
24+
25+
$stringifier = HandlerStringifier::create();
26+
$formatter = new PlaceholderFormatter(
27+
['name' => 'John', 'active' => true, 'items' => [1, 2]],
28+
new StringifyModifier($stringifier),
29+
);
1530

16-
$formatter = new PlaceholderFormatter([
17-
'name' => 'John',
18-
'active' => true,
19-
'data' => ['x' => 1],
20-
]);
31+
echo $formatter->format('User: {{name}}');
32+
// Output: User: "John"
2133

22-
echo $formatter->format('{{name}} is {{active}}');
23-
// Output: John is true
34+
echo $formatter->format('Active: {{active}}');
35+
// Output: Active: `true`
2436

25-
echo $formatter->format('Data: {{data}}');
26-
// Output: Data: ["x":1]
37+
echo $formatter->format('Items: {{items}}');
38+
// Output: Items: `[1, 2]`
2739
```
2840

41+
## Examples
42+
43+
| Parameters | Template | Output |
44+
| ------------------------ | ------------ | --------- |
45+
| `['name' => 'John']` | `{{name}}` | `"John"` |
46+
| `['count' => 42]` | `{{count}}` | `42` |
47+
| `['price' => 19.99]` | `{{price}}` | `19.99` |
48+
| `['active' => true]` | `{{active}}` | `true` |
49+
| `['active' => false]` | `{{active}}` | `false` |
50+
| `['value' => null]` | `{{value}}` | `null` |
51+
| `['items' => [1, 2, 3]]` | `{{items}}` | `[1,2,3]` |
52+
| `['data' => ['a' => 1]]` | `{{data}}` | `["a":1]` |
53+
2954
## Custom Stringifier
3055

3156
```php
32-
use Respect\StringFormatter\PlaceholderFormatter;
3357
use Respect\StringFormatter\Modifiers\StringifyModifier;
3458
use Respect\Stringifier\Stringifier;
3559

36-
$formatter = new PlaceholderFormatter(
37-
['data' => $value],
38-
new StringifyModifier($customStringifier),
39-
);
60+
final readonly class CustomStringifier implements Stringifier
61+
{
62+
public function stringify(mixed $raw): string|null
63+
{
64+
return is_bool($raw) ? ($raw ? 'YES' : 'NO') : json_encode($raw);
65+
}
66+
}
67+
68+
$modifier = new StringifyModifier(new CustomStringifier());
69+
echo $modifier->modify(true, null);
70+
// Output: YES
4071
```
4172

42-
See the [Respect\Stringifier documentation](https://github.com/Respect/Stringifier) for details on stringifiers.
73+
## Examples
74+
75+
| Parameters | Template | Output |
76+
| ----------------------------- | ----------- | ------------------- |
77+
| `['name' => 'John']` | `{{name}}` | `"John"` |
78+
| `['count' => 42]` | `{{count}}` | `"42"` |
79+
| `['on' => true]` | `{{on}}` | `` `true` `` |
80+
| `['off' => false]` | `{{off}}` | `` `false` `` |
81+
| `['items' => [1, 2]]` | `{{items}}` | `` `[1, 2]` `` |
82+
| `['obj' => (object)['a'=>1]]` | `{{obj}}` | `` `stdClass {}` `` |
83+
| `['val' => null]` | `{{val}}` | `` `null` `` |

docs/modifiers/TransModifier.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ Install `symfony/translation` and inject a real translator:
2828
```php
2929
use Respect\StringFormatter\PlaceholderFormatter;
3030
use Respect\StringFormatter\Modifiers\TransModifier;
31-
use Respect\StringFormatter\Modifiers\StringifyModifier;
31+
use Respect\StringFormatter\Modifiers\EnsureStringModifier;
3232
use Symfony\Component\Translation\Translator;
3333
use Symfony\Component\Translation\Loader\ArrayLoader;
3434

src/Modifiers/AutoQuoteModifier.php

Lines changed: 0 additions & 37 deletions
This file was deleted.
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Respect\StringFormatter\Modifiers;
6+
7+
use Respect\StringFormatter\Modifier;
8+
use Respect\Stringifier\HandlerStringifier;
9+
use Respect\Stringifier\Stringifier;
10+
11+
use function is_string;
12+
use function sprintf;
13+
14+
final readonly class EnsureStringModifier implements Modifier
15+
{
16+
private Stringifier $stringifier;
17+
18+
public function __construct(
19+
Stringifier|null $stringifier = null,
20+
) {
21+
$this->stringifier = $stringifier ?? HandlerStringifier::create();
22+
}
23+
24+
public function modify(mixed $value, string|null $pipe): string
25+
{
26+
if ($pipe !== null) {
27+
throw new InvalidModifierPipeException(sprintf('"%s" is not recognized as a valid pipe', $pipe));
28+
}
29+
30+
if (is_string($value)) {
31+
return $value;
32+
}
33+
34+
return $this->stringifier->stringify($value);
35+
}
36+
}

0 commit comments

Comments
 (0)