-
Notifications
You must be signed in to change notification settings - Fork 1
Add modifier support to PlaceholderFormatter #2
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
71f9492 to
0b046fc
Compare
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #2 +/- ##
============================================
+ Coverage 99.04% 99.22% +0.18%
- Complexity 97 127 +30
============================================
Files 4 10 +6
Lines 210 259 +49
============================================
+ Hits 208 257 +49
Misses 2 2 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
0b046fc to
3405a0a
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This pull request introduces a modifier system to PlaceholderFormatter, enabling fine-grained control over value transformations through a {{value|modifier}} syntax. The implementation uses a Chain of Responsibility pattern where modifiers can either handle transformations or pass values to the next modifier in the chain.
Changes:
- Introduced
Modifierinterface and two implementations:RawModifier(for clean scalar output) andStringifyModifier(default fallback) - Updated
PlaceholderFormatterto parse and apply modifiers using regex pattern/{{(\w+)(\|([^}]+))?}}/ - Added comprehensive documentation for modifiers including usage examples and custom modifier creation guide
Reviewed changes
Copilot reviewed 12 out of 12 changed files in this pull request and generated 16 comments.
Show a summary per file
| File | Description |
|---|---|
| src/Modifier.php | New interface defining the modifier contract with modify() method |
| src/Modifier/RawModifier.php | Implements raw modifier for scalar values, bypassing stringifier formatting |
| src/Modifier/StringifyModifier.php | Wrapper modifier delegating to stringifier for value conversion |
| src/PlaceholderFormatter.php | Updated to parse modifier syntax and apply modifier chain instead of direct stringifier |
| tests/Unit/PlaceholderFormatterTest.php | Updated test to verify custom modifier support |
| tests/Unit/Formatter/RawModifierTest.php | Comprehensive test suite for RawModifier behavior |
| tests/Helper/TestingModifier.php | Test helper implementing Modifier interface for testing |
| docs/PlaceholderFormatter.md | Updated with modifier documentation and corrected examples |
| docs/modifiers/Modifiers.md | New overview documentation for modifier system |
| docs/modifiers/RawModifier.md | Detailed documentation for RawModifier |
| docs/modifiers/StringifyModifier.md | Detailed documentation for StringifyModifier |
| docs/modifiers/CreatingCustomModifiers.md | Guide for implementing custom modifiers |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
|
||
| // Create the modifier chain | ||
| $modifierChain = new UppercaseModifier( | ||
| new RawModifier( | ||
| new StringifyModifier() |
Copilot
AI
Jan 19, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The StringifyModifier is instantiated without passing the required stringifier parameter. According to the constructor in src/Modifier/StringifyModifier.php (line 12-14), it requires a Stringifier instance. These examples should either pass a stringifier (e.g., new StringifyModifier(HandlerStringifier::create())) or explain that this is pseudo-code.
| // Create the modifier chain | |
| $modifierChain = new UppercaseModifier( | |
| new RawModifier( | |
| new StringifyModifier() | |
| use Respect\StringFormatter\Stringifier\HandlerStringifier; | |
| // Create the modifier chain | |
| $modifierChain = new UppercaseModifier( | |
| new RawModifier( | |
| new StringifyModifier(HandlerStringifier::create()) |
| // Usage with parameter | ||
| $formatter = new PlaceholderFormatter( | ||
| ['longText' => 'This is a very long piece of text'], | ||
| new TruncateModifier(new StringifyModifier()) |
Copilot
AI
Jan 19, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The StringifyModifier is instantiated without passing the required stringifier parameter. According to the constructor in src/Modifier/StringifyModifier.php (line 12-14), it requires a Stringifier instance. This should be new TruncateModifier(new StringifyModifier(HandlerStringifier::create())).
| ]); | ||
|
|
||
| echo $formatter->format('{{name}} is {{age}} years old and {{active}}'); | ||
| // Outputs: "John is "30" years old and "true"" |
Copilot
AI
Jan 19, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The expected output shows "John is "30" years old and "true"" but according to the code logic in PlaceholderFormatter.php (lines 63-64), string values without a pipe modifier are returned as-is. Since 'John' is a string and {{name}} has no pipe modifier, it should output as John without quotes. The expected output should be "John is "30" years old and "true"" (no quotes around John).
| // Outputs: "John is "30" years old and "true"" | |
| // Outputs: John is "30" years old and "true" |
| final readonly class StringifyModifier implements Modifier | ||
| { | ||
| public function __construct( | ||
| private Stringifier $stringifier, | ||
| ) { | ||
| } | ||
|
|
||
| public function modify(mixed $value, string|null $pipe): string | ||
| { | ||
| return $this->stringifier->stringify($value); | ||
| } | ||
| } |
Copilot
AI
Jan 19, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's no test coverage for StringifyModifier. Given that this is a new class being introduced and RawModifier has comprehensive tests, StringifyModifier should also have test coverage to ensure its behavior is properly validated, especially its interaction with the stringifier.
| | Parameters | Template | Output | | ||
| | ------------------------- | ------------------------------- | ------------------------- | | ||
| | `['count' => 42]` | `"Count: {{count|raw}}"` | `"Count: 42"` | | ||
| | `['name' => 'John']` | `"Name: {{name|raw}}"` | `"Name: John"` | | ||
| | `['active' => true]` | `"Active: {{active|raw}}"` | `"Active: 1"` | | ||
| | `['items' => ['a', 'b']]` | `"Items: {{items|raw}}"` | `"Items: [\"a\", \"b\"]"` | |
Copilot
AI
Jan 19, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The table has a formatting error with misaligned column separators. The pipe character in the template column (which is part of the placeholder syntax {{count|raw}}) is being interpreted as a table separator, creating an extra column. The template examples should use HTML entity encoding (|) for the pipe character, similar to how it's done in RawModifier.md lines 52-56.
| echo $formatter->format('Hello {{name}}, you are {{age}} years old.'); | ||
| // Outputs: "Hello --John--, you are --30-- years old." |
Copilot
AI
Jan 19, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The example uses the wrong parameter in the format call. The formatter is constructed with ['count' => 42, 'name' => 'John'], but the format string attempts to use {{age}} which doesn't exist in the parameters. This should likely be {{count}} to match the parameters array.
| echo $formatter->format('Hello {{name}}, you are {{age}} years old.'); | |
| // Outputs: "Hello --John--, you are --30-- years old." | |
| echo $formatter->format('Hello {{name}}, you are {{count}} years old.'); | |
| // Outputs: "Hello --John--, you are --42-- years old." |
|
|
||
| | Type | Without `raw` | With `raw` | | ||
| | ------ | ------------------- | ------------------- | | ||
| | string | `hello` → `"hello"` | `hello` → `hello` | |
Copilot
AI
Jan 19, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The "Without raw" column shows string values getting quotes (hello → "hello"), but according to PlaceholderFormatter.php (lines 63-64), string values without a pipe modifier are returned as-is. The table is misleading about default behavior for strings.
| | string | `hello` → `"hello"` | `hello` → `hello` | | |
| | string | `hello` → `hello` | `hello` → `hello` | |
| | int | `"42"` | `"42"` | | ||
| | bool | `"true"` | `"1"` | | ||
| | array | `["a"]` → `"["a"]"` | Passes through | | ||
| | null | `"`null`"` | `"`null`"` | |
Copilot
AI
Jan 19, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The comparison table entry for null shows "null" for both StringifyModifier and RawModifier, which is technically correct but potentially misleading. It would be clearer to indicate that RawModifier passes null through to StringifyModifier (since null is not scalar), rather than suggesting they handle it identically by showing the same output for both. Consider changing RawModifier column for null to "Passes through" to match the pattern used for arrays.
| | null | `"`null`"` | `"`null`"` | | |
| | null | `"`null`"` | Passes through | |
| | Type | Without `raw` | With `raw` | | ||
| | ------ | ------------------- | ------------------- | | ||
| | string | `hello` → `"hello"` | `hello` → `hello` | | ||
| | int | `42` → `"42"` | `42` → `"42"` | | ||
| | float | `19.99` → `"19.99"` | `19.99` → `"19.99"` | | ||
| | bool | `true` → `"true"` | `true` → `"1"` | | ||
|
|
Copilot
AI
Jan 19, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The "Without raw" column shows hello → "hello" for strings, but according to the PlaceholderFormatter.php logic (lines 63-64), string values without a pipe modifier are returned as-is without going through the modifier chain. So {{name}} where name is a string would output the string directly without quotes. The table should clarify that "Without raw" means using no modifier at all (which for strings returns them as-is), not that it forces them through StringifyModifier.
| | Type | Without `raw` | With `raw` | | |
| | ------ | ------------------- | ------------------- | | |
| | string | `hello` → `"hello"` | `hello` → `hello` | | |
| | int | `42` → `"42"` | `42` → `"42"` | | |
| | float | `19.99` → `"19.99"` | `19.99` → `"19.99"` | | |
| | bool | `true` → `"true"` | `true` → `"1"` | | |
| | Type | Without `raw` | With `raw` | | |
| | ------ | ----------------- | ------------------- | | |
| | string | `hello` → `hello` | `hello` → `hello` | | |
| | int | `42` → `"42"` | `42` → `"42"` | | |
| | float | `19.99` → `"19.99"` | `19.99` → `"19.99"` | | |
| | bool | `true` → `"true"` | `true` → `"1"` | | |
| **Note:** In this table, “Without `raw`” means using no modifier at all (e.g. `{{name}}`). For strings, this returns the value directly without sending it through the modifier chain, so no extra quotes are added. Non-scalar values still follow the default modifier chain. |
| use Respect\StringFormatter\Modifier\RawModifier; | ||
| use Respect\StringFormatter\Test\Helper\TestingModifier; | ||
|
|
||
| final class RawModifierTest extends TestCase |
Copilot
AI
Jan 19, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The test class is missing the #[CoversClass(RawModifier::class)] attribute that's used in other test classes like PlaceholderFormatterTest. This attribute should be added for consistency and proper code coverage reporting.
|
@copilot open a new pull request to apply changes based on the comments in this thread |
665ba42 to
bb58a9f
Compare
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)
Implement flexible ListModifier that formats arrays into human-readable lists with configurable conjunctions (and/or), replacing the limited ListAndModifier approach.
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
Implement translation support using symfony/translation with TranslatorInterface dependency injection. Supports simple 'trans' pipe modifier for string translation following established modifier patterns. - Create TransModifier implementing Modifier interface - Add TestingTranslator for isolated test dependencies - Follow Chain of Responsibility pattern with proper delegation - Support symfony/translation-contracts for interface compliance - Maintain PSR-12 compliance and comprehensive test coverage Co-authored-by: AI Assistant <ai@example.com>
bb58a9f to
7641c33
Compare
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|rawmodifier, which provides clean scalar output by bypassing the default stringifier formatting.The
StringifyModifierremains 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)