Skip to content

Commit 9f4d9d8

Browse files
committed
Add multiple pipes support for chaining modifiers in placeholders
Placeholders can now chain multiple modifiers sequentially using the pipe separator, e.g. {{value|date:Y/m/d|mask:5-8}}. Each modifier receives the output of the previous one, applied left to right. Escaped pipes (\|) within modifier arguments are preserved as literal characters rather than treated as separators. Assisted-by: Copilot Assisted-by: OpenCode (ollama-cloud/glm-4.7) Assisted-by: Claude Code (Claude Opus 4.6)
1 parent 1a07f89 commit 9f4d9d8

File tree

5 files changed

+155
-11
lines changed

5 files changed

+155
-11
lines changed

composer.lock

Lines changed: 8 additions & 8 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

docs/PlaceholderFormatter.md

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,35 @@ echo $formatter->format('Phone: {{phone|pattern:(###) ###-####}}');
5959

6060
See the [FormatterModifier](modifiers/FormatterModifier.md) documentation for all available formatters and options.
6161

62+
#### Multiple Pipes
63+
64+
You can chain multiple modifiers together using the pipe (`|`) character. Modifiers are applied sequentially from left to right.
65+
66+
```php
67+
$formatter = new PlaceholderFormatter([
68+
'phone' => '1234567890',
69+
'value' => '12345',
70+
]);
71+
72+
// Apply pattern formatting, then mask sensitive data
73+
echo $formatter->format('Phone: {{phone|pattern:(###) ###-####|mask:6-12}}');
74+
// Output: Phone: (123) ******90
75+
76+
// Apply number formatting, then mask
77+
echo $formatter->format('Value: {{value|number:0|mask:1-3}}');
78+
// Output: Value: ***45
79+
```
80+
81+
**Escaped Pipes:** If you need to use the pipe character (`|`) as part of a modifier argument (not as a separator), escape it with a backslash (`\|`):
82+
83+
```php
84+
$formatter = new PlaceholderFormatter(['value' => '123456']);
85+
86+
// Escaped pipe in pattern, then apply mask
87+
echo $formatter->format('{{value|pattern:###\|###|mask:1-3}}');
88+
// Output: ***|456
89+
```
90+
6291
You can also use other modifiers like `list` and `trans`:
6392

6493
```php
@@ -91,15 +120,17 @@ Formats with additional parameters merged with constructor parameters. Construct
91120

92121
## Template Syntax
93122

94-
Placeholders follow the format `{{name}}` where `name` is a valid parameter key. Modifiers can be added after a pipe: `{{name|modifier}}`.
123+
Placeholders follow the format `{{name}}` where `name` is a valid parameter key. Modifiers can be added after a pipe: `{{name|modifier}}`. Multiple modifiers can be chained: `{{name|modifier1|modifier2}}`.
95124

96125
**Rules:**
97126

98127
- Names must match `\w+` (letters, digits, underscore)
99128
- Names are case-sensitive
100129
- No whitespace inside braces or around the pipe
130+
- Multiple pipes are separated by `|` and applied sequentially
131+
- Escaped pipes (`\|`) within modifiers are treated as literal characters, not separators
101132

102-
**Valid:** `{{name}}`, `{{user_id}}`, `{{name|raw}}`
133+
**Valid:** `{{name}}`, `{{user_id}}`, `{{name|raw}}`, `{{value|date:Y-m-d|mask:1-5}}`
103134

104135
**Invalid:** `{name}`, `{{ name }}`, `{{first-name}}`, `{{}}`
105136

docs/modifiers/Modifiers.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,27 @@ Modifiers form a chain where each modifier can:
1515
1. **Handle the value** and return a transformed string
1616
2. **Pass the value** to the next modifier in the chain
1717

18+
### Chaining Multiple Modifiers
19+
20+
You can chain multiple modifiers together by separating them with the pipe (`|`) character. Modifiers are applied sequentially from left to right, with each modifier receiving the output of the previous one.
21+
22+
```php
23+
$formatter = new PlaceholderFormatter([
24+
'phone' => '1234567890',
25+
'value' => '123456',
26+
]);
27+
28+
// Apply pattern formatting, then mask sensitive data
29+
echo $formatter->format('Phone: {{phone|pattern:(###) ###-####|mask:6-12}}');
30+
// Output: Phone: (123) ******90
31+
32+
// Escaped pipe in pattern argument, then apply mask
33+
echo $formatter->format('{{value|pattern:###\|###|mask:1-3}}');
34+
// Output: ***|456
35+
```
36+
37+
**Important:** When using the pipe character (`|`) as part of a modifier argument (not as a separator), escape it with a backslash (`\|`).
38+
1839
## Basic Usage
1940

2041
```php

src/PlaceholderFormatter.php

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
use function array_key_exists;
2020
use function preg_replace_callback;
21+
use function preg_split;
2122

2223
final readonly class PlaceholderFormatter implements Formatter
2324
{
@@ -67,6 +68,20 @@ private function processPlaceholder(array $matches, array $parameters): string
6768
return $placeholder;
6869
}
6970

70-
return $this->modifier->modify($parameters[$name], $pipe);
71+
$value = $parameters[$name];
72+
if ($pipe === null) {
73+
return $this->modifier->modify($value, null);
74+
}
75+
76+
$pipes = preg_split('/(?<!\\\\)\|/', $pipe) ?: [];
77+
if ($pipes === []) {
78+
return $this->modifier->modify($value, null);
79+
}
80+
81+
foreach ($pipes as $pipe) {
82+
$value = $this->modifier->modify($value, $pipe);
83+
}
84+
85+
return $value;
7186
}
7287
}

tests/Unit/PlaceholderFormatterTest.php

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -671,4 +671,81 @@ public static function providerForEscapedPipes(): array
671671
],
672672
];
673673
}
674+
675+
/** @param array<string, mixed> $parameters */
676+
#[Test]
677+
#[DataProvider('providerForMultiplePipes')]
678+
public function itShouldHandleMultiplePipesInSequence(
679+
array $parameters,
680+
string $template,
681+
string $expected,
682+
): void {
683+
$formatter = new PlaceholderFormatter($parameters);
684+
$actual = $formatter->format($template);
685+
686+
self::assertSame($expected, $actual);
687+
}
688+
689+
/** @return array<string, array{0: array<string, mixed>, 1: string, 2: string}> */
690+
public static function providerForMultiplePipes(): array
691+
{
692+
return [
693+
'date then mask' => [
694+
['value' => '2024-01-15'],
695+
'{{value|date:Y/m/d|mask:5-8}}',
696+
'2024****15',
697+
],
698+
'pattern then mask' => [
699+
['phone' => '1234567890'],
700+
'{{phone|pattern:(###) ###-####|mask:7-12}}',
701+
'(123) ******90',
702+
],
703+
'number then mask' => [
704+
['value' => '12345'],
705+
'{{value|number:0|mask:1-2}}',
706+
'**,345',
707+
],
708+
'pattern then number' => [
709+
['value' => '12345'],
710+
'{{value|pattern:###.##|number:2}}',
711+
'123.45',
712+
],
713+
'three pipes: pattern, date, mask' => [
714+
['value' => '20240115'],
715+
'{{value|pattern:####-##-##|date:Y/m/d|mask:5-7}}',
716+
'2024***/15',
717+
],
718+
];
719+
}
720+
721+
/** @param array<string, mixed> $parameters */
722+
#[Test]
723+
#[DataProvider('providerForMultiplePipesWithEscaping')]
724+
public function itShouldHandleMultiplePipesWithEscapedCharacters(
725+
array $parameters,
726+
string $template,
727+
string $expected,
728+
): void {
729+
$formatter = new PlaceholderFormatter($parameters);
730+
$actual = $formatter->format($template);
731+
732+
self::assertSame($expected, $actual);
733+
}
734+
735+
/** @return array<string, array{0: array<string, mixed>, 1: string, 2: string}> */
736+
public static function providerForMultiplePipesWithEscaping(): array
737+
{
738+
return [
739+
'pattern with escaped pipe then mask' => [
740+
['value' => '123456'],
741+
'{{value|pattern:###\|###|mask:1-3}}',
742+
'***|456',
743+
],
744+
'pattern with escaped colon then pattern with escaped pipe' => [
745+
['value' => '12345678'],
746+
'{{value|pattern:####\:####|pattern:0000\|0000}}',
747+
'1234|5678',
748+
],
749+
];
750+
}
674751
}

0 commit comments

Comments
 (0)