Skip to content

Commit 98d3f24

Browse files
committed
feat: add form-safe empty string coercion for objects and arrays
- ObjectValidator: Empty strings ('') now coerce to empty stdClass objects - AssociativeValidator: Empty strings ('') now coerce to empty arrays [] - Real-world benefit: Form parameters like ?settings= create empty structures instead of failing - Type safety maintained: Non-empty strings still fail validation as expected - Composable with nullifyEmpty(): coerce().nullifyEmpty() converts empty structures to null Examples: - Validator::isObject()->coerce()->validate('') → new stdClass() - Validator::isAssociative()->coerce()->validate('') → [] - Validator::isObject()->coerce()->nullifyEmpty()->validate('') → null - Add comprehensive test coverage (6 new tests, 23 new assertions) - Update project documentation with enhanced form-safe coercion details - Total test suite: 126 tests, 358 assertions
1 parent d7ba385 commit 98d3f24

7 files changed

Lines changed: 87 additions & 4 deletions

File tree

AGENTS.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ A comprehensive, fluent validation library for PHP inspired by Valibot and Zod.
2424
### Validation Capabilities
2525
- **Static Logical Combinators** - `Validator::allOf()`, `Validator::anyOf()`, `Validator::not()` for complex rule composition and mixed-type validation
2626
- **Instance Logical Combinators** - `allOf()`, `anyOf()`, `not()` instance methods for chaining validation rules
27-
- **Form-Safe Coercion** - Empty strings convert to `null` (not dangerous `0`/`0.0`/`false`) for real-world form safety
27+
- **Form-Safe Coercion** - Empty strings convert to `null` (not dangerous `0`/`0.0`/`false`) for primitives, empty structures for objects/arrays
2828
- **Array Filtering** - `filterEmpty()` method removes empty values while maintaining indexed array structure
2929
- **Type-Aware Transformations** - Revolutionary `transform()` and `pipe()` system with intelligent type context switching
3030
- **Custom Validation** - `satisfies()` method for business logic integration with optional error messages
@@ -65,7 +65,7 @@ A comprehensive, fluent validation library for PHP inspired by Valibot and Zod.
6565
## 🔧 Development Workflow
6666

6767
### Code Quality
68-
- **Testing** - Pest PHP with organized test suite (10 focused test files, 120 tests, 335 assertions)
68+
- **Testing** - Pest PHP with organized test suite (10 focused test files, 126 tests, 358 assertions)
6969
- **Static Analysis** - PHPStan at maximum level for type safety
7070
- **Code Style** - PHP-CS-Fixer for consistent formatting
7171
- **Performance** - Optimized validation logic with eliminated code duplication
@@ -110,7 +110,7 @@ tests/
110110
- **8 validator types** covering all PHP data types
111111
- **35+ built-in validation methods** including static logical combinators, array filtering, type-aware transformations, form-safe coercion, and intuitive custom validation
112112
- **5,000+ lines of documentation** with practical examples and comprehensive coverage
113-
- **120 unit tests** with comprehensive coverage (335 assertions)
113+
- **126 unit tests** with comprehensive coverage (358 assertions)
114114
- **Zero technical debt** with modern PHP 8.1+ codebase
115115
- **Zero dead links** in documentation with seamless navigation
116116
- **Complete API documentation** with accurate method signatures and examples

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,14 @@ All notable changes to this project will be documented in this file.
44

55
## [Unreleased]
66

7+
### Added
8+
- **Form-Safe Empty String Coercion**: Enhanced ObjectValidator and AssociativeValidator coercion for better form handling
9+
- `ObjectValidator::coerce()`: Empty strings (`''`) now coerce to empty `stdClass` objects
10+
- `AssociativeValidator::coerce()`: Empty strings (`''`) now coerce to empty arrays `[]`
11+
- **Real-World Benefit**: Form parameters like `?settings=` now create empty structures instead of failing validation
12+
- **Type Safety Maintained**: Non-empty strings still fail validation as expected
13+
- Added comprehensive test coverage (6 new tests, 23 new assertions)
14+
715
### Fixed
816
- **CRITICAL BUG**: Fixed schema validation field inclusion behavior in ObjectValidator and AssociativeValidator
917
- **Issue**: Validators were incorrectly including ALL schema fields in results, even when not provided in input

IDEAS.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,13 +48,14 @@ $formatted = Validator::isString()
4848
- ✅ Smart array coercion (indexed arrays auto-reindex, associative keys preserved)
4949
- ✅ Clean variadic syntax with `pipe(...$transformers)`
5050
- ✅ Perfect integration with external libraries (Laravel, Symfony)
51-
- ✅ Comprehensive test coverage (120 tests, 335 assertions)
51+
- ✅ Comprehensive test coverage (126 tests, 358 assertions)
5252
- ✅ Complete documentation overhaul with comprehensive coverage across all guides
5353
- ✅ Fixed all dead links in documentation for seamless navigation
5454
- ✅ Added type-aware transformation system documentation with detailed examples
5555
- ✅ Documented `filterEmpty()` method with practical use cases
5656
- ✅ Updated API reference to match actual implementation
5757
- ✅ Fixed critical schema validation bug - only include provided fields or fields with defaults in results
58+
- ✅ Enhanced form-safe coercion - empty strings coerce to empty objects/arrays for better form handling
5859

5960
### Intuitive Custom Validation (v0.6.0)
6061
**Status**: ✅ **IMPLEMENTED**

src/Lemmon/AssociativeValidator.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,11 @@ protected function coerceValue(mixed $value): mixed
3737
return (array) $value;
3838
}
3939

40+
// Form-safe: empty string becomes empty array
41+
if ($value === '') {
42+
return [];
43+
}
44+
4045
// For other types (including arrays), return as-is.
4146
// The subsequent validateType method will handle non-array errors.
4247
return $value;

src/Lemmon/ObjectValidator.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,11 @@ protected function coerceValue(mixed $value): mixed
3737
return (object) $value;
3838
}
3939

40+
// Form-safe: empty string becomes empty object
41+
if ($value === '') {
42+
return new \stdClass();
43+
}
44+
4045
// For other types (including objects), return as-is.
4146
// The subsequent validateType method will handle non-object errors.
4247
return $value;

tests/AssociativeValidatorTest.php

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,3 +196,36 @@
196196
expect(fn() => $schema->validate($input))
197197
->toThrow(Lemmon\ValidationException::class, 'Value is required');
198198
});
199+
200+
it('should coerce empty string to empty array when coerce is enabled', function () {
201+
$schema = Validator::isAssociative()->coerce();
202+
203+
$result = $schema->validate('');
204+
205+
expect($result)->toBeArray();
206+
expect($result)->toHaveCount(0);
207+
});
208+
209+
it('should coerce empty string to array with defaults when schema has defaults', function () {
210+
$schema = Validator::isAssociative([
211+
'name' => Validator::isString(),
212+
'status' => Validator::isString()->default('active'),
213+
'role' => Validator::isString()->default('user')
214+
])->coerce();
215+
216+
$result = $schema->validate('');
217+
218+
expect($result)->toBeArray();
219+
expect($result)->toHaveKey('status');
220+
expect($result)->toHaveKey('role');
221+
expect($result['status'])->toBe('active');
222+
expect($result['role'])->toBe('user');
223+
expect($result)->not->toHaveKey('name'); // Not provided, no default
224+
});
225+
226+
it('should reject non-empty strings even with coerce enabled', function () {
227+
$schema = Validator::isAssociative()->coerce();
228+
229+
expect(fn() => $schema->validate('not-empty'))
230+
->toThrow(Lemmon\ValidationException::class, 'Input must be an associative array');
231+
});

tests/ObjectValidatorTest.php

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,3 +169,34 @@
169169
expect(fn() => $schema->validate($input))
170170
->toThrow(Lemmon\ValidationException::class, 'Value is required');
171171
});
172+
173+
it('should coerce empty string to empty object when coerce is enabled', function () {
174+
$schema = Validator::isObject()->coerce();
175+
176+
$result = $schema->validate('');
177+
178+
expect($result)->toBeInstanceOf(stdClass::class);
179+
expect(get_object_vars($result))->toHaveCount(0);
180+
});
181+
182+
it('should coerce empty string to object with defaults when schema has defaults', function () {
183+
$schema = Validator::isObject([
184+
'name' => Validator::isString(),
185+
'status' => Validator::isString()->default('active'),
186+
'role' => Validator::isString()->default('user')
187+
])->coerce();
188+
189+
$result = $schema->validate('');
190+
191+
expect($result)->toBeInstanceOf(stdClass::class);
192+
expect($result)->toHaveProperty('status', 'active');
193+
expect($result)->toHaveProperty('role', 'user');
194+
expect($result)->not->toHaveProperty('name'); // Not provided, no default
195+
});
196+
197+
it('should reject non-empty strings even with coerce enabled', function () {
198+
$schema = Validator::isObject()->coerce();
199+
200+
expect(fn() => $schema->validate('not-empty'))
201+
->toThrow(Lemmon\ValidationException::class, 'Input must be an object');
202+
});

0 commit comments

Comments
 (0)