Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 8 additions & 8 deletions composer.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

35 changes: 33 additions & 2 deletions docs/PlaceholderFormatter.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,35 @@ echo $formatter->format('Phone: {{phone|pattern:(###) ###-####}}');

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

#### Multiple Pipes

You can chain multiple modifiers together using the pipe (`|`) character. Modifiers are applied sequentially from left to right.

```php
$formatter = new PlaceholderFormatter([
'phone' => '1234567890',
'value' => '12345',
]);

// Apply pattern formatting, then mask sensitive data
echo $formatter->format('Phone: {{phone|pattern:(###) ###-####|mask:6-12}}');
// Output: Phone: (123) ******90

// Apply number formatting, then mask
echo $formatter->format('Value: {{value|number:0|mask:1-3}}');
// Output: Value: ***45
```

**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 (`\|`):

```php
$formatter = new PlaceholderFormatter(['value' => '123456']);

// Escaped pipe in pattern, then apply mask
echo $formatter->format('{{value|pattern:###\|###|mask:1-3}}');
// Output: ***|456
```

You can also use other modifiers like `list` and `trans`:

```php
Expand Down Expand Up @@ -91,15 +120,17 @@ Formats with additional parameters merged with constructor parameters. Construct

## Template Syntax

Placeholders follow the format `{{name}}` where `name` is a valid parameter key. Modifiers can be added after a pipe: `{{name|modifier}}`.
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}}`.

**Rules:**

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

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

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

Expand Down
21 changes: 21 additions & 0 deletions docs/modifiers/Modifiers.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,27 @@ Modifiers form a chain where each modifier can:
1. **Handle the value** and return a transformed string
2. **Pass the value** to the next modifier in the chain

### Chaining Multiple Modifiers

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.

```php
$formatter = new PlaceholderFormatter([
'phone' => '1234567890',
'value' => '123456',
]);

// Apply pattern formatting, then mask sensitive data
echo $formatter->format('Phone: {{phone|pattern:(###) ###-####|mask:6-12}}');
// Output: Phone: (123) ******90

// Escaped pipe in pattern argument, then apply mask
echo $formatter->format('{{value|pattern:###\|###|mask:1-3}}');
// Output: ***|456
```

**Important:** When using the pipe character (`|`) as part of a modifier argument (not as a separator), escape it with a backslash (`\|`).

## Basic Usage

```php
Expand Down
14 changes: 13 additions & 1 deletion src/PlaceholderFormatter.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

use function array_key_exists;
use function preg_replace_callback;
use function preg_split;

final readonly class PlaceholderFormatter implements Formatter
{
Expand Down Expand Up @@ -67,6 +68,17 @@ private function processPlaceholder(array $matches, array $parameters): string
return $placeholder;
}

return $this->modifier->modify($parameters[$name], $pipe);
$value = $parameters[$name];
if ($pipe === null) {
return $this->modifier->modify($value, null);
}

$pipes = preg_split('/(?<!\\\\)\|/', $pipe) ?: [];
foreach ($pipes as $pipe) {
$value = $this->modifier->modify($value, $pipe);
}

/** @phpstan-ignore return.type */
return $value;
}
}
118 changes: 118 additions & 0 deletions tests/Unit/PlaceholderFormatterTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -671,4 +671,122 @@ public static function providerForEscapedPipes(): array
],
];
}

/** @param array<string, mixed> $parameters */
#[Test]
#[DataProvider('providerForMultiplePipes')]
public function itShouldHandleMultiplePipesInSequence(
array $parameters,
string $template,
string $expected,
): void {
$formatter = new PlaceholderFormatter($parameters);
$actual = $formatter->format($template);

self::assertSame($expected, $actual);
}

/** @return array<string, array{0: array<string, mixed>, 1: string, 2: string}> */
public static function providerForMultiplePipes(): array
{
return [
'date then mask' => [
['value' => '2024-01-15'],
'{{value|date:Y/m/d|mask:5-8}}',
'2024****15',
],
'pattern then mask' => [
['phone' => '1234567890'],
'{{phone|pattern:(###) ###-####|mask:7-12}}',
'(123) ******90',
],
'number then mask' => [
['value' => '12345'],
'{{value|number:0|mask:1-2}}',
'**,345',
],
'pattern then number' => [
['value' => '12345'],
'{{value|pattern:###.##|number:2}}',
'123.45',
],
'three pipes: pattern, date, mask' => [
['value' => '20240115'],
'{{value|pattern:####-##-##|date:Y/m/d|mask:5-7}}',
'2024***/15',
],
];
}

/** @param array<string, mixed> $parameters */
#[Test]
#[DataProvider('providerForMultiplePipesWithEscaping')]
public function itShouldHandleMultiplePipesWithEscapedCharacters(
array $parameters,
string $template,
string $expected,
): void {
$formatter = new PlaceholderFormatter($parameters);
$actual = $formatter->format($template);

self::assertSame($expected, $actual);
}

/** @return array<string, array{0: array<string, mixed>, 1: string, 2: string}> */
public static function providerForMultiplePipesWithEscaping(): array
{
return [
'pattern with escaped pipe then mask' => [
['value' => '123456'],
'{{value|pattern:###\|###|mask:1-3}}',
'***|456',
],
'pattern with escaped colon then pattern with escaped pipe' => [
['value' => '12345678'],
'{{value|pattern:####\:####|pattern:0000\|0000}}',
'1234|5678',
],
];
}

/** @param array<string, mixed> $parameters */
#[Test]
#[DataProvider('providerForEmptyPipe')]
public function itShouldHandleEmptyPipe(
array $parameters,
string $template,
string $expected,
): void {
$formatter = new PlaceholderFormatter($parameters);
$actual = $formatter->format($template);

self::assertSame($expected, $actual);
}

/** @return array<string, array{0: array<string, mixed>, 1: string, 2: string}> */
public static function providerForEmptyPipe(): array
{
return [
'empty pipe at end' => [
['name' => 'John'],
'Hello {{name|}}!',
'Hello John!',
],
'empty pipes' => [
['name' => 'John'],
'Hello {{name||}}!',
'Hello John!',
],
'empty pipe in middle' => [
['first' => 'A', 'second' => 'B'],
'{{first|}}-{{second}}',
'A-B',
],
'empty pipe with missing parameter' => [
[],
'Hello {{name|}}!',
'Hello {{name|}}!',
],
];
}
}