Skip to content

Commit 2f1e9c1

Browse files
committed
feat(FieldValidator): add outputKey() for schema output key remapping
- Add outputKey(string $key) to allow schema fields to output under a different key - AssociativeValidator and ObjectValidator use output key when assigning validated values - Update llms.txt, docs (object-validation, validator-factory), CHANGELOG, ROADMAP
1 parent ee4a39a commit 2f1e9c1

10 files changed

Lines changed: 149 additions & 3 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ All notable changes to this project will be documented in this file.
55
## [Unreleased]
66

77
### Added
8+
- `outputKey(string $key)` on `FieldValidator` for schema fields: output validated values under a different key than the input field (e.g. input `service_id` -> output `service` after transform); works with `Validator::isAssociative()` and `Validator::isObject()`
89
- GitHub Actions CI: lint, platform-check, static analysis, tests on PHP 8.3
910

1011
### Changed

ROADMAP.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ Focused on core validation primitives, consistent pipelines, and extensibility v
1414
## Next Minor Release
1515

1616
- [ ] Universal allowed-value validators: `enum()` (PHP BackedEnum) and `const()` (single allowed value)
17-
- [ ] Schema output key remapping: `outputKey(string $key)`
17+
- [x] Schema output key remapping: `outputKey(string $key)`
1818
- [ ] Structured error codes in validation errors (backward compatible)
1919
- [ ] ArrayValidator `uniqueField(string $fieldName, ?string $message = null)`
2020
- [ ] Mutation testing pilot (Infection + baseline)

docs/api-reference/validator-factory.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -663,6 +663,28 @@ $result = $validator->validate(''); // Returns: 'N/A'
663663

664664
---
665665

666+
### `outputKey(string $key): self`
667+
668+
**Schema fields only.** Emits the validated value under a different key than the input field. Only applies when the validator is used inside `Validator::isAssociative()` or `Validator::isObject()`.
669+
670+
```php
671+
$schema = Validator::isAssociative([
672+
'service_id' => Validator::isString()->uuid()->outputKey('service'),
673+
]);
674+
675+
$result = $schema->validate(['service_id' => '550e8400-e29b-41d4-a716-446655440000']);
676+
// Result: ['service' => '550e8400-...'] (not 'service_id')
677+
```
678+
679+
**Parameters:**
680+
- `$key`: The key to use in the output structure
681+
682+
**Returns:** Same validator instance for method chaining.
683+
684+
**See Also:** [Object & Schema Validation Guide](../guides/object-validation.md#output-key-remapping)
685+
686+
---
687+
666688
### `in(array $values, ?string $message = null): self`
667689

668690
**Available on:** `StringValidator`, `IntValidator`, `FloatValidator`, `BoolValidator` only

docs/guides/object-validation.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ This guide covers validation of structured data using `AssociativeValidator` (fo
77
- [Associative Array Validation](#associative-array-validation)
88
- [Object Validation](#object-validation)
99
- [Schema Definition](#schema-definition)
10+
- [Output Key Remapping](#output-key-remapping)
1011
- [Type Coercion](#type-coercion)
1112
- [Advanced Features](#advanced-features)
1213
- [Common Patterns](#common-patterns)
@@ -121,6 +122,38 @@ $result = $schema->validate($input);
121122
// Result: ['name' => 'John', 'status' => 'active', 'priority' => 1]
122123
```
123124

125+
### Output Key Remapping
126+
127+
Use `outputKey()` on schema fields to emit validated values under a different key than the input field. This is useful when the input uses snake_case or IDs while the output should use different property names (e.g. for API responses or after transforming IDs to entities).
128+
129+
```php
130+
$schema = Validator::isAssociative([
131+
'service_id' => Validator::isString()->uuid()->outputKey('service'),
132+
'user_id' => Validator::isString()->uuid()->outputKey('user'),
133+
]);
134+
135+
$input = [
136+
'service_id' => '550e8400-e29b-41d4-a716-446655440000',
137+
'user_id' => '6ba7b810-9dad-11d1-80b4-00c04fd430c8',
138+
];
139+
140+
$result = $schema->validate($input);
141+
// Result: ['service' => '550e8400-...', 'user' => '6ba7b810-...']
142+
// Note: 'service_id' and 'user_id' are not in the result
143+
```
144+
145+
Works with `transform()` for ID-to-entity resolution:
146+
147+
```php
148+
$schema = Validator::isAssociative([
149+
'service_id' => Validator::isString()->uuid()
150+
->transform(fn(string $id) => $serviceRepo->find($id))
151+
->outputKey('service'),
152+
]);
153+
```
154+
155+
Works with both `Validator::isAssociative()` and `Validator::isObject()`.
156+
124157
### Field Inclusion Behavior
125158

126159
Schema validators only include fields in the result that were either:

llms.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ static not(FieldValidator $validator, ?string $message = null): FieldValidator
5050
required(?string $message = null): static
5151
default(mixed $value): static
5252
coerce(): static
53+
outputKey(string $key): static (schema fields only: output under different key, e.g. input service_id -> output service)
5354
nullifyEmpty(): static (transforms '' or [] to null)
5455
clone(): static (deep copy with bound closures)
5556

src/Lemmon/Validator/AssociativeValidator.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,8 @@ protected function validateType(mixed $value, string $key): mixed
8484
$hasDefault = $validator->hasDefault ?? false;
8585

8686
if ($wasProvided || $hasDefault) {
87-
$data[$fieldKey] = $validatedFieldValue;
87+
$dataKey = $validator->outputKey ?? $fieldKey;
88+
$data[$dataKey] = $validatedFieldValue;
8889
}
8990
}
9091

src/Lemmon/Validator/FieldValidator.php

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ abstract class FieldValidator
1111
protected bool $coerce = false;
1212
protected bool $required = false;
1313
protected ?string $requiredMessage = null;
14+
protected ?string $outputKey = null;
1415

1516
/**
1617
* Creates a deep copy of the validator, including pipeline closures bound to the clone.
@@ -68,6 +69,19 @@ public function coerce(): self
6869
return $this;
6970
}
7071

72+
/**
73+
* Sets the output key for schema fields. When used in AssociativeValidator or ObjectValidator,
74+
* the validated value is stored under this key instead of the input field key.
75+
*
76+
* @param string $key The key to use in the output structure
77+
* @return $this
78+
*/
79+
public function outputKey(string $key): self
80+
{
81+
$this->outputKey = $key;
82+
return $this;
83+
}
84+
7185
/**
7286
* Enables nullification of empty values (empty string, empty array) to null.
7387
*

src/Lemmon/Validator/ObjectValidator.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,8 @@ protected function validateType(mixed $value, string $key): mixed
8484
$hasDefault = $validator->hasDefault ?? false;
8585

8686
if ($wasProvided || $hasDefault) {
87-
$data->{$fieldKey} = $validatedFieldValue;
87+
$dataKey = $validator->outputKey ?? $fieldKey;
88+
$data->{$dataKey} = $validatedFieldValue;
8889
}
8990
}
9091

tests/AssociativeValidatorTest.php

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,3 +235,57 @@
235235
expect(fn() => $schema->validate('not-empty'))
236236
->toThrow(ValidationException::class, 'Input must be an associative array');
237237
});
238+
239+
it('should remap output key with outputKey when field is provided', function () {
240+
$schema = Validator::isAssociative([
241+
'service_id' => Validator::isString()->uuid()->outputKey('service'),
242+
'user_id' => Validator::isString()->uuid()->outputKey('user'),
243+
]);
244+
245+
$input = [
246+
'service_id' => '550e8400-e29b-41d4-a716-446655440000',
247+
'user_id' => '6ba7b810-9dad-11d1-80b4-00c04fd430c8',
248+
];
249+
250+
$data = $schema->validate($input);
251+
252+
expect($data)->toBe([
253+
'service' => '550e8400-e29b-41d4-a716-446655440000',
254+
'user' => '6ba7b810-9dad-11d1-80b4-00c04fd430c8',
255+
]);
256+
expect($data)->not->toHaveKey('service_id');
257+
expect($data)->not->toHaveKey('user_id');
258+
});
259+
260+
it('should remap output key with outputKey combined with transform', function () {
261+
$schema = Validator::isAssociative([
262+
'service_id' => Validator::isString()
263+
->uuid()
264+
->transform(fn(string $id) => ['id' => $id, 'type' => 'service'])
265+
->outputKey('service'),
266+
]);
267+
268+
$input = [
269+
'service_id' => '550e8400-e29b-41d4-a716-446655440000',
270+
];
271+
272+
$data = $schema->validate($input);
273+
274+
expect($data)->toBe([
275+
'service' => ['id' => '550e8400-e29b-41d4-a716-446655440000', 'type' => 'service'],
276+
]);
277+
});
278+
279+
it('should remap output key for fields with default values', function () {
280+
$schema = Validator::isAssociative([
281+
'level' => Validator::isInt()
282+
->coerce()
283+
->default(3)
284+
->outputKey('tier'),
285+
])->coerceAll();
286+
287+
$data = $schema->validate([]);
288+
289+
expect($data)->toBe(['tier' => 3]);
290+
expect($data)->not->toHaveKey('level');
291+
});

tests/ObjectValidatorTest.php

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,3 +203,22 @@
203203
expect(fn() => $schema->validate('not-empty'))
204204
->toThrow(ValidationException::class, 'Input must be an object');
205205
});
206+
207+
it('should remap output key with outputKey when field is provided', function () {
208+
$schema = Validator::isObject([
209+
'service_id' => Validator::isString()->uuid()->outputKey('service'),
210+
'user_id' => Validator::isString()->uuid()->outputKey('user'),
211+
]);
212+
213+
$input = (object) [
214+
'service_id' => '550e8400-e29b-41d4-a716-446655440000',
215+
'user_id' => '6ba7b810-9dad-11d1-80b4-00c04fd430c8',
216+
];
217+
218+
$data = $schema->validate($input);
219+
220+
expect($data)->toHaveProperty('service', '550e8400-e29b-41d4-a716-446655440000');
221+
expect($data)->toHaveProperty('user', '6ba7b810-9dad-11d1-80b4-00c04fd430c8');
222+
expect($data)->not->toHaveProperty('service_id');
223+
expect($data)->not->toHaveProperty('user_id');
224+
});

0 commit comments

Comments
 (0)