Skip to content

Commit 3405a0a

Browse files
committed
Add modifier support to PlaceholderFormatter
While basic template interpolation is useful, we know that a one-size-fits-all stringification approach isn't always desirable. Sometimes you need to bypass the standard formatting logic or transform a value before it hits the final string. This commit introduces a flexible modifier system to solve that. I’ve adopted the familiar `{{value|modifier}}` syntax found in many popular template engines. To handle this transformation, I implemented a Chain of Responsibility pattern. This allows us to pipe values through specific logic, such as the new `|raw` modifier, which provides clean scalar output by bypassing the default stringifier formatting. The `StringifyModifier` remains the default fallback, ensuring that we don't lose the robust type-handling we previously built. By separating these transformations into an extensible modifier interface, we’ve made the system future-proof—users can now plug in custom transformations without touching the core formatting logic. This change turns our simple placeholder replacement into a proper interpolation engine, giving developers fine-grained control over how their data is presented. Assisted-by: OpenCode (GLM-4.6)
1 parent f5f216a commit 3405a0a

12 files changed

Lines changed: 843 additions & 54 deletions

docs/PlaceholderFormatter.md

Lines changed: 44 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -13,26 +13,48 @@ use Respect\Stringifier\HandlerStringifier;
1313
$formatter = new PlaceholderFormatter(['name' => 'John', 'age' => 30]);
1414

1515
echo $formatter->format('Hello {{name}}, you are {{age}} years old.');
16-
// Outputs: "Hello John, you are 30 years old."
16+
// Outputs: "Hello "John", you are 30 years old."
1717
```
1818

19-
### With Custom Stringifier
19+
### With Custom Modifiers
2020

2121
```php
22+
use App\StringFormatter\Modifiers\MyModifier;
2223
use Respect\StringFormatter\PlaceholderFormatter;
23-
use Respect\Stringifier\HandlerStringifier;
2424

25-
$stringifier = HandlerStringifier::create();
25+
$modifier = new MyModifier();
2626

2727
$formatter = new PlaceholderFormatter(
28-
['name' => 'John', 'items' => ['a', 'b', 'c']],
29-
$stringifier
28+
['count' => 42, 'name' => 'John'],
29+
$modifier
3030
);
3131

32-
echo $formatter->format('Hello {{name}}, items: {{items}}');
33-
// Outputs: "Hello John, items: [a, b, c]"
32+
echo $formatter->format('Hello {{name}}, you are {{age}} years old.');
33+
// Outputs: "Hello --John--, you are --30-- years old."
34+
```
35+
36+
### Modifiers
37+
38+
Placeholders can include modifiers that transform values before final stringification. See the [Modifiers](modifiers/Modifiers.md) documentation for complete details.
39+
40+
```php
41+
use Respect\StringFormatter\PlaceholderFormatter;
42+
use Respect\StringFormatter\Modifier\RawModifier;
43+
use Respect\StringFormatter\Modifier\StringifyModifier;
44+
45+
$formatter = new PlaceholderFormatter(['name' => 'John', 'age' => 30]);
46+
47+
echo $formatter->format('Hello {{name|raw}}, you are {{age}} years old.');
48+
// Outputs: "Hello John, you are 30 years old."
3449
```
3550

51+
**Available Topics:**
52+
53+
- [Modifiers Overview](modifiers/Modifiers.md) - How modifiers work and basic usage
54+
- [RawModifier](modifiers/RawModifier.md) - Outputs scalar values without stringifier formatting
55+
- [StringifyModifier](modifiers/StringifyModifier.md) - Default modifier using stringifier
56+
- [Creating Custom Modifiers](modifiers/CreatingCustomModifiers.md) - Build your own modifiers
57+
3658
### Using Additional Parameters
3759

3860
The `formatWith` method allows passing additional parameters at format time. Constructor parameters take precedence and won't be overwritten.
@@ -55,14 +77,16 @@ echo $formatter->formatWith(
5577

5678
### `PlaceholderFormatter::__construct`
5779

58-
- `__construct(array $parameters, Stringifier|null $stringifier = null)`
80+
- `__construct(array $parameters, Modifier|null $modifier = null)`
5981

60-
Creates a new formatter instance with the specified parameters and stringifier.
82+
Creates a new formatter instance with the specified parameters and optional modifier chain.
6183

6284
**Parameters:**
6385

6486
- `$parameters`: Associative array of placeholder names to values
65-
- `$stringifier`: Stringifier instance for converting all non-string values to strings. If `null`, it creates its own stringifier.
87+
- `$modifier`: Optional modifier chain for processing values before final stringification. If `null`, uses a default `RawModifier` with `StringifyModifier`. See [Modifiers](modifiers/Modifiers.md) for details.
88+
89+
The formatter internally uses `HandlerStringifier::create()` for stringifying non-string values.
6690

6791
### `format`
6892

@@ -99,57 +123,48 @@ Formats the template string with additional parameters merged with constructor p
99123

100124
### Placeholder Format
101125

102-
Placeholders follow the format `{{name}}` where `name` is a valid parameter key.
126+
Placeholders follow the format `{{name}}` where `name` is a valid parameter key. Modifiers can be added after a pipe: `{{name|modifier}}`. See [Modifiers](modifiers/Modifiers.md) for complete modifier documentation.
103127

104128
**Rules:**
105129

106130
- Placeholder names must match the regex pattern `\w+` (word characters: letters, digits, underscore)
107131
- Names are case-sensitive: `{{Name}}` and `{{name}}` are different placeholders
108132
- Placeholders can appear multiple times in the template
109133
- Whitespace inside braces is not allowed: `{{ name }}` will not match
134+
- No whitespace is allowed between the pipe and modifier name: `{{name| raw}}` will not work
110135

111136
**Valid placeholders:**
112137

113138
- `{{name}}`
114139
- `{{firstName}}`
115140
- `{{value123}}`
116141
- `{{user_id}}`
142+
- `{{name|raw}}`
143+
- `{{count|raw}}`
117144

118145
**Invalid placeholders (treated as literals):**
119146

120147
- `{name}` (single braces)
121148
- `{{ name }}` (contains spaces)
122149
- `{{first-name}}` (contains hyphen)
123150
- `{{}}` (empty)
151+
- `{{name| raw}}` (space after pipe)
124152

125153
## Type Handling
126154

127-
The formatter uses the injected `Stringifier` to convert all parameter values to strings:
128-
129-
| Type | Behavior | Example |
130-
| ---------- | -------------------------------------------------------------- | ----------------------------------------------------- |
131-
| `string` | Used as-is | `"hello"``"hello"` |
132-
| `int` | Converted using stringifier | `42``"42"` |
133-
| `float` | Converted using stringifier | `19.99``"19.99"` |
134-
| `bool` | Converted using stringifier with backticks | `true``` `true` ``, `false``` `false` `` |
135-
| `null` | Converted using stringifier with backticks | `null``` `null` `` |
136-
| `array` | Converted using stringifier (or `print_r` as fallback) | `[1, 2]``"[1, 2]"` (varies) |
137-
| `object` | Converted using stringifier (or `print_r` as fallback) | Varies by object type |
138-
| Stringable | Converted using stringifier (includes metadata with backticks) | `__toString()``` `Stringable@anonymous { ... }` `` |
139-
| Resource | Converted using stringifier (or `print_r` as fallback) | Resource representation |
140-
| Missing | Keeps placeholder unchanged (parameter key doesn't exist) | N/A → `"{{name}}"` |
155+
The formatter uses the injected `Stringifier` to convert all parameter values to strings. See the [Respect\Stringifier](https://github.com/Respect/Stringifier) documentation for details on type conversions.
141156

142157
## Behavior
143158

144159
### Successful Replacement
145160

146161
When a placeholder name exists as a parameter key:
147162

148-
- The placeholder is replaced with the stringified value (using the injected `Stringifier`)
149-
- String values are used as-is without stringification
150-
- All non-string values (including `null`) are converted using the stringifier
163+
- The placeholder is replaced with the value processed through the modifier pipe
164+
- All values (including `null`) are converted using the default `HandlerStringifier` unless a modifier handles them differently
151165
- Empty strings are valid replacements: `""` replaces the placeholder with nothing
152166
- Zero values are valid: `0` and `0.0` are replaced with their string representations
167+
- Modifiers can transform values: `{{count|raw}}` outputs the raw scalar value without stringifier formatting (see [Modifiers](modifiers/Modifiers.md))
153168

154169
### Placeholder Preservation
155170

@@ -366,15 +381,3 @@ echo $formatter->format('{{greeting}} {{name}}');
366381
- **No expressions**: `{{x + y}}` is not evaluated; only simple value replacement
367382
- **No conditional logic**: No if/else or ternary operations
368383
- **No default values**: Use null checks in PHP before passing parameters
369-
370-
## Future Extensions
371-
372-
The implementation is designed to support modifiers in a future phase:
373-
374-
```php
375-
// Future syntax (not yet implemented)
376-
$formatter->format('Hello {{name|upper}}!');
377-
// Would output: "Hello JOHN!"
378-
```
379-
380-
The regex pattern and internal structure are prepared for this extension without breaking changes.

0 commit comments

Comments
 (0)