Skip to content

Commit 8fc5acf

Browse files
committed
docs: review and clarify all documentation
1 parent b2679e6 commit 8fc5acf

17 files changed

Lines changed: 133 additions & 141 deletions

AGENTS.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ A comprehensive, fluent validation library for PHP inspired by Valibot and Zod.
99
### Core Factory Pattern
1010
- **`Validator`** - Static factory creating type-specific validators (`isString()`, `isInt()`, `isFloat()`, `isArray()`, `isAssociative()`, `isObject()`, `isBool()`)
1111
- **`FieldValidator`** - Abstract base class with unified validation interface and shared functionality
12-
- **`ValidationException`** - Structured exception handling with comprehensive error collection
12+
- **`ValidationException`** - Structured exception handling with aggregated error reporting
1313

1414
### Type-Specific Validators
1515
- **`StringValidator`** - Format validation (email, URL, UUID with version variants, IP with version variants, hostname, domain, time, base64 with variants, hex), length constraints, pattern matching
@@ -29,14 +29,14 @@ A comprehensive, fluent validation library for PHP inspired by Valibot and Zod.
2929
- **Static Logical Combinators** - `Validator::allOf()`, `Validator::anyOf()`, `Validator::not()` for complex rule composition and mixed-type validation
3030
- **New `satisfies*` API** - Enhanced instance logical combinators (`satisfiesAny()`, `satisfiesAll()`, `satisfiesNone()`) with support for mixed validators/callables
3131
- **Enum-Based Variant Flags** - Type-safe variant selection for IP addresses (`IpVersion`), Base64 encoding (`Base64Variant`), and UUID versions (`UuidVariant`) with consistent API pattern
32-
- **Smart Null Handling** - Revolutionary null handling system where validations skip `null` unless `required()`, transformations always execute, and order is independent
32+
- **Smart Null Handling** - Validations skip `null` unless `required()`. `transform()` runs on `null`, while `pipe()` and `nullifyEmpty()` skip `null` for type safety.
3333
- **Form-Safe Coercion** - Empty strings convert to `null` (not dangerous `0`/`0.0`/`false`) for primitives, empty structures for objects/arrays
3434
- **Array Filtering** - `filterEmpty()` method removes empty values while maintaining indexed array structure
3535
- **Type-Aware Transformations** - Revolutionary `transform()` and `pipe()` system with intelligent type context switching
36-
- **Unified Pipeline Architecture** - Single conceptual pipeline with hybrid execution (error collection for validations, fail-fast for transformations)
36+
- **Unified Pipeline Architecture** - Single conceptual pipeline with ordered execution and fail-fast behavior per field
3737
- **Custom Validation** - Enhanced `satisfies()` method accepting `FieldValidator` instances or callables with optional error messages (all internal validators migrated from deprecated `addValidation()`)
3838
- **Context-Aware Validation** - Custom validators receive `(value, key, input)` parameters
39-
- **Comprehensive Error Collection** - All validation errors collected, not just the first failure
39+
- **Fail-Fast Per Field** - Each validator stops at the first failing rule, while schema validation aggregates errors across fields
4040
- **Smart Type Coercion** - Configurable automatic type conversion with form-friendly defaults
4141
- **Fluent API with guaranteed execution order** - Chainable method calls that execute in the exact order written
4242
- **Extensibility Philosophy** - Focus on core validation principles; external libraries encouraged for advanced/specialized validators via `satisfies()`

CHANGELOG.md

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

55
## [Unreleased]
66

7+
### Documentation
8+
- Clarified fail-fast behavior per field and schema-level error aggregation in guides and examples
9+
710
## [0.11.2] - 2026-01-05
811

912
### Fixed

CONTRIBUTING.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ Contributions are welcome! Please feel free to submit a pull request.
44

55
## Development
66

7-
To get started, you will need to have PHP 8.4+ and Composer installed.
7+
To get started, you will need to have PHP 8.3+ and Composer installed.
88

99
1. Fork the repository and clone it to your local machine.
1010
2. Install the dependencies:

README.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,22 +26,22 @@ Rather than reimplementing every possible transformation or validation rule, Lem
2626
**Key Design Principles:**
2727

2828
- **Type-Safe Architecture**: Modern PHP 8.1+ enums provide IDE autocomplete, refactoring safety, and eliminate magic strings throughout the codebase
29-
- **Smart Null Handling**: Validations skip `null` unless `required()`, transformations always execute, and method order is independent for intuitive behavior
29+
- **Smart Null Handling**: Validations skip `null` unless `required()`. `transform()` runs on `null`, while `pipe()` and `nullifyEmpty()` skip `null` for type safety.
3030
- **Form Safety First**: Empty strings coerce to `null` (not dangerous `0`/`false`) to prevent real-world issues like accidental zero bank balances
3131
- **Fluent API with Execution Order Guarantee**: Validation rules read like natural language and execute in the exact order written -- `Validator::isString()->pipe('trim')->nullifyEmpty()->required()`
32-
- **Comprehensive Error Collection**: All validation errors are collected, not just the first failure
32+
- **Fail-Fast Per Field**: Each validator stops at the first failing rule, while schema validation still aggregates errors across fields
3333
- **API-Friendly Error Format**: Flattened errors with field paths (`'_root'` for root-level, dot notation for nested) perfect for frontend consumption
3434
- **Type-Aware Transformations**: Intelligent transformation system that maintains type context and handles coercion automatically
3535
- **Extensible Architecture**: Generic transformation methods work with any PHP callable or external library
36-
- **Strict Typing**: All files use `declare(strict_types=1);`, so scalar mismatches raise clear errors; opt into `coerce()` when you need form-friendly conversions.
36+
- **Strict Typing**: All files use `declare(strict_types=1);`, keeping internal type hints strict; opt into `coerce()` when you need form-friendly conversions.
3737

3838
## Features
3939

4040
- **Type-safe architecture** - PHP 8.1+ enums with IDE autocomplete, refactoring safety, and zero magic strings
41-
- **Smart null handling** - validations skip `null` unless `required()`, transformations always execute, order-independent behavior
41+
- **Smart null handling** - validations skip `null` unless `required()`, `transform()` runs on `null`, `pipe()`/`nullifyEmpty()` skip `null`
4242
- **Type-safe validation** for strings, integers, floats, arrays, and objects
4343
- **Fluent, chainable API** with guaranteed execution order -- methods execute exactly as written in the chain
44-
- **Comprehensive error collection** with detailed, structured feedback
44+
- **Schema-level error aggregation** with fail-fast behavior per field for clear, early feedback
4545
- **API-friendly flattened errors** with field paths for easy frontend integration (`getFlattenedErrors()`, `ValidationException::flattenErrors()`)
4646
- **Intuitive custom validation** with `satisfies()` method and optional error messages
4747
- **Logical combinators** (`Validator::allOf()`, `Validator::anyOf()`, `Validator::not()`) for complex validation logic

ROADMAP.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ This roadmap outlines the strategic development plan for future releases, priori
88

99
**Core Principle:** Validate first, transform with the best tools available.
1010

11+
## Current Behavior Notes
12+
13+
- Validation fails fast per field; schema validation still aggregates errors across fields.
14+
1115
## Recently Completed
1216

1317
### Flattened Errors for API Consumption (Current)

docs/api-reference/validator-factory.md

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,15 @@ The `Validator` class serves as the main entry point and factory for creating va
55
## Class Overview
66

77
```php
8-
namespace Lemmon;
8+
namespace Lemmon\Validator;
99

1010
class Validator
1111
{
1212
// Factory methods for creating specific validators
1313
public static function isString(): StringValidator
1414
public static function isInt(): IntValidator
1515
public static function isFloat(): FloatValidator
16-
public static function isArray(?FieldValidator $itemValidator = null): ArrayValidator
16+
public static function isArray(): ArrayValidator
1717
public static function isAssociative(array $schema = []): AssociativeValidator
1818
public static function isObject(array $schema = []): ObjectValidator
1919
public static function isBool(): BoolValidator
@@ -79,7 +79,7 @@ $percentageValidator = Validator::isFloat()->min(0.0)->max(100.0);
7979

8080
---
8181

82-
### `isArray(?FieldValidator $itemValidator = null): ArrayValidator`
82+
### `isArray(): ArrayValidator`
8383

8484
Creates a validator for indexed arrays.
8585

@@ -89,7 +89,7 @@ $validator = Validator::isArray();
8989
$result = $validator->validate([1, 2, 3]); // Returns: [1, 2, 3]
9090

9191
// Array with item validation
92-
$numbersValidator = Validator::isArray(
92+
$numbersValidator = Validator::isArray()->items(
9393
Validator::isInt()->positive()
9494
);
9595
$numbers = $numbersValidator->validate([1, 2, 3]); // Each item validated as positive int
@@ -100,9 +100,6 @@ $emailsValidator = Validator::isArray()->items(
100100
);
101101
```
102102

103-
**Parameters:**
104-
- `$itemValidator` (optional): `FieldValidator` instance to validate each array item
105-
106103
**Returns:** `ArrayValidator` instance with array-specific validation methods.
107104

108105
**See Also:** [Array Validation Guide](../guides/array-validation.md)
@@ -242,7 +239,7 @@ $orderValidator = Validator::isAssociative([
242239
'email' => Validator::isString()->email()->required(),
243240
'phone' => Validator::isString()->pattern('/^\+?[1-9]\d{1,14}$/')
244241
])->required(),
245-
'items' => Validator::isArray(
242+
'items' => Validator::isArray()->items(
246243
Validator::isAssociative([
247244
'product_id' => Validator::isInt()->positive()->required(),
248245
'quantity' => Validator::isInt()->min(1)->required(),

docs/examples/form-validation.md

Lines changed: 13 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -123,26 +123,19 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
123123

124124
### The Problem with Empty Form Fields
125125

126-
HTML forms often submit empty strings for unfilled fields, which can create dangerous scenarios:
126+
HTML forms often submit empty strings for unfilled fields. In plain PHP casting (or legacy behavior), empty strings can become dangerous defaults:
127127

128128
```php
129-
// ❌ DANGEROUS: Without nullifyEmpty()
130-
$orderValidator = Validator::isAssociative([
131-
'quantity' => Validator::isInt()->coerce(), // Empty string → 0 (dangerous!)
132-
'price' => Validator::isFloat()->coerce(), // Empty string → 0.0 (dangerous!)
133-
'discount' => Validator::isFloat()->coerce()->default(5.0), // Empty → 0.0, not 5.0!
134-
]);
135-
136-
// Form data with empty fields
129+
// ❌ DANGEROUS: Plain PHP casting
137130
$formData = [
138131
'quantity' => '', // Empty field from form
139132
'price' => '', // Empty field from form
140133
'discount' => '', // Empty field from form
141134
];
142135

143-
$result = $orderValidator->validate($formData);
144-
// Result: ['quantity' => 0, 'price' => 0.0, 'discount' => 0.0]
145-
// ❌ All dangerous zero values!
136+
$quantity = (int) $formData['quantity']; // 0 (dangerous!)
137+
$price = (float) $formData['price']; // 0.0 (dangerous!)
138+
$discount = (float) ($formData['discount'] ?? 5.0); // 0.0, not 5.0!
146139
```
147140

148141
### The Solution: Form-Safe Validation
@@ -155,11 +148,13 @@ $safeOrderValidator = Validator::isAssociative([
155148
'quantity' => Validator::isInt()
156149
->coerce()
157150
->nullifyEmpty() // Empty strings → null
151+
->required('Quantity is required')
158152
->min(1, 'Quantity must be at least 1'), // Validation fails for null (safe!)
159153

160154
'price' => Validator::isFloat()
161155
->coerce()
162156
->nullifyEmpty() // Empty strings → null
157+
->required('Price is required')
163158
->positive('Price must be positive'), // Validation fails for null (safe!)
164159

165160
'discount' => Validator::isFloat()
@@ -329,7 +324,7 @@ class UserRegistrationValidator
329324
// Demographics
330325
'date_of_birth' => Validator::isString()
331326
->required('Date of birth is required')
332-
->date('Please enter a valid date (YYYY-MM-DD)')
327+
->date('Y-m-d', 'Please enter a valid date (YYYY-MM-DD)')
333328
->satisfies(
334329
function ($date) {
335330
$birthDate = new DateTime($date);
@@ -567,7 +562,7 @@ class ProductFormValidator
567562
'allow_backorder' => Validator::isBool()->coerce()->default(false),
568563

569564
// Categories and Tags
570-
'category_ids' => Validator::isArray(
565+
'category_ids' => Validator::isArray()->items(
571566
Validator::isInt()->positive()
572567
)
573568
->required('At least one category is required')
@@ -580,7 +575,7 @@ class ProductFormValidator
580575
'One or more selected categories do not exist'
581576
),
582577

583-
'tags' => Validator::isArray(
578+
'tags' => Validator::isArray()->items(
584579
Validator::isString()
585580
->minLength(2, 'Tags must be at least 2 characters')
586581
->maxLength(30, 'Tags cannot exceed 30 characters')
@@ -600,7 +595,7 @@ class ProductFormValidator
600595
->default('draft'),
601596

602597
'published_at' => Validator::isString()
603-
->datetime('Please enter a valid date and time')
598+
->datetime('Y-m-d\TH:i:s', 'Please enter a valid date and time')
604599
->satisfies(
605600
function ($datetime, $key, $input) {
606601
if ($input['status'] === 'active' && !$datetime) {
@@ -627,7 +622,7 @@ class ProductFormValidator
627622
// Digital Products
628623
'is_digital' => Validator::isBool()->coerce()->default(false),
629624

630-
'download_files' => Validator::isArray(
625+
'download_files' => Validator::isArray()->items(
631626
Validator::isAssociative([
632627
'name' => Validator::isString()->required(),
633628
'url' => Validator::isString()->url()->required(),
@@ -713,7 +708,7 @@ class MultiStepRegistrationValidator
713708

714709
'date_of_birth' => Validator::isString()
715710
->required('Date of birth is required')
716-
->date('Please enter a valid date')
711+
->date('Y-m-d', 'Please enter a valid date')
717712
]);
718713

719714
return $this->validateStep($validator, $data);

docs/getting-started/basic-usage.md

Lines changed: 14 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -136,9 +136,10 @@ $result = $optional->validate('John'); // Returns: 'John'
136136
$safeQuantity = Validator::isInt()
137137
->coerce()
138138
->nullifyEmpty() // Empty strings → null (not dangerous 0)
139+
->required('Quantity is required')
139140
->min(1, 'Quantity must be at least 1');
140141

141-
$result = $safeQuantity->validate(''); // Validation fails (safe!)
142+
$result = $safeQuantity->validate(''); // ❌ "Quantity is required" (safe!)
142143
$result = $safeQuantity->validate('5'); // Returns: 5
143144
```
144145

@@ -150,23 +151,21 @@ $result = $safeQuantity->validate('5'); // Returns: 5
150151

151152
### Execution Order Matters
152153

153-
**Critical**: Methods execute in the **exact order written**. This is especially important when combining transformations with `required()`:
154+
**Critical**: Pipeline steps execute in the **exact order written**. This matters for `pipe()`, `transform()`, and `nullifyEmpty()`. `required()` is a flag, so its position does not change execution order.
154155

155156
```php
156-
// CORRECT: Trim → nullify → require (fails on empty input)
157-
$requiredName = Validator::isString()
157+
// Order matters for transformations
158+
$trimThenNullify = Validator::isString()
158159
->pipe('trim') // 1. Remove whitespace
159-
->nullifyEmpty() // 2. Empty strings → null
160-
->required('Name is required'); // 3. Reject null values
160+
->nullifyEmpty(); // 2. Empty strings → null
161161

162-
$requiredName->validate(' '); // ❌ "Name is required" (correct!)
162+
$trimThenNullify->validate(' '); // Returns: null
163163

164-
// ❌ DIFFERENT BEHAVIOR: Require → nullify (allows empty input)
165-
$differentBehavior = Validator::isString()
166-
->required('Name is required') // 1. Check if empty string is null (passes)
167-
->nullifyEmpty(); // 2. Convert to null
164+
$nullifyThenTrim = Validator::isString()
165+
->nullifyEmpty() // 1. Empty strings → null (only if already empty)
166+
->pipe('trim'); // 2. Trim whitespace
168167

169-
$differentBehavior->validate(''); // Returns: null (different result!)
168+
$nullifyThenTrim->validate(' '); // Returns: '' (empty string)
170169
```
171170

172171
**Real-world form validation pattern:**
@@ -241,7 +240,7 @@ $validConfig = $configSchema->validate($config);
241240
### Plain Arrays
242241

243242
```php
244-
$numbersValidator = Validator::isArray(
243+
$numbersValidator = Validator::isArray()->items(
245244
Validator::isInt()->min(0) // Each item must be a non-negative integer
246245
);
247246

@@ -251,7 +250,7 @@ $validNumbers = $numbersValidator->validate($numbers);
251250

252251
## Error Handling
253252

254-
Validation errors are collected comprehensively:
253+
Validation fails fast per field. Schema validation still collects errors across fields:
255254

256255
```php
257256
$validator = Validator::isString()
@@ -263,8 +262,7 @@ $validator = Validator::isString()
263262

264263
// $errors might contain:
265264
// [
266-
// 'Value must be at least 5 characters long',
267-
// 'Value must be a valid email address'
265+
// 'Value must be at least 5 characters long'
268266
// ]
269267
```
270268

0 commit comments

Comments
 (0)