Skip to content

Commit 7fad073

Browse files
committed
Replace AutoQuoteModifier with a different modifier strategy
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: - `StringifyModifier`: I’ve introduced a new version of this that always stringifies every value it receives, regardless of type. - `StringPassthroughModifier`: This now has a very narrow focus. It only stringifies non-string values, leaving existing strings untouched. - `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, should not include `StringPassthroughModifier` but instead use `RawModifier` in the chain, so users can opt-out of stringification with 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 7fad073

16 files changed

+434
-294
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.

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+
- **[StringifyModifier](StringifyModifier.md)** - Always converts values to strings (default)
58+
- **[StringPassthroughModifier](StringPassthroughModifier.md)** - Returns strings unchanged, delegates non-strings to next modifier
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 only useful when [StringPassthroughModifier](StringPassthroughModifier.md) is **not** in the chain. When using `StringifyModifier` directly (without `StringPassthroughModifier`), strings get quoted by default and `RawModifier` provides the `|raw` pipe for unquoted output. The default chain includes `StringPassthroughModifier`, so strings are already unquoted.
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: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
# StringPassthroughModifier
2+
3+
The `StringPassthroughModifier` returns string values unchanged and delegates non-string values to the next modifier in the chain.
4+
5+
## Behavior
6+
7+
| Pipe | String | Other Types |
8+
| ----- | ------------------ | -------------------------- |
9+
| (any) | Returned unchanged | Delegates to next modifier |
10+
11+
- Strings are returned as-is without any processing
12+
- Non-strings are passed to the next modifier along with the pipe
13+
- This modifier does not handle any pipes itself
14+
15+
This modifier is useful when you want to preserve string values while allowing non-strings to be processed by another modifier (e.g., `StringifyModifier`).
16+
17+
## Usage
18+
19+
```php
20+
use Respect\StringFormatter\PlaceholderFormatter;
21+
use Respect\StringFormatter\Modifiers\StringPassthroughModifier;
22+
use Respect\StringFormatter\Modifiers\StringifyModifier;
23+
24+
$formatter = new PlaceholderFormatter(
25+
[
26+
'name' => 'John',
27+
'active' => true,
28+
'data' => ['x' => 1],
29+
],
30+
new StringPassthroughModifier(new StringifyModifier()),
31+
);
32+
33+
echo $formatter->format('{{name}} is {{active}}');
34+
// Output: John is true
35+
36+
echo $formatter->format('Data: {{data}}');
37+
// Output: Data: ["x":1]
38+
```
39+
40+
## Examples
41+
42+
When used with `StringifyModifier`:
43+
44+
| Parameters | Template | Output |
45+
| ------------------------ | ------------ | --------- |
46+
| `['name' => 'John']` | `{{name}}` | `John` |
47+
| `['count' => 42]` | `{{count}}` | `42` |
48+
| `['price' => 19.99]` | `{{price}}` | `19.99` |
49+
| `['active' => true]` | `{{active}}` | `true` |
50+
| `['active' => false]` | `{{active}}` | `false` |
51+
| `['value' => null]` | `{{value}}` | `null` |
52+
| `['items' => [1, 2, 3]]` | `{{items}}` | `[1,2,3]` |
53+
| `['data' => ['a' => 1]]` | `{{data}}` | `["a":1]` |
54+
55+
Note that string values like `John` are returned unchanged (not quoted), while non-string values are stringified by `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:** When used directly (without [StringPassthroughModifier](StringPassthroughModifier.md)), this modifier quotes strings. In that case, [RawModifier](RawModifier.md) can provide a `|raw` pipe for unquoted output. The default chain includes `StringPassthroughModifier`, which bypasses stringification for strings.
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 `StringPassthroughModifier`, 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\StringPassthroughModifier;
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.

src/Modifiers/RawModifier.php

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Respect\StringFormatter\Modifiers;
6+
7+
use Respect\StringFormatter\Modifier;
8+
9+
use function is_bool;
10+
use function is_scalar;
11+
12+
final readonly class RawModifier implements Modifier
13+
{
14+
public function __construct(
15+
private Modifier $nextModifier,
16+
) {
17+
}
18+
19+
public function modify(mixed $value, string|null $pipe): string
20+
{
21+
if ($pipe !== 'raw') {
22+
return $this->nextModifier->modify($value, $pipe);
23+
}
24+
25+
if (!is_scalar($value)) {
26+
return $this->nextModifier->modify($value, null);
27+
}
28+
29+
return is_bool($value) ? (string) (int) $value : (string) $value;
30+
}
31+
}

0 commit comments

Comments
 (0)