Skip to content

Commit 61ad070

Browse files
committed
Add QuoteModifier ported from Respect\Validation
Port QuoteModifier from Validation project to StringFormatter following the modifier pattern. Adapts validation logic to transformation logic using Stringifier Quoter instead of Quoted placeholder. - Implements Chain of Responsibility pattern with proper delegation - Uses CodeQuoter for consistent string escaping behavior - Includes comprehensive test suite with TestingQuoter helper - Updates AGENTS.md with modifier development guidelines
1 parent 68ac6ec commit 61ad070

6 files changed

Lines changed: 396 additions & 2 deletions

File tree

AGENTS.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,27 @@ When creating new formatters:
2929

3030
All formatters must implement the `Respect\StringFormatter\Formatter` interface.
3131

32+
## Modifier Development
33+
34+
When creating new modifiers:
35+
36+
1. **Follow Chain of Responsibility pattern**: Check pipe value and delegate to next modifier
37+
2. **Use template structure**: Similar to `src/Modifier/QuoteModifier.php`
38+
3. **Test with TestingModifier**: Located in `tests/Helper/TestingModifier.php`
39+
4. **Handle type checking**: Always check input types before processing
40+
5. **Return string values**: Modifiers must return strings
41+
6. **Use Stringifier Quoter**: For string operations, inject `\Respect\Stringifier\Quoter` with CodeQuoter as default
42+
43+
All modifiers must implement the `Respect\StringFormatter\Modifier` interface.
44+
45+
## Testing Guidelines
46+
47+
1. **Avoid PHPUnit mocks**: Create custom test implementations instead of using createMock()
48+
2. **Use custom test quoter**: Follow pattern in `tests/Helper/TestingQuoter.php`
49+
3. **Test contracts not implementations**: Verify interactions without depending on specific behavior
50+
4. **Make test properties public**: When using anonymous classes to access test state
51+
5. **Verify method calls**: Track whether methods were called and with what parameters
52+
3253
## Commit Guidelines
3354

3455
Follow the detailed rules in `docs/contributing/commit-guidelines.md`:

docs/modifiers/Modifiers.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,12 +33,13 @@ Modifiers form a chain where each modifier can:
3333
The default modifier chain is:
3434

3535
```
36-
RawModifier -> ListModifier -> StringifyModifier
36+
RawModifier -> ListModifier -> QuoteModifier -> StringifyModifier
3737
```
3838

3939
- `StringifyModifier` is always the last modifier, using the stringifier to convert values to strings
4040
- `RawModifier` processes scalar values without stringifier formatting
4141
- `ListModifier` formats arrays as human-readable lists with "and" or "or" conjunctions
42+
- `QuoteModifier` quotes string values using the stringifier quoter
4243

4344
## Syntax Rules
4445

@@ -72,6 +73,7 @@ For non-scalar values with `|raw`, the modifier falls back to the next modifier
7273
- **[RawModifier](RawModifier.md)** - Outputs scalar values directly without stringifier formatting
7374
- **[StringifyModifier](StringifyModifier.md)** - Default modifier that uses stringifier for all values
7475
- **[ListModifier](ListModifier.md)** - Formats arrays as human-readable lists with conjunctions
76+
- **[QuoteModifier](QuoteModifier.md)** - Quotes string values using a stringifier quoter
7577

7678
## Creating Custom Modifiers
7779

@@ -93,4 +95,4 @@ $formatter = new PlaceholderFormatter(
9395
);
9496
```
9597

96-
If no modifier is provided, the formatter uses a default `RawModifier` with `ListModifier` and `StringifyModifier`.
98+
If no modifier is provided, the formatter uses a default `RawModifier` with `ListModifier`, `QuoteModifier` and `StringifyModifier`.

docs/modifiers/QuoteModifier.md

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
# QuoteModifier
2+
3+
The `QuoteModifier` adds proper quoting to string values using a configurable quoter.
4+
5+
## Purpose
6+
7+
The `|quote` modifier applies string quoting to values, handling escape sequences and special characters according to the configured quoter implementation.
8+
9+
## Behavior
10+
11+
The modifier only processes string values. For non-string values, it passes them to the next modifier in the chain.
12+
13+
### Quoting Behavior
14+
15+
- Strings are quoted using the configured `Quoter` instance (default: `CodeQuoter` with 256 character limit)
16+
- Non-string values are passed to the next modifier unchanged
17+
- Handles escape sequences and special characters according to the quoter implementation
18+
19+
## Usage
20+
21+
```php
22+
use Respect\StringFormatter\PlaceholderFormatter;
23+
use Respect\StringFormatter\Modifier\QuoteModifier;
24+
use Respect\StringFormatter\Modifier\StringifyModifier;
25+
26+
$formatter = new PlaceholderFormatter([
27+
'name' => 'John "Johnny" Doe',
28+
'path' => 'C:\Users\Files\My Document.txt',
29+
'status' => 'It\'s working',
30+
'age' => 30,
31+
'data' => ['x' => 1]
32+
]);
33+
34+
echo $formatter->format('{{name|quote}}');
35+
// Outputs: '"John \"Johnny\" Doe"'
36+
37+
echo $formatter->format('{{status|quote}}');
38+
// Outputs: '"It\'s working"'
39+
40+
echo $formatter->format('{{age|quote}}');
41+
// Outputs: "30" (passed to StringifyModifier, no quoting applied)
42+
```
43+
44+
## Examples
45+
46+
### Basic Quoting
47+
48+
| Parameters | Template | Output |
49+
| ---------- | -------- | ------ |
50+
| `['name' => 'John']` | `"{{name|quote}}"` | `"\"John\""` |
51+
| `['text' => 'Hello "World"']` | `"{{text|quote}}"` | `"\"Hello \\\"World\\\"\""` |
52+
| `['path' => 'C:\temp']` | `"{{path|quote}}"` | `"\"C:\\temp\""` |
53+
54+
### Escape Sequences
55+
56+
| Parameters | Template | Output |
57+
| ---------- | -------- | ------ |
58+
| `['text' => "Line1\nLine2"]` | `"{{text|quote}}"` | `"\"Line1\\nLine2\""` |
59+
| `['text' => "Tab\tSeparated"]` | `"{{text|quote}}"` | `"\"Tab\\tSeparated\""` |
60+
| `['text' => "Back\\Slash"]` | `"{{text|quote}}"` | `"\"Back\\\\Slash\""` |
61+
62+
### Non-string Values
63+
64+
| Parameters | Template | Output |
65+
| ---------- | -------- | ------ |
66+
| `['age' => 30]` | `"{{age|quote}}"` | `"30"` |
67+
| `['active' => true]` | `"{{active|quote}}"` | `"true"` |
68+
| `['items' => ['a', 'b']]` | `"{{items|quote}}"` | `["a","b"]` |
69+
70+
## Custom Quoter
71+
72+
You can create a `QuoteModifier` with a custom quoter:
73+
74+
```php
75+
use Respect\StringFormatter\PlaceholderFormatter;
76+
use Respect\StringFormatter\Modifier\QuoteModifier;
77+
use Respect\StringFormatter\Modifier\RawModifier;
78+
use Respect\StringFormatter\Modifier\StringifyModifier;
79+
use Respect\Stringifier\Quoters\CodeQuoter;
80+
81+
// Custom quoter with different character limit
82+
$customQuoter = new CodeQuoter(512);
83+
$quoteModifier = new QuoteModifier($rawModifier, $customQuoter);
84+
85+
$formatter = new PlaceholderFormatter(['text' => $value], $quoteModifier);
86+
```
87+
88+
## Implementation Notes
89+
90+
The `QuoteModifier`:
91+
92+
1. Checks if the pipe value is `"quote"` and the input value is a string
93+
2. If both conditions are met, applies the configured quoter to the string
94+
3. Otherwise, passes the value to the next modifier in the chain
95+
4. Uses `CodeQuoter` by default with a 256 character limit
96+
97+
## Common Use Cases
98+
99+
1. **CSV output** - Properly quote fields containing special characters
100+
2. **Code generation** - Quote string literals in generated code
101+
3. **Configuration files** - Ensure string values are properly escaped
102+
4. **Logging** - Prepare string values for logging with proper escaping
103+
104+
## Integration
105+
106+
`QuoteModifier` is typically positioned early in the chain:
107+
108+
```php
109+
QuoteModifier -> RawModifier -> StringifyModifier
110+
```
111+
112+
This ensures string quoting is applied before other processing, while still allowing raw output and stringification fallbacks.

src/Modifier/QuoteModifier.php

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

tests/Helper/TestingQuoter.php

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\Test\Helper;
6+
7+
use Respect\Stringifier\Quoter;
8+
9+
final class TestingQuoter implements Quoter
10+
{
11+
public string $lastQuotedValue = '';
12+
13+
public int $lastQuotedDepth = -1;
14+
15+
public bool $quoteWasCalled = false;
16+
17+
public function __construct(public readonly string $returnValue = 'test-output')
18+
{
19+
}
20+
21+
public function quote(string $string, int $depth): string
22+
{
23+
$this->quoteWasCalled = true;
24+
$this->lastQuotedValue = $string;
25+
$this->lastQuotedDepth = $depth;
26+
27+
return $this->returnValue;
28+
}
29+
30+
public function reset(): void
31+
{
32+
$this->lastQuotedValue = '';
33+
$this->lastQuotedDepth = -1;
34+
$this->quoteWasCalled = false;
35+
}
36+
}

0 commit comments

Comments
 (0)